Repository: batchar2/fptn Branch: master Commit: 450be140e7ad Files: 233 Total size: 3.2 MB Directory structure: gitextract_4uyv479r/ ├── .clang-format ├── .clang-tidy ├── .cmake-format ├── .conan/ │ └── recipes/ │ └── boringssl/ │ └── conanfile.py ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── custom.md │ │ └── feature_request.md │ └── workflows/ │ └── main.yml ├── .gitignore ├── CLAUDE.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── README_RU.md ├── conanfile.py ├── cpplint.py ├── depends/ │ └── cmake/ │ ├── CamouflageTLS.cmake │ ├── FetchBase64.cmake │ ├── FetchLibTunTap.cmake │ ├── FetchWintun.cmake │ └── NtpClient.cmake ├── deploy/ │ ├── docker/ │ │ ├── Dockerfile │ │ ├── config/ │ │ │ ├── supervisor/ │ │ │ │ ├── dns-server.conf │ │ │ │ └── fptn-server.conf │ │ │ └── supervisord.conf │ │ └── scripts/ │ │ ├── start-dns-server.sh │ │ ├── start-fptn.sh │ │ └── token-generator.py │ ├── linux/ │ │ ├── deb/ │ │ │ ├── README.md │ │ │ ├── create-client-cli-deb-package.sh │ │ │ ├── create-client-gui-deb-package.sh │ │ │ └── create-server-deb-package.sh │ │ └── wifi/ │ │ ├── README.md │ │ ├── README.ru.md │ │ ├── dnsmasq/ │ │ │ ├── fptn-dnsmasq.conf │ │ │ └── fptn-dnsmasq.service │ │ ├── fptn-setup-network/ │ │ │ ├── fptn-setup-network.service │ │ │ └── fptn-setup-network.sh │ │ └── hostapd/ │ │ ├── fptn-hostapd.conf │ │ └── fptn-hostapd.service │ ├── macos/ │ │ ├── README.md │ │ ├── assets/ │ │ │ └── FptnClient.icns │ │ ├── create-pkg.py │ │ └── scripts/ │ │ ├── fptn-client-cli-wrapper.sh │ │ ├── fptn-client-gui-wrapper.sh │ │ ├── post_install.sh │ │ └── uninstall.sh │ ├── sni/ │ │ ├── global.sni │ │ └── russia.sni │ └── windows/ │ ├── .gitignore │ ├── README.md │ ├── conan-replace-version.py │ ├── create-installer.py │ └── installer/ │ ├── app.manifest │ └── fptn-installer.iss ├── docker-compose/ │ ├── .gitignore │ ├── README.md │ └── docker-compose.yml ├── docs/ │ ├── CNAME │ ├── index-ru.html │ └── index.html ├── pyproject.toml ├── renovate.json ├── src/ │ ├── common/ │ │ ├── client_id.h │ │ ├── data/ │ │ │ ├── channel.h │ │ │ └── channel_async.h │ │ ├── jwt_token/ │ │ │ └── token_manager.h │ │ ├── logger/ │ │ │ └── logger.h │ │ ├── network/ │ │ │ ├── ip_address.h │ │ │ ├── ip_packet.h │ │ │ ├── ipv4_generator.h │ │ │ ├── ipv6_generator.h │ │ │ ├── ipv6_utils.h │ │ │ ├── net_interface.h │ │ │ ├── resolv.h │ │ │ ├── tun/ │ │ │ │ ├── darwin_tun_device.h │ │ │ │ ├── linux_tun_device.h │ │ │ │ └── win_tun_device.h │ │ │ └── utils.h │ │ ├── system/ │ │ │ └── command.h │ │ ├── user/ │ │ │ └── common_user_manager.h │ │ └── utils/ │ │ ├── base64.h │ │ └── utils.h │ ├── fptn-client/ │ │ ├── CMakeLists.txt │ │ ├── config/ │ │ │ ├── config_file.cpp │ │ │ └── config_file.h │ │ ├── fptn-client-cli.cpp │ │ ├── fptn-client-gui.cpp │ │ ├── gui/ │ │ │ ├── autostart/ │ │ │ │ └── autostart.h │ │ │ ├── autoupdate/ │ │ │ │ └── autoupdate.h │ │ │ ├── resources/ │ │ │ │ ├── resources.qrc │ │ │ │ └── translations/ │ │ │ │ ├── .gitignore │ │ │ │ ├── fptn_en.ts │ │ │ │ └── fptn_ru.ts │ │ │ ├── server_menu_item_widget/ │ │ │ │ ├── server_menu_item_widget.cpp │ │ │ │ └── server_menu_item_widget.h │ │ │ ├── settingsmodel/ │ │ │ │ ├── settingsmodel.cpp │ │ │ │ └── settingsmodel.h │ │ │ ├── settingswidget/ │ │ │ │ ├── settings.cpp │ │ │ │ └── settings.h │ │ │ ├── sni_autoscan_dialog/ │ │ │ │ ├── sni_autoscan_dialog.cpp │ │ │ │ └── sni_autoscan_dialog.h │ │ │ ├── sni_manager/ │ │ │ │ ├── sni_manager.cpp │ │ │ │ └── sni_manager.h │ │ │ ├── speedwidget/ │ │ │ │ ├── speedwidget.cpp │ │ │ │ └── speedwidget.h │ │ │ ├── style/ │ │ │ │ ├── style.cpp │ │ │ │ └── style.h │ │ │ ├── tokendialog/ │ │ │ │ ├── tokendialog.cpp │ │ │ │ └── tokendialog.h │ │ │ ├── translations/ │ │ │ │ ├── translations.cpp │ │ │ │ └── translations.h │ │ │ └── tray/ │ │ │ ├── tray.cpp │ │ │ └── tray.h │ │ ├── plugins/ │ │ │ ├── base_plugin.h │ │ │ ├── blacklist/ │ │ │ │ ├── domain_blacklist.cpp │ │ │ │ └── domain_blacklist.h │ │ │ └── split/ │ │ │ ├── tunneling.cpp │ │ │ └── tunneling.h │ │ ├── routing/ │ │ │ ├── route_manager.cpp │ │ │ └── route_manager.h │ │ ├── utils/ │ │ │ ├── brotli/ │ │ │ │ └── brotli.h │ │ │ ├── macos/ │ │ │ │ └── admin.h │ │ │ ├── signal/ │ │ │ │ └── main_loop.h │ │ │ ├── speed_estimator/ │ │ │ │ ├── server_info.h │ │ │ │ ├── speed_estimator.cpp │ │ │ │ └── speed_estimator.h │ │ │ ├── utils.h │ │ │ └── windows/ │ │ │ └── vpn_conflict.h │ │ └── vpn/ │ │ ├── http/ │ │ │ ├── client.cpp │ │ │ └── client.h │ │ ├── vpn_client.cpp │ │ └── vpn_client.h │ ├── fptn-passwd/ │ │ ├── CMakeLists.txt │ │ └── fptn-passwd.cpp │ ├── fptn-protocol-lib/ │ │ ├── CMakeLists.txt │ │ ├── https/ │ │ │ ├── api_client/ │ │ │ │ ├── api_client.cpp │ │ │ │ └── api_client.h │ │ │ ├── censorship_strategy.h │ │ │ ├── obfuscator/ │ │ │ │ ├── methods/ │ │ │ │ │ ├── detector.h │ │ │ │ │ ├── obfuscator_interface.h │ │ │ │ │ ├── tls/ │ │ │ │ │ │ ├── tls_obfuscator.cpp │ │ │ │ │ │ └── tls_obfuscator.h │ │ │ │ │ └── tls2/ │ │ │ │ │ ├── tls_obfuscator2.cpp │ │ │ │ │ └── tls_obfuscator2.h │ │ │ │ └── tcp_stream/ │ │ │ │ └── tcp_stream.h │ │ │ ├── utils/ │ │ │ │ ├── change_cipher_spec.h │ │ │ │ └── tls/ │ │ │ │ ├── tls.cpp │ │ │ │ └── tls.h │ │ │ └── websocket_client/ │ │ │ ├── websocket_client.cpp │ │ │ └── websocket_client.h │ │ ├── protobuf/ │ │ │ ├── protocol.cpp │ │ │ ├── protocol.h │ │ │ └── protocol.proto │ │ └── time/ │ │ ├── time_provider.cpp │ │ └── time_provider.h │ └── fptn-server/ │ ├── .gitignore │ ├── CMakeLists.txt │ ├── client/ │ │ ├── session.cpp │ │ └── session.h │ ├── config/ │ │ ├── command_line_config.cpp │ │ └── command_line_config.h │ ├── filter/ │ │ ├── filters/ │ │ │ ├── antiscan/ │ │ │ │ ├── antiscan.cpp │ │ │ │ └── antiscan.h │ │ │ ├── base_filter.h │ │ │ └── bittorrent/ │ │ │ ├── bittorrent.cpp │ │ │ └── bittorrent.h │ │ ├── manager.cpp │ │ └── manager.h │ ├── fptn-server.cpp │ ├── nat/ │ │ ├── table.cpp │ │ └── table.h │ ├── network/ │ │ ├── virtual_interface.cpp │ │ └── virtual_interface.h │ ├── routing/ │ │ ├── iptables.cpp │ │ └── iptables.h │ ├── statistic/ │ │ ├── metrics.cpp │ │ └── metrics.h │ ├── traffic_shaper/ │ │ ├── leaky_bucket.cpp │ │ └── leaky_bucket.h │ ├── user/ │ │ ├── user_manager.cpp │ │ └── user_manager.h │ ├── vpn/ │ │ ├── manager.cpp │ │ └── manager.h │ └── web/ │ ├── api/ │ │ └── handle.h │ ├── handshake/ │ │ ├── handshake_cache_manager.cpp │ │ └── handshake_cache_manager.h │ ├── listener/ │ │ ├── listener.cpp │ │ └── listener.h │ ├── server.cpp │ ├── server.h │ └── session/ │ ├── session.cpp │ └── session.h ├── sysadmin-tools/ │ ├── grafana/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── configs/ │ │ │ ├── grafana/ │ │ │ │ ├── dashboards/ │ │ │ │ │ ├── dashboards.yaml │ │ │ │ │ ├── fptn_dashboard.json │ │ │ │ │ └── node-exporter-full.json │ │ │ │ └── datasources/ │ │ │ │ └── datasources.yaml │ │ │ ├── nginx/ │ │ │ │ └── nginx.conf.template │ │ │ └── prometheus/ │ │ │ └── prometheus.yaml.template │ │ ├── docker-compose.build.yml │ │ ├── docker-compose.yml │ │ └── proxy-server/ │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── CMakeLists.txt │ │ ├── Dockerfile │ │ ├── README.md │ │ ├── conanfile.py │ │ └── src/ │ │ └── proxy-server.cpp │ └── telegram-bot/ │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ ├── configs/ │ │ ├── premium_servers.json.demo │ │ ├── servers.json.demo │ │ └── servers_censored_zone.json.demo │ ├── docker-compose.yml │ └── src/ │ ├── bot.py │ └── requirements.txt └── tests/ ├── CMakeLists.txt ├── common/ │ ├── CMakeLists.txt │ ├── data/ │ │ └── ChannelTest.cpp │ └── network/ │ ├── ClientStopRaceTest.cpp │ ├── IPv4GeneratorTest.cpp │ ├── IPv6GeneratorTest.cpp │ ├── IPv6UtilsTest.cpp │ └── TunDeviceTest.cpp ├── fptnlib/ │ ├── CMakeLists.txt │ └── api_client/ │ └── ApiClientTest.cpp └── server/ ├── CMakeLists.txt ├── filter/ │ └── antiscan/ │ └── AntiScanTest.cpp └── statistic/ └── MetricTest.cpp ================================================ FILE CONTENTS ================================================ ================================================ FILE: .clang-format ================================================ # Use the Google style in this project. Language: Cpp BasedOnStyle: Google Standard: c++11 # Some folks prefer to write "int& foo" while others prefer "int &foo". The # Google Style Guide only asks for consistency within a project, we chose # "int& foo" for this project: DerivePointerAlignment: false PointerAlignment: Left UseTab: Never AlignAfterOpenBracket: DontAlign AllowAllParametersOfDeclarationOnNextLine: true BinPackParameters: false # The Google Style Guide only asks for consistency w.r.t. "east const" vs. # "const west" alignment of cv-qualifiers. In this project we use "const west". #QualifierAlignment: Right # Adjust the include ordering IncludeCategories: - Regex: '^' Priority: 4 - Regex: '^' # QT Core (QApplication, QWidget и т.д.) Priority: 3 - Regex: '^' # QT Modules (QtCore, QtGui и т.д.) Priority: 3 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^' Priority: 2 - Regex: '^"(common/.*)"' Priority: 5 - Regex: '^<.*>' Priority: 1 - Regex: '^".*"' Priority: 6 # Format raw string literals with a `pb` or `proto` tag as proto. RawStringFormats: - Language: TextProto Delimiters: - 'pb' - 'proto' BasedOnStyle: Google CommentPragmas: '(@copydoc|@copybrief|@see|@overload|@snippet)' ================================================ FILE: .clang-tidy ================================================ --- # Configure clang-tidy for this project. # Here is an explanation for why some of the checks are disabled: # # -google-readability-namespace-comments: the *_CLIENT_NS is a macro, and # clang-tidy fails to match it against the initial value. # # -modernize-use-trailing-return-type: clang-tidy recommends using # `auto Foo() -> std::string { return ...; }`, we think the code is less # readable in this form. # # --modernize-concat-nested-namespaces: clang-tidy recommends # `namespace google::cloud {}` over `namespace google { namespace cloud { } }` # We need to support C++14, which does not supported nested namespaces. # # --modernize-use-nodiscard: clang-tidy recommends adding a nodiscard annotation # to functions where the return value should not be ignored. # We need to support C++14, which does not supported the annotation. # # -modernize-return-braced-init-list: We think removing typenames and using # only braced-init can hurt readability. # # -modernize-avoid-c-arrays: We only use C arrays when they seem to be the # right tool for the job, such as `char foo[] = "hello"`. In these cases, # avoiding C arrays often makes the code less readable, and std::array is # not a drop-in replacement because it doesn't deduce the size. # # -modernize-type-traits: clang-tidy recommands using c++17 style variable # templates. We will enable this check after we moved to c++17. # # -modernize-unary-static-assert: clang-tidy asks removing empty string in # static_assert(), the check is only applicable for c++17 and later code. # We will enable this check after we moved to c++17. # # -performance-move-const-arg: This warning requires the developer to # know/care more about the implementation details of types/functions than # should be necessary. For example, `A a; F(std::move(a));` will trigger a # warning IFF `A` is a trivial type (and therefore the move is # meaningless). It would also warn if `F` accepts by `const&`, which is # another detail that the caller need not care about. # # -performance-avoid-endl: we would like to turn this on, but there are too # many legitimate uses in our samples. # # -performance-enum-size: Smaller enums may or not may be faster, it depends on # the architechture. If data size was a consideration, we might decide to # enable the warnings. # # -readability-redundant-declaration: A friend declaration inside a class # counts as a declaration, so if we also declare that friend outside the # class in order to document it as part of the public API, that will # trigger a redundant declaration warning from this check. # # -readability-avoid-return-with-void-value: We believe this is idiomatic # and saves typing, and the intent is obvious. # # -readability-function-cognitive-complexity: too many false positives with # clang-tidy-12. We need to disable this check in macros, and that setting # only appears in clang-tidy-13. # # -bugprone-narrowing-conversions: too many false positives around # `std::size_t` vs. `*::difference_type`. # # -bugprone-easily-swappable-parameters: too many false positives. # # -bugprone-implicit-widening-of-multiplication-result: too many false positives. # Almost any expression of the form `2 * variable` or `long x = a_int * b_int;` # generates an error. # # -bugprone-unchecked-optional-access: too many false positives in tests. # Despite what the documentation says, this warning appears after # `ASSERT_TRUE(variable)` or `ASSERT_TRUE(variable.has_value())`. # # TODO(#14162): Enable clang-tidy checks. We initially omitted these checks # because they require large cleanup efforts or were blocking the clang-tidy # X update. Checks: > -*, abseil-*, bugprone-*, google-*, misc-*, modernize-*, performance-*, portability-*, readability-*, -google-readability-braces-around-statements, -google-readability-namespace-comments, -google-runtime-references, -misc-non-private-member-variables-in-classes, -misc-const-correctness, -misc-include-cleaner, -modernize-return-braced-init-list, -modernize-use-trailing-return-type, -modernize-use-nodiscard, -modernize-avoid-c-arrays, -modernize-type-traits, -modernize-unary-static-assert, -performance-move-const-arg, -performance-avoid-endl, -performance-enum-size, -readability-braces-around-statements, -readability-identifier-length, -readability-magic-numbers, -readability-named-parameter, -readability-redundant-declaration, -readability-avoid-return-with-void-value, -readability-function-cognitive-complexity, -bugprone-narrowing-conversions, -bugprone-easily-swappable-parameters, -bugprone-inc-dec-in-conditions, -bugprone-implicit-widening-of-multiplication-result, -bugprone-unchecked-optional-access, -bugprone-unused-local-non-trivial-variable, -bugprone-unused-return-value, -portability-template-virtual-member-function # Turn all the warnings from the checks above into errors. WarningsAsErrors: "*" HeaderFilterRegex: "(google/cloud/|generator/).*\\.h$" CheckOptions: - { key: readability-identifier-naming.NamespaceCase, value: lower_case } - { key: readability-identifier-naming.ClassCase, value: CamelCase } - { key: readability-identifier-naming.StructCase, value: CamelCase } - { key: readability-identifier-naming.TemplateParameterCase, value: CamelCase } - { key: readability-identifier-naming.FunctionCase, value: aNy_CasE } - { key: readability-identifier-naming.VariableCase, value: lower_case } - { key: readability-identifier-naming.ClassMemberCase, value: lower_case } - { key: readability-identifier-naming.ClassMemberSuffix, value: _ } - { key: readability-identifier-naming.PrivateMemberSuffix, value: _ } - { key: readability-identifier-naming.ProtectedMemberSuffix, value: _ } - { key: readability-identifier-naming.EnumConstantCase, value: CamelCase } - { key: readability-identifier-naming.EnumConstantPrefix, value: k } - { key: readability-identifier-naming.ConstexprVariableCase, value: CamelCase } - { key: readability-identifier-naming.ConstexprVariablePrefix, value: k } - { key: readability-identifier-naming.GlobalConstantCase, value: CamelCase } - { key: readability-identifier-naming.GlobalConstantPrefix, value: k } - { key: readability-identifier-naming.MemberConstantCase, value: CamelCase } - { key: readability-identifier-naming.MemberConstantPrefix, value: k } - { key: readability-identifier-naming.StaticConstantCase, value: CamelCase } - { key: readability-identifier-naming.StaticConstantPrefix, value: k } - { key: readability-implicit-bool-conversion.AllowIntegerConditions, value: 1 } - { key: readability-implicit-bool-conversion.AllowPointerConditions, value: 1 } - { key: readability-function-cognitive-complexity.IgnoreMacros, value: 1 } ================================================ FILE: .cmake-format ================================================ _help_parse: Options affecting listfile parsing parse: _help_additional_commands: - Specify structure for custom cmake functions additional_commands: foo: flags: - BAR - BAZ kwargs: HEADERS: '*' SOURCES: '*' DEPENDS: '*' _help_override_spec: - Override configurations per-command where available override_spec: {} _help_vartags: - Specify variable tags. vartags: [] _help_proptags: - Specify property tags. proptags: [] _help_format: Options affecting formatting. format: _help_disable: - Disable formatting entirely, making cmake-format a no-op disable: false _help_line_width: - How wide to allow formatted cmake files line_width: 150 _help_tab_size: - How many spaces to tab for indent tab_size: 2 _help_max_subgroups_hwrap: - If an argument group contains more than this many sub-groups - (parg or kwarg groups) then force it to a vertical layout. max_subgroups_hwrap: 2 _help_max_pargs_hwrap: - If a positional argument group contains more than this many - arguments, then force it to a vertical layout. max_pargs_hwrap: 6 _help_max_rows_cmdline: - If a cmdline positional group consumes more than this many - lines without nesting, then invalidate the layout (and nest) max_rows_cmdline: 2 _help_separate_ctrl_name_with_space: - If true, separate flow control names from their parentheses - with a space separate_ctrl_name_with_space: false _help_separate_fn_name_with_space: - If true, separate function names from parentheses with a - space separate_fn_name_with_space: false _help_dangle_parens: - If a statement is wrapped to more than one line, than dangle - the closing parenthesis on its own line. dangle_parens: false _help_dangle_align: - If the trailing parenthesis must be 'dangled' on its on - 'line, then align it to this reference: `prefix`: the start' - 'of the statement, `prefix-indent`: the start of the' - 'statement, plus one indentation level, `child`: align to' - the column of the arguments dangle_align: prefix _help_min_prefix_chars: - If the statement spelling length (including space and - parenthesis) is smaller than this amount, then force reject - nested layouts. min_prefix_chars: 4 _help_max_prefix_chars: - If the statement spelling length (including space and - parenthesis) is larger than the tab width by more than this - amount, then force reject un-nested layouts. max_prefix_chars: 10 _help_max_lines_hwrap: - If a candidate layout is wrapped horizontally but it exceeds - this many lines, then reject the layout. max_lines_hwrap: 2 _help_line_ending: - What style line endings to use in the output. line_ending: unix _help_command_case: - Format command names consistently as 'lower' or 'upper' case command_case: canonical _help_keyword_case: - Format keywords consistently as 'lower' or 'upper' case keyword_case: unchanged _help_always_wrap: - A list of command names which should always be wrapped always_wrap: [] _help_enable_sort: - If true, the argument lists which are known to be sortable - will be sorted lexicographicall enable_sort: true _help_autosort: - If true, the parsers may infer whether or not an argument - list is sortable (without annotation). autosort: false _help_require_valid_layout: - By default, if cmake-format cannot successfully fit - everything into the desired linewidth it will apply the - last, most agressive attempt that it made. If this flag is - True, however, cmake-format will print error, exit with non- - zero status code, and write-out nothing require_valid_layout: false _help_layout_passes: - A dictionary mapping layout nodes to a list of wrap - decisions. See the documentation for more information. layout_passes: {} _help_markup: Options affecting comment reflow and formatting. markup: _help_bullet_char: - What character to use for bulleted lists bullet_char: '*' _help_enum_char: - What character to use as punctuation after numerals in an - enumerated list enum_char: . _help_first_comment_is_literal: - If comment markup is enabled, don't reflow the first comment - block in each listfile. Use this to preserve formatting of - your copyright/license statements. first_comment_is_literal: false _help_literal_comment_pattern: - If comment markup is enabled, don't reflow any comment block - which matches this (regex) pattern. Default is `None` - (disabled). literal_comment_pattern: null _help_fence_pattern: - Regular expression to match preformat fences in comments - default= ``r'^\s*([`~]{3}[`~]*)(.*)$'`` fence_pattern: ^\s*([`~]{3}[`~]*)(.*)$ _help_ruler_pattern: - Regular expression to match rulers in comments default= - '``r''^\s*[^\w\s]{3}.*[^\w\s]{3}$''``' ruler_pattern: ^\s*[^\w\s]{3}.*[^\w\s]{3}$ _help_explicit_trailing_pattern: - If a comment line matches starts with this pattern then it - is explicitly a trailing comment for the preceeding - argument. Default is '#<' explicit_trailing_pattern: '#<' _help_hashruler_min_length: - If a comment line starts with at least this many consecutive - hash characters, then don't lstrip() them off. This allows - for lazy hash rulers where the first hash char is not - separated by space hashruler_min_length: 10 _help_canonicalize_hashrulers: - If true, then insert a space between the first hash char and - remaining hash chars in a hash ruler, and normalize its - length to fill the column canonicalize_hashrulers: true _help_enable_markup: - enable comment markup parsing and reflow enable_markup: false _help_lint: Options affecting the linter lint: _help_disabled_codes: - a list of lint codes to disable disabled_codes: [] _help_function_pattern: - regular expression pattern describing valid function names function_pattern: '[0-9a-z_]+' _help_macro_pattern: - regular expression pattern describing valid macro names macro_pattern: '[0-9A-Z_]+' _help_global_var_pattern: - regular expression pattern describing valid names for - variables with global (cache) scope global_var_pattern: '[A-Z][0-9A-Z_]+' _help_internal_var_pattern: - regular expression pattern describing valid names for - variables with global scope (but internal semantic) internal_var_pattern: _[A-Z][0-9A-Z_]+ _help_local_var_pattern: - regular expression pattern describing valid names for - variables with local scope local_var_pattern: '[a-z][a-z0-9_]+' _help_private_var_pattern: - regular expression pattern describing valid names for - privatedirectory variables private_var_pattern: _[0-9a-z_]+ _help_public_var_pattern: - regular expression pattern describing valid names for public - directory variables public_var_pattern: '[A-Z][0-9A-Z_]+' _help_argument_var_pattern: - regular expression pattern describing valid names for - function/macro arguments and loop variables. argument_var_pattern: '[a-z][a-z0-9_]+' _help_keyword_pattern: - regular expression pattern describing valid names for - keywords used in functions or macros keyword_pattern: '[A-Z][0-9A-Z_]+' _help_max_conditionals_custom_parser: - In the heuristic for C0201, how many conditionals to match - within a loop in before considering the loop a parser. max_conditionals_custom_parser: 2 _help_min_statement_spacing: - Require at least this many newlines between statements min_statement_spacing: 1 _help_max_statement_spacing: - Require no more than this many newlines between statements max_statement_spacing: 2 max_returns: 6 max_branches: 12 max_arguments: 5 max_localvars: 15 max_statements: 50 _help_encode: Options affecting file encoding encode: _help_emit_byteorder_mark: - If true, emit the unicode byte-order mark (BOM) at the start - of the file emit_byteorder_mark: false _help_input_encoding: - Specify the encoding of the input file. Defaults to utf-8 input_encoding: utf-8 _help_output_encoding: - Specify the encoding of the output file. Defaults to utf-8. - Note that cmake only claims to support utf-8 so be careful - when using anything else output_encoding: utf-8 _help_misc: Miscellaneous configurations options. misc: _help_per_command: - A dictionary containing any per-command configuration - overrides. Currently only `command_case` is supported. per_command: {} ================================================ FILE: .conan/recipes/boringssl/conanfile.py ================================================ from conan import ConanFile from conan.tools.files import get from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout class BoringSSLConan(ConanFile): name = "openssl" version = "boringssl" settings = "os", "arch", "compiler", "build_type" options = {"shared": [True, False], "fPIC": [True, False]} default_options = {"shared": False, "fPIC": True} def config_options(self): if self.settings.os == "Windows": del self.options.fPIC def layout(self): cmake_layout(self) def source(self): url = "https://github.com/batchar2/boringssl/archive/refs/heads/working-russia-tls-handshake.tar.gz" get(self, url, strip_root=True) def generate(self): tc = CMakeToolchain(self) tc.variables["BUILD_TESTING"] = False tc.variables["ENABLE_EXPRESSION_TESTS"] = False # Правильная настройка для iOS if self.settings.os == "iOS": tc.variables["CMAKE_SYSTEM_NAME"] = "iOS" tc.variables["CMAKE_OSX_DEPLOYMENT_TARGET"] = str(self.settings.os.version) tc.variables["CMAKE_OSX_ARCHITECTURES"] = self.settings.arch tc.variables["CMAKE_MACOSX_BUNDLE"] = False if "simulator" in str(self.settings.os.sdk).lower(): tc.variables["CMAKE_OSX_SYSROOT"] = "iphonesimulator" else: tc.variables["CMAKE_OSX_SYSROOT"] = "iphoneos" tc.variables["CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED"] = "NO" tc.variables["CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED"] = "NO" tc.variables["CMAKE_XCODE_ATTRIBUTE_ENABLE_BITCODE"] = "YES" tc.variables["CMAKE_XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH"] = "NO" tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): cmake = CMake(self) cmake.install() def package_info(self): self.cpp_info.components["ssl"].libs = ["ssl"] self.cpp_info.components["crypto"].libs = ["crypto"] # include dir self.cpp_info.includedirs = ["include"] self.cpp_info.components["ssl"].includedirs = ["include"] self.cpp_info.components["crypto"].includedirs = ["include"] # for CMake find_package(OpenSSL) self.cpp_info.set_property("cmake_file_name", "OpenSSL") self.cpp_info.components["ssl"].set_property("cmake_target_name", "OpenSSL::SSL") self.cpp_info.components["crypto"].set_property("cmake_target_name", "OpenSSL::Crypto") if self.settings.os == "iOS": self.cpp_info.frameworks = ["Security", "Foundation", "CoreFoundation"] self.cpp_info.system_libs = ["c++"] ================================================ FILE: .dockerignore ================================================ .cache .vscode .vsconan .git/ README.md LICENSE CMakeUserPresets.json CI/ tmp/ keys/ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/custom.md ================================================ --- name: Custom issue template about: Describe this issue template's purpose here. title: '' labels: '' assignees: '' --- ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/main.yml ================================================ name: Build and Test on: push: branches: - '**' tags: - '*' release: types: [ published ] env: DOCKER_REGISTRY: fptnvpn IMAGE_NAME: fptn-vpn-server jobs: build_ubuntu_x86_64: runs-on: Ubuntu_x86_64 steps: - name: Checkout code uses: actions/checkout@v3 with: submodules: true - name: Set env if: github.event_name == 'release' run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Print Release Version if: github.event_name == 'release' run: | echo "Release Version: $RELEASE_VERSION" - name: Update FPTN_VERSION in conanfile.py if: github.event_name == 'release' run: | sed -i "s/^FPTN_VERSION = \".*\"/FPTN_VERSION = \"$RELEASE_VERSION\"/" conanfile.py - name: Install dependencies run: | conan install . --output-folder=build --build=missing -o with_gui_client=True --settings build_type=Release -s compiler.cppstd=17 - name: Run cpplint run: | python3 cpplint.py --recursive --filter=-build/c++17,-whitespace/newline,-readability/braces --counting=total ./src/ ./tests/ - name: Run cppcheck run: | cppcheck --error-exitcode=1 --enable=all --check-level=exhaustive --language=c++ --suppress=unusedFunction --inline-suppr --suppress=missingIncludeSystem --suppress=unknownMacro --suppress=unmatchedSuppression -I ./src/fptn-client/ -I ./src/fptn-server/ -I ./src/fptn-passwd/ -I ./src/fptn-client-protocol-lib -I ./src/ ./src/ ./tests/ - name: Run cmake-format run: | cmake-format -i CMakeLists.txt src/fptn-client/CMakeLists.txt src/fptn-passwd/CMakeLists.txt src/fptn-server/CMakeLists.txt src/fptn-protocol-lib/CMakeLists.txt depends/cmake/FetchBase64.cmake depends/cmake/FetchLibTunTap.cmake depends/cmake/FetchWintun.cmake - name: Build run: | cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release cmake --build . - name: Test run: | cd build ctest -C Release - name: Build Debian package run: | cd build cmake --build . --target build-deb cmake --build . --target build-deb-gui - name: Upload deb RELEASE if: github.event_name == 'release' uses: AButler/upload-release-assets@v3.0 with: files: "fptn-*.deb" repo-token: ${{ secrets.CI_TOKEN }} - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: name: Ubuntu86_64Artifacts path: | *.deb if-no-files-found: warn - name: Build and Push Docker Image (amd64) if: github.event_name == 'release' run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin docker buildx create --name amd64-builder --platform linux/amd64 --use || true docker buildx build \ --platform linux/amd64 \ -f ./deploy/docker/Dockerfile \ -t "$DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-amd64" \ --build-arg FPTN_SERVER_PATH=build/src/fptn-server/fptn-server \ --build-arg FPTN_PASSWD_PATH=build/src/fptn-passwd/fptn-passwd \ --push \ --provenance=false \ --sbom=false . build_ubuntu_arm64: runs-on: Ubuntu_ARM64_Desktop steps: - name: Checkout code uses: actions/checkout@v3 with: submodules: true - name: Set env if: github.event_name == 'release' run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Print Release Version if: github.event_name == 'release' run: | echo "Release Version: $RELEASE_VERSION" - name: Update FPTN_VERSION in conanfile.py if: github.event_name == 'release' run: | sed -i "s/^FPTN_VERSION = \".*\"/FPTN_VERSION = \"$RELEASE_VERSION\"/" conanfile.py - name: Install dependencies run: | conan install . --output-folder=build --build=missing -o with_gui_client=True --settings build_type=Release -s compiler.cppstd=17 - name: Run cpplint run: | python3 cpplint.py --recursive --filter=-build/c++17,-whitespace/newline,-readability/braces --counting=total ./src/ ./tests/ - name: Run cppcheck run: | cppcheck --error-exitcode=1 --enable=all --check-level=exhaustive --language=c++ --suppress=unusedFunction --inline-suppr --suppress=missingIncludeSystem --suppress=unmatchedSuppression -I ./src/fptn-client/ -I ./src/fptn-server/ -I ./src/fptn-passwd/ -I ./src/fptn-client-protocol-lib -I ./src/ ./src/ ./tests/ - name: Run cmake-format run: | cmake-format -i CMakeLists.txt src/fptn-client/CMakeLists.txt src/fptn-passwd/CMakeLists.txt src/fptn-server/CMakeLists.txt src/fptn-protocol-lib/CMakeLists.txt depends/cmake/FetchBase64.cmake depends/cmake/FetchLibTunTap.cmake depends/cmake/FetchWintun.cmake - name: Build run: | cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release cmake --build . - name: Test run: | cd build ctest -C Release - name: Build Debian package run: | cd build cmake --build . --target build-deb cmake --build . --target build-deb-gui - name: Upload deb if: github.event_name == 'release' uses: AButler/upload-release-assets@v3.0 with: files: "fptn-*.deb" repo-token: ${{ secrets.CI_TOKEN }} - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: name: UbuntuArmArtifacts path: | *.deb if-no-files-found: warn - name: Build and Push Docker Image (arm64) if: github.event_name == 'release' run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin docker buildx create --name arm64-builder --platform linux/arm64 --use || true docker buildx build \ --platform linux/arm64 \ -f ./deploy/docker/Dockerfile \ -t "$DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-arm64" \ --build-arg FPTN_SERVER_PATH=build/src/fptn-server/fptn-server \ --build-arg FPTN_PASSWD_PATH=build/src/fptn-passwd/fptn-passwd \ --push \ --provenance=false \ --sbom=false . build_macos_arm64: runs-on: MacOS_ARM64 steps: - name: Checkout code uses: actions/checkout@v3 with: submodules: true - name: Set env if: github.event_name == 'release' run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV - name: Print Release Version if: github.event_name == 'release' run: | echo "Release Version: $RELEASE_VERSION" - name: Update FPTN_VERSION in conanfile.py if: github.event_name == 'release' run: | sed -i '' "s/^FPTN_VERSION = \".*\"/FPTN_VERSION = \"$RELEASE_VERSION\"/" conanfile.py - name: Install dependencies run: | conan install . --output-folder=build --build=missing -o with_gui_client=True --settings build_type=Release -s compiler.cppstd=17 - name: Run cpplint run: | python3 cpplint.py --recursive --filter=-build/c++17,-whitespace/newline,-readability/braces --counting=total ./src/ ./tests/ - name: Run cppcheck run: | cppcheck --error-exitcode=1 --enable=all --check-level=exhaustive --language=c++ --suppress=unusedFunction --inline-suppr --suppress=missingIncludeSystem --suppress=unknownMacro --suppress=unmatchedSuppression -I ./src/fptn-client/ -I ./src/fptn-server/ -I ./src/fptn-passwd/ -I ./src/fptn-client-protocol-lib -I ./src/ ./src/ ./tests/ - name: Run cmake-format run: | cmake-format -i CMakeLists.txt src/fptn-client/CMakeLists.txt src/fptn-passwd/CMakeLists.txt src/fptn-server/CMakeLists.txt src/fptn-protocol-lib/CMakeLists.txt depends/cmake/FetchBase64.cmake depends/cmake/FetchLibTunTap.cmake depends/cmake/FetchWintun.cmake - name: Build run: | cd build cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release cmake --build . - name: Test run: | cd build ctest -C Release - name: Build MacOS pkg run: | cd build cmake --build . --target build-pkg - name: Upload macOS pkg if: github.event_name == 'release' uses: AButler/upload-release-assets@v3.0 with: files: "*.pkg" repo-token: ${{ secrets.CI_TOKEN }} - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: name: MacOsAppleSiliconArtifacts path: | *.pkg if-no-files-found: warn build_windows_AMD64: runs-on: Windows11_AMD64 steps: - name: Checkout code uses: actions/checkout@v3 with: submodules: true - name: Set env shell: powershell if: github.event_name == 'release' run: | $releaseVersion = "${{ github.ref_name }}" echo "RELEASE_VERSION=$releaseVersion" >> $env:GITHUB_ENV - name: Print Release Version shell: powershell if: github.event_name == 'release' run: | Write-Output "Release Version: $env:RELEASE_VERSION" - name: Update FPTN_VERSION in conanfile.py shell: powershell if: github.event_name == 'release' run: | python.exe deploy\windows\conan-replace-version.py conanfile.py "$env:RELEASE_VERSION" - name: Install dependencies shell: powershell run: | conan install . --output-folder=build --build=missing -o with_gui_client=True --settings build_type=Release -s compiler.cppstd=17 - name: Run cpplint run: | python cpplint.py --recursive --filter=-build/c++17,-whitespace/newline,-readability/braces --counting=total ./src/ ./tests/ - name: Run cppcheck shell: powershell run: | cppcheck --error-exitcode=1 --enable=all --check-level=exhaustive --language=c++ --suppress=unusedFunction --inline-suppr --suppress=missingIncludeSystem --suppress=unknownMacro --suppress=unmatchedSuppression -I ./src/fptn-client/ -I ./src/fptn-server/ -I ./src/fptn-passwd/ -I ./src/fptn-client-protocol-lib -I ./src/ ./src/ ./tests/ - name: Run cmake-format shell: powershell run: | cmake-format -i CMakeLists.txt src/fptn-client/CMakeLists.txt src/fptn-passwd/CMakeLists.txt src/fptn-server/CMakeLists.txt src/fptn-protocol-lib/CMakeLists.txt depends/cmake/FetchBase64.cmake depends/cmake/FetchLibTunTap.cmake depends/cmake/FetchWintun.cmake - name: Build shell: powershell run: | cd build cmake .. -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Release cmake --build . --config Release - name: Test shell: powershell run: | cd build ctest -C Release - name: Build Windows installer shell: powershell run: | cd build cmake --build . --config Release --target build-installer - name: Zip the .exe shell: powershell run: | $exePath = Get-ChildItem -Path "." -Filter "FptnClientInstaller-*-windows-x64_x86.exe" | Select-Object -First 1 Compress-Archive -Path $exePath.FullName -DestinationPath "FptnClientInstaller-$env:RELEASE_VERSION-windows-x64_x86.zip" - name: Upload Windows installer if: github.event_name == 'release' uses: AButler/upload-release-assets@v3.0 with: files: "*.zip" repo-token: ${{ secrets.CI_TOKEN }} - name: Upload Build Artifacts uses: actions/upload-artifact@v4 with: name: WindowsArtifacts path: | *.zip if-no-files-found: warn create_multiarch_manifest: needs: [build_ubuntu_x86_64, build_ubuntu_arm64] runs-on: ubuntu-latest steps: - name: Get release version if: github.event_name == 'release' run: | RELEASE_VERSION="${GITHUB_REF#refs/*/}" echo "RELEASE_VERSION=$RELEASE_VERSION" >> $GITHUB_ENV echo "Version: $RELEASE_VERSION" - name: Login to Docker Hub if: github.event_name == 'release' run: | echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - name: Create multi-arch manifests if: github.event_name == 'release' run: | docker pull --quiet $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-amd64 docker pull --quiet $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-arm64 docker manifest rm $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION 2>/dev/null || true docker manifest rm $DOCKER_REGISTRY/$IMAGE_NAME:latest 2>/dev/null || true docker buildx imagetools create \ -t $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION \ $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-amd64 \ $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-arm64 docker buildx imagetools create \ -t "$DOCKER_REGISTRY/$IMAGE_NAME":latest \ $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-amd64 \ $DOCKER_REGISTRY/$IMAGE_NAME:$RELEASE_VERSION-arm64 ================================================ FILE: .gitignore ================================================ .env .cache/ .vsconan/ Testing/ conan_provider.cmake .conan_provider.cmake logs/ *.dmg *.deb *.pkg .vs/ .vscode/ tmp/ keys/ build*/ cmake-build-*/ .idea/ libhv.* macos-build/ linux-build/ linux-build22/ linux-build24/ CMakeUserPresets.json .env # Prerequisites *.d # Compiled Object files *.slo *.lo *.o *.obj # Precompiled Headers *.gch *.pch # Compiled Dynamic libraries *.so *.dylib *.dll # Fortran module files *.mod *.smod # Compiled Static libraries *.lai *.la *.a *.lib # Executables *.exe *.out *.app build/ # General .DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed. **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment. ## 1. Think Before Coding **Don't assume. Don't hide confusion. Surface tradeoffs.** Before implementing: - State your assumptions explicitly. If uncertain, ask. - If multiple interpretations exist, present them - don't pick silently. - If a simpler approach exists, say so. Push back when warranted. - If something is unclear, stop. Name what's confusing. Ask. ## 2. Simplicity First **Minimum code that solves the problem. Nothing speculative.** - No features beyond what was asked. - No abstractions for single-use code. - No "flexibility" or "configurability" that wasn't requested. - No error handling for impossible scenarios. - If you write 200 lines and it could be 50, rewrite it. Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify. ## 3. Surgical Changes **Touch only what you must. Clean up only your own mess.** When editing existing code: - Don't "improve" adjacent code, comments, or formatting. - Don't refactor things that aren't broken. - Match existing style, even if you'd do it differently. - If you notice unrelated dead code, mention it - don't delete it. When your changes create orphans: - Remove imports/variables/functions that YOUR changes made unused. - Don't remove pre-existing dead code unless asked. The test: Every changed line should trace directly to the user's request. ## 4. Goal-Driven Execution **Define success criteria. Loop until verified.** Transform tasks into verifiable goals: - "Add validation" → "Write tests for invalid inputs, then make them pass" - "Fix the bug" → "Write a test that reproduces it, then make it pass" - "Refactor X" → "Ensure tests pass before and after" For multi-step tasks, state a brief plan: ``` 1. [Step] → verify: [check] 2. [Step] → verify: [check] 3. [Step] → verify: [check] ``` Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification. --- **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes. ================================================ FILE: CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.16) project(fptn VERSION "${FPTN_VERSION}" LANGUAGES CXX) # project settings set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (APPLE) set(CMAKE_CXX_STANDARD 20) else () add_definitions(-DBOOST_ASIO_HAS_CO_AWAIT) add_definitions(-DBOOST_ASIO_HAS_CO_SPAWN) add_definitions(-DBOOST_ASIO_HAS_COROUTINES) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") endif () endif () if(MSVC) add_compile_options("/bigobj") add_compile_options("/std:c++20") add_compile_options("/Zc:inline-") add_compile_options("/permissive-") add_compile_options("/Zc:__cplusplus") add_compile_options("/d2SSAOptimizer-") add_compile_definitions(_WIN32_WINNT=0x0A00) # Windows 10 endif() # Global project definitions if (FPTN_BUILD_ONLY_FPTN_LIB) # build fptnlib without pcappp set(FPTN_IP_ADDRESS_WITHOUT_PCAP ON) add_compile_definitions(FPTN_IP_ADDRESS_WITHOUT_PCAP) else() set(FPTN_WITH_LIBIDN2 ON) add_compile_definitions(FPTN_WITH_LIBIDN2) endif() add_compile_definitions(FPTN_VERSION=\"${FPTN_VERSION}\") add_compile_definitions(FPTN_MTU_SIZE=1500) add_compile_definitions(FPTN_DEFAULT_SNI=\"rutube.ru\") add_compile_definitions(FPTN_IP_PACKET_MAX_SIZE=1500) add_compile_definitions(FPTN_ENABLE_PACKET_PADDING=1) add_compile_definitions(FPTN_PROTOBUF_PROTOCOL_VERSION=0x01) # client add_compile_definitions(FPTN_CLIENT_DEFAULT_ADDRESS_IP6=\"fd00::1\") add_compile_definitions(FPTN_CLIENT_DEFAULT_ADDRESS_IP4=\"10.0.0.1\") add_compile_definitions(FPTN_CLIENT_DEFAULT_EXCLUDE_NETWORKS=\"10.0.0.0/8,192.168.0.0/16\") add_compile_definitions(FPTN_CLIENT_DEFAULT_SPLIT_TUNNEL_DOMAINS=\"domain:ru,domain:su,domain:рф,domain:vk.com,domain:yandex.com,domain:userapi.com,domain:yandex.net,domain:clstorage.net\") add_compile_definitions(FPTN_CLIENT_DEFAULT_BLACKLIST_DOMAINS=\"domain:solovev-live.ru,domain:ria.ru,domain:tass.ru,domain:1tv.ru,domain:ntv.ru,domain:rt.com,domain:lenta.ru\") # server add_compile_definitions(FPTN_SERVER_DEFAULT_ADDRESS_IP6=\"fc00:1::1\") add_compile_definitions(FPTN_SERVER_DEFAULT_NET_ADDRESS_IP6=\"fc00:1::\") add_compile_definitions(FPTN_SERVER_DEFAULT_ADDRESS_IP4=\"172.20.0.1\") add_compile_definitions(FPTN_SERVER_DEFAULT_NET_ADDRESS_IP4=\"172.20.0.0\") # github add_compile_definitions(FPTN_GITHUB_USERNAME=\"fptn-project\") add_compile_definitions(FPTN_GITHUB_REPOSITORY=\"fptn\") add_compile_definitions(FPTN_GITHUB_PAGE_LINK=\"https://storage.googleapis.com/fptn.org/index.html\") # Boost add_compile_definitions(BOOST_MPL_CFG_NO_PREPROCESSED_HEADERS) add_compile_definitions(BOOST_IOSTREAMS_USE_BZIP2) add_compile_definitions(BOOST_IOSTREAMS_USE_ZLIB) # Fix boringssl build for windows add_definitions(-DNOMINMAX) add_definitions(-DWIN32_LEAN_AND_MEAN) # Minimize Windows headers and avoid NOMINMAX conflict (needed for BoringSSL) add_definitions(-DNOMINMAX) add_definitions(-DWIN32_LEAN_AND_MEAN) set(FPTN_SERVER_PATH "${CMAKE_CURRENT_SOURCE_DIR}/src/fptn-server") # --- depends --- include(depends/cmake/FetchBase64.cmake) include(depends/cmake/NtpClient.cmake) include(depends/cmake/CamouflageTLS.cmake) if (NOT FPTN_BUILD_ONLY_FPTN_LIB) if (APPLE OR UNIX) include(depends/cmake/FetchLibTunTap.cmake) elseif (WIN32) include(depends/cmake/FetchWintun.cmake) else () message(FATAL_ERROR "Unsupported platform") endif () endif () # --- project --- set(CMAKE_COMPILE_WARNING_AS_ERROR ON) # check all warnings! if (MSVC) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) add_compile_options(/W4 /WX) else () add_compile_options(-Wall -Werror -pedantic) endif () # --- clang-tidy setup --- set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # search clang-tidy find_program( CLANG_TIDY_EXE NAMES clang-tidy-22 clang-tidy-21 clang-tidy-20 clang-tidy-19 clang-tidy-18 clang-tidy-17 clang-tidy-16 clang-tidy-15 clang-tidy) if (CLANG_TIDY_EXE) execute_process( COMMAND ${CLANG_TIDY_EXE} --version OUTPUT_VARIABLE CLANG_TIDY_VERSION_STRING OUTPUT_STRIP_TRAILING_WHITESPACE) message(STATUS "[CLANG-TIDY] clang-tidy version: ${CLANG_TIDY_VERSION_STRING}") if (CLANG_TIDY_VERSION_STRING MATCHES "version ([0-9]+)") set(CLANG_TIDY_VERSION ${CMAKE_MATCH_1}) if (CLANG_TIDY_VERSION GREATER_EQUAL 20) message(STATUS "[CLANG-TIDY] clang-tidy ${CLANG_TIDY_VERSION} accepted, enabling") set(CMAKE_CXX_CLANG_TIDY "${CLANG_TIDY_EXE};-extra-arg=-std=c++20") else () message(WARNING "[CLANG-TIDY] clang-tidy version ${CLANG_TIDY_VERSION} is too old (<15), disabling") endif () else () message(WARNING "[CLANG-TIDY] Could not parse clang-tidy version, disabling") endif () else () message(WARNING "[CLANG-TIDY] clang-tidy not found, skipping clang-tidy checks.") endif () # --- include --- include_directories(src/) # --- build --- add_subdirectory(src/fptn-protocol-lib) if (NOT FPTN_BUILD_ONLY_FPTN_LIB) if (APPLE OR UNIX) add_subdirectory(src/fptn-server) add_subdirectory(src/fptn-passwd) endif () add_subdirectory(src/fptn-client) # --- install --- install(TARGETS fptn-client-cli DESTINATION bin) if (APPLE OR UNIX) install(TARGETS fptn-server DESTINATION bin) install(TARGETS fptn-passwd DESTINATION bin) endif () # --- packaging --- if (CMAKE_SYSTEM_NAME STREQUAL "Linux") # deb add_custom_target( build-deb WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND bash -c "${CMAKE_CURRENT_SOURCE_DIR}/deploy/linux/deb/create-client-cli-deb-package.sh ${CMAKE_BINARY_DIR}/src/fptn-client/fptn-client-cli ${FPTN_VERSION}; ${CMAKE_CURRENT_SOURCE_DIR}/deploy/linux/deb/create-server-deb-package.sh ${CMAKE_BINARY_DIR}/src/fptn-server/fptn-server ${CMAKE_BINARY_DIR}/src/fptn-passwd/fptn-passwd ${FPTN_VERSION}" COMMENT "Building .deb package" VERBATIM) if ("${FPTN_BUILD_WITH_GUI_CLIENT}") add_custom_target( build-deb-gui WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND bash -c "${CMAKE_CURRENT_SOURCE_DIR}/deploy/linux/deb/create-client-gui-deb-package.sh ${CMAKE_BINARY_DIR}/src/fptn-client/fptn-client-gui ${CMAKE_CURRENT_SOURCE_DIR}/deploy/linux/deb/assets/FptnClient512x512.png ${FPTN_VERSION} ${CMAKE_CURRENT_SOURCE_DIR}/deploy/sni" COMMENT "Building .deb package" VERBATIM) endif () elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin") # MacOS if ("${FPTN_BUILD_WITH_GUI_CLIENT}") add_custom_target( build-pkg WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND bash -c "python3 \"${CMAKE_CURRENT_SOURCE_DIR}/deploy/macos/create-pkg.py\" --fptn-client-cli=\"${CMAKE_BINARY_DIR}/src/fptn-client/fptn-client-cli\" --fptn-client-gui=\"${CMAKE_BINARY_DIR}/src/fptn-client/fptn-client-gui\" --version=\"${FPTN_VERSION}\"" COMMENT "Building .dmg package" VERBATIM) endif () elseif (WIN32) if ("${FPTN_BUILD_WITH_GUI_CLIENT}") add_custom_target( build-installer WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${CMAKE_COMMAND} -E echo "Building installer for Windows..." COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/deploy/windows/create-installer.py --wintun-dll=${CMAKE_BINARY_DIR}/wintun/wintun.dll --fptn-client=${CMAKE_BINARY_DIR}/src/fptn-client/Release/fptn-client-gui.exe --fptn-client-cli=${CMAKE_BINARY_DIR}/src/fptn-client/Release/fptn-client-cli.exe --output-folder=${CMAKE_CURRENT_SOURCE_DIR} --version=${FPTN_VERSION} COMMENT "Building .exe installer" VERBATIM) endif () endif () # --- tests --- enable_testing() add_subdirectory(tests) endif () ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 Stas Skokov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

FPTN

Custom VPN technology
[\[English\]](README.md) • [\[Русский\]](README_RU.md) [![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?style=for-the-badge&logo=ubuntu&logoColor=white)](https://github.com/batchar2/fptn/releases) [![Mac OS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)](https://github.com/batchar2/fptn/releases) [![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white)](https://github.com/batchar2/fptn/releases) [![Android](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://github.com/batchar2/fptn/releases) [![Build and Test](https://img.shields.io/github/actions/workflow/status/batchar2/fptn/main.yml?style=for-the-badge&logo=github-actions&logoColor=white&label=Build&labelColor=2088FF)](https://github.com/batchar2/fptn/actions/workflows/main.yml) [![GitHub All Releases](https://img.shields.io/github/downloads/batchar2/fptn/total.svg?style=for-the-badge&logo=github&logoColor=white&label=Downloads&labelColor=181717)](https://github.com/batchar2/fptn/releases)
--- ### Core Features of FPTN FPTN is a VPN technology engineered from the ground up to provide secure, robust, and censorship-resistant connections capable of bypassing network filtering and deep packet inspection (DPI). Project website: [https://storage.googleapis.com/fptn.org/index.html](https://storage.googleapis.com/fptn.org/index.html) Key Technical Features: 1. **L3 Tunnel (Network Layer)** - **IP Packet Tunneling:** Encapsulates and transmits raw IP packets (IPv4/IPv6) over a secure tunnel to the VPN server. - **Split Tunneling:** Provides granular control over routing policies. Users can define rules (based on domains or IP networks) to specify which traffic is routed through the VPN tunnel; all other traffic uses the direct internet connection. - **Server-side NAT:** Implements Network Address Translation (NAT) on the server. Future roadmap includes support for user grouping into virtual LANs (VLANs) for peer-to-peer communication within the VPN. 2. **Traffic Obfuscation and Blocking Evasion** - **Resistance to active Deep Packet Inspection (DPI):** The server can identify FPTN clients during the TLS handshake by analyzing the session_id (which the FPTN client can set using a special time-based method). If the client is not recognized as an FPTN client, the server acts as a transparent proxy and returns legitimate content for the requested domain. - The VPN connection is masqueraded as regular HTTPS traffic (a mode for short-lived HTTPS connections is also under development). - Three implemented methods for bypassing blocks: - **SNI Spoofing:** A fake domain name is set in the TLS ClientHello packet that initiates the connection. Traffic analysis systems observe a legitimate TLS connection, while the traffic is actually routed to the VPN server. - **Obfuscation:** The traffic is disguised as an already established TLS session, hiding the initial TLS handshake and preventing detection by DPI systems. - **Reality Mode with SNI Spoofing:** The client initiates a connection to the VPN server using a spoofed Server Name Indication (SNI), receives a genuine TLS handshake response from the actual (spoofed) website, and then continues data exchange with the VPN server within the same connection. - The desktop client includes an integrated `SNI scanner utility`. 3. Transport Protocol - Uses a proprietary transport protocol based on Protocol Buffers (Protobuf) for data exchange between the client and server. - **Protocol-level padding:** Data packets are padded with random data to randomize traffic patterns and complicate analysis. - The server provides a **REST API** for client authentication and retrieving specific configuration settings. 4. **Advanced Functionality** - Built-in filtering of unwanted traffic (e.g., the BitTorrent protocol). - Per-user bandwidth and traffic control: The server employs a traffic shaper based on the **Leaky Bucket** algorithm, allowing for granular bandwidth policy configuration. - Support for a multi-server architecture with a single master server that stores all user data and configuration. - System monitoring via **Prometheus** and visualization dashboards in **Grafana**. - Ability for users to connect and manage their service via a **Telegram bot**. 5. **Cross-Platform Clients** - A cross-platform core library, **libfptn**, has been developed for use across various operating systems. It implements the FPTN network protocol, connection management, and data transmission mechanisms for the VPN tunnel. - **Desktop Clients**: Windows, macOS, Linux — a minimalist client focused on ease of use. - **Mobile Clients**: Android, iOS (under development). 6. **Simple Token-Based Configuration** - A **Token** is a specially generated configuration file containing all necessary settings for the system. - Enables connection to the VPN without manual configuration: the user simply imports the token into the client application to begin using the service. --- ### Demonstration Download the FPTN client from the [website](http://batchar2.github.io/fptn/) or [GitHub](https://github.com/batchar2/fptn/releases). After downloading, install and launch the client. The client is a compact application whose icon resides in the system tray. Simply click the icon to open the context menu. Application Navigate to the "Settings" menu, where you need to add an access token. Obtain a token by contacting our Telegram bot, Settings Copy the token, click the "Add Token" button, paste it into the form, and save. Settings After this, available servers will appear in the list. Settings Ease of use: Settings You can also easily turn your Raspberry Pi or Orange Pi into a WiFi access point and install the FPTN client on it. In this case, all devices connected to this WiFi network will be able to access the internet, bypassing any restrictions. [Read more here](https://github.com/batchar2/fptn/blob/master/deploy/linux/wifi/README.md) Settings --- ### Installation, Building, and Configuration
Installing and Configuring the FPTN Server Setting up and running your own FPTN server is done via Docker. This ensures easy deployment, convenient updates, and environment isolation. Instructions are available on [DockerHub](https://hub.docker.com/r/fptnvpn/fptn-vpn-server). You can also deploy your own management and monitoring tools: - **Telegram bot** – issuing tokens to users [sysadmin-tools/telegram-bot/README.md](sysadmin-tools/telegram-bot/README.md). - **Grafana + Prometheus** – monitoring server and user status [sysadmin-tools/grafana/README.md](sysadmin-tools/grafana/README.md)
Building the Project from Source 1. Install required dependencies - For [Windows](deploy/windows/README.md) - For [Ubuntu](deploy/linux/deb/README.md) - For [macOS](deploy/macos/README.md) 2. Install Conan (version 2.24.0): ```bash pip install conan==2.24.0 ``` 3. Detect and configure the Conan profile: ```bash conan profile detect --force ``` 4. Install dependencies, build, and install: *(For debugging and development purposes, use Debug instead of Release.)* ```bash conan install . --output-folder=build --build=missing -s compiler.cppstd=17 -o with_gui_client=True --settings build_type=Debug cd build # Linux & macOS only cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug # Windows only cmake .. -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Debug cmake --build . --config Debug ctest ``` 5. Building the Installer *(For debugging and development purposes, use Debug instead of Release.)* - Windows ```bash cmake --build . --config Release --target build-installer ``` - Ubuntu ```bash cmake --build . --config Release --target build-deb-gui ``` - macOS ```bash cmake --build . --target build-pkg ```
Using CLion IDE for Development Run the following command in the project's root folder: ```bash conan install . --output-folder=cmake-build-debug --build=missing -s compiler.cppstd=17 -o with_gui_client=True --settings build_type=Debug ``` Open the project in CLion. After opening, the Open Project Wizard window will appear automatically. In it, you need to add the following CMake parameter: ```bash -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake ```
--- ### About the Project FPTN is developed by a team of volunteers and independent developers. If you wish to support the project, you can donate via [Boosty](https://boosty.to/fptn). Project sponsors have speed limits removed on our servers and (optionally) have their usernames published in FPTN clients. Our Telegram chat for users and developers: [FPTN Project](https://t.me/fptn_project) Join the community and the development team! --- ## Community Tools The following tools are built and maintained by the community to extend or simplify working with FPTN. ### fptn-manager A small external management tool built around FPTN, focused on simplifying deployment and common day-to-day administrative tasks. It is especially useful for users who prefer not to work directly with Docker commands or internal configuration details. It provides: - A Docker-based installer - An interactive CLI for user, password, and token management - Easier initial setup and repeated operations Project repository: https://github.com/FarazFe/fptn-manager ================================================ FILE: README_RU.md ================================================

FPTN

Custom VPN technology
[\[English\]](README.md) • [\[Русский\]](README_RU.md) [![Ubuntu](https://img.shields.io/badge/Ubuntu-E95420?style=for-the-badge&logo=ubuntu&logoColor=white)](https://github.com/batchar2/fptn/releases) [![Mac OS](https://img.shields.io/badge/mac%20os-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)](https://github.com/batchar2/fptn/releases) [![Windows](https://img.shields.io/badge/Windows-0078D6?style=for-the-badge&logo=windows&logoColor=white)](https://github.com/batchar2/fptn/releases) [![Android](https://img.shields.io/badge/Android-3DDC84?style=for-the-badge&logo=android&logoColor=white)](https://github.com/batchar2/fptn/releases) [![Build and Test](https://img.shields.io/github/actions/workflow/status/batchar2/fptn/main.yml?style=for-the-badge&logo=github-actions&logoColor=white&label=Build&labelColor=2088FF)](https://github.com/batchar2/fptn/actions/workflows/main.yml) [![GitHub All Releases](https://img.shields.io/github/downloads/batchar2/fptn/total.svg?style=for-the-badge&logo=github&logoColor=white&label=Downloads&labelColor=181717)](https://github.com/batchar2/fptn/releases)
--- ### Основные возможности FPTN FPTN — это VPN-технология, созданная с нуля для безопасного и устойчивого к блокировкам соединения, позволяющего обходить цензуру и сетевую фильтрацию. Сайт проекта: [https://storage.googleapis.com/fptn.org/index.html](https://storage.googleapis.com/fptn.org/index.html) Основные возможности включают: 1. **L3-туннель (IP-уровень)** - Передача IP-пакетов (IPv4 и IPv6) через VPN-туннель до сервера. - Поддержка **split-tunneling** — возможность направлять через VPN только определённый трафик, а остальной трафик идёт напрямую. Позволяет гибко настраивать политику маршрутизации на основе указания правил для доменов и сетей. - На серверной стороне реализован **NAT**. В дальнейшем планируется поддержка объединения пользователей в группы с созданием виртуальных локальных сетей для совместного взаимодействия. 2. **Маскировка трафика и обход блокировок** - **Устойчивость к активному DPI**: сервер способен идентифицировать клиентов по TLS-handshake, анализируя session_id (значение которого умеет устанавливать FPTN-клиент по специальному методу от времени). Если определяется, что клиент не является FPTN-клиентом, сервер возвращает легитимный контент запрашиваемого домена, выступая в роли прозрачного прокси. - VPN-соединение маскируется под обычный HTTPS-трафик (еще в разработке — режим короткоживущих HTTPS-соединений). - Реализованы три метода обхода блокировок: 1. **Подмена SNI**: в инициирующем соединение TLS-пакете устанавливается поддельный домен. Системы анализа трафика видят легитимное соединение, а на самом деле трафик направляется на VPN-сервер. 2. **Обфускация**: трафик выглядит как уже установленная TLS-сессия, скрывая TLS-handshake и предотвращая детектирование DPI. 3. **Reality Mode + SNI**: клиент инициирует соединение с VPN-сервером с подменой SNI, получает реальный TLS-handshake от настоящего сайта, после чего в том же соединении продолжается обмен данными с VPN-сервером. - В десктопной версии клиента реализован `сканнер SNI`. 3. **Транспортный протокол** - Используется собственный транспортный протокол на основе **Protobuf** для передачи данных между клиентом и сервером. - **Padding на уровне протокола**: пакеты данных дополняются случайными данными для рандомизации трафика и затруднения анализа. - Сервер предоставляет **REST API** для авторизации клиентов и получения специальных настроек. 4. **Специальные возможности** - Встроенная фильтрация нежелательного трафика (например, протокол BitTorrent). - Контроль скорости и трафика каждого пользователя: сервер включает шейпер на основе алгоритма **Leaky Bucket**, что позволяет гибко настраивать политику скорости. - Поддержка многосерверной архитектуры с одним мастер-сервером, где хранится вся информация о пользователях. - Мониторинг работы системы через **Prometheus** и визуализация в **Grafana**. - Возможность подключения пользователей через **Telegram-бота**. 5. **Кроссплатформенные клиенты** - Разработана кроссплатформенная библиотека **`libfptn`**, которая может использоваться на различных операционных системах. Внутри реализованы сетевой протокол FPTN, управление соединением и механизмы передачи данных через VPN-туннель. - **Десктоп:** Windows, macOS, Linux — минималистичный клиент с акцентом на простоту использования. - **Мобильные устройства:** Android, iOS (в разработке). 6. **Простая настройка через токен** - **Токен** — это специально сгенерированный конфигурационный файл, который содержит все необходимые настройки системы. - Позволяет подключаться к VPN без ручной конфигурации и лишних действий: достаточно добавить токен в клиент, чтобы начать работу. --- ### Демонстрация работы *🍏🍎Пользователям MacOS рекомендуется ознакомиться с [руководством по установке для macOS](docs/macos/README.md), так как в macOS присутствуют дополнительные меры безопасности, которые могут потребовать особых действий.* Скачайте клиент FPTN с [веб-сайта](http://batchar2.github.io/fptn/) или [GitHub](https://github.com/batchar2/fptn/releases). После скачивания установите и запустите клиент. Клиент представляет собой компактное приложение, значок которого находится в системном трее. Просто нажмите на значок, чтобы открыть контекстное меню. Приложение Перейдите в меню "Настройки", где необходимо добавить токен доступа. Получите токен, обратившись к нашему Telegram-боту, Настройки Скопируйте токен, нажмите кнопку "Добавить токен", вставьте его в форму и сохраните. Настройки После этого в списке появятся доступные серверы. Настройки Простота использования: Настройки Вы также можете легко превратить свой Raspberry Pi или Orange Pi в точку доступа WiFi и установить на него клиент FPTN. В этом случае все устройства, подключенные к этой WiFi-сети, смогут выходить в интернет, обходя любые ограничения. [Подробнее читайте здесь](https://github.com/batchar2/fptn/blob/master/deploy/linux/wifi/README.md) Настройки --- ### Установка, сборка и настройка
Установка и настройка FPTN сервера Настройка и запуск собственного сервера FPTN выполняются через Docker. Это обеспечивает простое развертывание, удобное обновление и изоляцию окружения. Инструкция доступна в [DockerHub](https://hub.docker.com/r/fptnvpn/fptn-vpn-server). Так же вы можете развернуть собственные инструменты для управления и мониторинга: - **Telegram-бот** — выдача токенов пользователмм [sysadmin-tools/telegram-bot/README.md](sysadmin-tools/telegram-bot/README.md). - **Grafana + Prometheus** — мониторинг состояния серверов и пользователей [sysadmin-tools/grafana/README.md](sysadmin-tools/grafana/README.md)
Сборка проекта из исходников 1. Установите требуемые зависимости - Для [Windows](deploy/windows/README.md) - Для [Ubuntu](deploy/linux/deb/README.md) - Для [macOS](deploy/macos/README.md) 2. Установите Conan (версия 2.24.0): ```bash pip install conan==2.24.0 ``` 3. Определите и настройте профиль Conan: ```bash conan profile detect --force ``` 4. Установите зависимости, выполните сборку и установку: ```bash conan install . --output-folder=build --build=missing -s compiler.cppstd=17 -o with_gui_client=True --settings build_type=Release cd build # Только Linux & macOS cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Debug # Только для Windows cmake .. -G "Visual Studio 17 2022" -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" -DCMAKE_BUILD_TYPE=Debug cmake --build . --config Release ctest ```` 5. Сборка установщика - Windows ```bash cmake --build . --config Release --target build-installer ``` - Ubuntu ```bash cmake --build . --config Release --target build-deb-gui ``` - macOS ```bash cmake --build . --target build-pkg ```
Использование CLion IDE для разработки Выполните следующую команду в корневой папке проекта: ```bash conan install . --output-folder=cmake-build-debug --build=missing -s compiler.cppstd=17 -o with_gui_client=True --settings build_type=Debug ``` Откройте проект в CLion. После открытия автоматически появится окно **Open Project Wizard**. В нём необходимо добавить следующий параметр CMake: ```bash -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake ```
--- ### О проекте FPTN развивается командой волонтёров и независимых разработчиков. Если вы хотите поддержать проект, вы можете оформить донат на [Boosty](https://boosty.to/fptn). Спонсорам проекта снимаем ограничения скорости на наших серверах и (по желанию) публикуем их ники в FPTN-клиентах. Наш Telegram-чат для пользователей и разработчиков [FPTN Project](https://t.me/fptn_project) Присоединяйтесь к сообществу и команде разработчиков! --- ## Инструменты сообщества Следующие инструменты разработаны и поддерживаются сообществом для расширения возможностей или упрощения работы с FPTN. ### fptn-manager Небольшой внешний инструмент управления, построенный вокруг FPTN и ориентированный на упрощение развёртывания и повседневных административных задач. Особенно полезен для пользователей, которые не хотят работать напрямую с Docker-командами или внутренними настройками конфигурации. Возможности: - Установщик на базе Docker - Интерактивный CLI для управления пользователями, паролями и токенами - Упрощённая первичная настройка и повторяющиеся операции Репозиторий проекта: https://github.com/FarazFe/fptn-manager ================================================ FILE: conanfile.py ================================================ import os import subprocess from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake from conan.tools.files import copy # CI will replace this automatically FPTN_VERSION = "0.0.0" class FPTN(ConanFile): name = "fptn" version = FPTN_VERSION requires = ( "argparse/3.2", "boost/1.90.0", "brotli/1.2.0", "cpp-httplib/0.30.0", "fmt/12.1.0", "jwt-cpp/0.7.1", "nlohmann_json/3.12.0", "protobuf/5.29.3", "re2/20251105", "spdlog/1.17.0", "zlib/1.3.1", ) settings = ( "os", "arch", "compiler", "build_type", ) generators = ("CMakeDeps",) options = { "setup": [True, False], "with_gui_client": [True, False], "build_only_fptn_lib": [True, False], } default_options = { # --- program --- "setup": False, "with_gui_client": False, "build_only_fptn_lib": False, # -- depends -- "*:fPIC": True, "*:shared": False, # --- protobuf options --- "protobuf/*:lite": True, "protobuf/*:upb": False, "protobuf/*:with_rtti": False, "protobuf/*:with_zlib": False, "protobuf/*:upb": False, "protobuf/*:debug_suffix": False, # --- boost options --- "boost/*:without_atomic": False, "boost/*:without_system": False, "boost/*:without_process": False, "boost/*:without_exception": False, "boost/*:without_container": False, "boost/*:without_filesystem": False, "boost/*:without_coroutine": False, "boost/*:without_context": False, "boost/*:without_timer": False, "boost/*:without_json": False, "boost/*:without_random": False, "boost/*:without_iostreams": False, "boost/*:without_chrono": False, "boost/*:without_regex": False, "boost/*:without_zlib": False, "boost/*:without_nowide": False, "boost/*:without_locale": False, "boost/*:without_thread": False, "boost/*:without_python": True, "boost/*:without_contract": True, "boost/*:without_fiber": True, "boost/*:without_graph": True, "boost/*:without_graph_parallel": True, "boost/*:without_log": True, "boost/*:without_math": True, "boost/*:without_mpi": True, "boost/*:without_program_options": True, "boost/*:without_serialization": True, "boost/*:without_stacktrace": True, "boost/*:without_test": True, "boost/*:without_url": True, "boost/*:without_type_erasure": True, "boost/*:without_wave": True, # --- Qt --- "qt/*:shared": True, "qt/*:openssl": False, "qt/*:qttools": True, "qt/*:with_harfbuzz": False, "qt/*:with_mysql": False, "qt/*:with_pq": False, "qt/*:with_odbc": False, "qt/*:with_zstd": False, "qt/*:with_brotli": False, "qt/*:with_dbus": False, "qt/*:with_openal": False, "qt/*:with_gstreamer": False, "qt/*:with_pulseaudio": False, # --- prometheuscpp dependency --- "prometheus-cpp/*:with_compression": False, "prometheus-cpp/*:with_push": False, "civetweb/*:with_ssl": False, "civetweb/*:disable_werror": True, # --- freetype --- "freetype/*:with_brotli": False, } exports_sources = ( "CMakeLists.txt", "src/*", "depends/*", "tests/*", ) def requirements(self): self._register_local_recipe("boringssl", "openssl", "boringssl", True, False) if self.options.with_gui_client: self.requires("qt/6.7.3") if self.settings.os != "Windows": self.requires("meson/1.10.0", override=True, force=True) if not self.options.build_only_fptn_lib: self.requires("libidn2/2.3.8") self.requires("prometheus-cpp/1.3.0") # pcap++ does not support iOS and Android. # Since libfptn is built as a detached part of the whole project, we don't use pcap++ in that case. self.requires("pcapplusplus/25.05") def build_requirements(self): self.build_requires("cmake/3.22.0", override=True) self.tool_requires("protobuf/5.29.3") self.test_requires("gtest/1.17.0") if self.settings.os != "Windows": self.build_requires("meson/1.10.0", override=True) def generate(self): tc = CMakeToolchain(self) tc.variables["FPTN_VERSION"] = FPTN_VERSION if self.options.with_gui_client: tc.variables["FPTN_BUILD_WITH_GUI_CLIENT"] = "True" if self.options.build_only_fptn_lib: tc.variables["FPTN_BUILD_ONLY_FPTN_LIB"] = "True" # setup protobuf compiler protobuf_build = self.dependencies.build["protobuf"] protoc_path = os.path.join(protobuf_build.package_folder, "bin", "protoc") tc.cache_variables["Protobuf_PROTOC_EXECUTABLE"] = protoc_path tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def package(self): if self.options.build_only_fptn_lib: copy( self, "*.h", src=os.path.join(self.source_folder, "src", "fptn-protocol-lib"), dst=os.path.join(self.package_folder, "include", "fptn"), ) copy( self, "*.h", src=os.path.join(self.source_folder, "src", "common"), dst=os.path.join(self.package_folder, "include", "fptn", "common"), ) copy( self, "*.h", src=os.path.join(self.build_folder, "src", "fptn-protocol-lib", "protobuf"), dst=os.path.join(self.package_folder, "include", "fptn", "protobuf"), ) # copy lib copy( self, "*.a", src=os.path.join(self.build_folder, "src", "fptn-protocol-lib"), dst=os.path.join(self.package_folder, "lib"), ) copy( self, "*.lib", src=os.path.join(self.build_folder, "src", "fptn-protocol-lib"), dst=os.path.join(self.package_folder, "lib"), ) ntp_client_build_include = os.path.join(self.build_folder, "_deps", "ntp_client-src", "include") # copy NTP depends if os.path.exists(ntp_client_build_include): copy( self, "*.h", src=ntp_client_build_include, dst=os.path.join(self.package_folder, "include", "ntp_client"), ) ntp_client_lib_src = os.path.join(self.build_folder, "_deps", "ntp_client-build") if os.path.exists(ntp_client_lib_src): copy( self, "*.a", src=ntp_client_lib_src, dst=os.path.join(self.package_folder, "lib"), ) copy( self, "*.lib", src=ntp_client_lib_src, dst=os.path.join(self.package_folder, "lib"), ) # copy camouflage-tls depends camouflage_tls_build_include = os.path.join(self.build_folder, "_deps", "camouflagetls-src", "include") if os.path.exists(camouflage_tls_build_include): copy( self, "*.h", src=camouflage_tls_build_include, dst=os.path.join(self.package_folder, "include", "camouflage"), ) camouflage_tls_lib_src = os.path.join(self.build_folder, "_deps", "camouflagetls-build") if os.path.exists(camouflage_tls_lib_src): copy( self, "*.a", src=camouflage_tls_lib_src, dst=os.path.join(self.package_folder, "lib"), ) copy( self, "*.lib", src=camouflage_tls_lib_src, dst=os.path.join(self.package_folder, "lib"), ) def package_info(self): if self.options.build_only_fptn_lib: self.cpp_info.libs = [ "fptn-protocol-lib_static", "ntp_client", ] self.cpp_info.includedirs = ["include"] self.cpp_info.libdirs = ["lib"] self.cpp_info.set_property("cmake_file_name", "fptn") self.cpp_info.set_property("cmake_target_name", "fptn::fptn") self.cpp_info.set_property("cmake_find_mode", "both") # Add depends self.cpp_info.requires = [ "argparse::argparse", "cpp-httplib::cpp-httplib", "boost::boost", "fmt::fmt", "jwt-cpp::jwt-cpp", "nlohmann_json::nlohmann_json", "protobuf::protobuf", "spdlog::spdlog", "zlib::zlib", "re2::re2", "brotli::brotli", ] if self.settings.os == "iOS": self.cpp_info.frameworks = ["Security", "CFNetwork", "SystemConfiguration"] self.cpp_info.system_libs = ["resolv"] def config_options(self): if self.settings.os == "Windows": self.options.rm_safe("fPIC") if self.settings.os in ["iOS", "Android"] or self.options.build_only_fptn_lib: self.options["boost"].without_process = True def export(self): copy(self, f"*", src=self.recipe_folder, dst=self.export_folder) def _register_local_recipe(self, recipe, name, version, override=False, force=False): script_dir = os.path.dirname(os.path.abspath(__file__)) recipe_rel_path = os.path.join(script_dir, ".conan", "recipes", recipe) subprocess.run( [ "conan", "export", recipe_rel_path, f"--name={name}", f"--version={version}", "--user=local", "--channel=local", ], check=True, ) self.requires(f"{name}/{version}@local/local", override=override, force=force) ================================================ FILE: cpplint.py ================================================ #!/usr/bin/env python # # Copyright (c) 2009 Google Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are # met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above # copyright notice, this list of conditions and the following disclaimer # in the documentation and/or other materials provided with the # distribution. # * Neither the name of Google Inc. nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """Does google-lint on c++ files. The goal of this script is to identify places in the code that *may* be in non-compliance with google style. It does not attempt to fix up these problems -- the point is to educate. It does also not attempt to find all problems, or to ensure that everything it does find is legitimately a problem. In particular, we can get very confused by /* and // inside strings! We do a small hack, which is to ignore //'s with "'s after them on the same line, but it is far from perfect (in either direction). """ from __future__ import annotations # PEP 604 not in 3.9 import codecs import collections import copy import getopt import glob import itertools import math # for log import os import re import string import sys import sysconfig import unicodedata import xml.etree.ElementTree # if empty, use defaults _valid_extensions: set[str] = set() __VERSION__ = "2.0.2" _USAGE = """ Syntax: cpplint.py [--verbose=#] [--output=emacs|eclipse|vs7|junit|sed|gsed] [--filter=-x,+y,...] [--counting=total|toplevel|detailed] [--root=subdir] [--repository=path] [--linelength=digits] [--headers=x,y,...] [--recursive] [--exclude=path] [--extensions=hpp,cpp,...] [--includeorder=default|standardcfirst] [--config=filename] [--quiet] [--version] [file] ... Style checker for C/C++ source files. This is a fork of the Google style checker with minor extensions. The style guidelines this tries to follow are those in https://google.github.io/styleguide/cppguide.html Every problem is given a confidence score from 1-5, with 5 meaning we are certain of the problem, and 1 meaning it could be a legitimate construct. This will miss some errors, and is not a substitute for a code review. To suppress false-positive errors of certain categories, add a 'NOLINT(category[, category...])' comment to the line. NOLINT or NOLINT(*) suppresses errors of all categories on that line. To suppress categories on the next line use NOLINTNEXTLINE instead of NOLINT. To suppress errors in a block of code 'NOLINTBEGIN(category[, category...])' comment to a line at the start of the block and to end the block add a comment with 'NOLINTEND'. NOLINT blocks are inclusive so any statements on the same line as a BEGIN or END will have the error suppression applied. The files passed in will be linted; at least one file must be provided. Default linted extensions are %s. Other file types will be ignored. Change the extensions with the --extensions flag. Flags: output=emacs|eclipse|vs7|junit|sed|gsed By default, the output is formatted to ease emacs parsing. Visual Studio compatible output (vs7) may also be used. Further support exists for eclipse (eclipse), and JUnit (junit). XML parsers such as those used in Jenkins and Bamboo may also be used. The sed format outputs sed commands that should fix some of the errors. Note that this requires gnu sed. If that is installed as gsed on your routing (common e.g. on macOS with homebrew) you can use the gsed output format. Sed commands are written to stdout, not stderr, so you should be able to pipe output straight to a shell to run the fixes. verbose=# Specify a number 0-5 to restrict errors to certain verbosity levels. Errors with lower verbosity levels have lower confidence and are more likely to be false positives. quiet Don't print anything if no errors are found. filter=-x,+y,... Specify a comma-separated list of category-filters to apply: only error messages whose category names pass the filters will be printed. (Category names are printed with the message and look like "[whitespace/indent]".) Filters are evaluated left to right. "-FOO" means "do not print categories that start with FOO". "+FOO" means "do print categories that start with FOO". Examples: --filter=-whitespace,+whitespace/braces --filter=-whitespace,-runtime/printf,+runtime/printf_format --filter=-,+build/include_what_you_use To see a list of all the categories used in cpplint, pass no arg: --filter= Filters can directly be limited to files and also line numbers. The syntax is category:file:line , where line is optional. The filter limitation works for both + and - and can be combined with ordinary filters: Examples: --filter=-whitespace:foo.h,+whitespace/braces:foo.h --filter=-whitespace,-runtime/printf:foo.h:14,+runtime/printf_format:foo.h --filter=-,+build/include_what_you_use:foo.h:321 counting=total|toplevel|detailed The total number of errors found is always printed. If 'toplevel' is provided, then the count of errors in each of the top-level categories like 'build' and 'whitespace' will also be printed. If 'detailed' is provided, then a count is provided for each category like 'legal/copyright'. repository=path The top level directory of the repository, used to derive the header guard CPP variable. By default, this is determined by searching for a path that contains .git, .hg, or .svn. When this flag is specified, the given path is used instead. This option allows the header guard CPP variable to remain consistent even if members of a team have different repository root directories (such as when checking out a subdirectory with SVN). In addition, users of non-mainstream version control systems can use this flag to ensure readable header guard CPP variables. Examples: Assuming that Alice checks out ProjectName and Bob checks out ProjectName/trunk and trunk contains src/chrome/ui/browser.h, then with no --repository flag, the header guard CPP variable will be: Alice => TRUNK_SRC_CHROME_BROWSER_UI_BROWSER_H_ Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_ If Alice uses the --repository=trunk flag and Bob omits the flag or uses --repository=. then the header guard CPP variable will be: Alice => SRC_CHROME_BROWSER_UI_BROWSER_H_ Bob => SRC_CHROME_BROWSER_UI_BROWSER_H_ root=subdir The root directory used for deriving header guard CPP variable. This directory is relative to the top level directory of the repository which by default is determined by searching for a directory that contains .git, .hg, or .svn but can also be controlled with the --repository flag. If the specified directory does not exist, this flag is ignored. Examples: Assuming that src is the top level directory of the repository (and cwd=top/src), the header guard CPP variables for src/chrome/browser/ui/browser.h are: No flag => CHROME_BROWSER_UI_BROWSER_H_ --root=chrome => BROWSER_UI_BROWSER_H_ --root=chrome/browser => UI_BROWSER_H_ --root=.. => SRC_CHROME_BROWSER_UI_BROWSER_H_ linelength=digits This is the allowed line length for the project. The default value is 80 characters. Examples: --linelength=120 recursive Search for files to lint recursively. Each directory given in the list of files to be linted is replaced by all files that descend from that directory. Files with extensions not in the valid extensions list are excluded. exclude=path Exclude the given path from the list of files to be linted. Relative paths are evaluated relative to the current directory and shell globbing is performed. This flag can be provided multiple times to exclude multiple files. Examples: --exclude=one.cc --exclude=src/*.cc --exclude=src/*.cc --exclude=test/*.cc extensions=extension,extension,... The allowed file extensions that cpplint will check Examples: --extensions=%s includeorder=default|standardcfirst For the build/include_order rule, the default is to blindly assume angle bracket includes with file extension are c-routing-headers (default), even knowing this will have false classifications. The default is established at google. standardcfirst means to instead use an allow-list of known c headers and treat all others as separate group of "other routing headers". The C headers included are those of the C-standard lib and closely related ones. config=filename Search for config files with the specified name instead of CPPLINT.cfg headers=x,y,... The header extensions that cpplint will treat as .h in checks. Values are automatically added to --extensions list. (by default, only files with extensions %s will be assumed to be headers) Examples: --headers=%s --headers=hpp,hxx --headers=hpp cpplint.py supports per-directory configurations specified in CPPLINT.cfg files. CPPLINT.cfg file can contain a number of key=value pairs. Currently the following options are supported: set noparent filter=+filter1,-filter2,... exclude_files=regex linelength=80 root=subdir headers=x,y,... "set noparent" option prevents cpplint from traversing directory tree upwards looking for more .cfg files in parent directories. This option is usually placed in the top-level project directory. The "filter" option is similar in function to --filter flag. It specifies message filters in addition to the |_DEFAULT_FILTERS| and those specified through --filter command-line flag. "exclude_files" allows to specify a regular expression to be matched against a file name. If the expression matches, the file is skipped and not run through the linter. "linelength" allows to specify the allowed line length for the project. The "root" option is similar in function to the --root flag (see example above). Paths are relative to the directory of the CPPLINT.cfg. The "headers" option is similar in function to the --headers flag (see example above). CPPLINT.cfg has an effect on files in the same directory and all sub-directories, unless overridden by a nested configuration file. Example file: filter=-build/include_order,+build/include_alpha exclude_files=.*\\.cc The above example disables build/include_order warning and enables build/include_alpha as well as excludes all .cc from being processed by linter, in the current directory (where the .cfg file is located) and all sub-directories. """ # We categorize each error message we print. Here are the categories. # We want an explicit list so we can list them all in cpplint --filter=. # If you add a new error message with a new category, add it to the list # here! cpplint_unittest.py should tell you if you forget to do this. _ERROR_CATEGORIES = [ "build/c++11", "build/c++17", "build/deprecated", "build/endif_comment", "build/explicit_make_pair", "build/forward_decl", "build/header_guard", "build/include", "build/include_subdir", "build/include_alpha", "build/include_order", "build/include_what_you_use", "build/namespaces_headers", "build/namespaces_literals", "build/namespaces", "build/printf_format", "build/storage_class", "legal/copyright", "readability/alt_tokens", "readability/braces", "readability/casting", "readability/check", "readability/constructors", "readability/fn_size", "readability/inheritance", "readability/multiline_comment", "readability/multiline_string", "readability/namespace", "readability/nolint", "readability/nul", "readability/todo", "readability/utf8", "runtime/arrays", "runtime/casting", "runtime/explicit", "runtime/int", "runtime/init", "runtime/invalid_increment", "runtime/member_string_references", "runtime/memset", "runtime/operator", "runtime/printf", "runtime/printf_format", "runtime/references", "runtime/string", "runtime/threadsafe_fn", "runtime/vlog", "whitespace/blank_line", "whitespace/braces", "whitespace/comma", "whitespace/comments", "whitespace/empty_conditional_body", "whitespace/empty_if_body", "whitespace/empty_loop_body", "whitespace/end_of_line", "whitespace/ending_newline", "whitespace/forcolon", "whitespace/indent", "whitespace/indent_namespace", "whitespace/line_length", "whitespace/newline", "whitespace/operators", "whitespace/parens", "whitespace/semicolon", "whitespace/tab", "whitespace/todo", ] # keywords to use with --outputs which generate stdout for machine processing _MACHINE_OUTPUTS = ["junit", "sed", "gsed"] # These error categories are no longer enforced by cpplint, but for backwards- # compatibility they may still appear in NOLINT comments. _LEGACY_ERROR_CATEGORIES = [ "build/class", "readability/streams", "readability/function", ] # These prefixes for categories should be ignored since they relate to other # tools which also use the NOLINT syntax, e.g. clang-tidy. _OTHER_NOLINT_CATEGORY_PREFIXES = [ "clang-analyzer-", "abseil-", "altera-", "android-", "boost-", "bugprone-", "cert-", "concurrency-", "cppcoreguidelines-", "darwin-", "fuchsia-", "google-", "hicpp-", "linuxkernel-", "llvm-", "llvmlibc-", "misc-", "modernize-", "mpi-", "objc-", "openmp-", "performance-", "portability-", "readability-", "zircon-", ] # The default state of the category filter. This is overridden by the --filter= # flag. By default all errors are on, so only add here categories that should be # off by default (i.e., categories that must be enabled by the --filter= flags). # All entries here should start with a '-' or '+', as in the --filter= flag. _DEFAULT_FILTERS = [ "-build/include_alpha", "-readability/fn_size", "-runtime/references", ] # The default list of categories suppressed for C (not C++) files. _DEFAULT_C_SUPPRESSED_CATEGORIES = [ "readability/casting", ] # The default list of categories suppressed for Linux Kernel files. _DEFAULT_KERNEL_SUPPRESSED_CATEGORIES = [ "whitespace/tab", ] # We used to check for high-bit characters, but after much discussion we # decided those were OK, as long as they were in UTF-8 and didn't represent # hard-coded international strings, which belong in a separate i18n file. # C++ headers _CPP_HEADERS = frozenset( [ # Legacy "algobase.h", "algo.h", "alloc.h", "builtinbuf.h", "bvector.h", # 'complex.h', collides with System C header "complex.h" since C11 "defalloc.h", "deque.h", "editbuf.h", "fstream.h", "function.h", "hash_map", "hash_map.h", "hash_set", "hash_set.h", "hashtable.h", "heap.h", "indstream.h", "iomanip.h", "iostream.h", "istream.h", "iterator.h", "list.h", "map.h", "multimap.h", "multiset.h", "ostream.h", "pair.h", "parsestream.h", "pfstream.h", "procbuf.h", "pthread_alloc", "pthread_alloc.h", "rope", "rope.h", "ropeimpl.h", "set.h", "slist", "slist.h", "stack.h", "stdiostream.h", "stl_alloc.h", "stl_relops.h", "streambuf.h", "stream.h", "strfile.h", "strstream.h", "tempbuf.h", "tree.h", "type_traits.h", "vector.h", # C++ library headers "algorithm", "array", "atomic", "bitset", "chrono", "codecvt", "complex", "condition_variable", "deque", "exception", "forward_list", "fstream", "functional", "future", "initializer_list", "iomanip", "ios", "iosfwd", "iostream", "istream", "iterator", "limits", "list", "locale", "map", "memory", "mutex", "new", "numeric", "ostream", "queue", "random", "ratio", "regex", "scoped_allocator", "set", "sstream", "stack", "stdexcept", "streambuf", "string", "strstream", "system_error", "thread", "tuple", "typeindex", "typeinfo", "type_traits", "unordered_map", "unordered_set", "utility", "valarray", "vector", # C++14 headers "shared_mutex", # C++17 headers "any", "charconv", "codecvt", "execution", "filesystem", "memory_resource", "optional", "string_view", "variant", # C++20 headers "barrier", "bit", "compare", "concepts", "coroutine", "format", "latch", "numbers", "ranges", "semaphore", "source_location", "span", "stop_token", "syncstream", "version", # C++23 headers "expected", "flat_map", "flat_set", "generator", "mdspan", "print", "spanstream", "stacktrace", "stdfloat", # C++ headers for C library facilities "cassert", "ccomplex", "cctype", "cerrno", "cfenv", "cfloat", "cinttypes", "ciso646", "climits", "clocale", "cmath", "csetjmp", "csignal", "cstdalign", "cstdarg", "cstdbool", "cstddef", "cstdint", "cstdio", "cstdlib", "cstring", "ctgmath", "ctime", "cuchar", "cwchar", "cwctype", ] ) # C headers _C_HEADERS = frozenset( [ # System C headers "assert.h", "complex.h", "ctype.h", "errno.h", "fenv.h", "float.h", "inttypes.h", "iso646.h", "limits.h", "locale.h", "math.h", "setjmp.h", "signal.h", "stdalign.h", "stdarg.h", "stdatomic.h", "stdbool.h", "stddef.h", "stdint.h", "stdio.h", "stdlib.h", "stdnoreturn.h", "string.h", "tgmath.h", "threads.h", "time.h", "uchar.h", "wchar.h", "wctype.h", # C23 headers "stdbit.h", "stdckdint.h", # additional POSIX C headers "aio.h", "arpa/inet.h", "cpio.h", "dirent.h", "dlfcn.h", "fcntl.h", "fmtmsg.h", "fnmatch.h", "ftw.h", "glob.h", "grp.h", "iconv.h", "langinfo.h", "libgen.h", "monetary.h", "mqueue.h", "ndbm.h", "net/if.h", "netdb.h", "netinet/in.h", "netinet/tcp.h", "nl_types.h", "poll.h", "pthread.h", "pwd.h", "regex.h", "sched.h", "search.h", "semaphore.h", "setjmp.h", "signal.h", "spawn.h", "strings.h", "stropts.h", "syslog.h", "tar.h", "termios.h", "trace.h", "ulimit.h", "unistd.h", "utime.h", "utmpx.h", "wordexp.h", # additional GNUlib headers "a.out.h", "aliases.h", "alloca.h", "ar.h", "argp.h", "argz.h", "byteswap.h", "crypt.h", "endian.h", "envz.h", "err.h", "error.h", "execinfo.h", "fpu_control.h", "fstab.h", "fts.h", "getopt.h", "gshadow.h", "ieee754.h", "ifaddrs.h", "libintl.h", "mcheck.h", "mntent.h", "obstack.h", "paths.h", "printf.h", "pty.h", "resolv.h", "shadow.h", "sysexits.h", "ttyent.h", # Additional linux glibc headers "dlfcn.h", "elf.h", "features.h", "gconv.h", "gnu-versions.h", "lastlog.h", "libio.h", "link.h", "malloc.h", "memory.h", "netash/ash.h", "netatalk/at.h", "netax25/ax25.h", "neteconet/ec.h", "netipx/ipx.h", "netiucv/iucv.h", "netpacket/packet.h", "netrom/netrom.h", "netrose/rose.h", "nfs/nfs.h", "nl_types.h", "nss.h", "re_comp.h", "regexp.h", "sched.h", "sgtty.h", "stab.h", "stdc-predef.h", "stdio_ext.h", "syscall.h", "termio.h", "thread_db.h", "ucontext.h", "ustat.h", "utmp.h", "values.h", "wait.h", "xlocale.h", # Hardware specific headers "arm_neon.h", "emmintrin.h", "xmmintin.h", ] ) # Folders of C libraries so commonly used in C++, # that they have parity with standard C libraries. C_STANDARD_HEADER_FOLDERS = frozenset( [ # standard C library "sys", # glibc for linux "arpa", "asm-generic", "bits", "gnu", "net", "netinet", "protocols", "rpc", "rpcsvc", "scsi", # linux kernel header "drm", "linux", "misc", "mtd", "rdma", "sound", "video", "xen", ] ) # Type names _TYPES = re.compile( r"^(?:" # [dcl.type.simple] r"(char(16_t|32_t)?)|wchar_t|" r"bool|short|int|long|signed|unsigned|float|double|" # [support.types] r"(ptrdiff_t|size_t|max_align_t|nullptr_t)|" # [cstdint.syn] r"(u?int(_fast|_least)?(8|16|32|64)_t)|" r"(u?int(max|ptr)_t)|" r")$" ) # These headers are excluded from [build/include] and [build/include_order] # checks: # - Anything not following google file name conventions (containing an # uppercase character, such as Python.h or nsStringAPI.h, for example). # - Lua headers. _THIRD_PARTY_HEADERS_PATTERN = re.compile(r"^(?:[^/]*[A-Z][^/]*\.h|lua\.h|lauxlib\.h|lualib\.h)$") # Pattern for matching FileInfo.BaseName() against test file name _test_suffixes = ["_test", "_regtest", "_unittest"] _TEST_FILE_SUFFIX = "(" + "|".join(_test_suffixes) + r")$" # Pattern that matches only complete whitespace, possibly across multiple lines. _EMPTY_CONDITIONAL_BODY_PATTERN = re.compile(r"^\s*$", re.DOTALL) # Assertion macros. These are defined in base/logging.h and # testing/base/public/gunit.h. _CHECK_MACROS = [ "DCHECK", "CHECK", "EXPECT_TRUE", "ASSERT_TRUE", "EXPECT_FALSE", "ASSERT_FALSE", ] # Replacement macros for CHECK/DCHECK/EXPECT_TRUE/EXPECT_FALSE _CHECK_REPLACEMENT: dict[str, dict[str, str]] = {macro_var: {} for macro_var in _CHECK_MACROS} for op, replacement in [ ("==", "EQ"), ("!=", "NE"), (">=", "GE"), (">", "GT"), ("<=", "LE"), ("<", "LT"), ]: _CHECK_REPLACEMENT["DCHECK"][op] = f"DCHECK_{replacement}" _CHECK_REPLACEMENT["CHECK"][op] = f"CHECK_{replacement}" _CHECK_REPLACEMENT["EXPECT_TRUE"][op] = f"EXPECT_{replacement}" _CHECK_REPLACEMENT["ASSERT_TRUE"][op] = f"ASSERT_{replacement}" for op, inv_replacement in [ ("==", "NE"), ("!=", "EQ"), (">=", "LT"), (">", "LE"), ("<=", "GT"), ("<", "GE"), ]: _CHECK_REPLACEMENT["EXPECT_FALSE"][op] = f"EXPECT_{inv_replacement}" _CHECK_REPLACEMENT["ASSERT_FALSE"][op] = f"ASSERT_{inv_replacement}" # Alternative tokens and their replacements. For full list, see section 2.5 # Alternative tokens [lex.digraph] in the C++ standard. # # Digraphs (such as '%:') are not included here since it's a mess to # match those on a word boundary. _ALT_TOKEN_REPLACEMENT = { "and": "&&", "bitor": "|", "or": "||", "xor": "^", "compl": "~", "bitand": "&", "and_eq": "&=", "or_eq": "|=", "xor_eq": "^=", "not": "!", "not_eq": "!=", } # Compile regular expression that matches all the above keywords. The "[ =()]" # bit is meant to avoid matching these keywords outside of boolean expressions. # # False positives include C-style multi-line comments and multi-line strings # but those have always been troublesome for cpplint. _ALT_TOKEN_REPLACEMENT_PATTERN = re.compile( r"([ =()])(" + ("|".join(_ALT_TOKEN_REPLACEMENT.keys())) + r")([ (]|$)" ) # These constants define types of headers for use with # _IncludeState.CheckNextIncludeOrder(). _C_SYS_HEADER = 1 _CPP_SYS_HEADER = 2 _OTHER_SYS_HEADER = 3 _LIKELY_MY_HEADER = 4 _POSSIBLE_MY_HEADER = 5 _OTHER_HEADER = 6 # These constants define the current inline assembly state _NO_ASM = 0 # Outside of inline assembly block _INSIDE_ASM = 1 # Inside inline assembly block _END_ASM = 2 # Last line of inline assembly block _BLOCK_ASM = 3 # The whole block is an inline assembly block # Match start of assembly blocks _MATCH_ASM = re.compile( r"^\s*(?:asm|_asm|__asm|__asm__)" r"(?:\s+(volatile|__volatile__))?" r"\s*[{(]" ) # Match strings that indicate we're working on a C (not C++) file. _SEARCH_C_FILE = re.compile( r"\b(?:LINT_C_FILE|" r"vim?:\s*.*(\s*|:)filetype=c(\s*|:|$))" ) # Match string that indicates we're working on a Linux Kernel file. _SEARCH_KERNEL_FILE = re.compile(r"\b(?:LINT_KERNEL_FILE)") # Commands for sed to fix the problem _SED_FIXUPS = { "Remove spaces around =": r"s/ = /=/", "Remove spaces around !=": r"s/ != /!=/", "Remove space before ( in if (": r"s/if (/if(/", "Remove space before ( in for (": r"s/for (/for(/", "Remove space before ( in while (": r"s/while (/while(/", "Remove space before ( in switch (": r"s/switch (/switch(/", "Should have a space between // and comment": r"s/\/\//\/\/ /", "Missing space before {": r"s/\([^ ]\){/\1 {/", "Tab found, replace by spaces": r"s/\t/ /g", "Line ends in whitespace. Consider deleting these extra spaces.": r"s/\s*$//", "You don't need a ; after a }": r"s/};/}/", "Missing space after ,": r"s/,\([^ ]\)/, \1/g", } # The root directory used for deriving header guard CPP variable. # This is set by --root flag. _root = None _root_debug = False # The top level repository directory. If set, _root is calculated relative to # this directory instead of the directory containing version control artifacts. # This is set by the --repository flag. _repository = None # Files to exclude from linting. This is set by the --exclude flag. _excludes = None # Whether to suppress all PrintInfo messages, UNRELATED to --quiet flag _quiet = False # The allowed line length of files. # This is set by --linelength flag. _line_length = 80 # This allows to use different include order rule than default _include_order = "default" # This allows different config files to be used _config_filename = "CPPLINT.cfg" # Treat all headers starting with 'h' equally: .h, .hpp, .hxx etc. # This is set by --headers flag. _hpp_headers: set[str] = set() class ErrorSuppressions: """Class to track all error suppressions for cpplint""" class LineRange: """Class to represent a range of line numbers for which an error is suppressed""" def __init__(self, begin, end): self.begin = begin self.end = end def __str__(self): return f"[{self.begin}-{self.end}]" def __contains__(self, obj): return self.begin <= obj <= self.end def ContainsRange(self, other): return self.begin <= other.begin and self.end >= other.end def __init__(self): self._suppressions = collections.defaultdict(list) self._open_block_suppression = None def _AddSuppression(self, category, line_range): suppressed = self._suppressions[category] if not (suppressed and suppressed[-1].ContainsRange(line_range)): suppressed.append(line_range) def GetOpenBlockStart(self): """:return: The start of the current open block or `-1` if there is not an open block""" return self._open_block_suppression.begin if self._open_block_suppression else -1 def AddGlobalSuppression(self, category): """Add a suppression for `category` which is suppressed for the whole file""" self._AddSuppression(category, self.LineRange(0, math.inf)) def AddLineSuppression(self, category, linenum): """Add a suppression for `category` which is suppressed only on `linenum`""" self._AddSuppression(category, self.LineRange(linenum, linenum)) def StartBlockSuppression(self, category, linenum): """Start a suppression block for `category` on `linenum`. inclusive""" if self._open_block_suppression is None: self._open_block_suppression = self.LineRange(linenum, math.inf) self._AddSuppression(category, self._open_block_suppression) def EndBlockSuppression(self, linenum): """End the current block suppression on `linenum`. inclusive""" if self._open_block_suppression: self._open_block_suppression.end = linenum self._open_block_suppression = None def IsSuppressed(self, category, linenum): """:return: `True` if `category` is suppressed for `linenum`""" suppressed = self._suppressions[category] + self._suppressions[None] return any(linenum in lr for lr in suppressed) def HasOpenBlock(self): """:return: `True` if a block suppression was started but not ended""" return self._open_block_suppression is not None def Clear(self): """Clear all current error suppressions""" self._suppressions.clear() self._open_block_suppression = None # {str, set(int)}: a map from error categories to sets of linenumbers # on which those errors are expected and should be suppressed. _error_suppressions = ErrorSuppressions() def ProcessHppHeadersOption(val): global _hpp_headers try: _hpp_headers = {ext.strip() for ext in val.split(",")} except ValueError: PrintUsage("Header extensions must be comma separated list.") def ProcessIncludeOrderOption(val): if val is None or val == "default": pass elif val == "standardcfirst": global _include_order _include_order = val else: PrintUsage("Invalid includeorder value %s. Expected default|standardcfirst") def IsHeaderExtension(file_extension): return file_extension in GetHeaderExtensions() def GetHeaderExtensions(): if _hpp_headers: return _hpp_headers if _valid_extensions: return {h for h in _valid_extensions if "h" in h} return {"h", "hh", "hpp", "hxx", "h++", "cuh"} # The allowed extensions for file names # This is set by --extensions flag def GetAllExtensions(): return GetHeaderExtensions().union(_valid_extensions or {"c", "cc", "cpp", "cxx", "c++", "cu"}) def ProcessExtensionsOption(val): global _valid_extensions try: extensions = [ext.strip() for ext in val.split(",")] _valid_extensions = set(extensions) except ValueError: PrintUsage( "Extensions should be a comma-separated list of values;" "for example: extensions=hpp,cpp\n" f'This could not be parsed: "{val}"' ) def GetNonHeaderExtensions(): return GetAllExtensions().difference(GetHeaderExtensions()) def ParseNolintSuppressions(filename, raw_line, linenum, error): """Updates the global list of line error-suppressions. Parses any NOLINT comments on the current line, updating the global error_suppressions store. Reports an error if the NOLINT comment was malformed. Args: filename: str, the name of the input file. raw_line: str, the line of input text, with comments. linenum: int, the number of the current line. error: function, an error handler. """ if matched := re.search(r"\bNOLINT(NEXTLINE|BEGIN|END)?\b(\([^)]+\))?", raw_line): no_lint_type = matched.group(1) if no_lint_type == "NEXTLINE": def ProcessCategory(category): _error_suppressions.AddLineSuppression(category, linenum + 1) elif no_lint_type == "BEGIN": if _error_suppressions.HasOpenBlock(): error( filename, linenum, "readability/nolint", 5, ( "NONLINT block already defined on line " f"{_error_suppressions.GetOpenBlockStart()}" ), ) def ProcessCategory(category): _error_suppressions.StartBlockSuppression(category, linenum) elif no_lint_type == "END": if not _error_suppressions.HasOpenBlock(): error(filename, linenum, "readability/nolint", 5, "Not in a NOLINT block") def ProcessCategory(category): if category is not None: error( filename, linenum, "readability/nolint", 5, f"NOLINT categories not supported in block END: {category}", ) _error_suppressions.EndBlockSuppression(linenum) else: def ProcessCategory(category): _error_suppressions.AddLineSuppression(category, linenum) categories = matched.group(2) if categories in (None, "(*)"): # => "suppress all" ProcessCategory(None) elif categories.startswith("(") and categories.endswith(")"): for category in {c.strip() for c in categories[1:-1].split(",")}: if category in _ERROR_CATEGORIES: ProcessCategory(category) elif any(c for c in _OTHER_NOLINT_CATEGORY_PREFIXES if category.startswith(c)): # Ignore any categories from other tools. pass elif category not in _LEGACY_ERROR_CATEGORIES: error( filename, linenum, "readability/nolint", 5, f"Unknown NOLINT error category: {category}", ) def ProcessGlobalSuppressions(filename: str, lines: list[str]) -> None: """Updates the list of global error suppressions. Parses any lint directives in the file that have global effect. Args: lines: An array of strings, each representing a line of the file, with the last element being empty if the file is terminated with a newline. filename: str, the name of the input file. """ for line in lines: if _SEARCH_C_FILE.search(line) or filename.lower().endswith((".c", ".cu")): for category in _DEFAULT_C_SUPPRESSED_CATEGORIES: _error_suppressions.AddGlobalSuppression(category) if _SEARCH_KERNEL_FILE.search(line): for category in _DEFAULT_KERNEL_SUPPRESSED_CATEGORIES: _error_suppressions.AddGlobalSuppression(category) def ResetNolintSuppressions(): """Resets the set of NOLINT suppressions to empty.""" _error_suppressions.Clear() def IsErrorSuppressedByNolint(category, linenum): """Returns true if the specified error category is suppressed on this line. Consults the global error_suppressions map populated by ParseNolintSuppressions/ProcessGlobalSuppressions/ResetNolintSuppressions. Args: category: str, the category of the error. linenum: int, the current line number. Returns: bool, True iff the error should be suppressed due to a NOLINT comment, block suppression or global suppression. """ return _error_suppressions.IsSuppressed(category, linenum) def _IsSourceExtension(s): """File extension (excluding dot) matches a source file extension.""" return s in GetNonHeaderExtensions() class _IncludeState: """Tracks line numbers for includes, and the order in which includes appear. include_list contains list of lists of (header, line number) pairs. It's a lists of lists rather than just one flat list to make it easier to update across preprocessor boundaries. Call CheckNextIncludeOrder() once for each header in the file, passing in the type constants defined above. Calls in an illegal order will raise an _IncludeError with an appropriate error message. """ # self._section will move monotonically through this set. If it ever # needs to move backwards, CheckNextIncludeOrder will raise an error. _INITIAL_SECTION = 0 _MY_H_SECTION = 1 _C_SECTION = 2 _CPP_SECTION = 3 _OTHER_SYS_SECTION = 4 _OTHER_H_SECTION = 5 _TYPE_NAMES = { _C_SYS_HEADER: "C routing header", _CPP_SYS_HEADER: "C++ routing header", _OTHER_SYS_HEADER: "other routing header", _LIKELY_MY_HEADER: "header this file implements", _POSSIBLE_MY_HEADER: "header this file may implement", _OTHER_HEADER: "other header", } _SECTION_NAMES = { _INITIAL_SECTION: "... nothing. (This can't be an error.)", _MY_H_SECTION: "a header this file implements", _C_SECTION: "C routing header", _CPP_SECTION: "C++ routing header", _OTHER_SYS_SECTION: "other routing header", _OTHER_H_SECTION: "other header", } def __init__(self): self.include_list = [[]] self._section = None self._last_header = None self.ResetSection("") def FindHeader(self, header): """Check if a header has already been included. Args: header: header to check. Returns: Line number of previous occurrence, or -1 if the header has not been seen before. """ for section_list in self.include_list: for f in section_list: if f[0] == header: return f[1] return -1 def ResetSection(self, directive): """Reset section checking for preprocessor directive. Args: directive: preprocessor directive (e.g. "if", "else"). """ # The name of the current section. self._section = self._INITIAL_SECTION # The path of last found header. self._last_header = "" # Update list of includes. Note that we never pop from the # include list. if directive in ("if", "ifdef", "ifndef"): self.include_list.append([]) elif directive in ("else", "elif"): self.include_list[-1] = [] def SetLastHeader(self, header_path): self._last_header = header_path def CanonicalizeAlphabeticalOrder(self, header_path): """Returns a path canonicalized for alphabetical comparison. - replaces "-" with "_" so they both cmp the same. - removes '-inl' since we don't require them to be after the main header. - lowercase everything, just in case. Args: header_path: Path to be canonicalized. Returns: Canonicalized path. """ return header_path.replace("-inl.h", ".h").replace("-", "_").lower() def IsInAlphabeticalOrder(self, clean_lines, linenum, header_path): """Check if a header is in alphabetical order with the previous header. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. header_path: Canonicalized header to be checked. Returns: Returns true if the header is in alphabetical order. """ # If previous section is different from current section, _last_header will # be reset to empty string, so it's always less than current header. # # If previous line was a blank line, assume that the headers are # intentionally sorted the way they are. return not ( self._last_header > header_path and re.match(r"^\s*#\s*include\b", clean_lines.elided[linenum - 1]) ) def CheckNextIncludeOrder(self, header_type): """Returns a non-empty error message if the next header is out of order. This function also updates the internal state to be ready to check the next include. Args: header_type: One of the _XXX_HEADER constants defined above. Returns: The empty string if the header is in the right order, or an error message describing what's wrong. """ error_message = ( f"Found {self._TYPE_NAMES[header_type]} after {self._SECTION_NAMES[self._section]}" ) last_section = self._section if header_type == _C_SYS_HEADER: if self._section <= self._C_SECTION: self._section = self._C_SECTION else: self._last_header = "" return error_message elif header_type == _CPP_SYS_HEADER: if self._section <= self._CPP_SECTION: self._section = self._CPP_SECTION else: self._last_header = "" return error_message elif header_type == _OTHER_SYS_HEADER: if self._section <= self._OTHER_SYS_SECTION: self._section = self._OTHER_SYS_SECTION else: self._last_header = "" return error_message elif header_type == _LIKELY_MY_HEADER: if self._section <= self._MY_H_SECTION: self._section = self._MY_H_SECTION else: self._section = self._OTHER_H_SECTION elif header_type == _POSSIBLE_MY_HEADER: if self._section <= self._MY_H_SECTION: self._section = self._MY_H_SECTION else: # This will always be the fallback because we're not sure # enough that the header is associated with this file. self._section = self._OTHER_H_SECTION else: assert header_type == _OTHER_HEADER self._section = self._OTHER_H_SECTION if last_section != self._section: self._last_header = "" return "" class _CppLintState: """Maintains module-wide state..""" def __init__(self): self.verbose_level = 1 # global setting. self.error_count = 0 # global count of reported errors # filters to apply when emitting error messages self.filters = _DEFAULT_FILTERS[:] # backup of filter list. Used to restore the state after each file. self._filters_backup = self.filters[:] self.counting = "total" # In what way are we counting errors? self.errors_by_category = {} # string to int dict storing error counts self.quiet = False # Suppress non-error messages? # output format: # "emacs" - format that emacs can parse (default) # "eclipse" - format that eclipse can parse # "vs7" - format that Microsoft Visual Studio 7 can parse # "junit" - format that Jenkins, Bamboo, etc can parse # "sed" - returns a gnu sed command to fix the problem # "gsed" - like sed, but names the command gsed, e.g. for macOS homebrew users self.output_format = "emacs" # For JUnit output, save errors and failures until the end so that they # can be written into the XML self._junit_errors = [] self._junit_failures = [] def SetOutputFormat(self, output_format): """Sets the output format for errors.""" self.output_format = output_format def SetQuiet(self, quiet): """Sets the module's quiet settings, and returns the previous setting.""" last_quiet = self.quiet self.quiet = quiet return last_quiet def SetVerboseLevel(self, level): """Sets the module's verbosity, and returns the previous setting.""" last_verbose_level = self.verbose_level self.verbose_level = level return last_verbose_level def SetCountingStyle(self, counting_style): """Sets the module's counting options.""" self.counting = counting_style def SetFilters(self, filters): """Sets the error-message filters. These filters are applied when deciding whether to emit a given error message. Args: filters: A string of comma-separated filters (eg "+whitespace/indent"). Each filter should start with + or -; else we die. Raises: ValueError: The comma-separated filters did not all start with '+' or '-'. E.g. "-,+whitespace,-whitespace/indent,whitespace/badfilter" """ # Default filters always have less priority than the flag ones. self.filters = _DEFAULT_FILTERS[:] self.AddFilters(filters) def AddFilters(self, filters): """Adds more filters to the existing list of error-message filters.""" for filt in filters.split(","): clean_filt = filt.strip() if clean_filt: self.filters.append(clean_filt) for filt in self.filters: if not filt.startswith(("+", "-")): msg = f"Every filter in --filters must start with + or - ({filt} does not)" raise ValueError(msg) def BackupFilters(self): """Saves the current filter list to backup storage.""" self._filters_backup = self.filters[:] def RestoreFilters(self): """Restores filters previously backed up.""" self.filters = self._filters_backup[:] def ResetErrorCounts(self): """Sets the module's error statistic back to zero.""" self.error_count = 0 self.errors_by_category = {} def IncrementErrorCount(self, category): """Bumps the module's error statistic.""" self.error_count += 1 if self.counting in ("toplevel", "detailed"): if self.counting != "detailed": category = category.split("/")[0] if category not in self.errors_by_category: self.errors_by_category[category] = 0 self.errors_by_category[category] += 1 def PrintErrorCounts(self): """Print a summary of errors by category, and the total.""" for category, count in sorted(dict.items(self.errors_by_category)): self.PrintInfo(f"Category '{category}' errors found: {count}\n") if self.error_count > 0: self.PrintInfo(f"Total errors found: {self.error_count}\n") def PrintInfo(self, message): # _quiet does not represent --quiet flag. # Hide infos from stdout to keep stdout pure for machine consumption if not _quiet and self.output_format not in _MACHINE_OUTPUTS: sys.stdout.write(message) def PrintError(self, message): if self.output_format == "junit": self._junit_errors.append(message) else: sys.stderr.write(message) def AddJUnitFailure(self, filename, linenum, message, category, confidence): self._junit_failures.append((filename, linenum, message, category, confidence)) def FormatJUnitXML(self): num_errors = len(self._junit_errors) num_failures = len(self._junit_failures) testsuite = xml.etree.ElementTree.Element("testsuite") testsuite.attrib["errors"] = str(num_errors) testsuite.attrib["failures"] = str(num_failures) testsuite.attrib["name"] = "cpplint" if num_errors == 0 and num_failures == 0: testsuite.attrib["tests"] = str(1) xml.etree.ElementTree.SubElement(testsuite, "testcase", name="passed") else: testsuite.attrib["tests"] = str(num_errors + num_failures) if num_errors > 0: testcase = xml.etree.ElementTree.SubElement(testsuite, "testcase") testcase.attrib["name"] = "errors" error = xml.etree.ElementTree.SubElement(testcase, "error") error.text = "\n".join(self._junit_errors) if num_failures > 0: # Group failures by file failed_file_order = [] failures_by_file = {} for failure in self._junit_failures: failed_file = failure[0] if failed_file not in failed_file_order: failed_file_order.append(failed_file) failures_by_file[failed_file] = [] failures_by_file[failed_file].append(failure) # Create a testcase for each file for failed_file in failed_file_order: failures = failures_by_file[failed_file] testcase = xml.etree.ElementTree.SubElement(testsuite, "testcase") testcase.attrib["name"] = failed_file failure = xml.etree.ElementTree.SubElement(testcase, "failure") template = "{0}: {1} [{2}] [{3}]" texts = [template.format(f[1], f[2], f[3], f[4]) for f in failures] failure.text = "\n".join(texts) xml_decl = '\n' return xml_decl + xml.etree.ElementTree.tostring(testsuite, "utf-8").decode("utf-8") _cpplint_state = _CppLintState() def _OutputFormat(): """Gets the module's output format.""" return _cpplint_state.output_format def _SetOutputFormat(output_format): """Sets the module's output format.""" _cpplint_state.SetOutputFormat(output_format) def _Quiet(): """Return's the module's quiet setting.""" return _cpplint_state.quiet def _SetQuiet(quiet): """Set the module's quiet status, and return previous setting.""" return _cpplint_state.SetQuiet(quiet) def _VerboseLevel(): """Returns the module's verbosity setting.""" return _cpplint_state.verbose_level def _SetVerboseLevel(level): """Sets the module's verbosity, and returns the previous setting.""" return _cpplint_state.SetVerboseLevel(level) def _SetCountingStyle(level): """Sets the module's counting options.""" _cpplint_state.SetCountingStyle(level) def _Filters(): """Returns the module's list of output filters, as a list.""" return _cpplint_state.filters def _SetFilters(filters): """Sets the module's error-message filters. These filters are applied when deciding whether to emit a given error message. Args: filters: A string of comma-separated filters (eg "whitespace/indent"). Each filter should start with + or -; else we die. """ _cpplint_state.SetFilters(filters) def _AddFilters(filters): """Adds more filter overrides. Unlike _SetFilters, this function does not reset the current list of filters available. Args: filters: A string of comma-separated filters (eg "whitespace/indent"). Each filter should start with + or -; else we die. """ _cpplint_state.AddFilters(filters) def _BackupFilters(): """Saves the current filter list to backup storage.""" _cpplint_state.BackupFilters() def _RestoreFilters(): """Restores filters previously backed up.""" _cpplint_state.RestoreFilters() class _FunctionState: """Tracks current function name and the number of lines in its body.""" _NORMAL_TRIGGER = 250 # for --v=0, 500 for --v=1, etc. _TEST_TRIGGER = 400 # about 50% more than _NORMAL_TRIGGER. def __init__(self): self.in_a_function = False self.lines_in_function = 0 self.current_function = "" def Begin(self, function_name): """Start analyzing function body. Args: function_name: The name of the function being tracked. """ self.in_a_function = True self.lines_in_function = 0 self.current_function = function_name def Count(self): """Count line in current function body.""" if self.in_a_function: self.lines_in_function += 1 def Check(self, error, filename, linenum): """Report if too many lines in function body. Args: error: The function to call with any errors found. filename: The name of the current file. linenum: The number of the line to check. """ if not self.in_a_function: return if re.match(r"T(EST|est)", self.current_function): base_trigger = self._TEST_TRIGGER else: base_trigger = self._NORMAL_TRIGGER trigger = base_trigger * 2 ** _VerboseLevel() if self.lines_in_function > trigger: error_level = int(math.log2(self.lines_in_function / base_trigger)) # 50 => 0, 100 => 1, 200 => 2, 400 => 3, 800 => 4, 1600 => 5, ... error_level = min(error_level, 5) error( filename, linenum, "readability/fn_size", error_level, "Small and focused functions are preferred:" f" {self.current_function} has {self.lines_in_function} non-comment lines" f" (error triggered by exceeding {trigger} lines).", ) def End(self): """Stop analyzing function body.""" self.in_a_function = False class _IncludeError(Exception): """Indicates a problem with the include order in a file.""" pass class FileInfo: """Provides utility functions for filenames. FileInfo provides easy access to the components of a file's path relative to the project root. """ def __init__(self, filename): self._filename = filename def FullName(self): """Make Windows paths like Unix.""" return os.path.abspath(self._filename).replace("\\", "/") def RepositoryName(self): r"""FullName after removing the local path to the repository. If we have a real absolute path name here we can try to do something smart: detecting the root of the checkout and truncating /path/to/checkout from the name so that we get header guards that don't include things like "C:\\Documents and Settings\\..." or "/home/username/..." in them and thus people on different computers who have checked the source out to different locations won't see bogus errors. """ fullname = self.FullName() if os.path.exists(fullname): project_dir = os.path.dirname(fullname) # If the user specified a repository path, it exists, and the file is # contained in it, use the specified repository path if _repository: repo = FileInfo(_repository).FullName() root_dir = project_dir while os.path.exists(root_dir): # allow case insensitive compare on Windows if os.path.normcase(root_dir) == os.path.normcase(repo): return os.path.relpath(fullname, root_dir).replace("\\", "/") one_up_dir = os.path.dirname(root_dir) if one_up_dir == root_dir: break root_dir = one_up_dir if os.path.exists(os.path.join(project_dir, ".svn")): # If there's a .svn file in the current directory, we recursively look # up the directory tree for the top of the SVN checkout root_dir = project_dir one_up_dir = os.path.dirname(root_dir) while os.path.exists(os.path.join(one_up_dir, ".svn")): root_dir = os.path.dirname(root_dir) one_up_dir = os.path.dirname(one_up_dir) prefix = os.path.commonprefix([root_dir, project_dir]) return fullname[len(prefix) + 1 :] # Not SVN <= 1.6? Try to find a git, hg, or svn top level directory by # searching up from the current path. root_dir = current_dir = os.path.dirname(fullname) while current_dir != os.path.dirname(current_dir): if ( os.path.exists(os.path.join(current_dir, ".git")) or os.path.exists(os.path.join(current_dir, ".hg")) or os.path.exists(os.path.join(current_dir, ".svn")) ): root_dir = current_dir break current_dir = os.path.dirname(current_dir) if ( os.path.exists(os.path.join(root_dir, ".git")) or os.path.exists(os.path.join(root_dir, ".hg")) or os.path.exists(os.path.join(root_dir, ".svn")) ): prefix = os.path.commonprefix([root_dir, project_dir]) return fullname[len(prefix) + 1 :] # Don't know what to do; header guard warnings may be wrong... return fullname def Split(self): """Splits the file into the directory, basename, and extension. For 'chrome/browser/browser.cc', Split() would return ('chrome/browser', 'browser', '.cc') Returns: A tuple of (directory, basename, extension). """ googlename = self.RepositoryName() project, rest = os.path.split(googlename) return (project,) + os.path.splitext(rest) def BaseName(self): """File base name - text after the final slash, before the final period.""" return self.Split()[1] def Extension(self): """File extension - text following the final period, includes that period.""" return self.Split()[2] def NoExtension(self): """File has no source file extension.""" return "/".join(self.Split()[0:2]) def IsSource(self): """File has a source file extension.""" return _IsSourceExtension(self.Extension()[1:]) def _ShouldPrintError(category, confidence, filename, linenum): """If confidence >= verbose, category passes filter and is not suppressed.""" # There are three ways we might decide not to print an error message: # a "NOLINT(category)" comment appears in the source, # the verbosity level isn't high enough, or the filters filter it out. if IsErrorSuppressedByNolint(category, linenum): return False if confidence < _cpplint_state.verbose_level: return False is_filtered = False for one_filter in _Filters(): filter_cat, filter_file, filter_line = _ParseFilterSelector(one_filter[1:]) category_match = category.startswith(filter_cat) file_match = filter_file in ("", filename) line_match = filter_line in (linenum, -1) if one_filter.startswith("-"): if category_match and file_match and line_match: is_filtered = True elif one_filter.startswith("+"): if category_match and file_match and line_match: is_filtered = False else: # should have been checked for in SetFilter. msg = f"Invalid filter: {one_filter}" raise ValueError(msg) return not is_filtered def Error(filename, linenum, category, confidence, message): """Logs the fact we've found a lint error. We log where the error was found, and also our confidence in the error, that is, how certain we are this is a legitimate style regression, and not a misidentification or a use that's sometimes justified. False positives can be suppressed by the use of "NOLINT(category)" comments, NOLINTNEXTLINE or in blocks started by NOLINTBEGIN. These are parsed into _error_suppressions. Args: filename: The name of the file containing the error. linenum: The number of the line containing the error. category: A string used to describe the "category" this bug falls under: "whitespace", say, or "runtime". Categories may have a hierarchy separated by slashes: "whitespace/indent". confidence: A number from 1-5 representing a confidence score for the error, with 5 meaning that we are certain of the problem, and 1 meaning that it could be a legitimate construct. message: The error message. """ if _ShouldPrintError(category, confidence, filename, linenum): _cpplint_state.IncrementErrorCount(category) if _cpplint_state.output_format == "vs7": _cpplint_state.PrintError( f"{filename}({linenum}): error cpplint: [{category}] {message} [{confidence}]\n" ) elif _cpplint_state.output_format == "eclipse": sys.stderr.write( f"{filename}:{linenum}: warning: {message} [{category}] [{confidence}]\n" ) elif _cpplint_state.output_format == "junit": _cpplint_state.AddJUnitFailure(filename, linenum, message, category, confidence) elif _cpplint_state.output_format in ["sed", "gsed"]: if message in _SED_FIXUPS: sys.stdout.write( f"{_cpplint_state.output_format} -i" f" '{linenum}{_SED_FIXUPS[message]}' {filename}" f" # {message} [{category}] [{confidence}]\n" ) else: sys.stderr.write( f'# {filename}:{linenum}: "{message}" [{category}] [{confidence}]\n' ) else: final_message = f"{filename}:{linenum}: {message} [{category}] [{confidence}]\n" sys.stderr.write(final_message) # Matches standard C++ escape sequences per 2.13.2.3 of the C++ standard. _RE_PATTERN_CLEANSE_LINE_ESCAPES = re.compile(r'\\([abfnrtv?"\\\']|\d+|x[0-9a-fA-F]+)') # Match a single C style comment on the same line. _RE_PATTERN_C_COMMENTS = r"/\*(?:[^*]|\*(?!/))*\*/" # Matches multi-line C style comments. # This RE is a little bit more complicated than one might expect, because we # have to take care of space removals tools so we can handle comments inside # statements better. # The current rule is: We only clear spaces from both sides when we're at the # end of the line. Otherwise, we try to remove spaces from the right side, # if this doesn't work we try on left side but only if there's a non-character # on the right. _RE_PATTERN_CLEANSE_LINE_C_COMMENTS = re.compile( r"(\s*" + _RE_PATTERN_C_COMMENTS + r"\s*$|" + _RE_PATTERN_C_COMMENTS + r"\s+|" + r"\s+" + _RE_PATTERN_C_COMMENTS + r"(?=\W)|" + _RE_PATTERN_C_COMMENTS + r")" ) def IsCppString(line): """Does line terminate so, that the next symbol is in string constant. This function does not consider single-line nor multi-line comments. Args: line: is a partial line of code starting from the 0..n. Returns: True, if next character appended to 'line' is inside a string constant. """ line = line.replace(r"\\", "XX") # after this, \\" does not match to \" return ((line.count('"') - line.count(r"\"") - line.count("'\"'")) & 1) == 1 def CleanseRawStrings(raw_lines): """Removes C++11 raw strings from lines. Before: static const char kData[] = R"( multi-line string )"; After: static const char kData[] = "" (replaced by blank line) ""; Args: raw_lines: list of raw lines. Returns: list of lines with C++11 raw strings replaced by empty strings. """ delimiter = None lines_without_raw_strings = [] for line in raw_lines: if delimiter: # Inside a raw string, look for the end end = line.find(delimiter) if end >= 0: # Found the end of the string, match leading space for this # line and resume copying the original lines, and also insert # a "" on the last line. leading_space = re.match(r"^(\s*)\S", line) line = leading_space.group(1) + '""' + line[end + len(delimiter) :] delimiter = None else: # Haven't found the end yet, append a blank line. line = '""' # Look for beginning of a raw string, and replace them with # empty strings. This is done in a loop to handle multiple raw # strings on the same line. while delimiter is None: # Look for beginning of a raw string. # See 2.14.15 [lex.string] for syntax. # # Once we have matched a raw string, we check the prefix of the # line to make sure that the line is not part of a single line # comment. It's done this way because we remove raw strings # before removing comments as opposed to removing comments # before removing raw strings. This is because there are some # cpplint checks that requires the comments to be preserved, but # we don't want to check comments that are inside raw strings. matched = re.match(r'^(.*?)\b(?:R|u8R|uR|UR|LR)"([^\s\\()]*)\((.*)$', line) if matched and not re.match( r'^([^\'"]|\'(\\.|[^\'])*\'|"(\\.|[^"])*")*//', matched.group(1) ): delimiter = ")" + matched.group(2) + '"' end = matched.group(3).find(delimiter) if end >= 0: # Raw string ended on same line line = matched.group(1) + '""' + matched.group(3)[end + len(delimiter) :] delimiter = None else: # Start of a multi-line raw string line = matched.group(1) + '""' else: break lines_without_raw_strings.append(line) # TODO(google): if delimiter is not None here, we might want to # emit a warning for unterminated string. return lines_without_raw_strings def FindNextMultiLineCommentStart(lines, lineix): """Find the beginning marker for a multiline comment.""" while lineix < len(lines): if lines[lineix].strip().startswith("/*"): # Only return this marker if the comment goes beyond this line if lines[lineix].strip().find("*/", 2) < 0: return lineix lineix += 1 return len(lines) def FindNextMultiLineCommentEnd(lines, lineix): """We are inside a comment, find the end marker.""" while lineix < len(lines): if lines[lineix].strip().endswith("*/"): return lineix lineix += 1 return len(lines) def RemoveMultiLineCommentsFromRange(lines, begin, end): """Clears a range of lines for multi-line comments.""" # Having // comments makes the lines non-empty, so we will not get # unnecessary blank line warnings later in the code. for i in range(begin, end): lines[i] = "/**/" def RemoveMultiLineComments(filename, lines, error): """Removes multiline (c-style) comments from lines.""" lineix = 0 while lineix < len(lines): lineix_begin = FindNextMultiLineCommentStart(lines, lineix) if lineix_begin >= len(lines): return lineix_end = FindNextMultiLineCommentEnd(lines, lineix_begin) if lineix_end >= len(lines): error( filename, lineix_begin + 1, "readability/multiline_comment", 5, "Could not find end of multi-line comment", ) return RemoveMultiLineCommentsFromRange(lines, lineix_begin, lineix_end + 1) lineix = lineix_end + 1 def CleanseComments(line): """Removes //-comments and single-line C-style /* */ comments. Args: line: A line of C++ source. Returns: The line with single-line comments removed. """ commentpos = line.find("//") if commentpos != -1 and not IsCppString(line[:commentpos]): line = line[:commentpos].rstrip() # get rid of /* ... */ return _RE_PATTERN_CLEANSE_LINE_C_COMMENTS.sub("", line) def ReplaceAlternateTokens(line): """Replace any alternate token by its original counterpart. In order to comply with the google rule stating that unary operators should never be followed by a space, an exception is made for the 'not' and 'compl' alternate tokens. For these, any trailing space is removed during the conversion. Args: line: The line being processed. Returns: The line with alternate tokens replaced. """ for match in _ALT_TOKEN_REPLACEMENT_PATTERN.finditer(line): token = _ALT_TOKEN_REPLACEMENT[match.group(2)] tail = "" if match.group(2) in ["not", "compl"] and match.group(3) == " " else r"\3" line = re.sub(match.re, rf"\1{token}{tail}", line, count=1) return line class CleansedLines: """Holds 4 copies of all lines with different preprocessing applied to them. 1) elided member contains lines without strings and comments. 2) lines member contains lines without comments. 3) raw_lines member contains all the lines without processing. 4) lines_without_raw_strings member is same as raw_lines, but with C++11 raw strings removed. All these members are of , and of the same length. """ def __init__(self, lines): if "-readability/alt_tokens" in _cpplint_state.filters: for i, line in enumerate(lines): lines[i] = ReplaceAlternateTokens(line) self.elided = [] self.lines = [] self.raw_lines = lines self.num_lines = len(lines) self.lines_without_raw_strings = CleanseRawStrings(lines) for line in self.lines_without_raw_strings: self.lines.append(CleanseComments(line)) elided = self._CollapseStrings(line) self.elided.append(CleanseComments(elided)) def NumLines(self): """Returns the number of lines represented.""" return self.num_lines @staticmethod def _CollapseStrings(elided): """Collapses strings and chars on a line to simple "" or '' blocks. We nix strings first so we're not fooled by text like '"http://"' Args: elided: The line being processed. Returns: The line with collapsed strings. """ if _RE_PATTERN_INCLUDE.match(elided): return elided # Remove escaped characters first to make quote/single quote collapsing # basic. Things that look like escaped characters shouldn't occur # outside of strings and chars. elided = _RE_PATTERN_CLEANSE_LINE_ESCAPES.sub("", elided) # Replace quoted strings and digit separators. Both single quotes # and double quotes are processed in the same loop, otherwise # nested quotes wouldn't work. collapsed = "" while True: # Find the first quote character match = re.match(r'^([^\'"]*)([\'"])(.*)$', elided) if not match: collapsed += elided break head, quote, tail = match.groups() if quote == '"': # Collapse double quoted strings second_quote = tail.find('"') if second_quote >= 0: collapsed += head + '""' elided = tail[second_quote + 1 :] else: # Unmatched double quote, don't bother processing the rest # of the line since this is probably a multiline string. collapsed += elided break else: # Found single quote, check nearby text to eliminate digit separators. # # There is no special handling for floating point here, because # the integer/fractional/exponent parts would all be parsed # correctly as long as there are digits on both sides of the # separator. So we are fine as long as we don't see something # like "0.'3" (gcc 4.9.0 will not allow this literal). if re.search(r"\b(?:0[bBxX]?|[1-9])[0-9a-fA-F]*$", head): match_literal = re.match(r"^((?:\'?[0-9a-zA-Z_])*)(.*)$", "'" + tail) collapsed += head + match_literal.group(1).replace("'", "") elided = match_literal.group(2) else: second_quote = tail.find("'") if second_quote >= 0: collapsed += head + "''" elided = tail[second_quote + 1 :] else: # Unmatched single quote collapsed += elided break return collapsed def FindEndOfExpressionInLine(line, startpos, stack): """Find the position just after the end of current parenthesized expression. Args: line: a CleansedLines line. startpos: start searching at this position. stack: nesting stack at startpos. Returns: On finding matching end: (index just after matching end, None) On finding an unclosed expression: (-1, None) Otherwise: (-1, new stack at end of this line) """ for i in range(startpos, len(line)): char = line[i] if char in "([{": # Found start of parenthesized expression, push to expression stack stack.append(char) elif char == "<": # Found potential start of template argument list if i > 0 and line[i - 1] == "<": # Left shift operator if stack and stack[-1] == "<": stack.pop() if not stack: return (-1, None) elif i > 0 and re.search(r"\boperator\s*$", line[0:i]): # operator<, don't add to stack continue else: # Tentative start of template argument list stack.append("<") elif char in ")]}": # Found end of parenthesized expression. # # If we are currently expecting a matching '>', the pending '<' # must have been an operator. Remove them from expression stack. while stack and stack[-1] == "<": stack.pop() if not stack: return (-1, None) if ( (stack[-1] == "(" and char == ")") or (stack[-1] == "[" and char == "]") or (stack[-1] == "{" and char == "}") ): stack.pop() if not stack: return (i + 1, None) else: # Mismatched parentheses return (-1, None) elif char == ">": # Found potential end of template argument list. # Ignore "->" and operator functions if i > 0 and (line[i - 1] == "-" or re.search(r"\boperator\s*$", line[0 : i - 1])): continue # Pop the stack if there is a matching '<'. Otherwise, ignore # this '>' since it must be an operator. if stack and stack[-1] == "<": stack.pop() if not stack: return (i + 1, None) elif char == ";": # Found something that look like end of statements. If we are currently # expecting a '>', the matching '<' must have been an operator, since # template argument list should not contain statements. while stack and stack[-1] == "<": stack.pop() if not stack: return (-1, None) # Did not find end of expression or unbalanced parentheses on this line return (-1, stack) def CloseExpression(clean_lines, linenum, pos): """If input points to ( or { or [ or <, finds the position that closes it. If lines[linenum][pos] points to a '(' or '{' or '[' or '<', finds the linenum/pos that correspond to the closing of the expression. TODO(google): cpplint spends a fair bit of time matching parentheses. Ideally we would want to index all opening and closing parentheses once and have CloseExpression be just a simple lookup, but due to preprocessor tricks, this is not so easy. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. pos: A position on the line. Returns: A tuple (line, linenum, pos) pointer *past* the closing brace, or (line, len(lines), -1) if we never find a close. Note we ignore strings and comments when matching; and the line we return is the 'cleansed' line at linenum. """ line = clean_lines.elided[linenum] if (line[pos] not in "({[<") or re.match(r"<[<=]", line[pos:]): return (line, clean_lines.NumLines(), -1) # Check first line (end_pos, stack) = FindEndOfExpressionInLine(line, pos, []) if end_pos > -1: return (line, linenum, end_pos) # Continue scanning forward while stack and linenum < clean_lines.NumLines() - 1: linenum += 1 line = clean_lines.elided[linenum] (end_pos, stack) = FindEndOfExpressionInLine(line, 0, stack) if end_pos > -1: return (line, linenum, end_pos) # Did not find end of expression before end of file, give up return (line, clean_lines.NumLines(), -1) def FindStartOfExpressionInLine(line, endpos, stack): """Find position at the matching start of current expression. This is almost the reverse of FindEndOfExpressionInLine, but note that the input position and returned position differs by 1. Args: line: a CleansedLines line. endpos: start searching at this position. stack: nesting stack at endpos. Returns: On finding matching start: (index at matching start, None) On finding an unclosed expression: (-1, None) Otherwise: (-1, new stack at beginning of this line) """ i = endpos while i >= 0: char = line[i] if char in ")]}": # Found end of expression, push to expression stack stack.append(char) elif char == ">": # Found potential end of template argument list. # # Ignore it if it's a "->" or ">=" or "operator>" if i > 0 and ( line[i - 1] == "-" or re.match(r"\s>=\s", line[i - 1 :]) or re.search(r"\boperator\s*$", line[0:i]) ): i -= 1 else: stack.append(">") elif char == "<": # Found potential start of template argument list if i > 0 and line[i - 1] == "<": # Left shift operator i -= 1 else: # If there is a matching '>', we can pop the expression stack. # Otherwise, ignore this '<' since it must be an operator. if stack and stack[-1] == ">": stack.pop() if not stack: return (i, None) elif char in "([{": # Found start of expression. # # If there are any unmatched '>' on the stack, they must be # operators. Remove those. while stack and stack[-1] == ">": stack.pop() if not stack: return (-1, None) if ( (char == "(" and stack[-1] == ")") or (char == "[" and stack[-1] == "]") or (char == "{" and stack[-1] == "}") ): stack.pop() if not stack: return (i, None) else: # Mismatched parentheses return (-1, None) elif char == ";": # Found something that look like end of statements. If we are currently # expecting a '<', the matching '>' must have been an operator, since # template argument list should not contain statements. while stack and stack[-1] == ">": stack.pop() if not stack: return (-1, None) i -= 1 return (-1, stack) def ReverseCloseExpression(clean_lines, linenum, pos): """If input points to ) or } or ] or >, finds the position that opens it. If lines[linenum][pos] points to a ')' or '}' or ']' or '>', finds the linenum/pos that correspond to the opening of the expression. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. pos: A position on the line. Returns: A tuple (line, linenum, pos) pointer *at* the opening brace, or (line, 0, -1) if we never find the matching opening brace. Note we ignore strings and comments when matching; and the line we return is the 'cleansed' line at linenum. """ line = clean_lines.elided[linenum] if line[pos] not in ")}]>": return (line, 0, -1) # Check last line (start_pos, stack) = FindStartOfExpressionInLine(line, pos, []) if start_pos > -1: return (line, linenum, start_pos) # Continue scanning backward while stack and linenum > 0: linenum -= 1 line = clean_lines.elided[linenum] (start_pos, stack) = FindStartOfExpressionInLine(line, len(line) - 1, stack) if start_pos > -1: return (line, linenum, start_pos) # Did not find start of expression before beginning of file, give up return (line, 0, -1) def CheckForCopyright(filename, lines, error): """Logs an error if no Copyright message appears at the top of the file.""" # We'll say it should occur by line 10. Don't forget there's a # placeholder line at the front. for line in range(1, min(len(lines), 11)): if re.search(r"Copyright", lines[line], re.IGNORECASE): break else: # means no copyright line was found error( filename, 0, "legal/copyright", 5, "No copyright message found. " 'You should have a line: "Copyright [year] "', ) def GetIndentLevel(line): """Return the number of leading spaces in line. Args: line: A string to check. Returns: An integer count of leading spaces, possibly zero. """ if indent := re.match(r"^( *)\S", line): return len(indent.group(1)) return 0 def PathSplitToList(path): """Returns the path split into a list by the separator. Args: path: An absolute or relative path (e.g. '/a/b/c/' or '../a') Returns: A list of path components (e.g. ['a', 'b', 'c]). """ lst = [] while True: (head, tail) = os.path.split(path) if head == path: # absolute paths end lst.append(head) break if tail == path: # relative paths end lst.append(tail) break path = head lst.append(tail) lst.reverse() return lst def GetHeaderGuardCPPVariable(filename): """Returns the CPP variable that should be used as a header guard. Args: filename: The name of a C++ header file. Returns: The CPP variable that should be used as a header guard in the named file. """ # Restores original filename in case that cpplint is invoked from Emacs's # flymake. filename = re.sub(r"_flymake\.h$", ".h", filename) filename = re.sub(r"/\.flymake/([^/]*)$", r"/\1", filename) # Replace 'c++' with 'cpp'. filename = filename.replace("C++", "cpp").replace("c++", "cpp") fileinfo = FileInfo(filename) file_path_from_root = fileinfo.RepositoryName() def FixupPathFromRoot(): if _root_debug: sys.stderr.write( f"\n_root fixup, _root = '{_root}'," f" repository name = '{fileinfo.RepositoryName()}'\n" ) # Process the file path with the --root flag if it was set. if not _root: if _root_debug: sys.stderr.write("_root unspecified\n") return file_path_from_root def StripListPrefix(lst, prefix): # f(['x', 'y'], ['w, z']) -> None (not a valid prefix) if lst[: len(prefix)] != prefix: return None # f(['a, 'b', 'c', 'd'], ['a', 'b']) -> ['c', 'd'] return lst[(len(prefix)) :] # root behavior: # --root=subdir , lstrips subdir from the header guard maybe_path = StripListPrefix(PathSplitToList(file_path_from_root), PathSplitToList(_root)) if _root_debug: sys.stderr.write( ("_root lstrip (maybe_path=%s, file_path_from_root=%s," + " _root=%s)\n") % (maybe_path, file_path_from_root, _root) ) if maybe_path: return os.path.join(*maybe_path) # --root=.. , will prepend the outer directory to the header guard full_path = fileinfo.FullName() # adapt slashes for windows root_abspath = os.path.abspath(_root).replace("\\", "/") maybe_path = StripListPrefix(PathSplitToList(full_path), PathSplitToList(root_abspath)) if _root_debug: sys.stderr.write( ("_root prepend (maybe_path=%s, full_path=%s, " + "root_abspath=%s)\n") % (maybe_path, full_path, root_abspath) ) if maybe_path: return os.path.join(*maybe_path) if _root_debug: sys.stderr.write(f"_root ignore, returning {file_path_from_root}\n") # --root=FAKE_DIR is ignored return file_path_from_root file_path_from_root = FixupPathFromRoot() return re.sub(r"[^a-zA-Z0-9]", "_", file_path_from_root).upper() + "_" def CheckForHeaderGuard(filename, clean_lines, error, cppvar): """Checks that the file contains a header guard. Logs an error if no #ifndef header guard is present. For other headers, checks that the full pathname is used. Args: filename: The name of the C++ header file. clean_lines: A CleansedLines instance containing the file. error: The function to call with any errors found. """ # Don't check for header guards if there are error suppression # comments somewhere in this file. # # Because this is silencing a warning for a nonexistent line, we # only support the very specific NOLINT(build/header_guard) syntax, # and not the general NOLINT or NOLINT(*) syntax. raw_lines = clean_lines.lines_without_raw_strings for i in raw_lines: if re.search(r"//\s*NOLINT\(build/header_guard\)", i): return # Allow pragma once instead of header guards for i in raw_lines: if re.search(r"^\s*#pragma\s+once", i): return ifndef = "" ifndef_linenum = 0 define = "" endif = "" endif_linenum = 0 for linenum, line in enumerate(raw_lines): linesplit = line.split() if len(linesplit) >= 2: # find the first occurrence of #ifndef and #define, save arg if not ifndef and linesplit[0] == "#ifndef": # set ifndef to the header guard presented on the #ifndef line. ifndef = linesplit[1] ifndef_linenum = linenum if not define and linesplit[0] == "#define": define = linesplit[1] # find the last occurrence of #endif, save entire line if line.startswith("#endif"): endif = line endif_linenum = linenum if not ifndef or not define or ifndef != define: error( filename, 0, "build/header_guard", 5, f"No #ifndef header guard found, suggested CPP variable is: {cppvar}", ) return # The guard should be PATH_FILE_H_, but we also allow PATH_FILE_H__ # for backward compatibility. if ifndef != cppvar: error_level = 0 if ifndef != cppvar + "_": error_level = 5 ParseNolintSuppressions(filename, raw_lines[ifndef_linenum], ifndef_linenum, error) error( filename, ifndef_linenum, "build/header_guard", error_level, f"#ifndef header guard has wrong style, please use: {cppvar}", ) # Check for "//" comments on endif line. ParseNolintSuppressions(filename, raw_lines[endif_linenum], endif_linenum, error) match = re.match(r"#endif\s*//\s*" + cppvar + r"(_)?\b", endif) if match: if match.group(1) == "_": # Issue low severity warning for deprecated double trailing underscore error( filename, endif_linenum, "build/header_guard", 0, f'#endif line should be "#endif // {cppvar}"', ) return # Didn't find the corresponding "//" comment. If this file does not # contain any "//" comments at all, it could be that the compiler # only wants "/**/" comments, look for those instead. no_single_line_comments = True for i in range(1, len(raw_lines) - 1): line = raw_lines[i] if re.match(r'^(?:(?:\'(?:\.|[^\'])*\')|(?:"(?:\.|[^"])*")|[^\'"])*//', line): no_single_line_comments = False break if no_single_line_comments: match = re.match(r"#endif\s*/\*\s*" + cppvar + r"(_)?\s*\*/", endif) if match: if match.group(1) == "_": # Low severity warning for double trailing underscore error( filename, endif_linenum, "build/header_guard", 0, f'#endif line should be "#endif /* {cppvar} */"', ) return # Didn't find anything error( filename, endif_linenum, "build/header_guard", 5, f'#endif line should be "#endif // {cppvar}"', ) def CheckHeaderFileIncluded(filename, include_state, error): """Logs an error if a source file does not include its header.""" # Do not check test files fileinfo = FileInfo(filename) if re.search(_TEST_FILE_SUFFIX, fileinfo.BaseName()): return first_include = message = None basefilename = filename[0 : len(filename) - len(fileinfo.Extension())] for ext in GetHeaderExtensions(): headerfile = basefilename + "." + ext if not os.path.exists(headerfile): continue headername = FileInfo(headerfile).RepositoryName() include_uses_unix_dir_aliases = False for section_list in include_state.include_list: for f in section_list: include_text = f[0] if "./" in include_text: include_uses_unix_dir_aliases = True if headername in include_text or include_text in headername: return if not first_include: first_include = f[1] message = f"{fileinfo.RepositoryName()} should include its header file {headername}" if include_uses_unix_dir_aliases: message += ". Relative paths like . and .. are not allowed." if message: error(filename, first_include, "build/include", 5, message) def CheckForBadCharacters(filename, lines, error): """Logs an error for each line containing bad characters. Two kinds of bad characters: 1. Unicode replacement characters: These indicate that either the file contained invalid UTF-8 (likely) or Unicode replacement characters (which it shouldn't). Note that it's possible for this to throw off line numbering if the invalid UTF-8 occurred adjacent to a newline. 2. NUL bytes. These are problematic for some tools. Args: filename: The name of the current file. lines: An array of strings, each representing a line of the file. error: The function to call with any errors found. """ for linenum, line in enumerate(lines): if "\ufffd" in line: error( filename, linenum, "readability/utf8", 5, "Line contains invalid UTF-8 (or Unicode replacement character).", ) if "\0" in line: error(filename, linenum, "readability/nul", 5, "Line contains NUL byte.") def CheckForNewlineAtEOF(filename, lines, error): """Logs an error if there is no newline char at the end of the file. Args: filename: The name of the current file. lines: An array of strings, each representing a line of the file. error: The function to call with any errors found. """ # The array lines() was created by adding two newlines to the # original file (go figure), then splitting on \n. # To verify that the file ends in \n, we just have to make sure the # last-but-two element of lines() exists and is empty. if len(lines) < 3 or lines[-2]: error( filename, len(lines) - 2, "whitespace/ending_newline", 5, "Could not find a newline character at the end of the file.", ) def CheckForMultilineCommentsAndStrings(filename, clean_lines, linenum, error): """Logs an error if we see /* ... */ or "..." that extend past one line. /* ... */ comments are legit inside macros, for one line. Otherwise, we prefer // comments, so it's ok to warn about the other. Likewise, it's ok for strings to extend across multiple lines, as long as a line continuation character (backslash) terminates each line. Although not currently prohibited by the C++ style guide, it's ugly and unnecessary. We don't do well with either in this lint program, so we warn about both. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Remove all \\ (escaped backslashes) from the line. They are OK, and the # second (escaped) slash may trigger later \" detection erroneously. line = line.replace("\\\\", "") if line.count("/*") > line.count("*/"): error( filename, linenum, "readability/multiline_comment", 5, "Complex multi-line /*...*/-style comment found. " "Lint may give bogus warnings. " "Consider replacing these with //-style comments, " "with #if 0...#endif, " "or with more clearly structured multi-line comments.", ) if (line.count('"') - line.count('\\"')) % 2: error( filename, linenum, "readability/multiline_string", 5, 'Multi-line string ("...") found. This lint script doesn\'t ' "do well with such strings, and may give bogus warnings. " "Use C++11 raw strings or concatenation instead.", ) # (non-threadsafe name, thread-safe alternative, validation pattern) # # The validation pattern is used to eliminate false positives such as: # _rand(); // false positive due to substring match. # ->rand(); // some member function rand(). # ACMRandom rand(seed); // some variable named rand. # ISAACRandom rand(); // another variable named rand. # # Basically we require the return value of these functions to be used # in some expression context on the same line by matching on some # operator before the function name. This eliminates constructors and # member function calls. _UNSAFE_FUNC_PREFIX = r"(?:[-+*/=%^&|(<]\s*|>\s+)" _THREADING_LIST = ( ("asctime(", "asctime_r(", _UNSAFE_FUNC_PREFIX + r"asctime\([^)]+\)"), ("ctime(", "ctime_r(", _UNSAFE_FUNC_PREFIX + r"ctime\([^)]+\)"), ("getgrgid(", "getgrgid_r(", _UNSAFE_FUNC_PREFIX + r"getgrgid\([^)]+\)"), ("getgrnam(", "getgrnam_r(", _UNSAFE_FUNC_PREFIX + r"getgrnam\([^)]+\)"), ("getlogin(", "getlogin_r(", _UNSAFE_FUNC_PREFIX + r"getlogin\(\)"), ("getpwnam(", "getpwnam_r(", _UNSAFE_FUNC_PREFIX + r"getpwnam\([^)]+\)"), ("getpwuid(", "getpwuid_r(", _UNSAFE_FUNC_PREFIX + r"getpwuid\([^)]+\)"), ("gmtime(", "gmtime_r(", _UNSAFE_FUNC_PREFIX + r"gmtime\([^)]+\)"), ("localtime(", "localtime_r(", _UNSAFE_FUNC_PREFIX + r"localtime\([^)]+\)"), ("rand(", "rand_r(", _UNSAFE_FUNC_PREFIX + r"rand\(\)"), ("strtok(", "strtok_r(", _UNSAFE_FUNC_PREFIX + r"strtok\([^)]+\)"), ("ttyname(", "ttyname_r(", _UNSAFE_FUNC_PREFIX + r"ttyname\([^)]+\)"), ) def CheckPosixThreading(filename, clean_lines, linenum, error): """Checks for calls to thread-unsafe functions. Much code has been originally written without consideration of multi-threading. Also, engineers are relying on their old experience; they have learned posix before threading extensions were added. These tests guide the engineers to use thread-safe functions (when using posix directly). Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] for single_thread_func, multithread_safe_func, pattern in _THREADING_LIST: # Additional pattern matching check to confirm that this is the # function we are looking for if re.search(pattern, line): error( filename, linenum, "runtime/threadsafe_fn", 2, "Consider using " + multithread_safe_func + "...) instead of " + single_thread_func + "...) for improved thread safety.", ) def CheckVlogArguments(filename, clean_lines, linenum, error): """Checks that VLOG() is only used for defining a logging level. For example, VLOG(2) is correct. VLOG(INFO), VLOG(WARNING), VLOG(ERROR), and VLOG(FATAL) are not. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] if re.search(r"\bVLOG\((INFO|ERROR|WARNING|DFATAL|FATAL)\)", line): error( filename, linenum, "runtime/vlog", 5, "VLOG() should be used with numeric verbosity level. " "Use LOG() if you want symbolic severity levels.", ) # Matches invalid increment: *count++, which moves pointer instead of # incrementing a value. _RE_PATTERN_INVALID_INCREMENT = re.compile(r"^\s*\*\w+(\+\+|--);") def CheckInvalidIncrement(filename, clean_lines, linenum, error): """Checks for invalid increment *count++. For example following function: void increment_counter(int* count) { *count++; } is invalid, because it effectively does count++, moving pointer, and should be replaced with ++*count, (*count)++ or *count += 1. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] if _RE_PATTERN_INVALID_INCREMENT.match(line): error( filename, linenum, "runtime/invalid_increment", 5, "Changing pointer instead of value (or unused value of operator*).", ) def IsMacroDefinition(clean_lines, linenum): if re.search(r"^#define", clean_lines[linenum]): return True return bool(linenum > 0 and re.search(r"\\$", clean_lines[linenum - 1])) def IsForwardClassDeclaration(clean_lines, linenum): return re.match(r"^\s*(\btemplate\b)*.*class\s+\w+;\s*$", clean_lines[linenum]) class _BlockInfo: """Stores information about a generic block of code.""" def __init__(self, linenum, seen_open_brace): self.starting_linenum = linenum self.seen_open_brace = seen_open_brace self.open_parentheses = 0 self.inline_asm = _NO_ASM self.check_namespace_indentation = False def CheckBegin(self, filename, clean_lines, linenum, error): """Run checks that applies to text up to the opening brace. This is mostly for checking the text after the class identifier and the "{", usually where the base class is specified. For other blocks, there isn't much to check, so we always pass. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ pass def CheckEnd(self, filename, clean_lines, linenum, error): """Run checks that applies to text after the closing brace. This is mostly used for checking end of namespace comments. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ pass def IsBlockInfo(self): """Returns true if this block is a _BlockInfo. This is convenient for verifying that an object is an instance of a _BlockInfo, but not an instance of any of the derived classes. Returns: True for this class, False for derived classes. """ return self.__class__ == _BlockInfo class _ExternCInfo(_BlockInfo): """Stores information about an 'extern "C"' block.""" def __init__(self, linenum): _BlockInfo.__init__(self, linenum, True) class _ClassInfo(_BlockInfo): """Stores information about a class.""" def __init__(self, name, class_or_struct, clean_lines, linenum): _BlockInfo.__init__(self, linenum, False) self.name = name self.is_derived = False self.check_namespace_indentation = True if class_or_struct == "struct": self.access = "public" self.is_struct = True else: self.access = "private" self.is_struct = False # Remember initial indentation level for this class. Using raw_lines here # instead of elided to account for leading comments. self.class_indent = GetIndentLevel(clean_lines.raw_lines[linenum]) # Try to find the end of the class. This will be confused by things like: # class A { # } *x = { ... # # But it's still good enough for CheckSectionSpacing. self.last_line = 0 depth = 0 for i in range(linenum, clean_lines.NumLines()): line = clean_lines.elided[i] depth += line.count("{") - line.count("}") if not depth: self.last_line = i break def CheckBegin(self, filename, clean_lines, linenum, error): # Look for a bare ':' if re.search("(^|[^:]):($|[^:])", clean_lines.elided[linenum]): self.is_derived = True def CheckEnd(self, filename, clean_lines, linenum, error): # If there is a DISALLOW macro, it should appear near the end of # the class. seen_last_thing_in_class = False for i in range(linenum - 1, self.starting_linenum, -1): match = re.search( r"\b(DISALLOW_COPY_AND_ASSIGN|DISALLOW_IMPLICIT_CONSTRUCTORS)\(" + self.name + r"\)", clean_lines.elided[i], ) if match: if seen_last_thing_in_class: error( filename, i, "readability/constructors", 3, match.group(1) + " should be the last thing in the class", ) break if not re.match(r"^\s*$", clean_lines.elided[i]): seen_last_thing_in_class = True # Check that closing brace is aligned with beginning of the class. # Only do this if the closing brace is indented by only whitespaces. # This means we will not check single-line class definitions. indent = re.match(r"^( *)\}", clean_lines.elided[linenum]) if indent and len(indent.group(1)) != self.class_indent: if self.is_struct: parent = "struct " + self.name else: parent = "class " + self.name error( filename, linenum, "whitespace/indent", 3, f"Closing brace should be aligned with beginning of {parent}", ) class _ConstructorInfo(_BlockInfo): """Stores information about a constructor. For detecting member initializer lists.""" def __init__(self, linenum: int): _BlockInfo.__init__(self, linenum, seen_open_brace=False) class _NamespaceInfo(_BlockInfo): """Stores information about a namespace.""" def __init__(self, name, linenum): _BlockInfo.__init__(self, linenum, False) self.name = name or "" self.check_namespace_indentation = True def CheckEnd(self, filename, clean_lines, linenum, error): """Check end of namespace comments.""" line = clean_lines.raw_lines[linenum] # Check how many lines is enclosed in this namespace. Don't issue # warning for missing namespace comments if there aren't enough # lines. However, do apply checks if there is already an end of # namespace comment and it's incorrect. # # TODO(google): We always want to check end of namespace comments # if a namespace is large, but sometimes we also want to apply the # check if a short namespace contained nontrivial things (something # other than forward declarations). There is currently no logic on # deciding what these nontrivial things are, so this check is # triggered by namespace size only, which works most of the time. if linenum - self.starting_linenum < 10 and not re.match( r"^\s*};*\s*(//|/\*).*\bnamespace\b", line ): return # Look for matching comment at end of namespace. # # Note that we accept C style "/* */" comments for terminating # namespaces, so that code that terminate namespaces inside # preprocessor macros can be cpplint clean. # # We also accept stuff like "// end of namespace ." with the # period at the end. # # Besides these, we don't accept anything else, otherwise we might # get false negatives when existing comment is a substring of the # expected namespace. if self.name: # Named namespace if not re.match( (r"^\s*};*\s*(//|/\*).*\bnamespace\s+" + re.escape(self.name) + r"[\*/\.\\\s]*$"), line, ): error( filename, linenum, "readability/namespace", 5, f'Namespace should be terminated with "// namespace {self.name}"', ) else: # Anonymous namespace if not re.match(r"^\s*};*\s*(//|/\*).*\bnamespace[\*/\.\\\s]*$", line): # If "// namespace anonymous" or "// anonymous namespace (more text)", # mention "// anonymous namespace" as an acceptable form if re.match(r"^\s*}.*\b(namespace anonymous|anonymous namespace)\b", line): error( filename, linenum, "readability/namespace", 5, 'Anonymous namespace should be terminated with "// namespace"' ' or "// anonymous namespace"', ) else: error( filename, linenum, "readability/namespace", 5, 'Anonymous namespace should be terminated with "// namespace"', ) class _WrappedInfo(_BlockInfo): """Stores information about parentheses, initializer lists, etc. Not exactly a block but we do need the same signature. Needed to avoid namespace indentation false positives, though parentheses tracking would slow us down a lot and is effectively already done by open_parentheses.""" pass class _MemInitListInfo(_WrappedInfo): """Stores information about member initializer lists.""" pass class _PreprocessorInfo: """Stores checkpoints of nesting stacks when #if/#else is seen.""" def __init__(self, stack_before_if): # The entire nesting stack before #if self.stack_before_if = stack_before_if # The entire nesting stack up to #else self.stack_before_else = [] # Whether we have already seen #else or #elif self.seen_else = False class NestingState: """Holds states related to parsing braces.""" def __init__(self): # Stack for tracking all braces. An object is pushed whenever we # see a "{", and popped when we see a "}". Only 3 types of # objects are possible: # - _ClassInfo: a class or struct. # - _NamespaceInfo: a namespace. # - _BlockInfo: some other type of block. self.stack: list[_BlockInfo] = [] # Top of the previous stack before each Update(). # # Because the nesting_stack is updated at the end of each line, we # had to do some convoluted checks to find out what is the current # scope at the beginning of the line. This check is simplified by # saving the previous top of nesting stack. # # We could save the full stack, but we only need the top. Copying # the full nesting stack would slow down cpplint by ~10%. self.previous_stack_top: _BlockInfo | None = None # The number of open parentheses in the previous stack top before the last update. # Used to prevent false indentation detection when e.g. a function parameter is indented. # We can't use previous_stack_top, a shallow copy whose open_parentheses value is updated. self.previous_open_parentheses = 0 # The last stack item we popped. self.popped_top: _BlockInfo | None = None # Stack of _PreprocessorInfo objects. self.pp_stack = [] def SeenOpenBrace(self): """Check if we have seen the opening brace for the innermost block. Returns: True if we have seen the opening brace, False if the innermost block is still expecting an opening brace. """ return (not self.stack) or self.stack[-1].seen_open_brace def InNamespaceBody(self): """Check if we are currently one level inside a namespace body. Returns: True if top of the stack is a namespace block, False otherwise. """ return self.stack and isinstance(self.stack[-1], _NamespaceInfo) def InExternC(self): """Check if we are currently one level inside an 'extern "C"' block. Returns: True if top of the stack is an extern block, False otherwise. """ return self.stack and isinstance(self.stack[-1], _ExternCInfo) def InClassDeclaration(self): """Check if we are currently one level inside a class or struct declaration. Returns: True if top of the stack is a class/struct, False otherwise. """ return self.stack and isinstance(self.stack[-1], _ClassInfo) def InAsmBlock(self): """Check if we are currently one level inside an inline ASM block. Returns: True if the top of the stack is a block containing inline ASM. """ return self.stack and self.stack[-1].inline_asm != _NO_ASM def InTemplateArgumentList(self, clean_lines, linenum, pos): """Check if current position is inside template argument list. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. pos: position just after the suspected template argument. Returns: True if (linenum, pos) is inside template arguments. """ while linenum < clean_lines.NumLines(): # Find the earliest character that might indicate a template argument line = clean_lines.elided[linenum] match = re.match(r"^[^{};=\[\]\.<>]*(.)", line[pos:]) if not match: linenum += 1 pos = 0 continue token = match.group(1) pos += len(match.group(0)) # These things do not look like template argument list: # class Suspect { # class Suspect x; } if token in ("{", "}", ";"): return False # These things look like template argument list: # template # template # template # template if token in (">", "=", "[", "]", "."): return True # Check if token is an unmatched '<'. # If not, move on to the next character. if token != "<": pos += 1 if pos >= len(line): linenum += 1 pos = 0 continue # We can't be sure if we just find a single '<', and need to # find the matching '>'. (_, end_line, end_pos) = CloseExpression(clean_lines, linenum, pos - 1) if end_pos < 0: # Not sure if template argument list or syntax error in file return False linenum = end_line pos = end_pos return False def UpdatePreprocessor(self, line): """Update preprocessor stack. We need to handle preprocessors due to classes like this: #ifdef SWIG struct ResultDetailsPageElementExtensionPoint { #else struct ResultDetailsPageElementExtensionPoint : public Extension { #endif We make the following assumptions (good enough for most files): - Preprocessor condition evaluates to true from #if up to first #else/#elif/#endif. - Preprocessor condition evaluates to false from #else/#elif up to #endif. We still perform lint checks on these lines, but these do not affect nesting stack. Args: line: current line to check. """ if re.match(r"^\s*#\s*(if|ifdef|ifndef)\b", line): # Beginning of #if block, save the nesting stack here. The saved # stack will allow us to restore the parsing state in the #else case. self.pp_stack.append(_PreprocessorInfo(copy.deepcopy(self.stack))) elif re.match(r"^\s*#\s*(else|elif)\b", line): # Beginning of #else block if self.pp_stack: if not self.pp_stack[-1].seen_else: # This is the first #else or #elif block. Remember the # whole nesting stack up to this point. This is what we # keep after the #endif. self.pp_stack[-1].seen_else = True self.pp_stack[-1].stack_before_else = copy.deepcopy(self.stack) # Restore the stack to how it was before the #if self.stack = copy.deepcopy(self.pp_stack[-1].stack_before_if) else: # TODO(google): unexpected #else, issue warning? pass elif re.match(r"^\s*#\s*endif\b", line): # End of #if or #else blocks. if self.pp_stack: # If we saw an #else, we will need to restore the nesting # stack to its former state before the #else, otherwise we # will just continue from where we left off. if self.pp_stack[-1].seen_else: # Here we can just use a shallow copy since we are the last # reference to it. self.stack = self.pp_stack[-1].stack_before_else # Drop the corresponding #if self.pp_stack.pop() else: # TODO(google): unexpected #endif, issue warning? pass def _Pop(self): """Pop the innermost state (top of the stack) and remember the popped item.""" self.popped_top = self.stack.pop() def _CountOpenParentheses(self, line: str): # Count parentheses. This is to avoid adding struct arguments to # the nesting stack. if self.stack: inner_block = self.stack[-1] depth_change = line.count("(") - line.count(")") inner_block.open_parentheses += depth_change # Also check if we are starting or ending an inline assembly block. if inner_block.inline_asm in (_NO_ASM, _END_ASM): if ( depth_change != 0 and inner_block.open_parentheses == 1 and _MATCH_ASM.match(line) ): # Enter assembly block inner_block.inline_asm = _INSIDE_ASM else: # Not entering assembly block. If previous line was _END_ASM, # we will now shift to _NO_ASM state. inner_block.inline_asm = _NO_ASM elif inner_block.inline_asm == _INSIDE_ASM and inner_block.open_parentheses == 0: # Exit assembly block inner_block.inline_asm = _END_ASM def _UpdateNamesapce(self, line: str, linenum: int) -> str | None: """ Match start of namespace, append to stack, and consume line Args: line: Line to check and consume linenum: Line number of the line to check Returns: The consumed line if namespace matched; None otherwise """ # Match start of namespace. The "\b\s*" below catches namespace # declarations even if it weren't followed by a whitespace, this # is so that we don't confuse our namespace checker. The # missing spaces will be flagged by CheckSpacing. namespace_decl_match = re.match(r"^\s*namespace\b\s*([:\w]+)?(.*)$", line) if not namespace_decl_match: return None new_namespace = _NamespaceInfo(namespace_decl_match.group(1), linenum) self.stack.append(new_namespace) line = namespace_decl_match.group(2) if line.find("{") != -1: new_namespace.seen_open_brace = True line = line[line.find("{") + 1 :] return line def _UpdateConstructor(self, line: str, linenum: int, class_name: str | None = None): """ Check if the given line is a constructor. Args: line: Line to check. class_name: If line checked is inside of a class block, a str of the class's name; otherwise, None. """ if not class_name: if not re.match(r"\s*(\w*)\s*::\s*\1\s*\(", line): return elif not re.match(rf"\s*{re.escape(class_name)}\s*\(", line): return self.stack.append(_ConstructorInfo(linenum)) # TODO(google): Update() is too long, but we will refactor later. def Update(self, filename: str, clean_lines: CleansedLines, linenum: int, error): """Update nesting state with current line. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Remember top of the previous nesting stack. # # The stack is always pushed/popped and not modified in place, so # we can just do a shallow copy instead of copy.deepcopy. Using # deepcopy would slow down cpplint by ~28%. if self.stack: self.previous_stack_top = self.stack[-1] self.previous_open_parentheses = self.stack[-1].open_parentheses else: self.previous_stack_top = None # Update pp_stack self.UpdatePreprocessor(line) self._CountOpenParentheses(line) # Consume namespace declaration at the beginning of the line. Do # this in a loop so that we catch same line declarations like this: # namespace proto2 { namespace bridge { class MessageSet; } } while (new_line := self._UpdateNamesapce(line, linenum)) is not None: # could be empty str line = new_line # Look for a class declaration in whatever is left of the line # after parsing namespaces. The regexp accounts for decorated classes # such as in: # class LOCKABLE API Object { # }; class_decl_match = re.match( r"^(\s*(?:template\s*<[\w\s<>,:=]*>\s*)?" r"(class|struct)\s+(?:[a-zA-Z0-9_]+\s+)*(\w+(?:::\w+)*))" r"(.*)$", line, ) if class_decl_match and (not self.stack or self.stack[-1].open_parentheses == 0): # We do not want to accept classes that are actually template arguments: # template , # template class Ignore3> # void Function() {}; # # To avoid template argument cases, we scan forward and look for # an unmatched '>'. If we see one, assume we are inside a # template argument list. end_declaration = len(class_decl_match.group(1)) if not self.InTemplateArgumentList(clean_lines, linenum, end_declaration): self.stack.append( _ClassInfo( class_decl_match.group(3), class_decl_match.group(2), clean_lines, linenum ) ) line = class_decl_match.group(4) # If we have not yet seen the opening brace for the innermost block, # run checks here. if not self.SeenOpenBrace(): self.stack[-1].CheckBegin(filename, clean_lines, linenum, error) # Update access control if we are directly inside a class/struct if self.stack and isinstance(self.stack[-1], _ClassInfo): if self.stack[-1].seen_open_brace: classinfo: _ClassInfo = self.stack[-1] # Update access control if access_match := re.match( r"^(.*)\b(public|private|protected|signals)(\s+(?:slots\s*)?)?" r":([^:].*|$)", line, ): classinfo.access = access_match.group(2) # Check that access keywords are indented +1 space. Skip this # check if the keywords are not preceded by whitespaces. indent = access_match.group(1) if len(indent) != classinfo.class_indent + 1 and re.match(r"^\s*$", indent): if classinfo.is_struct: parent = "struct " + classinfo.name else: parent = "class " + classinfo.name slots = "" if access_match.group(3): slots = access_match.group(3) error( filename, linenum, "whitespace/indent", 3, f"{access_match.group(2)}{slots}:" f" should be indented +1 space inside {parent}", ) line = access_match.group(4) else: self._UpdateConstructor(line, linenum, class_name=classinfo.name) else: # Not in class self._UpdateConstructor(line, linenum) # If brace not open and we just finished a parenthetical definition, # check if we're in a member initializer list following a constructor. if ( self.stack and ( isinstance(self.stack[-1], _ConstructorInfo) or isinstance(self.previous_stack_top, _ConstructorInfo) ) and not self.stack[-1].seen_open_brace and re.search(r"[^:]:[^:]", line) ): self.stack.append(_MemInitListInfo(linenum, seen_open_brace=False)) # Consume braces or semicolons from what's left of the line while True: # Match first brace, semicolon, or closed parenthesis. matched = re.match(r"^[^{;)}]*([{;)}])(.*)$", line) if not matched: break token = matched.group(1) if token == "{": # If namespace or class hasn't seen an opening brace yet, mark # namespace/class head as complete. Push a new block onto the # stack otherwise. if not self.SeenOpenBrace(): # End of initializer list wrap if present if isinstance(self.stack[-1], _MemInitListInfo): self._Pop() self.stack[-1].seen_open_brace = True elif re.match(r'^extern\s*"[^"]*"\s*\{', line): self.stack.append(_ExternCInfo(linenum)) else: self.stack.append(_BlockInfo(linenum, True)) if _MATCH_ASM.match(line): self.stack[-1].inline_asm = _BLOCK_ASM elif token == ";": # If we haven't seen an opening brace yet, but we already saw # a semicolon, this is probably a forward declaration. Pop # the stack for these. if not self.SeenOpenBrace(): self._Pop() elif token == ")": # Similarly, if we haven't seen an opening brace yet, but we # already saw a closing parenthesis, then these are probably # function arguments with extra "class" or "struct" keywords. # Also pop these stack for these. if ( self.stack and not self.stack[-1].seen_open_brace and isinstance(self.stack[-1], _ClassInfo) ): self._Pop() else: # token == '}' # Perform end of block checks and pop the stack. if self.stack: self.stack[-1].CheckEnd(filename, clean_lines, linenum, error) self._Pop() line = matched.group(2) def InnermostClass(self): """Get class info on the top of the stack. Returns: A _ClassInfo object if we are inside a class, or None otherwise. """ for i in range(len(self.stack), 0, -1): classinfo = self.stack[i - 1] if isinstance(classinfo, _ClassInfo): return classinfo return None def CheckForNonStandardConstructs(filename, clean_lines, linenum, nesting_state, error): r"""Logs an error if we see certain non-ANSI constructs ignored by gcc-2. Complain about several constructs which gcc-2 accepts, but which are not standard C++. Warning about these in lint is one way to ease the transition to new compilers. - put storage class first (e.g. "static const" instead of "const static"). - "%lld" instead of %qd" in printf-type functions. - "%1$d" is non-standard in printf-type functions. - "\%" is an undefined character escape sequence. - text after #endif is not allowed. - invalid inner-style forward declaration. - >? and ?= and )\?=?\s*(\w+|[+-]?\d+)(\.\d*)?", line): error( filename, linenum, "build/deprecated", 3, ">? and ))?' # r'\s*const\s*' + type_name + '\s*&\s*\w+\s*;' error( filename, linenum, "runtime/member_string_references", 2, "const string& members are dangerous. It is much better to use " "alternatives, such as pointers or simple constants.", ) # Everything else in this function operates on class declarations. # Return early if the top of the nesting stack is not a class, or if # the class head is not completed yet. classinfo = nesting_state.InnermostClass() if not classinfo or not classinfo.seen_open_brace: return # The class may have been declared with namespace or classname qualifiers. # The constructor and destructor will not have those qualifiers. base_classname = classinfo.name.split("::")[-1] # Look for single-argument constructors that aren't marked explicit. # Technically a valid construct, but against style. explicit_constructor_match = re.match( r"\s+(?:(?:inline|constexpr)\s+)*(explicit\s+)?" rf"(?:(?:inline|constexpr)\s+)*{re.escape(base_classname)}\s*" r"\(((?:[^()]|\([^()]*\))*)\)", line, ) if explicit_constructor_match: is_marked_explicit = explicit_constructor_match.group(1) if not explicit_constructor_match.group(2): constructor_args = [] else: constructor_args = explicit_constructor_match.group(2).split(",") # collapse arguments so that commas in template parameter lists and function # argument parameter lists don't split arguments in two i = 0 while i < len(constructor_args): constructor_arg = constructor_args[i] while constructor_arg.count("<") > constructor_arg.count(">") or constructor_arg.count( "(" ) > constructor_arg.count(")"): constructor_arg += "," + constructor_args[i + 1] del constructor_args[i + 1] constructor_args[i] = constructor_arg i += 1 variadic_args = [arg for arg in constructor_args if "&&..." in arg] defaulted_args = [arg for arg in constructor_args if "=" in arg] noarg_constructor = ( not constructor_args # empty arg list or # 'void' arg specifier (len(constructor_args) == 1 and constructor_args[0].strip() == "void") ) onearg_constructor = ( ( len(constructor_args) == 1 # exactly one arg and not noarg_constructor ) or # all but at most one arg defaulted ( len(constructor_args) >= 1 and not noarg_constructor and len(defaulted_args) >= len(constructor_args) - 1 ) or # variadic arguments with zero or one argument (len(constructor_args) <= 2 and len(variadic_args) >= 1) ) initializer_list_constructor = bool( onearg_constructor and re.search(r"\bstd\s*::\s*initializer_list\b", constructor_args[0]) ) copy_constructor = bool( onearg_constructor and re.match( r"((const\s+(volatile\s+)?)?|(volatile\s+(const\s+)?))?" rf"{re.escape(base_classname)}(\s*<[^>]*>)?(\s+const)?\s*(?:<\w+>\s*)?&", constructor_args[0].strip(), ) ) if ( not is_marked_explicit and onearg_constructor and not initializer_list_constructor and not copy_constructor ): if defaulted_args or variadic_args: error( filename, linenum, "runtime/explicit", 4, "Constructors callable with one argument should be marked explicit.", ) else: error( filename, linenum, "runtime/explicit", 4, "Single-parameter constructors should be marked explicit.", ) def CheckSpacingForFunctionCall(filename, clean_lines, linenum, error): """Checks for the correctness of various spacing around function calls. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Since function calls often occur inside if/for/while/switch # expressions - which have their own, more liberal conventions - we # first see if we should be looking inside such an expression for a # function call, to which we can apply more strict standards. fncall = line # if there's no control flow construct, look at whole line for pattern in ( r"\bif\s*\((.*)\)\s*{", r"\bfor\s*\((.*)\)\s*{", r"\bwhile\s*\((.*)\)\s*[{;]", r"\bswitch\s*\((.*)\)\s*{", ): match = re.search(pattern, line) if match: fncall = match.group(1) # look inside the parens for function calls break # Except in if/for/while/switch, there should never be space # immediately inside parens (eg "f( 3, 4 )"). We make an exception # for nested parens ( (a+b) + c ). Likewise, there should never be # a space before a ( when it's a function argument. I assume it's a # function argument when the char before the whitespace is legal in # a function name (alnum + _) and we're not starting a macro. Also ignore # pointers and references to arrays and functions coz they're too tricky: # we use a very simple way to recognize these: # " (something)(maybe-something)" or # " (something)(maybe-something," or # " (something)[something]" # Note that we assume the contents of [] to be short enough that # they'll never need to wrap. if ( # Ignore control structures. not re.search(r"\b(if|elif|for|while|switch|return|new|delete|catch|sizeof)\b", fncall) and # Ignore pointers/references to functions. not re.search(r" \([^)]+\)\([^)]*(\)|,$)", fncall) and # Ignore pointers/references to arrays. not re.search(r" \([^)]+\)\[[^\]]+\]", fncall) ): if re.search(r"\w\s*\(\s(?!\s*\\$)", fncall): # a ( used for a fn call error(filename, linenum, "whitespace/parens", 4, "Extra space after ( in function call") elif re.search(r"\(\s+(?!(\s*\\)|\()", fncall): error(filename, linenum, "whitespace/parens", 2, "Extra space after (") if ( re.search(r"\w\s+\(", fncall) and not re.search(r"_{0,2}asm_{0,2}\s+_{0,2}volatile_{0,2}\s+\(", fncall) and not re.search(r"#\s*define|typedef|using\s+\w+\s*=", fncall) and not re.search(r"\w\s+\((\w+::)*\*\w+\)\(", fncall) and not re.search(r"\bcase\s+\(", fncall) ): # TODO(google): Space after an operator function seem to be a common # error, silence those for now by restricting them to highest verbosity. if re.search(r"\boperator_*\b", line): error( filename, linenum, "whitespace/parens", 0, "Extra space before ( in function call", ) else: error( filename, linenum, "whitespace/parens", 4, "Extra space before ( in function call", ) # If the ) is followed only by a newline or a { + newline, assume it's # part of a control statement (if/while/etc), and don't complain if re.search(r"[^)]\s+\)\s*[^{\s]", fncall): # If the closing parenthesis is preceded by only whitespaces, # try to give a more descriptive error message. if re.search(r"^\s+\)", fncall): error( filename, linenum, "whitespace/parens", 2, "Closing ) should be moved to the previous line", ) else: error(filename, linenum, "whitespace/parens", 2, "Extra space before )") def IsBlankLine(line): """Returns true if the given line is blank. We consider a line to be blank if the line is empty or consists of only white spaces. Args: line: A line of a string. Returns: True, if the given line is blank. """ return not line or line.isspace() def CheckForNamespaceIndentation(filename, nesting_state, clean_lines, line, error): is_namespace_indent_item = len(nesting_state.stack) >= 1 and ( isinstance(nesting_state.stack[-1], _NamespaceInfo) or (isinstance(nesting_state.previous_stack_top, _NamespaceInfo)) ) if ShouldCheckNamespaceIndentation( nesting_state, is_namespace_indent_item, clean_lines.elided, line ): CheckItemIndentationInNamespace(filename, clean_lines.elided, line, error) def CheckForFunctionLengths(filename, clean_lines, linenum, function_state, error): """Reports for long function bodies. For an overview why this is done, see: https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Write_Short_Functions Uses a simplistic algorithm assuming other style guidelines (especially spacing) are followed. Only checks unindented functions, so class members are unchecked. Trivial bodies are unchecked, so constructors with huge initializer lists may be missed. Blank/comment lines are not counted so as to avoid encouraging the removal of vertical space and comments just to get through a lint check. NOLINT *on the last line of a function* disables this check. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. function_state: Current function name and lines in body so far. error: The function to call with any errors found. """ lines = clean_lines.lines line = lines[linenum] joined_line = "" starting_func = False regexp = r"(\w(\w|::|\*|\&|\s)*)\(" # decls * & space::name( ... if match_result := re.match(regexp, line): # If the name is all caps and underscores, figure it's a macro and # ignore it, unless it's TEST or TEST_F. function_name = match_result.group(1).split()[-1] if function_name in {"TEST", "TEST_F"} or not re.match(r"[A-Z_]+$", function_name): starting_func = True if starting_func: body_found = False for start_linenum in range(linenum, clean_lines.NumLines()): start_line = lines[start_linenum] joined_line += " " + start_line.lstrip() if re.search(r"(;|})", start_line): # Declarations and trivial functions body_found = True break # ... ignore if re.search(r"{", start_line): body_found = True function = re.search(r"((\w|:)*)\(", line).group(1) if re.match(r"TEST", function): # Handle TEST... macros parameter_regexp = re.search(r"(\(.*\))", joined_line) if parameter_regexp: # Ignore bad syntax function += parameter_regexp.group(1) else: function += "()" function_state.Begin(function) break if not body_found: # No body for the function (or evidence of a non-function) was found. error( filename, linenum, "readability/fn_size", 5, "Lint failed to find start of function body.", ) elif re.match(r"^\}\s*$", line): # function end function_state.Check(error, filename, linenum) function_state.End() elif not re.match(r"^\s*$", line): function_state.Count() # Count non-blank/non-comment lines. _RE_PATTERN_TODO = re.compile(r"^//(\s*)TODO(\(.+?\))?:?(\s|$)?") def CheckComment(line, filename, linenum, next_line_start, error): """Checks for common mistakes in comments. Args: line: The line in question. filename: The name of the current file. linenum: The number of the line to check. next_line_start: The first non-whitespace column of the next line. error: The function to call with any errors found. """ commentpos = line.find("//") if commentpos != -1: # Check if the // may be in quotes. If so, ignore it if re.sub(r"\\.", "", line[0:commentpos]).count('"') % 2 == 0: # Allow one space for new scopes, two spaces otherwise: if not (re.match(r"^.*{ *//", line) and next_line_start == commentpos) and ( (commentpos >= 1 and line[commentpos - 1] not in string.whitespace) or (commentpos >= 2 and line[commentpos - 2] not in string.whitespace) ): error( filename, linenum, "whitespace/comments", 2, "At least two spaces is best between code and comments", ) # Checks for common mistakes in TODO comments. comment = line[commentpos:] match = _RE_PATTERN_TODO.match(comment) if match: # One whitespace is correct; zero whitespace is handled elsewhere. leading_whitespace = match.group(1) if len(leading_whitespace) > 1: error(filename, linenum, "whitespace/todo", 2, "Too many spaces before TODO") username = match.group(2) if not username: error( filename, linenum, "readability/todo", 2, "Missing username in TODO; it should look like " '"// TODO(my_username): Stuff."', ) middle_whitespace = match.group(3) # Comparisons made explicit for correctness # -- pylint: disable=g-explicit-bool-comparison if middle_whitespace not in {" ", ""}: error( filename, linenum, "whitespace/todo", 2, "TODO(my_username) should be followed by a space", ) # If the comment contains an alphanumeric character, there # should be a space somewhere between it and the // unless # it's a /// or //! Doxygen comment. if re.match(r"//[^ ]*\w", comment) and not re.match(r"(///|//\!)(\s+|$)", comment): error( filename, linenum, "whitespace/comments", 4, "Should have a space between // and comment", ) def CheckSpacing(filename, clean_lines, linenum, nesting_state, error): """Checks for the correctness of various spacing issues in the code. Things we check for: spaces around operators, spaces after if/for/while/switch, no spaces around parens in function calls, two spaces between code and comment, don't start a block with a blank line, don't end a function with a blank line, don't add a blank line after public/protected/private, don't have too many blank lines in a row. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. nesting_state: A NestingState instance which maintains information about the current stack of nested blocks being parsed. error: The function to call with any errors found. """ # Don't use "elided" lines here, otherwise we can't check commented lines. # Don't want to use "raw" either, because we don't want to check inside C++11 # raw strings, raw = clean_lines.lines_without_raw_strings line = raw[linenum] # Before nixing comments, check if the line is blank for no good # reason. This includes the first line after a block is opened, and # blank lines at the end of a function (ie, right before a line like '}' # # Skip all the blank line checks if we are immediately inside a # namespace body. In other words, don't issue blank line warnings # for this block: # namespace { # # } # # A warning about missing end of namespace comments will be issued instead. # # Also skip blank line checks for 'extern "C"' blocks, which are formatted # like namespaces. if IsBlankLine(line) and not nesting_state.InNamespaceBody() and not nesting_state.InExternC(): elided = clean_lines.elided prev_line = elided[linenum - 1] prevbrace = prev_line.rfind("{") # TODO(google): Don't complain if line before blank line, and line after, # both start with alnums and are indented the same amount. # This ignores whitespace at the start of a namespace block # because those are not usually indented. if prevbrace != -1 and prev_line[prevbrace:].find("}") == -1: # OK, we have a blank line at the start of a code block. Before we # complain, we check if it is an exception to the rule: The previous # non-empty line has the parameters of a function header that are indented # 4 spaces (because they did not fit in a 80 column line when placed on # the same line as the function name). We also check for the case where # the previous line is indented 6 spaces, which may happen when the # initializers of a constructor do not fit into a 80 column line. exception = False if re.match(r" {6}\w", prev_line): # Initializer list? # We are looking for the opening column of initializer list, which # should be indented 4 spaces to cause 6 space indentation afterwards. search_position = linenum - 2 while search_position >= 0 and re.match(r" {6}\w", elided[search_position]): search_position -= 1 exception = search_position >= 0 and elided[search_position][:5] == " :" else: # Search for the function arguments or an initializer list. We use a # simple heuristic here: If the line is indented 4 spaces; and we have a # closing paren, without the opening paren, followed by an opening brace # or colon (for initializer lists) we assume that it is the last line of # a function header. If we have a colon indented 4 spaces, it is an # initializer list. exception = re.match( r" {4}\w[^\(]*\)\s*(const\s*)?(\{\s*$|:)", prev_line ) or re.match(r" {4}:", prev_line) if not exception: error( filename, linenum, "whitespace/blank_line", 2, "Redundant blank line at the start of a code block should be deleted.", ) # Ignore blank lines at the end of a block in a long if-else # chain, like this: # if (condition1) { # // Something followed by a blank line # # } else if (condition2) { # // Something else # } if linenum + 1 < clean_lines.NumLines(): next_line = raw[linenum + 1] if next_line and re.match(r"\s*}", next_line) and next_line.find("} else ") == -1: error( filename, linenum, "whitespace/blank_line", 3, "Redundant blank line at the end of a code block should be deleted.", ) matched = re.match(r"\s*(public|protected|private):", prev_line) if matched: error( filename, linenum, "whitespace/blank_line", 3, f'Do not leave a blank line after "{matched.group(1)}:"', ) # Next, check comments next_line_start = 0 if linenum + 1 < clean_lines.NumLines(): next_line = raw[linenum + 1] next_line_start = len(next_line) - len(next_line.lstrip()) CheckComment(line, filename, linenum, next_line_start, error) # get rid of comments and strings line = clean_lines.elided[linenum] # You shouldn't have spaces before your brackets, except for C++11 attributes # or maybe after 'delete []', 'return []() {};', or 'auto [abc, ...] = ...;'. if re.search(r"\w\s+\[(?!\[)", line) and not re.search(r"(?:auto&?|delete|return)\s+\[", line): error(filename, linenum, "whitespace/braces", 5, "Extra space before [") # In range-based for, we wanted spaces before and after the colon, but # not around "::" tokens that might appear. if re.search(r"for *\(.*[^:]:[^: ]", line) or re.search(r"for *\(.*[^: ]:[^:]", line): error( filename, linenum, "whitespace/forcolon", 2, "Missing space around colon in range-based for loop", ) def CheckOperatorSpacing(filename, clean_lines, linenum, error): """Checks for horizontal spacing around operators. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Don't try to do spacing checks for operator methods. Do this by # replacing the troublesome characters with something else, # preserving column position for all other characters. # # The replacement is done repeatedly to avoid false positives from # operators that call operators. while True: match = re.match(r"^(.*\boperator\b)(\S+)(\s*\(.*)$", line) if match: line = match.group(1) + ("_" * len(match.group(2))) + match.group(3) else: break # We allow no-spaces around = within an if: "if ( (a=Foo()) == 0 )". # Otherwise not. Note we only check for non-spaces on *both* sides; # sometimes people put non-spaces on one side when aligning ='s among # many lines (not that this is behavior that I approve of...) if ( (re.search(r"[\w.]=", line) or re.search(r"=[\w.]", line)) and not re.search(r"\b(if|while|for) ", line) # Operators taken from [lex.operators] in C++11 standard. and not re.search(r"(>=|<=|==|!=|&=|\^=|\|=|\+=|\*=|\/=|\%=)", line) and not re.search(r"operator=", line) ): error(filename, linenum, "whitespace/operators", 4, "Missing spaces around =") # It's ok not to have spaces around binary operators like + - * /, but if # there's too little whitespace, we get concerned. It's hard to tell, # though, so we punt on this one for now. TODO(google). # You should always have whitespace around binary operators. # # Check <= and >= first to avoid false positives with < and >, then # check non-include lines for spacing around < and >. # # If the operator is followed by a comma, assume it's be used in a # macro context and don't do any checks. This avoids false # positives. # # Note that && is not included here. This is because there are too # many false positives due to RValue references. match = re.search(r"[^<>=!\s](==|!=|<=|>=|\|\|)[^<>=!\s,;\)]", line) if match: # TODO(google): support alternate operators error( filename, linenum, "whitespace/operators", 3, f"Missing spaces around {match.group(1)}" ) elif not re.match(r"#.*include", line): # Look for < that is not surrounded by spaces. This is only # triggered if both sides are missing spaces, even though # technically should should flag if at least one side is missing a # space. This is done to avoid some false positives with shifts. match = re.match(r"^(.*[^\s<])<[^\s=<,]", line) if match: (_, _, end_pos) = CloseExpression(clean_lines, linenum, len(match.group(1))) if end_pos <= -1: error(filename, linenum, "whitespace/operators", 3, "Missing spaces around <") # Look for > that is not surrounded by spaces. Similar to the # above, we only trigger if both sides are missing spaces to avoid # false positives with shifts. match = re.match(r"^(.*[^-\s>])>[^\s=>,]", line) if match: (_, _, start_pos) = ReverseCloseExpression(clean_lines, linenum, len(match.group(1))) if start_pos <= -1: error(filename, linenum, "whitespace/operators", 3, "Missing spaces around >") # We allow no-spaces around << when used like this: 10<<20, but # not otherwise (particularly, not when used as streams) # # We also allow operators following an opening parenthesis, since # those tend to be macros that deal with operators. match = re.search(r"(operator|[^\s(<])(?:L|UL|LL|ULL|l|ul|ll|ull)?<<([^\s,=<])", line) if ( match and not (match.group(1).isdigit() and match.group(2).isdigit()) and not (match.group(1) == "operator" and match.group(2) == ";") ): error(filename, linenum, "whitespace/operators", 3, "Missing spaces around <<") # We allow no-spaces around >> for almost anything. This is because # C++11 allows ">>" to close nested templates, which accounts for # most cases when ">>" is not followed by a space. # # We still warn on ">>" followed by alpha character, because that is # likely due to ">>" being used for right shifts, e.g.: # value >> alpha # # When ">>" is used to close templates, the alphanumeric letter that # follows would be part of an identifier, and there should still be # a space separating the template type and the identifier. # type> alpha match = re.search(r">>[a-zA-Z_]", line) if match: error(filename, linenum, "whitespace/operators", 3, "Missing spaces around >>") # There shouldn't be space around unary operators match = re.search(r"(!\s|~\s|[\s]--[\s;]|[\s]\+\+[\s;])", line) if match: error( filename, linenum, "whitespace/operators", 4, f"Extra space for operator {match.group(1)}", ) def CheckParenthesisSpacing(filename, clean_lines, linenum, error): """Checks for horizontal spacing around parentheses. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # No spaces after an if, while, switch, or for match = re.search(r" (if\(|for\(|while\(|switch\()", line) if match: error( filename, linenum, "whitespace/parens", 5, f"Missing space before ( in {match.group(1)}" ) # For if/for/while/switch, the left and right parens should be # consistent about how many spaces are inside the parens, and # there should either be zero or one spaces inside the parens. # We don't want: "if ( foo)" or "if ( foo )". # Exception: "for ( ; foo; bar)" and "for (foo; bar; )" are allowed. match = re.search( r"\b(if|for|while|switch)\s*" r"\(([ ]*)(.).*[^ ]+([ ]*)\)\s*{\s*$", line, ) if match: if len(match.group(2)) != len(match.group(4)) and not ( match.group(3) == ";" and len(match.group(2)) == 1 + len(match.group(4)) or not match.group(2) and re.search(r"\bfor\s*\(.*; \)", line) ): error( filename, linenum, "whitespace/parens", 5, f"Mismatching spaces inside () in {match.group(1)}", ) if len(match.group(2)) not in [0, 1]: error( filename, linenum, "whitespace/parens", 5, f"Should have zero or one spaces inside ( and ) in {match.group(1)}", ) def CheckCommaSpacing(filename, clean_lines, linenum, error): """Checks for horizontal spacing near commas and semicolons. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ raw = clean_lines.lines_without_raw_strings line = clean_lines.elided[linenum] # You should always have a space after a comma (either as fn arg or operator) # # This does not apply when the non-space character following the # comma is another comma, since the only time when that happens is # for empty macro arguments. # # We run this check in two passes: first pass on elided lines to # verify that lines contain missing whitespaces, second pass on raw # lines to confirm that those missing whitespaces are not due to # elided comments. match = re.search( r",[^,\s]", re.sub(r"\b__VA_OPT__\s*\(,\)", "", re.sub(r"\boperator\s*,\s*\(", "F(", line)) ) if match and re.search(r",[^,\s]", raw[linenum]): error(filename, linenum, "whitespace/comma", 3, "Missing space after ,") # You should always have a space after a semicolon # except for few corner cases # TODO(google): clarify if 'if (1) { return 1;}' is requires one more # space after ; if re.search(r";[^\s};\\)/]", line): error(filename, linenum, "whitespace/semicolon", 3, "Missing space after ;") def _IsType(clean_lines, nesting_state, expr): """Check if expression looks like a type name, returns true if so. Args: clean_lines: A CleansedLines instance containing the file. nesting_state: A NestingState instance which maintains information about the current stack of nested blocks being parsed. expr: The expression to check. Returns: True, if token looks like a type. """ # Keep only the last token in the expression if last_word := re.match(r"^.*(\b\S+)$", expr): token = last_word.group(1) else: token = expr # Match native types and stdint types if _TYPES.match(token): return True # Try a bit harder to match templated types. Walk up the nesting # stack until we find something that resembles a typename # declaration for what we are looking for. typename_pattern = r"\b(?:typename|class|struct)\s+" + re.escape(token) + r"\b" block_index = len(nesting_state.stack) - 1 while block_index >= 0: if isinstance(nesting_state.stack[block_index], _NamespaceInfo): return False # Found where the opening brace is. We want to scan from this # line up to the beginning of the function, minus a few lines. # template # class C # : public ... { // start scanning here last_line = nesting_state.stack[block_index].starting_linenum next_block_start = 0 if block_index > 0: next_block_start = nesting_state.stack[block_index - 1].starting_linenum first_line = last_line while first_line >= next_block_start: if clean_lines.elided[first_line].find("template") >= 0: break first_line -= 1 if first_line < next_block_start: # Didn't find any "template" keyword before reaching the next block, # there are probably no template things to check for this block block_index -= 1 continue # Look for typename in the specified range for i in range(first_line, last_line + 1, 1): if re.search(typename_pattern, clean_lines.elided[i]): return True block_index -= 1 return False def CheckBracesSpacing(filename, clean_lines, linenum, nesting_state, error): """Checks for horizontal spacing near commas. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. nesting_state: A NestingState instance which maintains information about the current stack of nested blocks being parsed. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Except after an opening paren, or after another opening brace (in case of # an initializer list, for instance), you should have spaces before your # braces when they are delimiting blocks, classes, namespaces etc. # And since you should never have braces at the beginning of a line, # this is an easy test. Except that braces used for initialization don't # follow the same rule; we often don't want spaces before those. if match := re.match(r"^(.*[^ ({>]){", line): # Try a bit harder to check for brace initialization. This # happens in one of the following forms: # Constructor() : initializer_list_{} { ... } # Constructor{}.MemberFunction() # Type variable{}; # FunctionCall(type{}, ...); # LastArgument(..., type{}); # LOG(INFO) << type{} << " ..."; # map_of_type[{...}] = ...; # ternary = expr ? new type{} : nullptr; # OuterTemplate{}> # # We check for the character following the closing brace, and # silence the warning if it's one of those listed above, i.e. # "{.;,)<>]:". # # To account for nested initializer list, we allow any number of # closing braces up to "{;,)<". We can't simply silence the # warning on first sight of closing brace, because that would # cause false negatives for things that are not initializer lists. # Silence this: But not this: # Outer{ if (...) { # Inner{...} if (...){ // Missing space before { # }; } # # There is a false negative with this approach if people inserted # spurious semicolons, e.g. "if (cond){};", but we will catch the # spurious semicolon with a separate check. leading_text = match.group(1) (endline, endlinenum, endpos) = CloseExpression(clean_lines, linenum, len(match.group(1))) trailing_text = "" if endpos > -1: trailing_text = endline[endpos:] for offset in range(endlinenum + 1, min(endlinenum + 3, clean_lines.NumLines() - 1)): trailing_text += clean_lines.elided[offset] # We also suppress warnings for `uint64_t{expression}` etc., as the style # guide recommends brace initialization for integral types to avoid # overflow/truncation. if not re.match(r"^[\s}]*[{.;,)<>\]:]", trailing_text) and not _IsType( clean_lines, nesting_state, leading_text ): error(filename, linenum, "whitespace/braces", 5, "Missing space before {") # Make sure '} else {' has spaces. if re.search(r"}else", line): error(filename, linenum, "whitespace/braces", 5, "Missing space before else") # You shouldn't have a space before a semicolon at the end of the line. # There's a special case for "for" since the style guide allows space before # the semicolon there. if re.search(r":\s*;\s*$", line): error( filename, linenum, "whitespace/semicolon", 5, "Semicolon defining empty statement. Use {} instead.", ) elif re.search(r"^\s*;\s*$", line): error( filename, linenum, "whitespace/semicolon", 5, "Line contains only semicolon. If this should be an empty statement, use {} instead.", ) elif re.search(r"\s+;\s*$", line) and not re.search(r"\bfor\b", line): error( filename, linenum, "whitespace/semicolon", 5, "Extra space before last semicolon. If this should be an empty " "statement, use {} instead.", ) def IsDecltype(clean_lines, linenum, column): """Check if the token ending on (linenum, column) is decltype(). Args: clean_lines: A CleansedLines instance containing the file. linenum: the number of the line to check. column: end column of the token to check. Returns: True if this token is decltype() expression, False otherwise. """ (text, _, start_col) = ReverseCloseExpression(clean_lines, linenum, column) if start_col < 0: return False return bool(re.search(r"\bdecltype\s*$", text[0:start_col])) def CheckSectionSpacing(filename, clean_lines, class_info, linenum, error): """Checks for additional blank line issues related to sections. Currently the only thing checked here is blank line before protected/private. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. class_info: A _ClassInfo objects. linenum: The number of the line to check. error: The function to call with any errors found. """ # Skip checks if the class is small, where small means 25 lines or less. # 25 lines seems like a good cutoff since that's the usual height of # terminals, and any class that can't fit in one screen can't really # be considered "small". # # Also skip checks if we are on the first line. This accounts for # classes that look like # class Foo { public: ... }; # # If we didn't find the end of the class, last_line would be zero, # and the check will be skipped by the first condition. if ( class_info.last_line - class_info.starting_linenum <= 24 or linenum <= class_info.starting_linenum ): return matched = re.match(r"\s*(public|protected|private):", clean_lines.lines[linenum]) if matched: # Issue warning if the line before public/protected/private was # not a blank line, but don't do this if the previous line contains # "class" or "struct". This can happen two ways: # - We are at the beginning of the class. # - We are forward-declaring an inner class that is semantically # private, but needed to be public for implementation reasons. # Also ignores cases where the previous line ends with a backslash as can be # common when defining classes in C macros. prev_line = clean_lines.lines[linenum - 1] if ( not IsBlankLine(prev_line) and not re.search(r"\b(class|struct)\b", prev_line) and not re.search(r"\\$", prev_line) ): # Try a bit harder to find the beginning of the class. This is to # account for multi-line base-specifier lists, e.g.: # class Derived # : public Base { end_class_head = class_info.starting_linenum for i in range(class_info.starting_linenum, linenum): if re.search(r"\{\s*$", clean_lines.lines[i]): end_class_head = i break if end_class_head < linenum - 1: error( filename, linenum, "whitespace/blank_line", 3, f'"{matched.group(1)}:" should be preceded by a blank line', ) def GetPreviousNonBlankLine(clean_lines, linenum): """Return the most recent non-blank line and its line number. Args: clean_lines: A CleansedLines instance containing the file contents. linenum: The number of the line to check. Returns: A tuple with two elements. The first element is the contents of the last non-blank line before the current line, or the empty string if this is the first non-blank line. The second is the line number of that line, or -1 if this is the first non-blank line. """ prevlinenum = linenum - 1 while prevlinenum >= 0: prevline = clean_lines.elided[prevlinenum] if not IsBlankLine(prevline): # if not a blank line... return (prevline, prevlinenum) prevlinenum -= 1 return ("", -1) def CheckBraces(filename, clean_lines, linenum, error): """Looks for misplaced braces (e.g. at the end of line). Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # get rid of comments and strings if re.match(r"\s*{\s*$", line): # We allow an open brace to start a line in the case where someone is using # braces in a block to explicitly create a new scope, which is commonly used # to control the lifetime of stack-allocated variables. Braces are also # used for brace initializers inside function calls. We don't detect this # perfectly: we just don't complain if the last non-whitespace character on # the previous non-blank line is ',', ';', ':', '(', '{', or '}', or if the # previous line starts a preprocessor block. We also allow a brace on the # following line if it is part of an array initialization and would not fit # within the 80 character limit of the preceding line. prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0] if ( not re.search(r"[,;:}{(]\s*$", prevline) and not re.match(r"\s*#", prevline) and not (GetLineWidth(prevline) > _line_length - 2 and "[]" in prevline) ): error( filename, linenum, "whitespace/braces", 4, "{ should almost always be at the end of the previous line", ) # An else clause should be on the same line as the preceding closing brace. if last_wrong := re.match(r"\s*else\b\s*(?:if\b|\{|$)", line): prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0] if re.match(r"\s*}\s*$", prevline): error( filename, linenum, "whitespace/newline", 4, "An else should appear on the same line as the preceding }", ) else: last_wrong = False # If braces come on one side of an else, they should be on both. # However, we have to worry about "else if" that spans multiple lines! if re.search(r"else if\s*\(", line): # could be multi-line if brace_on_left = bool(re.search(r"}\s*else if\s*\(", line)) # find the ( after the if pos = line.find("else if") pos = line.find("(", pos) if pos > 0: (endline, _, endpos) = CloseExpression(clean_lines, linenum, pos) brace_on_right = endline[endpos:].find("{") != -1 if brace_on_left != brace_on_right: # must be brace after if error( filename, linenum, "readability/braces", 5, "If an else has a brace on one side, it should have it on both", ) # Prevent detection if statement has { and we detected an improper newline after } elif re.search(r"}\s*else[^{]*$", line) or ( re.match(r"[^}]*else\s*{", line) and not last_wrong ): error( filename, linenum, "readability/braces", 5, "If an else has a brace on one side, it should have it on both", ) # No control clauses with braces should have its contents on the same line # Exclude } which will be covered by empty-block detect # Exclude ; which may be used by while in a do-while if ( keyword := re.search( r"\b(else if|if|while|for|switch)" # These have parens r"\s*\(.*\)\s*(?:\[\[(?:un)?likely\]\]\s*)?{\s*[^\s\\};]", line, ) ) or ( keyword := re.search( r"\b(else|do|try)" # These don't have parens r"\s*(?:\[\[(?:un)?likely\]\]\s*)?{\s*[^\s\\}]", line, ) ): error( filename, linenum, "whitespace/newline", 5, f"Controlled statements inside brackets of {keyword.group(1)} clause" " should be on a separate line", ) # TODO(aaronliu0130): Err on if...else and do...while statements without braces; # style guide has changed since the below comment was written # Check single-line if/else bodies. The style guide says 'curly braces are not # required for single-line statements'. We additionally allow multi-line, # single statements, but we reject anything with more than one semicolon in # it. This means that the first semicolon after the if should be at the end of # its line, and the line after that should have an indent level equal to or # lower than the if. We also check for ambiguous if/else nesting without # braces. if_else_match = re.search(r"\b(if\s*(|constexpr)\s*\(|else\b)", line) if if_else_match and not re.match(r"\s*#", line): if_indent = GetIndentLevel(line) endline, endlinenum, endpos = line, linenum, if_else_match.end() if_match = re.search(r"\bif\s*(|constexpr)\s*\(", line) if if_match: # This could be a multiline if condition, so find the end first. pos = if_match.end() - 1 (endline, endlinenum, endpos) = CloseExpression(clean_lines, linenum, pos) # Check for an opening brace, either directly after the if or on the next # line. If found, this isn't a single-statement conditional. if not re.match(r"\s*(?:\[\[(?:un)?likely\]\]\s*)?{", endline[endpos:]) and not ( re.match(r"\s*$", endline[endpos:]) and endlinenum < (len(clean_lines.elided) - 1) and re.match(r"\s*{", clean_lines.elided[endlinenum + 1]) ): while ( endlinenum < len(clean_lines.elided) and ";" not in clean_lines.elided[endlinenum][endpos:] ): endlinenum += 1 endpos = 0 if endlinenum < len(clean_lines.elided): endline = clean_lines.elided[endlinenum] # We allow a mix of whitespace and closing braces (e.g. for one-liner # methods) and a single \ after the semicolon (for macros) endpos = endline.find(";") if not re.match(r";[\s}]*(\\?)$", endline[endpos:]): # Semicolon isn't the last character, there's something trailing. # Output a warning if the semicolon is not contained inside # a lambda expression. if not re.match(r"^[^{};]*\[[^\[\]]*\][^{}]*\{[^{}]*\}\s*\)*[;,]\s*$", endline): error( filename, linenum, "readability/braces", 4, "If/else bodies with multiple statements require braces", ) elif endlinenum < len(clean_lines.elided) - 1: # Make sure the next line is dedented next_line = clean_lines.elided[endlinenum + 1] next_indent = GetIndentLevel(next_line) # With ambiguous nested if statements, this will error out on the # if that *doesn't* match the else, regardless of whether it's the # inner one or outer one. if if_match and re.match(r"\s*else\b", next_line) and next_indent != if_indent: error( filename, linenum, "readability/braces", 4, "Else clause should be indented at the same level as if. " "Ambiguous nested if/else chains require braces.", ) elif next_indent > if_indent: error( filename, linenum, "readability/braces", 4, "If/else bodies with multiple statements require braces", ) def CheckTrailingSemicolon(filename, clean_lines, linenum, error): """Looks for redundant trailing semicolon. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Block bodies should not be followed by a semicolon. Due to C++11 # brace initialization, there are more places where semicolons are # required than not, so we explicitly list the allowed rules rather # than listing the disallowed ones. These are the places where "};" # should be replaced by just "}": # 1. Some flavor of block following closing parenthesis: # for (;;) {}; # while (...) {}; # switch (...) {}; # Function(...) {}; # if (...) {}; # if (...) else if (...) {}; # # 2. else block: # if (...) else {}; # # 3. const member function: # Function(...) const {}; # # 4. Block following some statement: # x = 42; # {}; # # 5. Block at the beginning of a function: # Function(...) { # {}; # } # # Note that naively checking for the preceding "{" will also match # braces inside multi-dimensional arrays, but this is fine since # that expression will not contain semicolons. # # 6. Block following another block: # while (true) {} # {}; # # 7. End of namespaces: # namespace {}; # # These semicolons seems far more common than other kinds of # redundant semicolons, possibly due to people converting classes # to namespaces. For now we do not warn for this case. # # Try matching case 1 first. match = re.match(r"^(.*\)\s*)\{", line) if match: # Matched closing parenthesis (case 1). Check the token before the # matching opening parenthesis, and don't warn if it looks like a # macro. This avoids these false positives: # - macro that defines a base class # - multi-line macro that defines a base class # - macro that defines the whole class-head # # But we still issue warnings for macros that we know are safe to # warn, specifically: # - TEST, TEST_F, TEST_P, MATCHER, MATCHER_P # - TYPED_TEST # - INTERFACE_DEF # - EXCLUSIVE_LOCKS_REQUIRED, SHARED_LOCKS_REQUIRED, LOCKS_EXCLUDED: # # We implement a list of safe macros instead of a list of # unsafe macros, even though the latter appears less frequently in # google code and would have been easier to implement. This is because # the downside for getting the allowed checks wrong means some extra # semicolons, while the downside for getting disallowed checks wrong # would result in compile errors. # # In addition to macros, we also don't want to warn on # - Compound literals # - Lambdas # - alignas specifier with anonymous structs # - decltype # - concepts (requires expression) closing_brace_pos = match.group(1).rfind(")") opening_parenthesis = ReverseCloseExpression(clean_lines, linenum, closing_brace_pos) if opening_parenthesis[2] > -1: line_prefix = opening_parenthesis[0][0 : opening_parenthesis[2]] macro = re.search(r"\b([A-Z_][A-Z0-9_]*)\s*$", line_prefix) func = re.match(r"^(.*\])\s*$", line_prefix) if ( ( macro and macro.group(1) not in ( "TEST", "TEST_F", "MATCHER", "MATCHER_P", "TYPED_TEST", "EXCLUSIVE_LOCKS_REQUIRED", "SHARED_LOCKS_REQUIRED", "LOCKS_EXCLUDED", "INTERFACE_DEF", ) ) or (func and not re.search(r"\boperator\s*\[\s*\]", func.group(1))) or re.search(r"\b(?:struct|union)\s+alignas\s*$", line_prefix) or re.search(r"\bdecltype$", line_prefix) or re.search(r"\brequires.*$", line_prefix) or re.search(r"\s+=\s*$", line_prefix) ): match = None if ( match and opening_parenthesis[1] > 1 and re.search(r"\]\s*$", clean_lines.elided[opening_parenthesis[1] - 1]) ): # Multi-line lambda-expression match = None else: # Try matching cases 2-3. match = re.match(r"^(.*(?:else|\)\s*const)\s*)\{", line) if not match: # Try matching cases 4-6. These are always matched on separate lines. # # Note that we can't simply concatenate the previous line to the # current line and do a single match, otherwise we may output # duplicate warnings for the blank line case: # if (cond) { # // blank line # } prevline = GetPreviousNonBlankLine(clean_lines, linenum)[0] if prevline and re.search(r"[;{}]\s*$", prevline): match = re.match(r"^(\s*)\{", line) # Check matching closing brace if match: (endline, endlinenum, endpos) = CloseExpression(clean_lines, linenum, len(match.group(1))) if endpos > -1 and re.match(r"^\s*;", endline[endpos:]): # Current {} pair is eligible for semicolon check, and we have found # the redundant semicolon, output warning here. # # Note: because we are scanning forward for opening braces, and # outputting warnings for the matching closing brace, if there are # nested blocks with trailing semicolons, we will get the error # messages in reversed order. # We need to check the line forward for NOLINT raw_lines = clean_lines.raw_lines ParseNolintSuppressions(filename, raw_lines[endlinenum - 1], endlinenum - 1, error) ParseNolintSuppressions(filename, raw_lines[endlinenum], endlinenum, error) error(filename, endlinenum, "readability/braces", 4, "You don't need a ; after a }") def CheckEmptyBlockBody(filename, clean_lines, linenum, error): """Look for empty loop/conditional body with only a single semicolon. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ # Search for loop keywords at the beginning of the line. Because only # whitespaces are allowed before the keywords, this will also ignore most # do-while-loops, since those lines should start with closing brace. # # We also check "if" blocks here, since an empty conditional block # is likely an error. line = clean_lines.elided[linenum] if matched := re.match(r"\s*(for|while|if)\s*\(", line): # Find the end of the conditional expression. (end_line, end_linenum, end_pos) = CloseExpression(clean_lines, linenum, line.find("(")) # Output warning if what follows the condition expression is a semicolon. # No warning for all other cases, including whitespace or newline, since we # have a separate check for semicolons preceded by whitespace. if end_pos >= 0 and re.match(r";", end_line[end_pos:]): if matched.group(1) == "if": error( filename, end_linenum, "whitespace/empty_conditional_body", 5, "Empty conditional bodies should use {}", ) else: error( filename, end_linenum, "whitespace/empty_loop_body", 5, "Empty loop bodies should use {} or continue", ) # Check for if statements that have completely empty bodies (no comments) # and no else clauses. if end_pos >= 0 and matched.group(1) == "if": # Find the position of the opening { for the if statement. # Return without logging an error if it has no brackets. opening_linenum = end_linenum opening_line_fragment = end_line[end_pos:] # Loop until EOF or find anything that's not whitespace or opening {. while not re.search(r"^\s*\{", opening_line_fragment): if re.search(r"^(?!\s*$)", opening_line_fragment): # Conditional has no brackets. return opening_linenum += 1 if opening_linenum == len(clean_lines.elided): # Couldn't find conditional's opening { or any code before EOF. return opening_line_fragment = clean_lines.elided[opening_linenum] # Set opening_line (opening_line_fragment may not be entire opening line). opening_line = clean_lines.elided[opening_linenum] # Find the position of the closing }. opening_pos = opening_line_fragment.find("{") if opening_linenum == end_linenum: # We need to make opening_pos relative to the start of the entire line. opening_pos += end_pos (closing_line, closing_linenum, closing_pos) = CloseExpression( clean_lines, opening_linenum, opening_pos ) if closing_pos < 0: return # Now construct the body of the conditional. This consists of the portion # of the opening line after the {, all lines until the closing line, # and the portion of the closing line before the }. if clean_lines.raw_lines[opening_linenum] != CleanseComments( clean_lines.raw_lines[opening_linenum] ): # Opening line ends with a comment, so conditional isn't empty. return if closing_linenum > opening_linenum: # Opening line after the {. Ignore comments here since we checked above. bodylist = list(opening_line[opening_pos + 1 :]) # All lines until closing line, excluding closing line, with comments. bodylist.extend(clean_lines.raw_lines[opening_linenum + 1 : closing_linenum]) # Closing line before the }. Won't (and can't) have comments. bodylist.append(clean_lines.elided[closing_linenum][: closing_pos - 1]) body = "\n".join(bodylist) else: # If statement has brackets and fits on a single line. body = opening_line[opening_pos + 1 : closing_pos - 1] # Check if the body is empty if not _EMPTY_CONDITIONAL_BODY_PATTERN.search(body): return # The body is empty. Now make sure there's not an else clause. current_linenum = closing_linenum current_line_fragment = closing_line[closing_pos:] # Loop until EOF or find anything that's not whitespace or else clause. while re.search(r"^\s*$|^(?=\s*else)", current_line_fragment): if re.search(r"^(?=\s*else)", current_line_fragment): # Found an else clause, so don't log an error. return current_linenum += 1 if current_linenum == len(clean_lines.elided): break current_line_fragment = clean_lines.elided[current_linenum] # The body is empty and there's no else clause until EOF or other code. error( filename, end_linenum, "whitespace/empty_if_body", 4, ("If statement had no body and no else clause"), ) def FindCheckMacro(line): """Find a replaceable CHECK-like macro. Args: line: line to search on. Returns: (macro name, start position), or (None, -1) if no replaceable macro is found. """ for macro in _CHECK_MACROS: i = line.find(macro) if i >= 0: # Find opening parenthesis. Do a regular expression match here # to make sure that we are matching the expected CHECK macro, as # opposed to some other macro that happens to contain the CHECK # substring. matched = re.match(r"^(.*\b" + macro + r"\s*)\(", line) if not matched: continue return (macro, len(matched.group(1))) return (None, -1) def CheckCheck(filename, clean_lines, linenum, error): """Checks the use of CHECK and EXPECT macros. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ # Decide the set of replacement macros that should be suggested lines = clean_lines.elided (check_macro, start_pos) = FindCheckMacro(lines[linenum]) if not check_macro: return # Find end of the boolean expression by matching parentheses (last_line, end_line, end_pos) = CloseExpression(clean_lines, linenum, start_pos) if end_pos < 0: return # If the check macro is followed by something other than a # semicolon, assume users will log their own custom error messages # and don't suggest any replacements. if not re.match(r"\s*;", last_line[end_pos:]): return if linenum == end_line: expression = lines[linenum][start_pos + 1 : end_pos - 1] else: expression = lines[linenum][start_pos + 1 :] for i in range(linenum + 1, end_line): expression += lines[i] expression += last_line[0 : end_pos - 1] # Parse expression so that we can take parentheses into account. # This avoids false positives for inputs like "CHECK((a < 4) == b)", # which is not replaceable by CHECK_LE. lhs = "" rhs = "" operator = None while expression: matched = re.match( r"^\s*(<<|<<=|>>|>>=|->\*|->|&&|\|\||" r"==|!=|>=|>|<=|<|\()(.*)$", expression, ) if matched: token = matched.group(1) if token == "(": # Parenthesized operand expression = matched.group(2) (end, _) = FindEndOfExpressionInLine(expression, 0, ["("]) if end < 0: return # Unmatched parenthesis lhs += "(" + expression[0:end] expression = expression[end:] elif token in ("&&", "||"): # Logical and/or operators. This means the expression # contains more than one term, for example: # CHECK(42 < a && a < b); # # These are not replaceable with CHECK_LE, so bail out early. return elif token in ("<<", "<<=", ">>", ">>=", "->*", "->"): # Non-relational operator lhs += token expression = matched.group(2) else: # Relational operator operator = token rhs = matched.group(2) break else: # Unparenthesized operand. Instead of appending to lhs one character # at a time, we do another regular expression match to consume several # characters at once if possible. Trivial benchmark shows that this # is more efficient when the operands are longer than a single # character, which is generally the case. matched = re.match(r"^([^-=!<>()&|]+)(.*)$", expression) if not matched: matched = re.match(r"^(\s*\S)(.*)$", expression) if not matched: break lhs += matched.group(1) expression = matched.group(2) # Only apply checks if we got all parts of the boolean expression if not (lhs and operator and rhs): return # Check that rhs do not contain logical operators. We already know # that lhs is fine since the loop above parses out && and ||. if rhs.find("&&") > -1 or rhs.find("||") > -1: return # At least one of the operands must be a constant literal. This is # to avoid suggesting replacements for unprintable things like # CHECK(variable != iterator) # # The following pattern matches decimal, hex integers, strings, and # characters (in that order). lhs = lhs.strip() rhs = rhs.strip() match_constant = r'^([-+]?(\d+|0[xX][0-9a-fA-F]+)[lLuU]{0,3}|".*"|\'.*\')$' if re.match(match_constant, lhs) or re.match(match_constant, rhs): # Note: since we know both lhs and rhs, we can provide a more # descriptive error message like: # Consider using CHECK_EQ(x, 42) instead of CHECK(x == 42) # Instead of: # Consider using CHECK_EQ instead of CHECK(a == b) # # We are still keeping the less descriptive message because if lhs # or rhs gets long, the error message might become unreadable. error( filename, linenum, "readability/check", 2, f"Consider using {_CHECK_REPLACEMENT[check_macro][operator]}" f" instead of {check_macro}(a {operator} b)", ) def CheckAltTokens(filename, clean_lines, linenum, error): """Check alternative keywords being used in boolean expressions. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Avoid preprocessor lines if re.match(r"^\s*#", line): return # Last ditch effort to avoid multi-line comments. This will not help # if the comment started before the current line or ended after the # current line, but it catches most of the false positives. At least, # it provides a way to workaround this warning for people who use # multi-line comments in preprocessor macros. # # TODO(google): remove this once cpplint has better support for # multi-line comments. if line.find("/*") >= 0 or line.find("*/") >= 0: return for match in _ALT_TOKEN_REPLACEMENT_PATTERN.finditer(line): error( filename, linenum, "readability/alt_tokens", 2, f"Use operator {_ALT_TOKEN_REPLACEMENT[match.group(2)]} instead of {match.group(2)}", ) def GetLineWidth(line): """Determines the width of the line in column positions. Args: line: A string, which may be a Unicode string. Returns: The width of the line in column positions, accounting for Unicode combining characters and wide characters. """ if isinstance(line, str): width = 0 for uc in unicodedata.normalize("NFC", line): if unicodedata.east_asian_width(uc) in ("W", "F"): width += 2 elif not unicodedata.combining(uc): # Issue 337 # https://mail.python.org/pipermail/python-list/2012-August/628809.html if (sys.version_info.major, sys.version_info.minor) <= (3, 2): # https://github.com/python/cpython/blob/2.7/Include/unicodeobject.h#L81 is_wide_build = sysconfig.get_config_var("Py_UNICODE_SIZE") >= 4 # https://github.com/python/cpython/blob/2.7/Objects/unicodeobject.c#L564 is_low_surrogate = 0xDC00 <= ord(uc) <= 0xDFFF if not is_wide_build and is_low_surrogate: width -= 1 width += 1 return width return len(line) def CheckStyle(filename, clean_lines, linenum, file_extension, nesting_state, error, cppvar=None): """Checks rules from the 'C++ style rules' section of cppguide.html. Most of these rules are hard to test (naming, comment style), but we do what we can. In particular we check for 2-space indents, line lengths, tab usage, spaces inside code, etc. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. file_extension: The extension (without the dot) of the filename. nesting_state: A NestingState instance which maintains information about the current stack of nested blocks being parsed. error: The function to call with any errors found. cppvar: The header guard variable returned by GetHeaderGuardCPPVar. """ # Don't use "elided" lines here, otherwise we can't check commented lines. # Don't want to use "raw" either, because we don't want to check inside C++11 # raw strings, raw_lines = clean_lines.lines_without_raw_strings line = raw_lines[linenum] prev = raw_lines[linenum - 1] if linenum > 0 else "" if line.find("\t") != -1: error(filename, linenum, "whitespace/tab", 1, "Tab found; better to use spaces") # One or three blank spaces at the beginning of the line is weird; it's # hard to reconcile that with 2-space indents. # NOTE: here are the conditions rob pike used for his tests. Mine aren't # as sophisticated, but it may be worth becoming so: RLENGTH==initial_spaces # if(RLENGTH > 20) complain = 0; # if(match($0, " +(error|private|public|protected):")) complain = 0; # if(match(prev, "&& *$")) complain = 0; # if(match(prev, "\\|\\| *$")) complain = 0; # if(match(prev, "[\",=><] *$")) complain = 0; # if(match($0, " <<")) complain = 0; # if(match(prev, " +for \\(")) complain = 0; # if(prevodd && match(prevprev, " +for \\(")) complain = 0; scope_or_label_pattern = r"\s*(?:public|private|protected|signals)(?:\s+(?:slots\s*)?)?:\s*\\?$" classinfo = nesting_state.InnermostClass() initial_spaces = 0 cleansed_line = clean_lines.elided[linenum] while initial_spaces < len(line) and line[initial_spaces] == " ": initial_spaces += 1 # There are certain situations we allow one space, notably for # section labels, and also lines containing multi-line raw strings. # We also don't check for lines that look like continuation lines # (of lines ending in double quotes, commas, equals, or angle brackets) # because the rules for how to indent those are non-trivial. if ( not re.search(r'[",=><] *$', prev) and (initial_spaces in {1, 3}) and not re.match(scope_or_label_pattern, cleansed_line) and not (clean_lines.raw_lines[linenum] != line and re.match(r'^\s*""', line)) ): error( filename, linenum, "whitespace/indent", 3, "Weird number of spaces at line-start. Are you using a 2-space indent?", ) if line and line[-1].isspace(): error( filename, linenum, "whitespace/end_of_line", 4, "Line ends in whitespace. Consider deleting these extra spaces.", ) # Check if the line is a header guard. is_header_guard = IsHeaderExtension(file_extension) and line.startswith( (f"#ifndef {cppvar}", f"#define {cppvar}", f"#endif // {cppvar}") ) # #include lines and header guards can be long, since there's no clean way to # split them. # # URLs can be long too. It's possible to split these, but it makes them # harder to cut&paste. # # The "$Id:...$" comment may also get very long without it being the # developers fault. # # Doxygen documentation copying can get pretty long when using an overloaded # function declaration if ( not line.startswith("#include") and not is_header_guard and not re.match(r"^\s*//.*http(s?)://\S*$", line) and not re.match(r"^\s*//\s*[^\s]*$", line) and not re.match(r"^// \$Id:.*#[0-9]+ \$$", line) and not re.match(r"^\s*/// [@\\](copydoc|copydetails|copybrief) .*$", line) ): line_width = GetLineWidth(line) if line_width > _line_length: error( filename, linenum, "whitespace/line_length", 2, f"Lines should be <= {_line_length} characters long", ) if ( cleansed_line.count(";") > 1 and # allow simple single line lambdas not re.match(r"^[^{};]*\[[^\[\]]*\][^{}]*\{[^{}\n\r]*\}", line) and # for loops are allowed two ;'s (and may run over two lines). cleansed_line.find("for") == -1 and ( GetPreviousNonBlankLine(clean_lines, linenum)[0].find("for") == -1 or GetPreviousNonBlankLine(clean_lines, linenum)[0].find(";") != -1 ) and # It's ok to have many commands in a switch case that fits in 1 line not ( (cleansed_line.find("case ") != -1 or cleansed_line.find("default:") != -1) and cleansed_line.find("break;") != -1 ) ): error(filename, linenum, "whitespace/newline", 0, "More than one command on the same line") # Some more style checks CheckBraces(filename, clean_lines, linenum, error) CheckTrailingSemicolon(filename, clean_lines, linenum, error) CheckEmptyBlockBody(filename, clean_lines, linenum, error) CheckSpacing(filename, clean_lines, linenum, nesting_state, error) CheckOperatorSpacing(filename, clean_lines, linenum, error) CheckParenthesisSpacing(filename, clean_lines, linenum, error) CheckCommaSpacing(filename, clean_lines, linenum, error) CheckBracesSpacing(filename, clean_lines, linenum, nesting_state, error) CheckSpacingForFunctionCall(filename, clean_lines, linenum, error) CheckCheck(filename, clean_lines, linenum, error) CheckAltTokens(filename, clean_lines, linenum, error) classinfo = nesting_state.InnermostClass() if classinfo: CheckSectionSpacing(filename, clean_lines, classinfo, linenum, error) _RE_PATTERN_INCLUDE = re.compile(r'^\s*#\s*include\s*([<"])([^>"]*)[>"].*$') # Matches the first component of a filename delimited by -s and _s. That is: # _RE_FIRST_COMPONENT.match('foo').group(0) == 'foo' # _RE_FIRST_COMPONENT.match('foo.cc').group(0) == 'foo' # _RE_FIRST_COMPONENT.match('foo-bar_baz.cc').group(0) == 'foo' # _RE_FIRST_COMPONENT.match('foo_bar-baz.cc').group(0) == 'foo' _RE_FIRST_COMPONENT = re.compile(r"^[^-_.]+") def _DropCommonSuffixes(filename): """Drops common suffixes like _test.cc or -inl.h from filename. For example: >>> _DropCommonSuffixes('foo/foo-inl.h') 'foo/foo' >>> _DropCommonSuffixes('foo/bar/foo.cc') 'foo/bar/foo' >>> _DropCommonSuffixes('foo/foo_internal.h') 'foo/foo' >>> _DropCommonSuffixes('foo/foo_unusualinternal.h') 'foo/foo_unusualinternal' Args: filename: The input filename. Returns: The filename with the common suffix removed. """ for suffix in itertools.chain( ( f"{test_suffix.lstrip('_')}.{ext}" for test_suffix, ext in itertools.product(_test_suffixes, GetNonHeaderExtensions()) ), ( f"{suffix}.{ext}" for suffix, ext in itertools.product(["inl", "imp", "internal"], GetHeaderExtensions()) ), ): if ( filename.endswith(suffix) and len(filename) > len(suffix) and filename[-len(suffix) - 1] in ("-", "_") ): return filename[: -len(suffix) - 1] return os.path.splitext(filename)[0] def _ClassifyInclude(fileinfo, include, used_angle_brackets, include_order="default"): """Figures out what kind of header 'include' is. Args: fileinfo: The current file cpplint is running over. A FileInfo instance. include: The path to a #included file. used_angle_brackets: True if the #include used <> rather than "". include_order: "default" or other value allowed in program arguments Returns: One of the _XXX_HEADER constants. For example: >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'stdio.h', True) _C_SYS_HEADER >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'string', True) _CPP_SYS_HEADER >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'foo/foo.h', True, "standardcfirst") _OTHER_SYS_HEADER >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'foo/foo.h', False) _LIKELY_MY_HEADER >>> _ClassifyInclude(FileInfo('foo/foo_unknown_extension.cc'), ... 'bar/foo_other_ext.h', False) _POSSIBLE_MY_HEADER >>> _ClassifyInclude(FileInfo('foo/foo.cc'), 'foo/bar.h', False) _OTHER_HEADER """ # This is a list of all standard c++ header files, except # those already checked for above. is_cpp_header = include in _CPP_HEADERS # Mark include as C header if in list or in a known folder for standard-ish C headers. is_std_c_header = (include_order == "default") or ( include in _C_HEADERS # additional linux glibc header folders or re.search(rf"(?:{'|'.join(C_STANDARD_HEADER_FOLDERS)})\/.*\.h", include) ) # Headers with C++ extensions shouldn't be considered C system headers include_ext = os.path.splitext(include)[1] is_system = used_angle_brackets and include_ext not in [".hh", ".hpp", ".hxx", ".h++"] if is_system: if is_cpp_header: return _CPP_SYS_HEADER if is_std_c_header: return _C_SYS_HEADER return _OTHER_SYS_HEADER # If the target file and the include we're checking share a # basename when we drop common extensions, and the include # lives in . , then it's likely to be owned by the target file. target_dir, target_base = os.path.split(_DropCommonSuffixes(fileinfo.RepositoryName())) include_dir, include_base = os.path.split(_DropCommonSuffixes(include)) target_dir_pub = os.path.normpath(target_dir + "/../public") target_dir_pub = target_dir_pub.replace("\\", "/") if target_base == include_base and (include_dir in (target_dir, target_dir_pub)): return _LIKELY_MY_HEADER # If the target and include share some initial basename # component, it's possible the target is implementing the # include, so it's allowed to be first, but we'll never # complain if it's not there. target_first_component = _RE_FIRST_COMPONENT.match(target_base) include_first_component = _RE_FIRST_COMPONENT.match(include_base) if ( target_first_component and include_first_component and target_first_component.group(0) == include_first_component.group(0) ): return _POSSIBLE_MY_HEADER return _OTHER_HEADER def CheckIncludeLine(filename, clean_lines, linenum, include_state, error): """Check rules that are applicable to #include lines. Strings on #include lines are NOT removed from elided line, to make certain tasks easier. However, to prevent false positives, checks applicable to #include lines in CheckLanguage must be put here. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. include_state: An _IncludeState instance in which the headers are inserted. error: The function to call with any errors found. """ fileinfo = FileInfo(filename) line = clean_lines.lines[linenum] # "include" should use the new style "foo/bar.h" instead of just "bar.h" # Only do this check if the included header follows google naming # conventions. If not, assume that it's a 3rd party API that # requires special include conventions. # # We also make an exception for Lua headers, which follow google # naming convention but not the include convention. match = re.match(r'#include\s*"([^/]+\.(.*))"', line) if ( match and IsHeaderExtension(match.group(2)) and not _THIRD_PARTY_HEADERS_PATTERN.match(match.group(1)) ): error( filename, linenum, "build/include_subdir", 4, "Include the directory when naming header files", ) # we shouldn't include a file more than once. actually, there are a # handful of instances where doing so is okay, but in general it's # not. match = _RE_PATTERN_INCLUDE.search(line) if match: include = match.group(2) used_angle_brackets = match.group(1) == "<" duplicate_line = include_state.FindHeader(include) if duplicate_line >= 0: error( filename, linenum, "build/include", 4, f'"{include}" already included at {filename}:{duplicate_line}', ) return for extension in GetNonHeaderExtensions(): if include.endswith("." + extension) and os.path.dirname( fileinfo.RepositoryName() ) != os.path.dirname(include): error( filename, linenum, "build/include", 4, "Do not include ." + extension + " files from other packages", ) return # We DO want to include a 3rd party looking header if it matches the # filename. Otherwise we get an erroneous error "...should include its # header" error later. third_src_header = False for ext in GetHeaderExtensions(): basefilename = filename[0 : len(filename) - len(fileinfo.Extension())] headerfile = basefilename + "." + ext headername = FileInfo(headerfile).RepositoryName() if headername in include or include in headername: third_src_header = True break if third_src_header or not _THIRD_PARTY_HEADERS_PATTERN.match(include): include_state.include_list[-1].append((include, linenum)) # We want to ensure that headers appear in the right order: # 1) for foo.cc, foo.h (preferred location) # 2) c system files # 3) cpp system files # 4) for foo.cc, foo.h (deprecated location) # 5) other google headers # # We classify each include statement as one of those 5 types # using a number of techniques. The include_state object keeps # track of the highest type seen, and complains if we see a # lower type after that. error_message = include_state.CheckNextIncludeOrder( _ClassifyInclude(fileinfo, include, used_angle_brackets, _include_order) ) if error_message: error( filename, linenum, "build/include_order", 4, f"{error_message}. Should be: {fileinfo.BaseName()}.h, c routing," " c++ routing, other.", ) canonical_include = include_state.CanonicalizeAlphabeticalOrder(include) if not include_state.IsInAlphabeticalOrder(clean_lines, linenum, canonical_include): error( filename, linenum, "build/include_alpha", 4, f'Include "{include}" not in alphabetical order', ) include_state.SetLastHeader(canonical_include) def _GetTextInside(text, start_pattern): r"""Retrieves all the text between matching open and close parentheses. Given a string of lines and a regular expression string, retrieve all the text following the expression and between opening punctuation symbols like (, [, or {, and the matching close-punctuation symbol. This properly nested occurrences of the punctuation, so for the text like printf(a(), b(c())); a call to _GetTextInside(text, r'printf\(') will return 'a(), b(c())'. start_pattern must match string having an open punctuation symbol at the end. Args: text: The lines to extract text. Its comments and strings must be elided. It can be single line and can span multiple lines. start_pattern: The regexp string indicating where to start extracting the text. Returns: The extracted text. None if either the opening string or ending punctuation could not be found. """ # TODO(google): Audit cpplint.py to see what places could be profitably # rewritten to use _GetTextInside (and use inferior regexp matching today). # Give opening punctuation to get the matching close-punctuation. matching_punctuation = {"(": ")", "{": "}", "[": "]"} closing_punctuation = set(dict.values(matching_punctuation)) # Find the position to start extracting text. match = re.search(start_pattern, text, re.MULTILINE) if not match: # start_pattern not found in text. return None start_position = match.end(0) assert start_position > 0, "start_pattern must ends with an opening punctuation." assert text[start_position - 1] in matching_punctuation, ( "start_pattern must ends with an opening punctuation." ) # Stack of closing punctuation we expect to have in text after position. punctuation_stack = [matching_punctuation[text[start_position - 1]]] position = start_position while punctuation_stack and position < len(text): if text[position] == punctuation_stack[-1]: punctuation_stack.pop() elif text[position] in closing_punctuation: # A closing punctuation without matching opening punctuation. return None elif text[position] in matching_punctuation: punctuation_stack.append(matching_punctuation[text[position]]) position += 1 if punctuation_stack: # Opening punctuation left without matching close-punctuation. return None # punctuation match. return text[start_position : position - 1] # Patterns for matching call-by-reference parameters. # # Supports nested templates up to 2 levels deep using this messy pattern: # < (?: < (?: < [^<>]* # > # | [^<>] )* # > # | [^<>] )* # > _RE_PATTERN_IDENT = r"[_a-zA-Z]\w*" # =~ [[:alpha:]][[:alnum:]]* _RE_PATTERN_TYPE = ( r"(?:const\s+)?(?:typename\s+|class\s+|struct\s+|union\s+|enum\s+)?" r"(?:\w|" r"\s*<(?:<(?:<[^<>]*>|[^<>])*>|[^<>])*>|" r"::)+" ) # A call-by-reference parameter ends with '& identifier'. _RE_PATTERN_REF_PARAM = re.compile( r"(" + _RE_PATTERN_TYPE + r"(?:\s*(?:\bconst\b|[*]))*\s*" r"&\s*" + _RE_PATTERN_IDENT + r")\s*(?:=[^,()]+)?[,)]" ) # A call-by-const-reference parameter either ends with 'const& identifier' # or looks like 'const type& identifier' when 'type' is atomic. _RE_PATTERN_CONST_REF_PARAM = ( r"(?:.*\s*\bconst\s*&\s*" + _RE_PATTERN_IDENT + r"|const\s+" + _RE_PATTERN_TYPE + r"\s*&\s*" + _RE_PATTERN_IDENT + r")" ) # Stream types. _RE_PATTERN_REF_STREAM_PARAM = r"(?:.*stream\s*&\s*" + _RE_PATTERN_IDENT + r")" def CheckLanguage( filename, clean_lines, linenum, file_extension, include_state, nesting_state, error ): """Checks rules from the 'C++ language rules' section of cppguide.html. Some of these rules are hard to test (function overloading, using uint32_t inappropriately), but we do the best we can. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. file_extension: The extension (without the dot) of the filename. include_state: An _IncludeState instance in which the headers are inserted. nesting_state: A NestingState instance which maintains information about the current stack of nested blocks being parsed. error: The function to call with any errors found. """ # If the line is empty or consists of entirely a comment, no need to # check it. line = clean_lines.elided[linenum] if not line: return match = _RE_PATTERN_INCLUDE.search(line) if match: CheckIncludeLine(filename, clean_lines, linenum, include_state, error) return # Reset include state across preprocessor directives. This is meant # to silence warnings for conditional includes. match = re.match(r"^\s*#\s*(if|ifdef|ifndef|elif|else|endif)\b", line) if match: include_state.ResetSection(match.group(1)) # Perform other checks now that we are sure that this is not an include line CheckCasts(filename, clean_lines, linenum, error) CheckGlobalStatic(filename, clean_lines, linenum, error) CheckPrintf(filename, clean_lines, linenum, error) if IsHeaderExtension(file_extension): # TODO(google): check that 1-arg constructors are explicit. # How to tell it's a constructor? # (handled in CheckForNonStandardConstructs for now) # TODO(google): check that classes declare or disable copy/assign # (level 1 error) pass # Check if people are using the verboten C basic types. The only exception # we regularly allow is "unsigned short port" for port. if re.search(r"\bshort port\b", line): if not re.search(r"\bunsigned short port\b", line): error( filename, linenum, "runtime/int", 4, 'Use "unsigned short" for ports, not "short"' ) else: match = re.search(r"\b(short|long(?! +double)|long long)\b", line) if match: error( filename, linenum, "runtime/int", 4, f"Use int16_t/int64_t/etc, rather than the C type {match.group(1)}", ) # Check if some verboten operator overloading is going on # TODO(google): catch out-of-line unary operator&: # class X {}; # int operator&(const X& x) { return 42; } // unary operator& # The trick is it's hard to tell apart from binary operator&: # class Y { int operator&(const Y& x) { return 23; } }; // binary operator& if re.search(r"\boperator\s*&\s*\(\s*\)", line): error( filename, linenum, "runtime/operator", 4, "Unary operator& is dangerous. Do not use it.", ) # Check for suspicious usage of "if" like # } if (a == b) { if re.search(r"\}\s*if\s*\(", line): error( filename, linenum, "readability/braces", 4, 'Did you mean "else if"? If not, start a new line for "if".', ) # Check for potential format string bugs like printf(foo). # We constrain the pattern not to pick things like DocidForPrintf(foo). # Not perfect but it can catch printf(foo.c_str()) and printf(foo->c_str()) # TODO(google): Catch the following case. Need to change the calling # convention of the whole function to process multiple line to handle it. # printf( # boy_this_is_a_really_long_variable_that_cannot_fit_on_the_prev_line); if printf_args := _GetTextInside(line, r"(?i)\b(string)?printf\s*\("): match = re.match(r"([\w.\->()]+)$", printf_args) if match and match.group(1) != "__VA_ARGS__": function_name = re.search(r"\b((?:string)?printf)\s*\(", line, re.IGNORECASE).group(1) error( filename, linenum, "runtime/printf", 4, f'Potential format string bug. Do {function_name}("%s", {match.group(1)}) instead.', ) # Check for potential memset bugs like memset(buf, sizeof(buf), 0). match = re.search(r"memset\s*\(([^,]*),\s*([^,]*),\s*0\s*\)", line) if match and not re.match(r"^''|-?[0-9]+|0x[0-9A-Fa-f]$", match.group(2)): error( filename, linenum, "runtime/memset", 4, f'Did you mean "memset({match.group(1)}, 0, {match.group(2)})"?', ) if re.search(r"\busing namespace\b", line): if re.search(r"\bliterals\b", line): error( filename, linenum, "build/namespaces_literals", 5, "Do not use namespace using-directives. Use using-declarations instead.", ) else: error( filename, linenum, "build/namespaces", 5, "Do not use namespace using-directives. Use using-declarations instead.", ) # Detect variable-length arrays. match = re.match(r"\s*(.+::)?(\w+) [a-z]\w*\[(.+)];", line) if ( match and match.group(2) != "return" and match.group(2) != "delete" and match.group(3).find("]") == -1 ): # Split the size using space and arithmetic operators as delimiters. # If any of the resulting tokens are not compile time constants then # report the error. tokens = re.split(r"\s|\+|\-|\*|\/|<<|>>]", match.group(3)) is_const = True skip_next = False for tok in tokens: if skip_next: skip_next = False continue if re.search(r"sizeof\(.+\)", tok): continue if re.search(r"arraysize\(\w+\)", tok): continue tok = tok.lstrip("(") tok = tok.rstrip(")") if not tok: continue if re.match(r"\d+", tok): continue if re.match(r"0[xX][0-9a-fA-F]+", tok): continue if re.match(r"k[A-Z0-9]\w*", tok): continue if re.match(r"(.+::)?k[A-Z0-9]\w*", tok): continue if re.match(r"(.+::)?[A-Z][A-Z0-9_]*", tok): continue # A catch all for tricky sizeof cases, including 'sizeof expression', # 'sizeof(*type)', 'sizeof(const type)', 'sizeof(struct StructName)' # requires skipping the next token because we split on ' ' and '*'. if tok.startswith("sizeof"): skip_next = True continue is_const = False break if not is_const: error( filename, linenum, "runtime/arrays", 1, "Do not use variable-length arrays. Use an appropriately named " "('k' followed by CamelCase) compile-time constant for the size.", ) # Check for use of unnamed namespaces in header files. Registration # macros are typically OK, so we allow use of "namespace {" on lines # that end with backslashes. if ( IsHeaderExtension(file_extension) and re.search(r"\bnamespace\s*{", line) and line[-1] != "\\" ): error( filename, linenum, "build/namespaces_headers", 4, "Do not use unnamed namespaces in header files. See " "https://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Namespaces" " for more information.", ) def CheckGlobalStatic(filename, clean_lines, linenum, error): """Check for unsafe global or static objects. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Match two lines at a time to support multiline declarations if linenum + 1 < clean_lines.NumLines() and not re.search(r"[;({]", line): line += clean_lines.elided[linenum + 1].strip() # Check for people declaring static/global STL strings at the top level. # This is dangerous because the C++ language does not guarantee that # globals with constructors are initialized before the first access, and # also because globals can be destroyed when some threads are still running. # TODO(google): Generalize this to also find static unique_ptr instances. # TODO(google): File bugs for clang-tidy to find these. match = re.match( r"((?:|static +)(?:|const +))(?::*std::)?string( +const)? +" r"([a-zA-Z0-9_:]+)\b(.*)", line, ) # Remove false positives: # - String pointers (as opposed to values). # string *pointer # const string *pointer # string const *pointer # string *const pointer # # - Functions and template specializations. # string Function(... # string Class::Method(... # # - Operators. These are matched separately because operator names # cross non-word boundaries, and trying to match both operators # and functions at the same time would decrease accuracy of # matching identifiers. # string Class::operator*() if ( match and not re.search(r"\bstring\b(\s+const)?\s*[\*\&]\s*(const\s+)?\w", line) and not re.search(r"\boperator\W", line) and not re.match(r'\s*(<.*>)?(::[a-zA-Z0-9_]+)*\s*\(([^"]|$)', match.group(4)) ): if re.search(r"\bconst\b", line): error( filename, linenum, "runtime/string", 4, "For a static/global string constant, use a C style string instead:" f' "{match.group(1)}char{match.group(2) or ""} {match.group(3)}[]".', ) else: error( filename, linenum, "runtime/string", 4, "Static/global string variables are not permitted.", ) if re.search(r"\b([A-Za-z0-9_]*_)\(\1\)", line) or re.search( r"\b([A-Za-z0-9_]*_)\(CHECK_NOTNULL\(\1\)\)", line ): error( filename, linenum, "runtime/init", 4, "You seem to be initializing a member variable with itself.", ) def CheckPrintf(filename, clean_lines, linenum, error): """Check for printf related issues. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # When snprintf is used, the second argument shouldn't be a literal. match = re.search(r"snprintf\s*\(([^,]*),\s*([0-9]*)\s*,", line) if match and match.group(2) != "0": # If 2nd arg is zero, snprintf is used to calculate size. error( filename, linenum, "runtime/printf", 3, "If you can, use" f" sizeof({match.group(1)}) instead of {match.group(2)}" " as the 2nd arg to snprintf.", ) # Check if some verboten C functions are being used. if re.search(r"\bsprintf\s*\(", line): error(filename, linenum, "runtime/printf", 5, "Never use sprintf. Use snprintf instead.") match = re.search(r"\b(strcpy|strcat)\s*\(", line) if match: error( filename, linenum, "runtime/printf", 4, f"Almost always, snprintf is better than {match.group(1)}", ) def IsDerivedFunction(clean_lines, linenum): """Check if current line contains an inherited function. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. Returns: True if current line contains a function with "override" virt-specifier. """ # Scan back a few lines for start of current function for i in range(linenum, max(-1, linenum - 10), -1): match = re.match(r"^([^()]*\w+)\(", clean_lines.elided[i]) if match: # Look for "override" after the matching closing parenthesis line, _, closing_paren = CloseExpression(clean_lines, i, len(match.group(1))) return closing_paren >= 0 and re.search(r"\boverride\b", line[closing_paren:]) return False def IsOutOfLineMethodDefinition(clean_lines, linenum): """Check if current line contains an out-of-line method definition. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. Returns: True if current line contains an out-of-line method definition. """ # Scan back a few lines for start of current function for i in range(linenum, max(-1, linenum - 10), -1): if re.match(r"^([^()]*\w+)\(", clean_lines.elided[i]): return re.match(r"^[^()]*\w+::\w+\(", clean_lines.elided[i]) is not None return False def IsInitializerList(clean_lines, linenum): """Check if current line is inside constructor initializer list. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. Returns: True if current line appears to be inside constructor initializer list, False otherwise. """ for i in range(linenum, 1, -1): line = clean_lines.elided[i] if i == linenum: remove_function_body = re.match(r"^(.*)\{\s*$", line) if remove_function_body: line = remove_function_body.group(1) if re.search(r"\s:\s*\w+[({]", line): # A lone colon tend to indicate the start of a constructor # initializer list. It could also be a ternary operator, which # also tend to appear in constructor initializer lists as # opposed to parameter lists. return True if re.search(r"\}\s*,\s*$", line): # A closing brace followed by a comma is probably the end of a # brace-initialized member in constructor initializer list. return True if re.search(r"[{};]\s*$", line): # Found one of the following: # - A closing brace or semicolon, probably the end of the previous # function. # - An opening brace, probably the start of current class or namespace. # # Current line is probably not inside an initializer list since # we saw one of those things without seeing the starting colon. return False # Got to the beginning of the file without seeing the start of # constructor initializer list. return False def CheckForNonConstReference(filename, clean_lines, linenum, nesting_state, error): """Check for non-const references. Separate from CheckLanguage since it scans backwards from current line, instead of scanning forward. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. nesting_state: A NestingState instance which maintains information about the current stack of nested blocks being parsed. error: The function to call with any errors found. """ # Do nothing if there is no '&' on current line. line = clean_lines.elided[linenum] if "&" not in line: return # If a function is inherited, current function doesn't have much of # a choice, so any non-const references should not be blamed on # derived function. if IsDerivedFunction(clean_lines, linenum): return # Don't warn on out-of-line method definitions, as we would warn on the # in-line declaration, if it isn't marked with 'override'. if IsOutOfLineMethodDefinition(clean_lines, linenum): return # Long type names may be broken across multiple lines, usually in one # of these forms: # LongType # ::LongTypeContinued &identifier # LongType:: # LongTypeContinued &identifier # LongType< # ...>::LongTypeContinued &identifier # # If we detected a type split across two lines, join the previous # line to current line so that we can match const references # accordingly. # # Note that this only scans back one line, since scanning back # arbitrary number of lines would be expensive. If you have a type # that spans more than 2 lines, please use a typedef. if linenum > 1: previous = None if re.match(r"\s*::(?:[\w<>]|::)+\s*&\s*\S", line): # previous_line\n + ::current_line previous = re.search( r"\b((?:const\s*)?(?:[\w<>]|::)+[\w<>])\s*$", clean_lines.elided[linenum - 1] ) elif re.match(r"\s*[a-zA-Z_]([\w<>]|::)+\s*&\s*\S", line): # previous_line::\n + current_line previous = re.search( r"\b((?:const\s*)?(?:[\w<>]|::)+::)\s*$", clean_lines.elided[linenum - 1] ) if previous: line = previous.group(1) + line.lstrip() else: # Check for templated parameter that is split across multiple lines endpos = line.rfind(">") if endpos > -1: (_, startline, startpos) = ReverseCloseExpression(clean_lines, linenum, endpos) if startpos > -1 and startline < linenum: # Found the matching < on an earlier line, collect all # pieces up to current line. line = "" for i in range(startline, linenum + 1): line += clean_lines.elided[i].strip() # Check for non-const references in function parameters. A single '&' may # found in the following places: # inside expression: binary & for bitwise AND # inside expression: unary & for taking the address of something # inside declarators: reference parameter # We will exclude the first two cases by checking that we are not inside a # function body, including one that was just introduced by a trailing '{'. # TODO(google): Doesn't account for 'catch(Exception& e)' [rare]. if nesting_state.previous_stack_top and not ( isinstance(nesting_state.previous_stack_top, (_ClassInfo, _NamespaceInfo)) ): # Not at toplevel, not within a class, and not within a namespace return # Avoid initializer lists. We only need to scan back from the # current line for something that starts with ':'. # # We don't need to check the current line, since the '&' would # appear inside the second set of parentheses on the current line as # opposed to the first set. if linenum > 0: for i in range(linenum - 1, max(0, linenum - 10), -1): previous_line = clean_lines.elided[i] if not re.search(r"[),]\s*$", previous_line): break if re.match(r"^\s*:\s+\S", previous_line): return # Avoid preprocessors if re.search(r"\\\s*$", line): return # Avoid constructor initializer lists if IsInitializerList(clean_lines, linenum): return # We allow non-const references in a few standard places, like functions # called "swap()" or iostream operators like "<<" or ">>". Do not check # those function parameters. # # We also accept & in static_assert, which looks like a function but # it's actually a declaration expression. allowed_functions = ( r"(?:[sS]wap(?:<\w:+>)?|" r"operator\s*[<>][<>]|" r"static_assert|COMPILE_ASSERT" r")\s*\(" ) if re.search(allowed_functions, line): return if not re.search(r"\S+\([^)]*$", line): # Don't see an allowed function on this line. Actually we # didn't see any function name on this line, so this is likely a # multi-line parameter list. Try a bit harder to catch this case. for i in range(2): if linenum > i and re.search(allowed_functions, clean_lines.elided[linenum - i - 1]): return decls = re.sub(r"{[^}]*}", " ", line) # exclude function body for parameter in re.findall(_RE_PATTERN_REF_PARAM, decls): if not re.match(_RE_PATTERN_CONST_REF_PARAM, parameter) and not re.match( _RE_PATTERN_REF_STREAM_PARAM, parameter ): error( filename, linenum, "runtime/references", 2, "Is this a non-const reference? " "If so, make const or use a pointer: " + re.sub(" *<", "<", parameter), ) def CheckCasts(filename, clean_lines, linenum, error): """Various cast related checks. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] # Check to see if they're using an conversion function cast. # I just try to capture the most common basic types, though there are more. # Parameterless conversion functions, such as bool(), are allowed as they are # probably a member operator declaration or default constructor. match = re.search( r"(\bnew\s+(?:const\s+)?|\S<\s*(?:const\s+)?)?\b" r"(int|float|double|bool|char|int16_t|uint16_t|int32_t|uint32_t|int64_t|uint64_t)" r"(\([^)].*)", line, ) expecting_function = ExpectingFunctionArgs(clean_lines, linenum) if match and not expecting_function: matched_type = match.group(2) # matched_new_or_template is used to silence two false positives: # - New operators # - Template arguments with function types # # For template arguments, we match on types immediately following # an opening bracket without any spaces. This is a fast way to # silence the common case where the function type is the first # template argument. False negative with less-than comparison is # avoided because those operators are usually followed by a space. # # function // bracket + no space = false positive # value < double(42) // bracket + space = true positive matched_new_or_template = match.group(1) # Avoid arrays by looking for brackets that come after the closing # parenthesis. if re.match(r"\([^()]+\)\s*\[", match.group(3)): return # Other things to ignore: # - Function pointers # - Casts to pointer types # - Placement new # - Alias declarations matched_funcptr = match.group(3) if ( matched_new_or_template is None and not ( matched_funcptr and ( re.match(r"\((?:[^() ]+::\s*\*\s*)?[^() ]+\)\s*\(", matched_funcptr) or matched_funcptr.startswith("(*)") ) ) and not re.match(r"\s*using\s+\S+\s*=\s*" + matched_type, line) and not re.search(r"new\(\S+\)\s*" + matched_type, line) ): error( filename, linenum, "readability/casting", 4, f"Using deprecated casting style. Use static_cast<{matched_type}>(...) instead", ) if not expecting_function: CheckCStyleCast( filename, clean_lines, linenum, "static_cast", r"\((int|float|double|bool|char|u?int(16|32|64)_t|size_t)\)", error, ) # This doesn't catch all cases. Consider (const char * const)"hello". # # (char *) "foo" should always be a const_cast (reinterpret_cast won't # compile). if CheckCStyleCast( filename, clean_lines, linenum, "const_cast", r'\((char\s?\*+\s?)\)\s*"', error ): pass else: # Check pointer casts for other than string constants CheckCStyleCast( filename, clean_lines, linenum, "reinterpret_cast", r"\((\w+\s?\*+\s?)\)", error ) # In addition, we look for people taking the address of a cast. This # is dangerous -- casts can assign to temporaries, so the pointer doesn't # point where you think. # # Some non-identifier character is required before the '&' for the # expression to be recognized as a cast. These are casts: # expression = &static_cast(temporary()); # function(&(int*)(temporary())); # # This is not a cast: # reference_type&(int* function_param); match = re.search( r"(?:[^\w]&\(([^)*][^)]*)\)[\w(])|" r"(?:[^\w]&(static|dynamic|down|reinterpret)_cast\b)", line, ) if match: # Try a better error message when the & is bound to something # dereferenced by the casted pointer, as opposed to the casted # pointer itself. parenthesis_error = False match = re.match(r"^(.*&(?:static|dynamic|down|reinterpret)_cast\b)<", line) if match: _, y1, x1 = CloseExpression(clean_lines, linenum, len(match.group(1))) if x1 >= 0 and clean_lines.elided[y1][x1] == "(": _, y2, x2 = CloseExpression(clean_lines, y1, x1) if x2 >= 0: extended_line = clean_lines.elided[y2][x2:] if y2 < clean_lines.NumLines() - 1: extended_line += clean_lines.elided[y2 + 1] if re.match(r"\s*(?:->|\[)", extended_line): parenthesis_error = True if parenthesis_error: error( filename, linenum, "readability/casting", 4, ( "Are you taking an address of something dereferenced " "from a cast? Wrapping the dereferenced expression in " "parentheses will make the binding more obvious" ), ) else: error( filename, linenum, "runtime/casting", 4, ( "Are you taking an address of a cast? " "This is dangerous: could be a temp var. " "Take the address before doing the cast, rather than after" ), ) def CheckCStyleCast(filename, clean_lines, linenum, cast_type, pattern, error): """Checks for a C-style cast by looking for the pattern. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. cast_type: The string for the C++ cast to recommend. This is either reinterpret_cast, static_cast, or const_cast, depending. pattern: The regular expression used to find C-style casts. error: The function to call with any errors found. Returns: True if an error was emitted. False otherwise. """ line = clean_lines.elided[linenum] match = re.search(pattern, line) if not match: return False # Exclude lines with keywords that tend to look like casts context = line[0 : match.start(1) - 1] if re.match(r".*\b(?:sizeof|alignof|alignas|[_A-Z][_A-Z0-9]*)\s*$", context): return False # Try expanding current context to see if we one level of # parentheses inside a macro. if linenum > 0: for i in range(linenum - 1, max(0, linenum - 5), -1): context = clean_lines.elided[i] + context if re.match(r".*\b[_A-Z][_A-Z0-9]*\s*\((?:\([^()]*\)|[^()])*$", context): return False # operator++(int) and operator--(int) if context.endswith((" operator++", " operator--", "::operator++", "::operator--")): return False # A single unnamed argument for a function tends to look like old style cast. # If we see those, don't issue warnings for deprecated casts. remainder = line[match.end(0) :] if re.match(r"^\s*(?:;|const\b|throw\b|final\b|override\b|[=>{),]|->)", remainder): return False # At this point, all that should be left is actual casts. error( filename, linenum, "readability/casting", 4, f"Using C-style cast. Use {cast_type}<{match.group(1)}>(...) instead", ) return True def ExpectingFunctionArgs(clean_lines, linenum): """Checks whether where function type arguments are expected. Args: clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. Returns: True if the line at 'linenum' is inside something that expects arguments of function types. """ line = clean_lines.elided[linenum] return re.match(r"^\s*MOCK_(CONST_)?METHOD\d+(_T)?\(", line) or ( linenum >= 2 and ( re.match( r"^\s*MOCK_(?:CONST_)?METHOD\d+(?:_T)?\((?:\S+,)?\s*$", clean_lines.elided[linenum - 1], ) or re.match( r"^\s*MOCK_(?:CONST_)?METHOD\d+(?:_T)?\(\s*$", clean_lines.elided[linenum - 2] ) or re.search(r"\bstd::m?function\s*\<\s*$", clean_lines.elided[linenum - 1]) ) ) _HEADERS_CONTAINING_TEMPLATES: tuple[tuple[str, tuple[str, ...]], ...] = ( ("", ("deque",)), ( "", ( "unary_function", "binary_function", "plus", "minus", "multiplies", "divides", "modulus", "negate", "equal_to", "not_equal_to", "greater", "less", "greater_equal", "less_equal", "logical_and", "logical_or", "logical_not", "unary_negate", "not1", "binary_negate", "not2", "bind1st", "bind2nd", "pointer_to_unary_function", "pointer_to_binary_function", "ptr_fun", "mem_fun_t", "mem_fun", "mem_fun1_t", "mem_fun1_ref_t", "mem_fun_ref_t", "const_mem_fun_t", "const_mem_fun1_t", "const_mem_fun_ref_t", "const_mem_fun1_ref_t", "mem_fun_ref", ), ), ("", ("numeric_limits",)), ("", ("list",)), ("", ("multimap",)), ( "", ("allocator", "make_shared", "make_unique", "shared_ptr", "unique_ptr", "weak_ptr"), ), ( "", ( "queue", "priority_queue", ), ), ( "", ( "set", "multiset", ), ), ("", ("stack",)), ( "", ( "char_traits", "basic_string", ), ), ("", ("tuple",)), ("", ("unordered_map", "unordered_multimap")), ("", ("unordered_set", "unordered_multiset")), ("", ("pair",)), ("", ("vector",)), # gcc extensions. # Note: std::hash is their hash, ::hash is our hash ( "", ( "hash_map", "hash_multimap", ), ), ( "", ( "hash_set", "hash_multiset", ), ), ("", ("slist",)), ) _HEADERS_MAYBE_TEMPLATES: tuple[tuple[str, tuple[str, ...]], ...] = ( ( "", ( "copy", "max", "min", "min_element", "sort", "transform", ), ), ("", ("forward", "make_pair", "move", "swap")), ) # Non templated types or global objects _HEADERS_TYPES_OR_OBJS: tuple[tuple[str, tuple[str, ...]], ...] = ( # String and others are special -- it is a non-templatized type in STL. ("", ("string",)), ("", ("cin", "cout", "cerr", "clog", "wcin", "wcout", "wcerr", "wclog")), ("", ("FILE", "fpos_t")), ) # Non templated functions _HEADERS_FUNCTIONS: tuple[tuple[str, tuple[str, ...]], ...] = ( ( "", ( "fopen", "freopen", "fclose", "fflush", "setbuf", "setvbuf", "fread", "fwrite", "fgetc", "getc", "fgets", "fputc", "putc", "fputs", "getchar", "gets", "putchar", "puts", "ungetc", "scanf", "fscanf", "sscanf", "vscanf", "vfscanf", "vsscanf", "printf", "fprintf", "sprintf", "snprintf", "vprintf", "vfprintf", "vsprintf", "vsnprintf", "ftell", "fgetpos", "fseek", "fsetpos", "clearerr", "feof", "ferror", "perror", "tmpfile", "tmpnam", ), ), ) _re_pattern_headers_maybe_templates: list[tuple[re.Pattern, str, str]] = [] for _header, _templates in _HEADERS_MAYBE_TEMPLATES: # Match max(..., ...), max(..., ...), but not foo->max, foo.max or # 'type::max()'. _re_pattern_headers_maybe_templates.extend( (re.compile(r"((\bstd::)|[^>.:])\b" + _template + r"(<.*?>)?\([^\)]"), _template, _header) for _template in _templates ) # Map is often overloaded. Only check, if it is fully qualified. # Match 'std::map(...)', but not 'map(...)'' _re_pattern_headers_maybe_templates.append( (re.compile(r"(std\b::\bmap\s*\<)|(^(std\b::\b)map\b\(\s*\<)"), "map<>", "") ) # Other scripts may reach in and modify this pattern. _re_pattern_templates: list[tuple[re.Pattern, str, str]] = [] for _header, _templates in _HEADERS_CONTAINING_TEMPLATES: _re_pattern_templates.extend( ( re.compile(r"((^|(^|\s|((^|\W)::))std::)|[^>.:]\b)" + _template + r"\s*\<"), _template + "<>", _header, ) for _template in _templates ) _re_pattern_types_or_objs: list[tuple[re.Pattern, object | type, str]] = [] for _header, _types_or_objs in _HEADERS_TYPES_OR_OBJS: _re_pattern_types_or_objs.extend( (re.compile(r"\b" + _type_or_obj + r"\b"), _type_or_obj, _header) for _type_or_obj in _types_or_objs ) _re_pattern_functions: list[tuple[re.Pattern, str, str]] = [] for _header, _functions in _HEADERS_FUNCTIONS: # Match printf(..., ...), but not foo->printf, foo.printf or # 'type::printf()'. _re_pattern_functions.extend( (re.compile(r"([^>.]|^)\b" + _function + r"\([^\)]"), _function, _header) for _function in _functions ) def FilesBelongToSameModule(filename_cc, filename_h): """Check if these two filenames belong to the same module. The concept of a 'module' here is a as follows: foo.h, foo-inl.h, foo.cc, foo_test.cc and foo_unittest.cc belong to the same 'module' if they are in the same directory. some/path/public/xyzzy and some/path/internal/xyzzy are also considered to belong to the same module here. If the filename_cc contains a longer path than the filename_h, for example, '/absolute/path/to/base/sysinfo.cc', and this file would include 'base/sysinfo.h', this function also produces the prefix needed to open the header. This is used by the caller of this function to more robustly open the header file. We don't have access to the real include paths in this context, so we need this guesswork here. Known bugs: tools/base/bar.cc and base/bar.h belong to the same module according to this implementation. Because of this, this function gives some false positives. This should be sufficiently rare in practice. Args: filename_cc: is the path for the source (e.g. .cc) file filename_h: is the path for the header path Returns: Tuple with a bool and a string: bool: True if filename_cc and filename_h belong to the same module. string: the additional prefix needed to open the header file. """ fileinfo_cc = FileInfo(filename_cc) if fileinfo_cc.Extension().lstrip(".") not in GetNonHeaderExtensions(): return (False, "") fileinfo_h = FileInfo(filename_h) if not IsHeaderExtension(fileinfo_h.Extension().lstrip(".")): return (False, "") filename_cc = filename_cc[: -(len(fileinfo_cc.Extension()))] if matched_test_suffix := re.search(_TEST_FILE_SUFFIX, fileinfo_cc.BaseName()): filename_cc = filename_cc[: -len(matched_test_suffix.group(1))] filename_cc = filename_cc.replace("/public/", "/") filename_cc = filename_cc.replace("/internal/", "/") filename_h = filename_h[: -(len(fileinfo_h.Extension()))] filename_h = filename_h.removesuffix("-inl") filename_h = filename_h.replace("/public/", "/") filename_h = filename_h.replace("/internal/", "/") files_belong_to_same_module = filename_cc.endswith(filename_h) common_path = "" if files_belong_to_same_module: common_path = filename_cc[: -len(filename_h)] return files_belong_to_same_module, common_path def CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error, io=codecs): """Reports for missing stl includes. This function will output warnings to make sure you are including the headers necessary for the stl containers and functions that you use. We only give one reason to include a header. For example, if you use both equal_to<> and less<> in a .h file, only one (the latter in the file) of these will be reported as a reason to include the . Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. include_state: An _IncludeState instance. error: The function to call with any errors found. io: The IO factory to use to read the header file. Provided for unittest injection. """ required = {} # A map of header name to linenumber and the template entity. # Example of required: { '': (1219, 'less<>') } for linenum in range(clean_lines.NumLines()): line = clean_lines.elided[linenum] if not line or line[0] == "#": continue _re_patterns = [] _re_patterns.extend(_re_pattern_types_or_objs) _re_patterns.extend(_re_pattern_functions) for pattern, item, header in _re_patterns: matched = pattern.search(line) if matched: # Don't warn about strings in non-STL namespaces: # (We check only the first match per line; good enough.) prefix = line[: matched.start()] if prefix.endswith("std::") or not prefix.endswith("::"): required[header] = (linenum, item) for pattern, template, header in _re_pattern_headers_maybe_templates: if pattern.search(line): required[header] = (linenum, template) # The following function is just a speed up, no semantics are changed. if "<" not in line: # Reduces the cpu time usage by skipping lines. continue for pattern, template, header in _re_pattern_templates: matched = pattern.search(line) if matched: # Don't warn about IWYU in non-STL namespaces: # (We check only the first match per line; good enough.) prefix = line[: matched.start()] if prefix.endswith("std::") or not prefix.endswith("::"): required[header] = (linenum, template) # Let's flatten the include_state include_list and copy it into a dictionary. include_dict = dict([item for sublist in include_state.include_list for item in sublist]) # All the lines have been processed, report the errors found. for header in sorted(required, key=required.__getitem__): template = required[header][1] header_stripped = header.strip('<>"') if header_stripped not in include_dict and not ( header_stripped[0] == "c" and (header_stripped[1:] + ".h") in include_dict ): error( filename, required[header][0], "build/include_what_you_use", 4, "Add #include " + header + " for " + template, ) _RE_PATTERN_EXPLICIT_MAKEPAIR = re.compile(r"\bmake_pair\s*<") def CheckMakePairUsesDeduction(filename, clean_lines, linenum, error): """Check that make_pair's template arguments are deduced. G++ 4.6 in C++11 mode fails badly if make_pair's template arguments are specified explicitly, and such use isn't intended in any case. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] match = _RE_PATTERN_EXPLICIT_MAKEPAIR.search(line) if match: error( filename, linenum, "build/explicit_make_pair", 4, # 4 = high confidence "For C++11-compatibility, omit template arguments from make_pair" " OR use pair directly OR if appropriate, construct a pair directly", ) def CheckRedundantVirtual(filename, clean_lines, linenum, error): """Check if line contains a redundant "virtual" function-specifier. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ # Look for "virtual" on current line. line = clean_lines.elided[linenum] virtual = re.match(r"^(.*)(\bvirtual\b)(.*)$", line) if not virtual: return # Ignore "virtual" keywords that are near access-specifiers. These # are only used in class base-specifier and do not apply to member # functions. if re.search(r"\b(public|protected|private)\s+$", virtual.group(1)) or re.match( r"^\s+(public|protected|private)\b", virtual.group(3) ): return # Ignore the "virtual" keyword from virtual base classes. Usually # there is a column on the same line in these cases (virtual base # classes are rare in google3 because multiple inheritance is rare). if re.match(r"^.*[^:]:[^:].*$", line): return # Look for the next opening parenthesis. This is the start of the # parameter list (possibly on the next line shortly after virtual). # TODO(google): doesn't work if there are virtual functions with # decltype() or other things that use parentheses, but csearch suggests # that this is rare. end_col = -1 end_line = -1 start_col = len(virtual.group(2)) for start_line in range(linenum, min(linenum + 3, clean_lines.NumLines())): line = clean_lines.elided[start_line][start_col:] parameter_list = re.match(r"^([^(]*)\(", line) if parameter_list: # Match parentheses to find the end of the parameter list (_, end_line, end_col) = CloseExpression( clean_lines, start_line, start_col + len(parameter_list.group(1)) ) break start_col = 0 if end_col < 0: return # Couldn't find end of parameter list, give up # Look for "override" or "final" after the parameter list # (possibly on the next few lines). for i in range(end_line, min(end_line + 3, clean_lines.NumLines())): line = clean_lines.elided[i][end_col:] match = re.search(r"\b(override|final)\b", line) if match: error( filename, linenum, "readability/inheritance", 4, ( '"virtual" is redundant since function is ' f'already declared as "{match.group(1)}"' ), ) # Set end_col to check whole lines after we are done with the # first line. end_col = 0 if re.search(r"[^\w]\s*$", line): break def CheckRedundantOverrideOrFinal(filename, clean_lines, linenum, error): """Check if line contains a redundant "override" or "final" virt-specifier. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ # Look for closing parenthesis nearby. We need one to confirm where # the declarator ends and where the virt-specifier starts to avoid # false positives. line = clean_lines.elided[linenum] if (declarator_end := line.rfind(")")) >= 0: fragment = line[declarator_end:] else: if linenum > 1 and clean_lines.elided[linenum - 1].rfind(")") >= 0: fragment = line else: return # Check that at most one of "override" or "final" is present, not both if re.search(r"\boverride\b", fragment) and re.search(r"\bfinal\b", fragment): error( filename, linenum, "readability/inheritance", 4, ('"override" is redundant since function is already declared as "final"'), ) # Returns true if we are at a new block, and it is directly # inside of a namespace. def IsBlockInNameSpace(nesting_state: NestingState, is_forward_declaration: bool): # noqa: FBT001 """Checks that the new block is directly in a namespace. Args: nesting_state: The NestingState object that contains info about our state. is_forward_declaration: If the class is a forward declared class. Returns: Whether or not the new block is directly in a namespace. """ if is_forward_declaration: return len(nesting_state.stack) >= 1 and ( isinstance(nesting_state.stack[-1], _NamespaceInfo) ) if len(nesting_state.stack) >= 1: if isinstance(nesting_state.stack[-1], _NamespaceInfo): return True if ( len(nesting_state.stack) > 1 and isinstance(nesting_state.previous_stack_top, _NamespaceInfo) and ( isinstance(nesting_state.stack[-2], _NamespaceInfo) or len(nesting_state.stack) > 2 # Accommodate for WrappedInfo and issubclass(type(nesting_state.stack[-1]), _WrappedInfo) and not nesting_state.stack[-2].seen_open_brace and isinstance(nesting_state.stack[-3], _NamespaceInfo) ) ): return True return False def ShouldCheckNamespaceIndentation( nesting_state: NestingState, is_namespace_indent_item, raw_lines_no_comments, linenum ): """This method determines if we should apply our namespace indentation check. Args: nesting_state: The current nesting state. is_namespace_indent_item: If we just put a new class on the stack, True. If the top of the stack is not a class, or we did not recently add the class, False. raw_lines_no_comments: The lines without the comments. linenum: The current line number we are processing. Returns: True if we should apply our namespace indentation check. Currently, it only works for classes and namespaces inside of a namespace. """ # Required by all checks involving nesting_state if not nesting_state.stack: return False is_forward_declaration = IsForwardClassDeclaration(raw_lines_no_comments, linenum) if not (is_namespace_indent_item or is_forward_declaration): return False # If we are in a macro, we do not want to check the namespace indentation. if IsMacroDefinition(raw_lines_no_comments, linenum): return False # Skip if we are inside an open parenthesis block (e.g. function parameters). if nesting_state.previous_stack_top and nesting_state.previous_open_parentheses > 0: return False # Skip if we are extra-indenting a member initializer list. if ( isinstance(nesting_state.previous_stack_top, _ConstructorInfo) # F/N (A::A() : _a(0) {/{}) and ( isinstance(nesting_state.stack[-1], _MemInitListInfo) or isinstance(nesting_state.popped_top, _MemInitListInfo) ) ) or ( # popping constructor after MemInitList on the same line (: _a(a) {}) isinstance(nesting_state.previous_stack_top, _ConstructorInfo) and isinstance(nesting_state.popped_top, _ConstructorInfo) and re.search(r"[^:]:[^:]", raw_lines_no_comments[linenum]) ): return False return IsBlockInNameSpace(nesting_state, is_forward_declaration) # Call this method if the line is directly inside of a namespace. # If the line above is blank (excluding comments) or the start of # an inner namespace, it cannot be indented. def CheckItemIndentationInNamespace(filename, raw_lines_no_comments, linenum, error): line = raw_lines_no_comments[linenum] if re.match(r"^\s+", line): error( filename, linenum, "whitespace/indent_namespace", 4, "Do not indent within a namespace." ) def ProcessLine( filename, file_extension, clean_lines, line, include_state, function_state, nesting_state, error, extra_check_functions=None, cppvar=None, ): """Processes a single line in the file. Args: filename: Filename of the file that is being processed. file_extension: The extension (dot not included) of the file. clean_lines: An array of strings, each representing a line of the file, with comments stripped. line: Number of line being processed. include_state: An _IncludeState instance in which the headers are inserted. function_state: A _FunctionState instance which counts function lines, etc. nesting_state: A NestingState instance which maintains information about the current stack of nested blocks being parsed. error: A callable to which errors are reported, which takes 4 arguments: filename, line number, error level, and message extra_check_functions: An array of additional check functions that will be run on each source line. Each function takes 4 arguments: filename, clean_lines, line, error cppvar: The header guard variable returned by GetHeaderGuardCPPVar. """ raw_lines = clean_lines.raw_lines ParseNolintSuppressions(filename, raw_lines[line], line, error) nesting_state.Update(filename, clean_lines, line, error) CheckForNamespaceIndentation(filename, nesting_state, clean_lines, line, error) if nesting_state.InAsmBlock(): return CheckForFunctionLengths(filename, clean_lines, line, function_state, error) CheckForMultilineCommentsAndStrings(filename, clean_lines, line, error) CheckStyle(filename, clean_lines, line, file_extension, nesting_state, error, cppvar) CheckLanguage(filename, clean_lines, line, file_extension, include_state, nesting_state, error) CheckForNonConstReference(filename, clean_lines, line, nesting_state, error) CheckForNonStandardConstructs(filename, clean_lines, line, nesting_state, error) CheckVlogArguments(filename, clean_lines, line, error) CheckPosixThreading(filename, clean_lines, line, error) CheckInvalidIncrement(filename, clean_lines, line, error) CheckMakePairUsesDeduction(filename, clean_lines, line, error) CheckRedundantVirtual(filename, clean_lines, line, error) CheckRedundantOverrideOrFinal(filename, clean_lines, line, error) if extra_check_functions: for check_fn in extra_check_functions: check_fn(filename, clean_lines, line, error) def FlagCxxHeaders(filename, clean_lines, linenum, error): """Flag C++ headers that the styleguide restricts. Args: filename: The name of the current file. clean_lines: A CleansedLines instance containing the file. linenum: The number of the line to check. error: The function to call with any errors found. """ line = clean_lines.elided[linenum] include = re.match(r'\s*#\s*include\s+[<"]([^<"]+)[">]', line) # Flag unapproved C++11 headers. if include and include.group(1) in ( "cfenv", "fenv.h", "ratio", ): error( filename, linenum, "build/c++11", 5, f"<{include.group(1)}> is an unapproved C++11 header.", ) # filesystem is the only unapproved C++17 header if include and include.group(1) == "filesystem": error(filename, linenum, "build/c++17", 5, " is an unapproved C++17 header.") def ProcessFileData(filename, file_extension, lines, error, extra_check_functions=None): """Performs lint checks and reports any errors to the given error function. Args: filename: Filename of the file that is being processed. file_extension: The extension (dot not included) of the file. lines: An array of strings, each representing a line of the file, with the last element being empty if the file is terminated with a newline. error: A callable to which errors are reported, which takes 4 arguments: filename, line number, error level, and message extra_check_functions: An array of additional check functions that will be run on each source line. Each function takes 4 arguments: filename, clean_lines, line, error """ lines = ( ["// marker so line numbers and indices both start at 1"] + lines + ["// marker so line numbers end in a known way"] ) include_state = _IncludeState() function_state = _FunctionState() nesting_state = NestingState() ResetNolintSuppressions() CheckForCopyright(filename, lines, error) ProcessGlobalSuppressions(filename, lines) RemoveMultiLineComments(filename, lines, error) clean_lines = CleansedLines(lines) cppvar = None if IsHeaderExtension(file_extension): cppvar = GetHeaderGuardCPPVariable(filename) CheckForHeaderGuard(filename, clean_lines, error, cppvar) for line in range(clean_lines.NumLines()): ProcessLine( filename, file_extension, clean_lines, line, include_state, function_state, nesting_state, error, extra_check_functions, cppvar, ) FlagCxxHeaders(filename, clean_lines, line, error) if _error_suppressions.HasOpenBlock(): error( filename, _error_suppressions.GetOpenBlockStart(), "readability/nolint", 5, "NONLINT block never ended", ) CheckForIncludeWhatYouUse(filename, clean_lines, include_state, error) # Check that the .cc file has included its header if it exists. if _IsSourceExtension(file_extension): CheckHeaderFileIncluded(filename, include_state, error) # We check here rather than inside ProcessLine so that we see raw # lines rather than "cleaned" lines. CheckForBadCharacters(filename, lines, error) CheckForNewlineAtEOF(filename, lines, error) def ProcessConfigOverrides(filename): """Loads the configuration files and processes the config overrides. Args: filename: The name of the file being processed by the linter. Returns: False if the current |filename| should not be processed further. """ abs_filename = os.path.abspath(filename) cfg_filters = [] keep_looking = True while keep_looking: abs_path, base_name = os.path.split(abs_filename) if not base_name: break # Reached the root directory. cfg_file = os.path.join(abs_path, _config_filename) abs_filename = abs_path if not os.path.isfile(cfg_file): continue try: with codecs.open(cfg_file, "r", "utf8", "replace") as file_handle: for line in file_handle: line, _, _ = line.partition("#") # Remove comments. if not line.strip(): continue name, _, val = line.partition("=") name = name.strip() val = val.strip() if name == "set noparent": keep_looking = False elif name == "filter": cfg_filters.append(val) elif name == "exclude_files": # When matching exclude_files pattern, use the base_name of # the current file name or the directory name we are processing. # For example, if we are checking for lint errors in /foo/bar/baz.cc # and we found the .cfg file at /foo/CPPLINT.cfg, then the config # file's "exclude_files" filter is meant to be checked against "bar" # and not "baz" nor "bar/baz.cc". if base_name: pattern = re.compile(val) if pattern.match(base_name): if _cpplint_state.quiet: # Suppress "Ignoring file" warning when using --quiet. return False _cpplint_state.PrintInfo( f'Ignoring "{filename}": file excluded by "{cfg_file}". ' 'File path component "%s" matches ' 'pattern "%s"\n' % (base_name, val) ) return False elif name == "linelength": global _line_length try: _line_length = int(val) except ValueError: _cpplint_state.PrintError("Line length must be numeric.") elif name == "extensions": ProcessExtensionsOption(val) elif name == "root": global _root # root directories are specified relative to CPPLINT.cfg dir. _root = os.path.join(os.path.dirname(cfg_file), val) elif name == "headers": ProcessHppHeadersOption(val) elif name == "includeorder": ProcessIncludeOrderOption(val) else: _cpplint_state.PrintError( f"Invalid configuration option ({name}) in file {cfg_file}\n" ) except OSError: _cpplint_state.PrintError( f"Skipping config file '{cfg_file}': Can't open for reading\n" ) keep_looking = False # Apply all the accumulated filters in reverse order (top-level directory # config options having the least priority). for cfg_filter in reversed(cfg_filters): _AddFilters(cfg_filter) return True def ProcessFile(filename, vlevel, extra_check_functions=None): """Does google-lint on a single file. Args: filename: The name of the file to parse. vlevel: The level of errors to report. Every error of confidence >= verbose_level will be reported. 0 is a good default. extra_check_functions: An array of additional check functions that will be run on each source line. Each function takes 4 arguments: filename, clean_lines, line, error """ _SetVerboseLevel(vlevel) _BackupFilters() old_errors = _cpplint_state.error_count if not ProcessConfigOverrides(filename): _RestoreFilters() return lf_lines = [] crlf_lines = [] try: # Support the UNIX convention of using "-" for stdin. Note that # we are not opening the file with universal newline support # (which codecs doesn't support anyway), so the resulting lines do # contain trailing '\r' characters if we are reading a file that # has CRLF endings. # If after the split a trailing '\r' is present, it is removed # below. if filename == "-": lines = sys.stdin.read().split("\n") else: with codecs.open(filename, "r", "utf8", "replace") as target_file: lines = target_file.read().split("\n") # Remove trailing '\r'. # The -1 accounts for the extra trailing blank line we get from split() for linenum in range(len(lines) - 1): if lines[linenum].endswith("\r"): lines[linenum] = lines[linenum].rstrip("\r") crlf_lines.append(linenum + 1) else: lf_lines.append(linenum + 1) except OSError: # TODO(aaronliu0130): Maybe make this have an exit code of 2 after all is done _cpplint_state.PrintError(f"Skipping input '{filename}': Can't open for reading\n") _RestoreFilters() return # Note, if no dot is found, this will give the entire filename as the ext. file_extension = filename[filename.rfind(".") + 1 :] # When reading from stdin, the extension is unknown, so no cpplint tests # should rely on the extension. if filename != "-" and file_extension not in GetAllExtensions(): _cpplint_state.PrintError( f"Ignoring {filename}; not a valid file name ({(', '.join(GetAllExtensions()))})\n" ) else: ProcessFileData(filename, file_extension, lines, Error, extra_check_functions) # If end-of-line sequences are a mix of LF and CR-LF, issue # warnings on the lines with CR. # # Don't issue any warnings if all lines are uniformly LF or CR-LF, # since critique can handle these just fine, and the style guide # doesn't dictate a particular end of line sequence. # # We can't depend on os.linesep to determine what the desired # end-of-line sequence should be, since that will return the # server-side end-of-line sequence. if lf_lines and crlf_lines: # Warn on every line with CR. An alternative approach might be to # check whether the file is mostly CRLF or just LF, and warn on the # minority, we bias toward LF here since most tools prefer LF. for linenum in crlf_lines: Error( filename, linenum, "whitespace/newline", 1, "Unexpected \\r (^M) found; better to use only \\n", ) # Suppress printing anything if --quiet was passed unless the error # count has increased after processing this file. if not _cpplint_state.quiet or old_errors != _cpplint_state.error_count: _cpplint_state.PrintInfo(f"Done processing {filename}\n") _RestoreFilters() def PrintUsage(message): """Prints a brief usage string and exits, optionally with an error message. Args: message: The optional error message. """ sys.stderr.write( _USAGE % ( sorted(GetAllExtensions()), ",".join(sorted(GetAllExtensions())), sorted(GetHeaderExtensions()), ",".join(sorted(GetHeaderExtensions())), ) ) if message: sys.exit("\nFATAL ERROR: " + message) else: sys.exit(0) def PrintVersion(): sys.stdout.write("Cpplint fork (https://github.com/cpplint/cpplint)\n") sys.stdout.write("cpplint " + __VERSION__ + "\n") sys.stdout.write("Python " + sys.version + "\n") sys.exit(0) def PrintCategories(): """Prints a list of all the error-categories used by error messages. These are the categories used to filter messages via --filter. """ sys.stderr.write("".join(f" {cat}\n" for cat in _ERROR_CATEGORIES)) sys.exit(0) def ParseArguments(args): """Parses the command line arguments. This may set the output format and verbosity level as side-effects. Args: args: The command line arguments: Returns: The list of filenames to lint. """ try: (opts, filenames) = getopt.getopt( args, "", [ "help", "output=", "verbose=", "v=", "version", "counting=", "filter=", "root=", "repository=", "linelength=", "extensions=", "exclude=", "recursive", "headers=", "includeorder=", "config=", "quiet", ], ) except getopt.GetoptError: PrintUsage("Invalid arguments.") verbosity = _VerboseLevel() output_format = _OutputFormat() filters = "" quiet = _Quiet() counting_style = "" recursive = False for opt, val in opts: if opt == "--help": PrintUsage(None) if opt == "--version": PrintVersion() elif opt == "--output": if val not in ("emacs", "vs7", "eclipse", "junit", "sed", "gsed"): PrintUsage( "The only allowed output formats are emacs, vs7, eclipse sed, gsed and junit." ) output_format = val elif opt == "--quiet": quiet = True elif opt in {"--verbose", "--v"}: verbosity = int(val) elif opt == "--filter": filters = val if not filters: PrintCategories() elif opt == "--counting": if val not in ("total", "toplevel", "detailed"): PrintUsage("Valid counting options are total, toplevel, and detailed") counting_style = val elif opt == "--root": global _root _root = val elif opt == "--repository": global _repository _repository = val elif opt == "--linelength": global _line_length try: _line_length = int(val) except ValueError: PrintUsage("Line length must be digits.") elif opt == "--exclude": global _excludes if not _excludes: _excludes = set() _excludes.update(glob.glob(val)) elif opt == "--extensions": ProcessExtensionsOption(val) elif opt == "--headers": ProcessHppHeadersOption(val) elif opt == "--recursive": recursive = True elif opt == "--includeorder": ProcessIncludeOrderOption(val) elif opt == "--config": global _config_filename _config_filename = val if os.path.basename(_config_filename) != _config_filename: PrintUsage("Config file name must not include directory components.") if not filenames: PrintUsage("No files were specified.") if recursive: filenames = _ExpandDirectories(filenames) if _excludes: filenames = _FilterExcludedFiles(filenames) _SetOutputFormat(output_format) _SetQuiet(quiet) _SetVerboseLevel(verbosity) _SetFilters(filters) _SetCountingStyle(counting_style) filenames.sort() return filenames def _ParseFilterSelector(parameter): """Parses the given command line parameter for file- and line-specific exclusions. readability/casting:file.cpp readability/casting:file.cpp:43 Args: parameter: The parameter value of --filter Returns: [category, filename, line]. Category is always given. Filename is either a filename or empty if all files are meant. Line is either a line in filename or -1 if all lines are meant. """ colon_pos = parameter.find(":") if colon_pos == -1: return parameter, "", -1 category = parameter[:colon_pos] second_colon_pos = parameter.find(":", colon_pos + 1) if second_colon_pos == -1: return category, parameter[colon_pos + 1 :], -1 return ( category, parameter[colon_pos + 1 : second_colon_pos], int(parameter[second_colon_pos + 1 :]), ) def _ExpandDirectories(filenames): """Searches a list of filenames and replaces directories in the list with all files descending from those directories. Files with extensions not in the valid extensions list are excluded. Args: filenames: A list of files or directories Returns: A list of all files that are members of filenames or descended from a directory in filenames """ expanded = set() for filename in filenames: if not os.path.isdir(filename): expanded.add(filename) continue for root, _, files in os.walk(filename): for loopfile in files: fullname = os.path.join(root, loopfile) fullname = fullname.removeprefix("." + os.path.sep) expanded.add(fullname) return [ filename for filename in expanded if os.path.splitext(filename)[1][1:] in GetAllExtensions() ] def _FilterExcludedFiles(fnames): """Filters out files listed in the --exclude command line switch. File paths in the switch are evaluated relative to the current working directory """ exclude_paths = [os.path.abspath(f) for f in _excludes] # because globbing does not work recursively, exclude all subpath of all excluded entries return [ f for f in fnames if not any(e for e in exclude_paths if _IsParentOrSame(e, os.path.abspath(f))) ] def _IsParentOrSame(parent, child): """Return true if child is subdirectory of parent. Assumes both paths are absolute and don't contain symlinks. """ parent = os.path.normpath(parent) child = os.path.normpath(child) if parent == child: return True prefix = os.path.commonprefix([parent, child]) if prefix != parent: return False # Note: os.path.commonprefix operates on character basis, so # take extra care of situations like '/foo/ba' and '/foo/bar/baz' child_suffix = child[len(prefix) :] child_suffix = child_suffix.lstrip(os.sep) return child == os.path.join(prefix, child_suffix) def main(): filenames = ParseArguments(sys.argv[1:]) backup_err = sys.stderr try: # Change stderr to write with replacement characters so we don't die # if we try to print something containing non-ASCII characters. sys.stderr = codecs.StreamReader(sys.stderr, "replace") _cpplint_state.ResetErrorCounts() for filename in filenames: ProcessFile(filename, _cpplint_state.verbose_level) # If --quiet is passed, suppress printing error count unless there are errors. if not _cpplint_state.quiet or _cpplint_state.error_count > 0: _cpplint_state.PrintErrorCounts() if _cpplint_state.output_format == "junit": sys.stderr.write(_cpplint_state.FormatJUnitXML()) finally: sys.stderr = backup_err sys.exit(_cpplint_state.error_count > 0) if __name__ == "__main__": main() ================================================ FILE: depends/cmake/CamouflageTLS.cmake ================================================ include(FetchContent) FetchContent_Declare(CamouflageTLS URL https://github.com/fptn-project/camouflage-tls/archive/refs/heads/main.zip) FetchContent_MakeAvailable(CamouflageTLS) set(CamouflageTLS_INCLUDE_DIR "${camouflagetls_SOURCE_DIR}/include") set(CamouflageTLS_INCLUDE_DIRS ${CamouflageTLS_INCLUDE_DIR} CACHE PATH "Camouflage Include Directories") if(NOT TARGET CamouflageTLS) add_library(CamouflageTLS INTERFACE) target_include_directories(CamouflageTLS INTERFACE "${CamouflageTLS_INCLUDE_DIR}") endif() target_include_directories(CamouflageTLS INTERFACE "${camouflagetls_SOURCE_DIR}/include") ================================================ FILE: depends/cmake/FetchBase64.cmake ================================================ include(FetchContent) FetchContent_Declare(Base64 URL https://github.com/tobiaslocker/base64/archive/refs/heads/master.zip) FetchContent_GetProperties(Base64) if(NOT Base64_POPULATED) FetchContent_Populate(Base64) endif() set(Base64_SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/_deps/base64-src") set(Base64_INCLUDE_DIR "${Base64_SOURCE_DIR}/include") set(Base64_INCLUDE_DIRS ${Base64_INCLUDE_DIR} CACHE PATH "Base64 Include Directories") add_library(Base64 INTERFACE) target_include_directories(Base64 INTERFACE "${Base64_INCLUDE_DIR}") include_directories(${Base64_INCLUDE_DIR}) ================================================ FILE: depends/cmake/FetchLibTunTap.cmake ================================================ cmake_minimum_required(VERSION 3.16) option(BUILD_TESTING "Build tests" OFF) include(FetchContent) FetchContent_Declare(Libtuntap URL https://github.com/LaKabane/libtuntap/archive/ec1213733eb2e66e033ff8864d9fd476f9e35ffe.zip) #FetchContent_Declare(Libtuntap URL https://github.com/LaKabane/libtuntap/archive/refs/heads/master.zip) FetchContent_GetProperties(Libtuntap) if(NOT Libtuntap_POPULATED) FetchContent_Populate(Libtuntap) endif() set(Libtuntap_SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/libtuntap-src") set(Libtuntap_BINARY_DIR "${CMAKE_BINARY_DIR}/_deps/libtuntap-build") set(Libtuntap_INCLUDE_DIR "${Libtuntap_BINARY_DIR}/include") set(BUILD_TESTING OFF) set(LIBTUNTAP_BUILD_TESTS OFF CACHE BOOL "Disable libtuntap tests") add_subdirectory("${Libtuntap_SOURCE_DIR}" "${Libtuntap_BINARY_DIR}" EXCLUDE_FROM_ALL) include_directories("${Libtuntap_SOURCE_DIR}/libtuntap/") include_directories("${Libtuntap_SOURCE_DIR}/libtuntap/bindings/cpp") link_directories("${Libtuntap_BINARY_DIR}/lib/") ================================================ FILE: depends/cmake/FetchWintun.cmake ================================================ include(FetchContent) FetchContent_Declare(Wintun URL https://www.wintun.net/builds/wintun-0.14.1.zip) FetchContent_GetProperties(Wintun) if(NOT wintun_POPULATED) FetchContent_Populate(Wintun) endif() set(Wintun_INCLUDE_DIR "${wintun_SOURCE_DIR}/include") if(CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "AMD64") set(Wintun_REDISTRIBUTABLE "${wintun_SOURCE_DIR}/bin/amd64/wintun.dll") set(Wintun_REDISTRIBUTABLE_DIR "${wintun_SOURCE_DIR}/bin/amd64") elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "arm64" OR CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64") set(Wintun_REDISTRIBUTABLE "${wintun_SOURCE_DIR}/bin/arm64/wintun.dll") set(Wintun_REDISTRIBUTABLE_DIR "${wintun_SOURCE_DIR}/bin/arm64") else() message(FATAL_ERROR "Unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}") endif() set(TARGET_DIRECTORY "${CMAKE_BINARY_DIR}/wintun") file(MAKE_DIRECTORY ${TARGET_DIRECTORY}) file(COPY ${Wintun_REDISTRIBUTABLE} DESTINATION ${TARGET_DIRECTORY}) add_library(Wintun INTERFACE) target_include_directories(Wintun INTERFACE "${Wintun_INCLUDE_DIR}") ================================================ FILE: depends/cmake/NtpClient.cmake ================================================ include(FetchContent) FetchContent_Declare(ntp_client URL https://github.com/batchar2/NTP-client/archive/refs/heads/master.zip) FetchContent_GetProperties(ntp_client) if(NOT ntp_client_POPULATED) FetchContent_Populate(ntp_client) set(ntp_client_SOURCE_DIR "${ntp_client_SOURCE_DIR}") set(ntp_client_BINARY_DIR "${ntp_client_BINARY_DIR}") set(ntp_client_INCLUDE_DIR "${ntp_client_SOURCE_DIR}/include") add_subdirectory("${ntp_client_SOURCE_DIR}" "${ntp_client_BINARY_DIR}" EXCLUDE_FROM_ALL) include_directories("${ntp_client_INCLUDE_DIR}") endif() ================================================ FILE: deploy/docker/Dockerfile ================================================ FROM ubuntu:24.04 ARG FPTN_SERVER_PATH ARG FPTN_PASSWD_PATH EXPOSE 443/tcp ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC RUN apt-get update && \ apt-get upgrade -y && \ apt-get install -y unbound iproute2 iptables supervisor wget dnsmasq iputils-ping && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* RUN mkdir -p /etc/supervisor/conf.d # Copy binaries COPY "${FPTN_SERVER_PATH}" /usr/local/bin/fptn-server COPY "${FPTN_PASSWD_PATH}" /usr/local/bin/fptn-passwd RUN chmod +x /usr/local/bin/fptn-server && \ chmod +x /usr/local/bin/fptn-passwd # Copy configuration files COPY deploy/docker/config/supervisord.conf /etc/supervisor/supervisord.conf COPY deploy/docker/config/supervisor/*.conf /etc/supervisor/conf.d/ # Copy scripts COPY deploy/docker/scripts/start-fptn.sh /usr/local/bin/start-fptn.sh COPY deploy/docker/scripts/start-dns-server.sh /usr/local/bin/start-dns-server.sh COPY deploy/docker/scripts/token-generator.py /usr/local/bin/token-generator RUN chmod +x /usr/local/bin/start-fptn.sh && \ chmod +x /usr/local/bin/token-generator && \ chmod +x /usr/local/bin/start-dns-server.sh CMD ["/usr/bin/supervisord", "--nodaemon", "-c", "/etc/supervisor/supervisord.conf"] ================================================ FILE: deploy/docker/config/supervisor/dns-server.conf ================================================ [program:dns-server] command=/usr/local/bin/start-dns-server.sh autostart=true autorestart=true startretries=3 startsecs=2 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 redirect_stderr=true user=root ================================================ FILE: deploy/docker/config/supervisor/fptn-server.conf ================================================ [program:fptn-server] command=/usr/local/bin/start-fptn.sh autostart=true autorestart=true startretries=3 startsecs=5 stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 redirect_stderr=true user=root ================================================ FILE: deploy/docker/config/supervisord.conf ================================================ [supervisord] user=root nodaemon=true logfile=/dev/stdout logfile_maxbytes=0 pidfile=/var/run/supervisord.pid [rpcinterface:supervisor] supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface [unix_http_server] file=/var/run/supervisor.sock chmod=0700 user=root [supervisorctl] serverurl=unix:///var/run/supervisor.sock [include] files = /etc/supervisor/conf.d/*.conf ================================================ FILE: deploy/docker/scripts/start-dns-server.sh ================================================ #!/bin/bash start_unbound() { echo "Starting unbound..." DO_IP6="no" [ "${DNS_IPV6_ENABLE:-false}" = "true" ] && DO_IP6="yes" CONFIG_FILE="/etc/unbound/unbound.conf" ROOT_HINTS="/etc/unbound/root.hints" echo "Using DNS settings:" echo " IPv4: yes" echo " IPv6: $DO_IP6" if [ "${DNS_IPV6_ENABLE:-false}" = "true" ]; then cat > "$CONFIG_FILE" << 'EOF' server: interface: 0.0.0.0 interface: ::0 port: 53 do-ip4: yes do-ip6: yes do-udp: yes do-tcp: yes access-control: 0.0.0.0/0 allow access-control: ::0/0 allow root-hints: "/etc/unbound/root.hints" cache-max-ttl: 86400 cache-min-ttl: 3600 hide-identity: yes hide-version: yes EOF else cat > "$CONFIG_FILE" << 'EOF' server: interface: 0.0.0.0 port: 53 do-ip4: yes do-ip6: no do-udp: yes do-tcp: yes access-control: 0.0.0.0/0 allow root-hints: "/etc/unbound/root.hints" cache-max-ttl: 86400 cache-min-ttl: 3600 hide-identity: yes hide-version: yes prefer-ip4: yes prefer-ip6: no private-address: ::/0 do-not-query-localhost: no unwanted-reply-threshold: 0 EOF fi wget -q -O "$ROOT_HINTS" https://www.internic.net/domain/named.root exec unbound -d -c "$CONFIG_FILE" } start_dnsmasq() { echo "Starting dnsmasq..." CONFIG_FILE="/etc/dnsmasq.conf" DNS_IPV6_ENABLE="${DNS_IPV6_ENABLE:-false}" rm -f "$CONFIG_FILE" if [ "$DNS_IPV6_ENABLE" = "true" ]; then if [ -n "$DNS_IPV4_PRIMARY" ]; then echo "server=$DNS_IPV4_PRIMARY" >> "$CONFIG_FILE" fi if [ -n "$DNS_IPV4_SECONDARY" ]; then echo "server=$DNS_IPV4_SECONDARY" >> "$CONFIG_FILE" fi if [ -n "$DNS_IPV6_PRIMARY" ]; then echo "server=$DNS_IPV6_PRIMARY" >> "$CONFIG_FILE" fi if [ -n "$DNS_IPV6_SECONDARY" ]; then echo "server=$DNS_IPV6_SECONDARY" >> "$CONFIG_FILE" fi if [ ! -s "$CONFIG_FILE" ]; then echo "server=8.8.8.8" >> "$CONFIG_FILE" echo "server=8.8.4.4" >> "$CONFIG_FILE" echo "server=2001:4860:4860::8888" >> "$CONFIG_FILE" echo "server=2001:4860:4860::8844" >> "$CONFIG_FILE" fi else if [ -n "$DNS_IPV4_PRIMARY" ]; then echo "server=$DNS_IPV4_PRIMARY" >> "$CONFIG_FILE" fi if [ -n "$DNS_IPV4_SECONDARY" ]; then echo "server=$DNS_IPV4_SECONDARY" >> "$CONFIG_FILE" fi if [ ! -s "$CONFIG_FILE" ]; then echo "server=8.8.8.8" >> "$CONFIG_FILE" echo "server=8.8.4.4" >> "$CONFIG_FILE" fi echo "filter-AAAA" >> "$CONFIG_FILE" fi cat >> "$CONFIG_FILE" << EOF dns-forward-max=512 cache-size=16384 local-ttl=14400 neg-ttl=120 no-resolv no-poll EOF echo "Using DNS servers:" echo " IPv4: ENABLED" echo " Primary: ${DNS_IPV4_PRIMARY:-8.8.8.8 (default)}" echo " Secondary: ${DNS_IPV4_SECONDARY:-8.8.4.4 (default)}" if [ "$DNS_IPV6_ENABLE" = "true" ]; then echo " IPv6: ENABLED" echo " Primary: ${DNS_IPV6_PRIMARY:-2001:4860:4860::8888 (default)}" echo " Secondary: ${DNS_IPV6_SECONDARY:-2001:4860:4860::8844 (default)}" else echo " IPv6: DISABLED" fi exec /usr/sbin/dnsmasq --no-daemon } case "$USING_DNS_SERVER" in "unbound") start_unbound ;; "dnsmasq") start_dnsmasq ;; *) echo "Unknown USING_DNS_SERVER: $USING_DNS_SERVER, using dnsmasq" start_dnsmasq ;; esac exit 0 ================================================ FILE: deploy/docker/scripts/start-fptn.sh ================================================ #!/bin/bash export OUT_NETWORK_INTERFACE=$(ip -o -4 route show to default | awk '{print $5}') echo "[FPTN] Using network interface: $OUT_NETWORK_INTERFACE" exec /usr/local/bin/fptn-server \ --server-key=/etc/fptn/server.key \ --server-crt=/etc/fptn/server.crt \ --out-network-interface="${OUT_NETWORK_INTERFACE}" \ --server-port=443 \ --enable-detect-probing="${ENABLE_DETECT_PROBING}" \ --default-proxy-domain="${DEFAULT_PROXY_DOMAIN}" \ --allowed-sni-list="${ALLOWED_SNI_LIST}" \ --tun-interface-name=fptn0 \ --disable-bittorrent="$DISABLE_BITTORRENT" \ --prometheus-access-key="$PROMETHEUS_SECRET_ACCESS_KEY" \ --use-remote-server-auth="$USE_REMOTE_SERVER_AUTH" \ --remote-server-auth-host="$REMOTE_SERVER_AUTH_HOST" \ --remote-server-auth-port="$REMOTE_SERVER_AUTH_PORT" \ --max-active-sessions-per-user="$MAX_ACTIVE_SESSIONS_PER_USER" \ --server-external-ips="${SERVER_EXTERNAL_IPS}" ================================================ FILE: deploy/docker/scripts/token-generator.py ================================================ #!/usr/bin/env python3 import argparse import base64 import json import subprocess import os import sys def get_md5_fingerprint(cert_path="/etc/fptn/server.crt"): if not os.path.exists(cert_path): print(f"Certificate file not found: {cert_path}", file=sys.stderr) sys.exit(1) try: cmd = ["openssl", "x509", "-noout", "-fingerprint", "-md5", "-in", cert_path] output = subprocess.check_output(cmd).decode("utf-8").strip() # Output format: MD5 Fingerprint=AB:CD:EF:... fingerprint = output.split("=")[1].replace(":", "").lower() return fingerprint except subprocess.CalledProcessError as e: print(f"Error computing MD5 fingerprint: {e}", file=sys.stderr) sys.exit(1) def generate_token(username, password, server_ip, service_name, md5_fingerprint, port=443): token_data = { "version": 1, "service_name": service_name, "username": username, "password": password, "servers": [{"name": service_name, "host": server_ip, "md5_fingerprint": md5_fingerprint, "port": port}], "censored_zone_servers": [], } json_str = json.dumps(token_data, separators=(",", ":")) b64_bytes = base64.b64encode(json_str.encode("utf-8")) b64_str = b64_bytes.decode("utf-8").rstrip("=") return f"fptn:{b64_str}" def main(): parser = argparse.ArgumentParser(description="FPTN VPN Token Generator") parser.add_argument("--user", required=True, help="VPN username") parser.add_argument("--password", required=True, help="VPN password") parser.add_argument("--server-ip", required=True, help="VPN server public IP") parser.add_argument("--service-name", default="MyFptnServer", help="VPN service name") parser.add_argument("--cert-path", default="/etc/fptn/server.crt", help="Path to server certificate") parser.add_argument("--port", type=int, default=443, help="Port number (default: 443)") args = parser.parse_args() # Validate port if not (1 <= args.port <= 65535): print(f"Error: Port {args.port} is out of range (1-65535).", file=sys.stderr) sys.exit(1) md5_fingerprint = get_md5_fingerprint(args.cert_path) token = generate_token( username=args.user, password=args.password, server_ip=args.server_ip, service_name=args.service_name, md5_fingerprint=md5_fingerprint, port=args.port, ) print(token) if __name__ == "__main__": main() ================================================ FILE: deploy/linux/deb/README.md ================================================ ### Setup GitHub Runner Before setting up the GitHub runner, you need to install the following packages on your system: ```bash pip install clang-tidy pip install clang-format pip install cmake-format sudo wget -qO- https://apt.llvm.org/llvm.sh | sudo bash -s -- 20 sudo apt install cppcheck sudo apt-get update sudo apt-get install -y libx11-dev libx11-xcb-dev libfontenc-dev libice-dev libsm-dev libxau-dev libxaw7-dev \ libxcomposite-dev libxcursor-dev libxdamage-dev libxfixes-dev libxi-dev libxinerama-dev libxkbfile-dev \ libxmuu-dev libxrandr-dev libxrender-dev libxres-dev libxss-dev libxtst-dev libxv-dev libxxf86vm-dev \ libxcb-glx0-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-xkb-dev libxcb-icccm4-dev libxcb-image0-dev \ libxcb-keysyms1-dev libxcb-randr0-dev libxcb-shape0-dev libxcb-sync-dev libxcb-xfixes0-dev libxcb-xinerama0-dev \ libxcb-dri3-dev uuid-dev libxcb-cursor-dev libxcb-dri2-0-dev libxcb-dri3-dev libxcb-present-dev libxcb-composite0-dev \ libxcb-ewmh-dev libxcb-res0-dev libxcb-util-dev pkg-config libgl-dev libgl1-mesa-dev ``` ================================================ FILE: deploy/linux/deb/create-client-cli-deb-package.sh ================================================ #!/usr/bin/env bash # Function to print usage print_usage() { echo "Usage: $0 " exit 1 } # Check if the correct number of arguments are provided if [ "$#" -ne 2 ]; then print_usage fi CLIENT_CLI="$1" VERSION="$2" MAINTAINER="FPTN Project" OS_NAME=$(lsb_release -i | awk -F':\t' '{print $2}' | tr '[:upper:]' '[:lower:]') OS_VERSION=$(lsb_release -r | awk -F':\t' '{print $2}') CLIENT_TMP_DIR=$(mktemp -d -t fptn-client-cli-XXXXXX) mkdir -p "$CLIENT_TMP_DIR/DEBIAN" mkdir -p "$CLIENT_TMP_DIR/usr/bin" mkdir -p "$CLIENT_TMP_DIR/etc/fptn-client" mkdir -p "$CLIENT_TMP_DIR/lib/systemd/system" # Copy client binary cp "$CLIENT_CLI" "$CLIENT_TMP_DIR/usr/bin/" chmod 755 "$CLIENT_TMP_DIR/usr/bin/$(basename "$CLIENT_CLI")" # Create client configuration file cat < "$CLIENT_TMP_DIR/etc/fptn-client/client.conf" # FPTN Client Configuration # ========================= # Required: Authentication token for server access ACCESS_TOKEN= # Required: Domain name used for SNI (Server Name Indication) # This should be a popular, non-blocked domain in your region # Example: rutube.ru, youtube.com, cloudflare.com SNI=rutube.ru # Optional: Connect to specific server instead of auto-selecting fastest # Name matching is case-insensitive PREFERRED_SERVER= # Optional: Bind to specific network interface # Leave empty to use default interface. Examples: eth0, wlan0, tun0 NETWORK_INTERFACE= # Optional: Specify the gateway IPv4 (e.g., router IPv4) GATEWAY_IP= # Optional: Specify the gateway IPv6 (e.g., router IPv6) GATEWAY_IPv6= # Censorship Bypass Settings # Optional: Method to bypass censorship mechanisms # Available options: # - obfuscation - Masks traffic as regular HTTPS using TLS obfuscation # - sni-reality-chrome147 - SNI reality with Chrome 147 browser fingerprint # - sni-reality-chrome146 - SNI reality with Chrome 146 browser fingerprint # - sni-reality-chrome145 - SNI reality with Chrome 145 browser fingerprint # - sni-reality-firefox149 - SNI reality with Firefox 149 browser fingerprint # - sni-reality-yandex26 - SNI reality with Yandex Browser 26 fingerprint # - sni-reality-yandex25 - SNI reality with Yandex Browser 25 fingerprint # - sni-reality-yandex24 - SNI reality with Yandex Browser 24 fingerprint # - sni-reality-safari26 - SNI reality with Safari 26 browser fingerprint BYPASS_METHOD=sni-reality-yandex25 # Blacklist domains - always blocked regardless of other settings # Completely block access to the main domain AND all its subdomains # Format: domain:[,domain:...] # Example: domain:ria.ru blocks ria.ru and all *.ria.ru sites BLACKLIST_DOMAINS=domain:solovev-live.ru,domain:ria.ru,domain:tass.ru,domain:1tv.ru,domain:ntv.ru,domain:rt.com # Networks that always bypass VPN (highest priority) # Traffic to these networks never uses VPN, regardless of other settings # Format: CIDR notation, comma-separated # Default: Private networks (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16) bypass VPN EXCLUDE_TUNNEL_NETWORKS=10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 # Networks that always use VPN (highest priority) # Traffic to these networks always uses VPN, regardless of other settings # Format: CIDR notation, comma-separated # Empty by default - no networks forced through VPN INCLUDE_TUNNEL_NETWORKS= # Split Tunneling Settings # Enable split tunneling feature # When enabled, traffic routing is controlled by TUNNEL_MODE and domain/network lists # true: Enable split tunneling - configure routing with TUNNEL_MODE below # false: Disable split tunneling - all traffic goes through VPN tunnel ENABLE_SPLIT_TUNNEL=true # Split tunneling mode - defines traffic routing strategy # exclude: Route specified domains/networks directly, all other traffic through VPN # Use case: Local/trusted sites bypass VPN for better performance # include: Route only specified domains/networks through VPN, all other traffic directly # Use case: Protect only sensitive/blocked sites with VPN SPLIT_TUNNEL_MODE=exclude # Domains for split tunneling - traffic classification based on TUNNEL_MODE # Format: domain:[,domain:...] # With TUNNEL_MODE=exclude: These domains bypass VPN tunnel # With TUNNEL_MODE=include: Only these domains use VPN tunnel # Examples for Russia: Local sites (.ru, .su) bypass VPN, foreign sites use VPN SPLIT_TUNNEL_DOMAINS=domain:ru,domain:su,domain:рф,domain:vk.com,domain:yandex.com,domain:userapi.com,domain:yandex.net,domain:clstorage.net EOL # Create systemd service file for client cat < "$CLIENT_TMP_DIR/lib/systemd/system/fptn-client.service" [Unit] Description=FPTN Client Service After=network.target [Service] EnvironmentFile=/etc/fptn-client/client.conf ExecStart=/usr/bin/$(basename "$CLIENT_CLI") \ --access-token "\${ACCESS_TOKEN}" \ --out-network-interface "\${NETWORK_INTERFACE}" \ --gateway-ip "\${GATEWAY_IP}" \ --gateway-ipv6 "\${GATEWAY_IPv6}" \ --sni "\${SNI}" \ --bypass-method "\${BYPASS_METHOD}" \ --blacklist-domains "\${BLACKLIST_DOMAINS}" \ --enable-split-tunnel=\${ENABLE_SPLIT_TUNNEL} \ --split-tunnel-domains "\${SPLIT_TUNNEL_DOMAINS}" \ --exclude-tunnel-networks "\${EXCLUDE_TUNNEL_NETWORKS}" \ --include-tunnel-networks "\${INCLUDE_TUNNEL_NETWORKS}" \ --preferred-server "\${PREFERRED_SERVER}" Restart=always RestartSec=5 User=root [Install] WantedBy=multi-user.target EOL # Create control file for client package INSTALLED_SIZE=$(du -s "$CLIENT_TMP_DIR/usr" | cut -f1) cat < "$CLIENT_TMP_DIR/DEBIAN/control" Package: fptn-client-cli Version: ${VERSION} Architecture: $(dpkg --print-architecture) Maintainer: ${MAINTAINER} Installed-Size: ${INSTALLED_SIZE} Depends: iptables, iproute2, net-tools Section: admin Replaces: fptn-client-cli Conflicts: fptn-client-cli Provides: fptn-client-cli Priority: optional Description: fptn client EOL # Create prerm file cat < "$CLIENT_TMP_DIR/DEBIAN/prerm" #!/bin/bash systemctl daemon-reload || echo "Failed to reload" systemctl stop fptn-client 2>/dev/null || echo "Failed to stop" EOL chmod 755 "$CLIENT_TMP_DIR/DEBIAN/prerm" # Create postrm file cat < "$CLIENT_TMP_DIR/DEBIAN/postrm" #!/bin/bash if [ "\$1" != "upgrade" ]; then systemctl disable fptn-client.service 2>/dev/null || true rm -f /lib/systemd/system/fptn-client.service 2>/dev/null || echo "Failed to remove" fi EOL chmod 755 "$CLIENT_TMP_DIR/DEBIAN/postrm" # Build the Debian package dpkg-deb --root-owner-group --build "$CLIENT_TMP_DIR" "fptn-client-cli-${VERSION}-${OS_NAME}${OS_VERSION}-$(dpkg --print-architecture).deb" # Clean up temporary directories rm -rf "$CLIENT_TMP_DIR" echo "Client Debian package created successfully." ================================================ FILE: deploy/linux/deb/create-client-gui-deb-package.sh ================================================ #!/usr/bin/env bash print_usage() { echo "Usage: $0 " exit 1 } if [ "$#" -ne 4 ]; then print_usage fi CLIENT_GUI="$1" CLIENT_ICON="$2" VERSION="$3" SNI_SOURCE_DIR="$4" MAINTAINER="FPTN Project" OS_NAME=$(lsb_release -i | awk -F':\t' '{print $2}' | tr '[:upper:]' '[:lower:]') OS_VERSION=$(lsb_release -r | awk -F':\t' '{print $2}') CLIENT_TMP_DIR=$(mktemp -d -t fptn-client-XXXXXX) # create structure mkdir -p "$CLIENT_TMP_DIR/DEBIAN" mkdir -p "$CLIENT_TMP_DIR/usr/bin" mkdir -p "$CLIENT_TMP_DIR/opt/fptn/qt6" # Qt directory mkdir -p "$CLIENT_TMP_DIR/opt/fptn/SNI" # SNI directory mkdir -p "$CLIENT_TMP_DIR/usr/share/applications" # Directory for .desktop file mkdir -p "$CLIENT_TMP_DIR/usr/share/icons/hicolor/512x512/apps" # Directory for icon # copy program cp -v "$CLIENT_GUI" "$CLIENT_TMP_DIR/opt/fptn/fptn-client-gui" chmod 755 "$CLIENT_TMP_DIR/opt/fptn/fptn-client-gui" # copy SNI files if [ -d "$SNI_SOURCE_DIR" ]; then echo "Copying SNI files from $SNI_SOURCE_DIR to $CLIENT_TMP_DIR/opt/fptn/SNI" cp -r "$SNI_SOURCE_DIR"/* "$CLIENT_TMP_DIR/opt/fptn/SNI/" else echo "Warning: SNI source directory not found: $SNI_SOURCE_DIR" fi # copy qt QT_LIBS_DIR=$CLIENT_TMP_DIR/opt/fptn/qt6/ QT_PLUGINS_DIR="$QT_LIBS_DIR/plugins" mkdir -p "$QT_LIBS_DIR" mkdir -p "$QT_PLUGINS_DIR" find ~/.conan2 -path "*/Release/qtbase/lib/libQt6*.so*" -print0 | xargs -0 cp -av -t "$QT_LIBS_DIR" find ~/.conan2 -type d -path "*/Release/qtbase/plugins" | grep -E "/qt[0-9a-f]+" | while read -r dir; do cp -rv "$dir"/* "$QT_PLUGINS_DIR" done # Create wrapper script cat < "$CLIENT_TMP_DIR/usr/bin/fptn-client" #!/usr/bin/env bash export FPTN_QT_DIR="/opt/fptn/qt6" export QT_PLUGIN_PATH="\$FPTN_QT_DIR/plugins" export QT_QPA_PLATFORM_PLUGIN_PATH="\$FPTN_QT_DIR/plugins/platforms" export LD_LIBRARY_PATH="\$FPTN_QT_DIR:\$QT_QPA_PLATFORM_PLUGIN_PATH" cleanup_dns() { echo "Cleaning up DNS settings..." resolvectl revert tun0 } notify_error() { local message="\$1" notify-send -u critical "Error in Script" "\$message" || echo "D'oh" } declare -a VARS VARS+=( "QT_PLUGIN_PATH=\$QT_PLUGIN_PATH" "QT_QPA_PLATFORM_PLUGIN_PATH=\$QT_QPA_PLATFORM_PLUGIN_PATH" "LD_LIBRARY_PATH=\$LD_LIBRARY_PATH" ) for VAR in \$(env | sed 's/=/\t/g' | awk '{ print \$1 }' | tr '\n' ' '); do if [[ ! " \${VARS[@]} " =~ " \$VAR=" ]]; then VARS+=("\$VAR=\${!VAR}") fi done PROCESS_NAME="fptn-client-gui" PID=\$(pgrep "\$PROCESS_NAME") if [ -n "\$PID" ]; then cleanup_dns || echo "Failed to clean dns" echo "Process \$PROCESS_NAME found with PID \$PID. Attempting to kill it." kill "\$PID" || echo "Failed to stop" sleep 5 PID=\$(pgrep "\$PROCESS_NAME") if [ -n "\$PID" ]; then echo "Process \$PROCESS_NAME still running. Force killing it." pkill -9 "\$PROCESS_NAME" || echo "Failed to kill" else echo "Process \$PROCESS_NAME successfully terminated." fi else echo "Process \$PROCESS_NAME not found." fi TUN_INTERFACE=\$(ip link show | grep -o 'tun0') if [ -n "\$TUN_INTERFACE" ]; then notify_error "TUN interface \$TUN_INTERFACE found. Disabling another VPN." else echo "No TUN interface found." fi chattr -i /etc/resolv.conf trap cleanup_dns EXIT exec pkexec env -u PKEXEC_UID "SUDO_USER=\$USER" "SUDO_UID=\$(id -u)" "SUDO_GID=\$(id -g)" "\${VARS[@]}" "/bin/sh" -c "exec /opt/fptn/fptn-client-gui \"\$@\"" "\$@" EOL chmod 755 "$CLIENT_TMP_DIR/usr/bin/fptn-client" # Create .desktop file cp "$CLIENT_ICON" "$CLIENT_TMP_DIR/usr/share/icons/hicolor/512x512/apps/fptn-client.png" cat < "$CLIENT_TMP_DIR/usr/share/applications/fptn-client.desktop" [Desktop Entry] Name=FPTN Client Comment=FPTN VPN Client Exec=/usr/bin/fptn-client Icon=/usr/share/icons/hicolor/512x512/apps/fptn-client.png Terminal=false Type=Application Categories=Network;Utility; EOL # Create control file for client package INSTALLED_SIZE=$(du -s "$CLIENT_TMP_DIR/usr" | cut -f1) cat < "$CLIENT_TMP_DIR/DEBIAN/control" Package: fptn-client Version: ${VERSION} Architecture: $(dpkg --print-architecture) Maintainer: ${MAINTAINER} Installed-Size: ${INSTALLED_SIZE} Depends: iptables, iproute2, net-tools, libgl-dev, libgl1-mesa-dev, libx11-dev, libx11-xcb-dev, libfontenc-dev, libxcb-cursor0 Provides: fptn-client Replaces: fptn-client Conflicts: fptn-client Section: admin Priority: optional Description: fptn client EOL # Create postinst file cat < "$CLIENT_TMP_DIR/DEBIAN/postinst" #!/bin/bash update-desktop-database || true EOL chmod 755 "$CLIENT_TMP_DIR/DEBIAN/postinst" # Create prerm file cat < "$CLIENT_TMP_DIR/DEBIAN/prerm" #!/bin/bash PROCESS_NAME="fptn-client-gui" PID=\$(pgrep "\$PROCESS_NAME") if [ -n "\$PID" ]; then echo "Process \$PROCESS_NAME found with PID \$PID. Attempting to kill it." kill "\$PID" || echo "Failed to stop process \$PROCESS_NAME" sleep 5 PID=\$(pgrep "\$PROCESS_NAME") if [ -n "\$PID" ]; then echo "Process \$PROCESS_NAME still running. Force killing it." pkill -9 "\$PROCESS_NAME" || echo "Failed to force kill process \$PROCESS_NAME" else echo "Process \$PROCESS_NAME successfully terminated." fi else echo "Process \$PROCESS_NAME not found." fi EOL chmod 755 "$CLIENT_TMP_DIR/DEBIAN/prerm" # Create postrm file cat < "$CLIENT_TMP_DIR/DEBIAN/postrm" #!/bin/bash if [ "\$1" != "upgrade" ]; then rm -rf "/opt/fptn" || true rm -f "/usr/bin/fptn-client" || true rm -f "/usr/share/icons/hicolor/512x512/apps/fptn-client.png" || true rm -f "/usr/share/applications/fptn-client.desktop" || true fi update-desktop-database || true EOL chmod 755 "$CLIENT_TMP_DIR/DEBIAN/postrm" # Build the Debian package dpkg-deb --root-owner-group --build "$CLIENT_TMP_DIR" "fptn-client-${VERSION}-${OS_NAME}${OS_VERSION}-$(dpkg --print-architecture).deb" # Clean up temporary directories rm -rf "$CLIENT_TMP_DIR" echo "Client Debian package created successfully." ================================================ FILE: deploy/linux/deb/create-server-deb-package.sh ================================================ #!/usr/bin/env bash # Function to print usage print_usage() { echo "Usage: $0 " exit 1 } if [ "$#" -ne 3 ]; then print_usage fi SERVER_BIN="$1" PASSWD_BIN="$2" VERSION="$3" MAINTAINER="FPTN Project" OS_NAME=$(lsb_release -i | awk -F':\t' '{print $2}' | tr '[:upper:]' '[:lower:]') OS_VERSION=$(lsb_release -r | awk -F':\t' '{print $2}') SERVER_TMP_DIR=$(mktemp -d -t fptn-server-XXXXXX) mkdir -p "$SERVER_TMP_DIR/DEBIAN" mkdir -p "$SERVER_TMP_DIR/usr/bin" mkdir -p "$SERVER_TMP_DIR/etc/fptn" mkdir -p "$SERVER_TMP_DIR/lib/systemd/system" # Copy server files cp "$SERVER_BIN" "$SERVER_TMP_DIR/usr/bin/" chmod 755 "$SERVER_TMP_DIR/usr/bin/$(basename "$SERVER_BIN")" cp "$PASSWD_BIN" "$SERVER_TMP_DIR/usr/bin/" chmod 755 "$SERVER_TMP_DIR/usr/bin/$(basename "$PASSWD_BIN")" # Create server configuration file cat < "$SERVER_TMP_DIR/etc/fptn/server.conf" # Configuration for fptn server OUT_NETWORK_INTERFACE= # KEYS SERVER_KEY= SERVER_CRT= PORT=443 TUN_INTERFACE_NAME=fptn0 # Enable detection of probing attempts (experimental; accepted values: true or false) ENABLE_DETECT_PROBING=false # Default domain where non-VPN client traffic will be redirected # When someone scans your server (not using VPN), their connection will be forwarded to this domain instead DEFAULT_PROXY_DOMAIN=cdnvideo.com # Comma-separated list of allowed website domains for non-VPN clients # This acts like a "whitelist" of websites that scanning bots are allowed to reach # Behavior logic: # - List is empty (default): allows ALL domains, proxy all non-VPN traffic to the SNI in the TLS-handshake # - List is NOT empty: use as whitelist: # - Client SNI in list -> proxy to client's SNI # - Client SNI not in list -> proxy to --default-proxy-domain # Domain matching includes all subdomains: # - If "example.com" is in the list, it will match: # - example.com (exact match) # - www.example.com # - api.example.com # - any.other.sub.example.com # Examples: # ALLOWED_SNI_LIST=example.com,test.org # This allows: example.com, test.org and ALL their subdomains ALLOWED_SNI_LIST= # Block BitTorrent traffic to prevent abuse (accepted values: true or false) DISABLE_BITTORRENT=true # Set the USE_REMOTE_SERVER_AUTH variable to true if you need to # redirect requests to a master FPTN server for authorization. # This is used for cluster operations. USE_REMOTE_SERVER_AUTH=false # Specify the remote FPTN server's host address for authorization. # This should be the IP address or domain name of the server. REMOTE_SERVER_AUTH_HOST= # Specify the port of the remote FPTN server for authorization. # The default is port 443 for secure HTTPS connections. REMOTE_SERVER_AUTH_PORT=443 # Set a secret key to allow Prometheus to access the server's statistics. # This key must be alphanumeric (letters and numbers only) and must not include spaces or special characters. PROMETHEUS_SECRET_ACCESS_KEY= # Maximum number of active sessions allowed per VPN user MAX_ACTIVE_SESSIONS_PER_USER=3 # Public IPv4 addresses of this VPN server (comma-separated). # Used to prevent proxy loops when clients connect. # Example: 1.2.3.4,5.6.7.8 SERVER_EXTERNAL_IPS= EOL # Create systemd service file for server cat < "$SERVER_TMP_DIR/lib/systemd/system/fptn-server.service" [Unit] Description=FPTN Server Service After=network.target [Service] EnvironmentFile=/etc/fptn/server.conf ExecStart=/usr/bin/$(basename "$SERVER_BIN") \ --server-key=\${SERVER_KEY} \ --server-crt=\${SERVER_CRT} \ --out-network-interface=\${OUT_NETWORK_INTERFACE} \ --server-port=\${PORT} \ --enable-detect-probing=\${ENABLE_DETECT_PROBING} \ --default-proxy-domain=\${DEFAULT_PROXY_DOMAIN} \ --allowed-sni-list=\${ALLOWED_SNI_LIST} \ --tun-interface-name=\${TUN_INTERFACE_NAME} \ --disable-bittorrent=\${DISABLE_BITTORRENT} \ --prometheus-access-key=\${PROMETHEUS_SECRET_ACCESS_KEY} \ --use-remote-server-auth=\${USE_REMOTE_SERVER_AUTH} \ --remote-server-auth-host=\${REMOTE_SERVER_AUTH_HOST} \ --remote-server-auth-port=\${REMOTE_SERVER_AUTH_PORT} \ --max-active-sessions-per-user=\${MAX_ACTIVE_SESSIONS_PER_USER} \ --server-external-ips=\${SERVER_EXTERNAL_IPS} Restart=always WorkingDirectory=/etc/fptn RestartSec=5 User=root [Install] WantedBy=multi-user.target EOL # Create control file for server package INSTALLED_SIZE=$(du -s "$SERVER_TMP_DIR/usr" | cut -f1) cat < "$SERVER_TMP_DIR/DEBIAN/control" Package: fptn-server Version: ${VERSION} Architecture: $(dpkg --print-architecture) Maintainer: ${MAINTAINER} Installed-Size: ${INSTALLED_SIZE} Depends: iptables, iproute2, net-tools Section: admin Replaces: fptn-server Conflicts: fptn-server Provides: fptn-server Priority: optional Description: fptn server EOL # Create preinst file cat < "$SERVER_TMP_DIR/DEBIAN/preinst" #!/bin/bash if [ -f /etc/fptn/server.conf ]; then cp /etc/fptn/server.conf "/etc/fptn/server.conf.backup.\$(date +'%Y-%m-%d__%H-%M-%S')" fi EOL chmod 755 "$SERVER_TMP_DIR/DEBIAN/preinst" # Create postinst file cat < "$SERVER_TMP_DIR/DEBIAN/postinst" #!/bin/bash chown root:root /etc/fptn/server.conf 2>/dev/null || true EOL chmod 755 "$SERVER_TMP_DIR/DEBIAN/postinst" cat < "$SERVER_TMP_DIR/DEBIAN/prerm" #!/bin/bash systemctl daemon-reload 2>/dev/null || echo "Failed to reload" systemctl stop fptn-server 2>/dev/null || echo "Failed to stop" EOL chmod 755 "$SERVER_TMP_DIR/DEBIAN/prerm" cat < "$SERVER_TMP_DIR/DEBIAN/postrm" #!/bin/bash if [ "\$1" != "upgrade" ]; then systemctl disable fptn-server.service 2>/dev/null || echo "Failed to disable" rm -f /lib/systemd/system/fptn-server.service 2>/dev/null || echo "Failed to remove" fi EOL chmod 755 "$SERVER_TMP_DIR/DEBIAN/postrm" # Build the Debian package dpkg-deb --build "$SERVER_TMP_DIR" "fptn-server-${VERSION}-${OS_NAME}${OS_VERSION}-$(dpkg --print-architecture).deb" # Clean up temporary directory rm -rf "$SERVER_TMP_DIR" chmod 644 "fptn-server-${VERSION}-${OS_NAME}${OS_VERSION}-$(dpkg --print-architecture).deb" echo "Server Debian package created successfully." ================================================ FILE: deploy/linux/wifi/README.md ================================================ ### Setting Up a WiFi-VPN Access Point on Raspberry Pi This guide outlines the process of setting up a WiFi access point on a Raspberry Pi or another computer with all traffic routed through a VPN. Follow the instructions to turn your Raspberry Pi into a full-fledged access point. #### Step 1: Download VPN Client Version for ARM Set up the VPN client according to [this section](https://github.com/batchar2/fptn?tab=readme-ov-file#fptn-client-installation-and-configuration). #### Step 2: Install Required Packages You will need the following packages to set up the access point: ```bash sudo apt install hostapd dnsmasq ``` #### Step 3: System Configuration Disable and stop the hostapd and dnsmasq services to avoid conflicts: ```bash sudo systemctl stop hostapd sudo systemctl disable hostapd sudo systemctl stop dnsmasq sudo systemctl disable dnsmasq ``` #### Step 4: Additional settings for systemd If you are using Ubuntu 24.04/22.04, follow these additional steps: Open the file `/etc/systemd/resolved.conf` Find the DNSStubListener parameter, uncomment it, and change the value to no: ```bash DNSStubListener=no ``` Restart the systemd-resolved service: ```bash sudo systemctl restart systemd-resolved ``` Reboot your system: ```bash sudo reboot ``` #### Step 5: Configure Hostapd Hostapd is a utility that creates a WiFi access point. Copy the hostapd configuration file: ```bash sudo cp hostapd/fptn-hostapd.conf /etc/ ``` Copy the hostapd service file: ```bash sudo cp hostapd/fptn-hostapd.service /etc/systemd/system/ ``` Open the file /etc/fptn-hostapd.conf and replace the values with your own: ```bash # Replace with your WiFi interface interface=wlan0 # Replace with your WiFi network name ssid=VPN-FPTN # Replace with your WiFi password wpa_passphrase=1passwordpassword ``` #### Step 6: Configure Dnsmasq Dnsmasq is a tool that automatically assigns IP addresses to all clients connected to the WiFi. Copy the dnsmasq configuration file: ```bash sudo cp dnsmasq/fptn-dnsmasq.conf /etc/ ``` Copy the dnsmasq service file: ```bash sudo cp dnsmasq/fptn-dnsmasq.service /etc/systemd/system/ ``` ### Step 7: Traffic Routing Setup To route packets from the WiFi interface through the VPN, perform the following steps: Copy the network setup service file: ```bash sudo cp fptn-setup-network/fptn-setup-network.service /etc/systemd/system/ ``` Copy the network setup script: ```bash sudo cp fptn-setup-network/fptn-setup-network.sh /usr/sbin/ ``` Откройте файл `/usr/sbin/fptn-setup-network.sh` и замените данные на ваши: ```bash # Replace with your WiFi interface WIFI_INTERFACE=wlan0 # Replace with your Ethernet interface ETH_INTERFACE=eth0 ``` ### Step 8: Restart and Enable Services Reload the systemd daemon: ```bash sudo systemctl daemon-reload ``` Enable and restart the hostapd service: ```bash sudo systemctl enable fptn-hostapd.service sudo systemctl restart fptn-hostapd.service ``` Enable and restart the dnsmasq service: ```bash sudo systemctl enable fptn-dnsmasq.service sudo systemctl restart fptn-dnsmasq.service ``` Enable and start the network setup service: ```bash sudo systemctl enable fptn-setup-network.service sudo systemctl start fptn-setup-network.service ``` Enable and start FPNT ```bash sudo systemctl enable fptn-client.service sudo systemctl start fptn-client.service ``` After completing these steps, your Raspberry Pi will be configured as a WiFi access point with VPN functionality. ================================================ FILE: deploy/linux/wifi/README.ru.md ================================================ ### Настройка WiFi-VPN точки доступа на Raspberry Pi В этом руководстве описан процесс настройки точки доступа WiFi на Raspberry Pi или другом компьютере с функцией пропусканием всего трафика через VPN. Следуйте инструкциям, чтобы ваш Raspberry Pi стал полноценной точкой доступа. #### Шаг 1: Скачайте клиентскую версию VPN клиента для ARM Выполните настройку VPN клиента в соответсвии с этим [пунктом](https://github.com/batchar2/fptn?tab=readme-ov-file#fptn-client-installation-and-configuration) #### Шаг 2: Установите необходимые пакеты Для настройки точки доступа вам потребуются следующие пакеты: ```bash sudo apt install hostapd dnsmasq ``` #### Шаг 3: Настройки системы Отключите и остановите службы hostapd и dnsmasq, чтобы избежать конфликтов: ```bash sudo systemctl stop hostapd sudo systemctl disable hostapd sudo systemctl stop dnsmasq sudo systemctl disable dnsmasq ``` #### Шаг 4: Дополнительные настройки для systemd Если вы используете Ubuntu 24.04/22.04, выполните следующие дополнительные шаги: Откройте файл `/etc/systemd/resolved.conf` Найдите параметр DNSStubListener, раскомментируйте его и измените значение на no: ```bash DNSStubListener=no ``` Перезапустите службу systemd-resolved: ```bash sudo systemctl restart systemd-resolved ``` Перезагрузите систему: ```bash sudo reboot ``` #### Шаг 5: Настройка Hostapd Hostapd утилита которая создаст wifi-точку доступа. Скопируйте файл конфигурации hostapd: ```bash sudo cp hostapd/fptn-hostapd.conf /etc/ ``` Скопируйте файл службы hostapd: ```bash sudo cp hostapd/fptn-hostapd.service /etc/systemd/system/ ``` Откройте файл /etc/fptn-hostapd.conf и замените значения на ваши: ```bash # Замените на ваш интерфейс WiFi interface=wlan0 # Замените на имя вашей WiFi сети ssid=VPN-FPTN # Замените на ваш пароль wpa_passphrase=1passwordpassword ``` #### Шаг 6: Настройка dnsmasq Dnsmasq инструмент, который всем клиентам подключенным к WiFi будет автоматически выдавать IP адреса. Скопируйте файл конфигурации dnsmasq: ```bash sudo cp hostapd/fptn-dnsmasq.conf /etc/ ``` Скопируйте файл службы dnsmasq: ```bash sudo cp hostapd/fptn-dnsmasq.service /etc/systemd/system/ ``` ### Шаг 7: Настройка маршрутизации трафика Для того, чтобы пакеты с wifi-интерфейса попадали в VPN нужно сделать маршрутизацию трафика Скопируйте файл службы для настройки сети: ```bash sudo cp fptn-setup-network/fptn-setup-network.service /etc/systemd/system/ ``` Скопируйте скрипт настройки сети: ```bash sudo cp fptn-setup-network/fptn-setup-network.sh /usr/sbin/ ``` Откройте файл `/usr/sbin/fptn-setup-network.sh` и замените данные на ваши: ```bash # Замените на ваш WiFi интерфейс WIFI_INTERFACE=wlan0 # Замените на ваш Ethernet интерфейс ETH_INTERFACE=eth0 ``` ### Шаг 8: Перезапустите и включите службы Перезагрузите демон systemd: ```bash sudo systemctl daemon-reload ``` Включите и перезапустите службу hostapd: ```bash sudo systemctl enable fptn-hostapd.service sudo systemctl restart fptn-hostapd.service ``` Включите и перезапустите службу dnsmasq: ```bash sudo systemctl enable fptn-dnsmasq.service sudo systemctl restart fptn-dnsmasq.service ``` Включите и запустите службу настройки сети: ```bash sudo systemctl enable fptn-setup-network.service sudo systemctl start fptn-setup-network.service ``` Включите и запустите FPTN ```bash sudo systemctl enable fptn-client.service sudo systemctl start fptn-client.service ``` После выполнения этих шагов ваш Raspberry Pi будет настроен как точка доступа WiFi с функцией VPN. ================================================ FILE: deploy/linux/wifi/dnsmasq/fptn-dnsmasq.conf ================================================ # /etc/fptn-dnsmasq.conf # REPALCE IT interface=wlan0 log-facility=/var/log/dnsmasq.log dhcp-range=192.168.180.100,192.168.180.250,12h dhcp-option=3,192.168.180.1 listen-address=192.168.180.1,127.0.0.1 #dhcp-option=option:router,10.10.0.1 # dns dhcp-option=6,8.8.8.8 #no-resolv log-queries ================================================ FILE: deploy/linux/wifi/dnsmasq/fptn-dnsmasq.service ================================================ [Unit] Description=FPTN dnsmasq Service After=network.target fptn-client.service fptn-hostapd.service [Service] ExecStart=bash -c "sleep 20 && dnsmasq --conf-file=/etc/fptn-dnsmasq.conf -d" Restart=always User=root RestartSec=5 [Install] WantedBy=multi-user.target ================================================ FILE: deploy/linux/wifi/fptn-setup-network/fptn-setup-network.service ================================================ [Unit] Description=FPTN dnsmasq Service After=network.target fptn-client.service fptn-hostapd.service fptn-dnsmasq.service [Service] ExecStart=bash -c "sleep 2 && bash /usr/sbin/fptn-setup-network.sh" Restart=always User=root RestartSec=5 [Install] WantedBy=multi-user.target ================================================ FILE: deploy/linux/wifi/fptn-setup-network/fptn-setup-network.sh ================================================ ## /usr/sbin/fptn-setup-network.sh #replace to your WIFI_INTERFACE=wlan0 #replace to your ETH_INTERFACE=eth0 echo "Telling kernel to turn on ipv4 ip_forwarding" echo 1 > /proc/sys/net/ipv4/ip_forward echo "Done. Setting up iptables rules to allow FORWARDING" iptables -A FORWARD -i $WIFI_INTERFACE -o tun0 -j ACCEPT iptables -A FORWARD -i tun0 -o $WIFI_INTERFACE -j ACCEPT iptables -A FORWARD -i tun0 -o $ETH_INTERFACE -j ACCEPT iptables -A FORWARD -i $ETH_INTERFACE -o tun0 -j ACCEPT iptables -t nat -A POSTROUTING -o $ETH_INTERFACE -j MASQUERADE iptables -t nat -A POSTROUTING -o tun0 -j MASQUERADE ip addr add 192.168.180.1/24 dev $WIFI_INTERFACE ## fix route wifi -> tun #IP_TUN0=$(ip -o -4 addr list tun0 | awk '{print $4}' | cut -d/ -f1) #ip route add default via $IP_TUN0 dev tun0 table 100 #ip rule add from 192.168.1.180 lookup 100 sleep 60 echo "Done setting up iptables rules. Forwarding enabled" ================================================ FILE: deploy/linux/wifi/hostapd/fptn-hostapd.conf ================================================ # Replace with your WiFi interface interface=wlan0 # Replace with your WiFi network name ssid=VPN-FPTN # Replace with your WiFi password wpa_passphrase=1passwordpassword ignore_broadcast_ssid=0 country_code=BR channel=11 wpa=1 wpa_pairwise=TKIP CCMP rsn_pairwise=CCMP logger_syslog=-1 logger_syslog_level=2 logger_stdout=-1 logger_stdout_level=2 ctrl_interface=/var/run/hostapd ctrl_interface_group=0 hw_mode=b beacon_int=100 dtim_period=2 max_num_sta=255 rts_threshold=2347 fragm_threshold=2346 macaddr_acl=0 auth_algs=1 logger_stdout=-1 logger_stdout_level=2 wmm_enabled=1 # # WMM-PS Unscheduled Automatic Power Save Delivery [U-APSD] # Enable this flag if U-APSD supported outside hostapd (eg., Firmware/driver) #uapsd_advertisement_enabled=1 # # Low priority / AC_BK = background wmm_ac_bk_cwmin=4 wmm_ac_bk_cwmax=10 wmm_ac_bk_aifs=7 wmm_ac_bk_txop_limit=0 wmm_ac_bk_acm=0 # Note: for IEEE 802.11b mode: cWmin=5 cWmax=10 # # Normal priority / AC_BE = best effort wmm_ac_be_aifs=3 wmm_ac_be_cwmin=4 wmm_ac_be_cwmax=10 wmm_ac_be_txop_limit=0 wmm_ac_be_acm=0 # Note: for IEEE 802.11b mode: cWmin=5 cWmax=7 # # High priority / AC_VI = video wmm_ac_vi_aifs=2 wmm_ac_vi_cwmin=3 wmm_ac_vi_cwmax=4 wmm_ac_vi_txop_limit=94 wmm_ac_vi_acm=0 # Note: for IEEE 802.11b mode: cWmin=4 cWmax=5 txop_limit=188 # # Highest priority / AC_VO = voice wmm_ac_vo_aifs=2 wmm_ac_vo_cwmin=2 wmm_ac_vo_cwmax=3 wmm_ac_vo_txop_limit=47 wmm_ac_vo_acm=0 eapol_key_index_workaround=0 eap_server=0 own_ip_addr=127.0.0.1 ================================================ FILE: deploy/linux/wifi/hostapd/fptn-hostapd.service ================================================ [Unit] Description=FPTN hostapd Service After=network.target fptn-client.service [Service] ExecStart=bash -c "sleep 20 && hostapd /etc/fptn-hostapd.conf" Restart=always User=root RestartSec=5 [Install] WantedBy=multi-user.target ================================================ FILE: deploy/macos/README.md ================================================ # README.md Requires Python 3.6+ and macOS host with pkgbuild and productbuild available pip3 install macos-pkg-builder pip install clang-tidy pip install clang-format pip install cmake-format brew install cppcheck ================================================ FILE: deploy/macos/create-pkg.py ================================================ #!/usr/bin/env python3 import os import sys import shutil import pathlib import argparse import platform import plistlib import tempfile import subprocess from macos_pkg_builder import Packages APP_NAME = "FptnClient" SCRIPT_FOLDER = pathlib.Path(__file__).parent REPOSITORY_FOLDER = pathlib.Path(__file__).parent.parent.parent ICON = SCRIPT_FOLDER / "assets" / "FptnClient.icns" def run_command(command, cwd=None): result = subprocess.run( command, shell=True, cwd=cwd, text=True, capture_output=True, ) if result.returncode != 0: raise RuntimeError(f"Command '{command}' failed with error: {result.stderr.strip()}") return result.stdout.strip() def create_app( app_path: pathlib.Path, fptn_client_cli: pathlib.Path, fptn_client_gui: pathlib.Path, version: str, ) -> bool: print(app_path) try: app_contents_path = app_path / "Contents" macos_path = app_contents_path / "MacOS" resources_path = app_contents_path / "Resources" frameworks_path = app_contents_path / "Frameworks" frameworks_qt_plugins_path = app_contents_path / "Frameworks" / "plugins" os.makedirs(macos_path, exist_ok=True) os.makedirs(resources_path, exist_ok=True) os.makedirs(frameworks_path, exist_ok=True) os.makedirs(frameworks_qt_plugins_path, exist_ok=True) # Сopy SNI files from deploy/sni to Resources/SNI sni_source = REPOSITORY_FOLDER / "deploy" / "sni" sni_dest = resources_path / "SNI" if sni_source.exists(): print(f"Copying SNI files from {sni_source} to {sni_dest}") shutil.copytree(sni_source, sni_dest, dirs_exist_ok=True) else: print(f"Warning: SNI source folder not found: {sni_source}") # copy cli program binary_dest = macos_path / "fptn-client-cli" shutil.copy(fptn_client_cli, binary_dest) os.chmod(binary_dest, 0o755) # copy wrapper of program fptn_client_cli_wrapper_sh = SCRIPT_FOLDER / "scripts" / "fptn-client-cli-wrapper.sh" fptn_client_cli_wrapper_sh_dest = macos_path / "fptn-client-cli-wrapper.sh" shutil.copy(fptn_client_cli_wrapper_sh, fptn_client_cli_wrapper_sh_dest) os.chmod(binary_dest, 0o755) # --- copy gui program --- binary_dest = macos_path / "fptn-client-gui" shutil.copy(fptn_client_gui, binary_dest) # Fix rpath run_command(f'install_name_tool -add_rpath @executable_path/../Frameworks {macos_path / "fptn-client-gui"}') os.chmod(binary_dest, 0o755) fptn_client_gui_wrapper_sh = SCRIPT_FOLDER / "scripts" / "fptn-client-gui-wrapper.sh" fptn_client_gui_wrapper_sh_dest = macos_path / "fptn-client-gui-wrapper.sh" shutil.copy(fptn_client_gui_wrapper_sh, fptn_client_gui_wrapper_sh_dest) os.chmod(binary_dest, 0o4755) # 0o755) qt_libs = run_command(r'find ~/.conan2 -type f \( -name "*.dylib" \) | grep Release') qt_lib_paths = qt_libs.splitlines() for lib in qt_lib_paths: lib_path = pathlib.Path(lib) lib_name = lib_path.name if r"qtbase/lib/" in lib: if r".6.7.3.dylib" in lib_name: lib_name = lib_name.replace(".6.7.3.dylib", ".6.dylib") print(f"Copy {lib} -> {frameworks_path / lib_name}") shutil.copy(lib, frameworks_path / lib_name) elif "qtbase/plugins/" in str(lib_path): separeted = lib.split(r"qtbase/plugins/") plugin_folder = frameworks_qt_plugins_path / separeted[1].replace(lib_name, "") os.makedirs(plugin_folder, exist_ok=True) print(f"Copy {lib_path} -> {plugin_folder / lib_name}") shutil.copy(lib, plugin_folder / lib_name) # copy icon icon_dest = resources_path / ICON.name shutil.copy(ICON, icon_dest) # Create Info.plist plist = { "CFBundleName": APP_NAME, "CFBundleExecutable": "fptn-client-gui-wrapper.sh", "CFBundleIdentifier": "com.fptn.vpn", "CFBundlePackageType": "APPL", "CFBundleVersion": version, "CFBundleIconFile": ICON.name, "LSUIElement": True, "LSApplicationCategoryType": "public.app-category.utilities", "LSRequiresNativeExecution": True, "NSHighResolutionCapable": True, "LD_LIBRARY_PATH": "@executable_path/../Frameworks", "RunAtLoad": True, "KeepAlive": True, "UserName": "root", "NSAllowsArbitraryLoads": True, "NSClipboardUsageDescription": "Requires clipboard access to copy and paste data.", } with open(app_contents_path / "Info.plist", "wb") as plist_file: plistlib.dump(plist, plist_file) except Exception as e: print(f"Error creating .app: {e}") raise e return True def create_pkg(app_path: pathlib.Path, version: str) -> bool: try: post_install = SCRIPT_FOLDER / "scripts" / "post_install.sh" machine = "apple-silicon" if platform.machine() == "arm64" else "intel" pkg_obj = Packages( pkg_output=f"fptn-client-{version}-{machine}.pkg", pkg_bundle_id="com.fptn-vpn.installer", pkg_as_distribution=True, pkg_title="FPTN-VPN", pkg_postinstall_script=str(post_install.resolve()), pkg_file_structure={ str(app_path.resolve()): f"/Applications/{APP_NAME}.app", }, ) if pkg_obj.build(): return True else: print("Error building package.") except Exception as e: print(f"Error creating package: {e}") raise e return False if __name__ == "__main__": parser = argparse.ArgumentParser(description="PKG build configuration.") parser.add_argument("--fptn-client-cli", required=True, help="Path to the cli application binary.") parser.add_argument("--fptn-client-gui", required=True, help="Path to the gui application binary.") parser.add_argument("--version", required=True, help="Version") args = parser.parse_args() fptn_client_cli = pathlib.Path(args.fptn_client_cli) fptn_client_gui = pathlib.Path(args.fptn_client_gui) if not fptn_client_cli.is_file() or not fptn_client_gui.is_file(): print(f"Binary file does not exist: {fptn_client_cli} or {fptn_client_gui}") sys.exit(1) with tempfile.TemporaryDirectory() as temp_dir: app_path = pathlib.Path(temp_dir) / f"{APP_NAME}-{args.version}.app" if create_app(app_path, fptn_client_cli, fptn_client_gui, args.version): if create_pkg(app_path, args.version): print(f"Package created successfully") else: print("Failed to create package.") sys.exit(1) else: print("Failed to create .app") sys.exit(1) ================================================ FILE: deploy/macos/scripts/fptn-client-cli-wrapper.sh ================================================ #!/bin/bash # Check for root privileges if [ "$(id -u)" -ne 0 ]; then echo "This operation requires root privileges." exit 1 fi # Function to clean DNS settings cleanup_dns() { echo "Cleaning up DNS settings..." networksetup -listallnetworkservices | grep -v '^An asterisk' | xargs -I {} networksetup -setdnsservers "{}" empty } cd /tmp/ networksetup -listallnetworkservices | grep -v '^An asterisk' | xargs -I {} networksetup -setdnsservers "{}" empty trap cleanup_dns EXIT /Applications/FptnClient.app/Contents/MacOS/fptn-client-cli "$@" ================================================ FILE: deploy/macos/scripts/fptn-client-gui-wrapper.sh ================================================ #!/bin/bash # Dialog display functions show_error() { osascript -e "display dialog \"$1\" buttons {\"OK\"} default button \"OK\" with icon stop" exit 1 } is_client_running() { pgrep -f "fptn-client-gui" > /dev/null } show_dialog() { local message="$1" local buttons="$2" local default_button="$3" local icon="$4" osascript -e "display dialog \"$message\" buttons {$buttons} default button \"$default_button\" with icon $icon" } copy_sni_files() { local SNI_SRC="/Applications/FptnClient.app/Contents/Resources/SNI" local USER_HOME="$HOME" local SNI_USER_DIR="$USER_HOME/Library/Preferences/SNI" # Check if SNI directory exists and has all files local need_copy=false if [ ! -d "$SNI_USER_DIR" ]; then echo "SNI directory doesn't exist, need to copy files" need_copy=true else # Check if any SNI files are missing in user directory for file in "$SNI_SRC"/*; do if [ -f "$file" ]; then filename=$(basename "$file") user_file="$SNI_USER_DIR/$filename" if [ ! -f "$user_file" ]; then echo "File $filename missing in user directory, need to copy" need_copy=true break fi fi done fi # Copy files if needed if [ "$need_copy" = true ] && [ -d "$SNI_SRC" ]; then echo "Copying SNI files to user directory..." mkdir -p "$SNI_USER_DIR" cp -R "$SNI_SRC/"* "$SNI_USER_DIR/" echo "SNI files copied successfully to $SNI_USER_DIR" elif [ ! -d "$SNI_SRC" ]; then echo "Warning: SNI source folder not found: $SNI_SRC" else echo "SNI files are already up to date" fi } # Reset DNS settings clean_dns() { networksetup -listallnetworkservices | grep -v '^An asterisk' | \ xargs -I {} networksetup -setdnsservers "{}" empty } copy_sni_files # Set environment and launch application export QT_PLUGIN_PATH=/Applications/FptnClient.app/Contents/Frameworks/plugins export QT_QPA_PLATFORM_PLUGIN_PATH=$QT_PLUGIN_PATH/platforms export LD_LIBRARY_PATH=/Applications/FptnClient.app/Contents/Frameworks clean_dns # run in background /Applications/FptnClient.app/Contents/MacOS/fptn-client-gui "$@" & # wait for end of client while is_client_running; do sleep 1 done clean_dns ================================================ FILE: deploy/macos/scripts/post_install.sh ================================================ #!/bin/bash set -e # Function to show error messages show_error() { local message="$1" osascript < ") sys.exit(1) file_path = sys.argv[1] new_version = sys.argv[2] print("new_version>>>", new_version) replace_version(file_path, new_version) print(f"Replaced version in {file_path} with {new_version}") ================================================ FILE: deploy/windows/create-installer.py ================================================ import os import re import sys import shutil import pathlib import platform import datetime import argparse import subprocess import requests INNOSETUP_DEFAULT_PATH = r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe" REPOSITORY_FOLDER = pathlib.Path(__file__).parent.parent.parent INSTALLER_DIR = pathlib.Path(__file__).parent.resolve() / "installer" TMP_DIR = INSTALLER_DIR / "tmp" TMP_DIR.mkdir(parents=True, exist_ok=True) DEPENDS_DIR = INSTALLER_DIR / "depends" DEPENDS_DIR.mkdir(parents=True, exist_ok=True) DEPENDS_QT_DIR = DEPENDS_DIR / "qt" DEPENDS_QT_DIR.mkdir(parents=True, exist_ok=True) OUTPUT_DIR = INSTALLER_DIR / "Output" OUTPUT_DIR.mkdir(parents=True, exist_ok=True) DEPENDS_WINTUN_DLL_PATH = DEPENDS_DIR / "wintun.dll" DEPENDS_VC_REDIST_PATH = DEPENDS_DIR / "vc_redist.exe" APP_FPTN_CLIENT = DEPENDS_DIR / "fptn-client.exe" APP_FPTN_CLIENT_CLI = DEPENDS_DIR / "fptn-client-cli.exe" def is_windows_x86_64() -> bool: if platform.system() != "Windows": raise EnvironmentError("This function is only for Windows systems.") architecture = platform.machine().lower() return architecture in ("x86_64", "amd64") def is_arm_64() -> bool: architecture = platform.machine().lower() return architecture in ("aarch64", "arm64") def download_file(url: str, destination_path: pathlib.Path) -> bool: try: response = requests.get(url, allow_redirects=True, timeout=120) if response.status_code == 200: with open(destination_path, "wb") as file: file.write(response.content) print(f"File downloaded successfully: {destination_path}") return True raise ConnectionError(f"Failed to download file: HTTP {response.status_code} {response.reason}") except Exception as err: raise err def replace_values_in_innosetupfile(file_path: pathlib.Path, replacements: dict): with open(file_path, "r", encoding="utf-8") as file: content = file.read() for key, value in replacements.items(): pattern = rf'#define {key} ".*?"' replacement = rf'#define {key} "{value}"' content = re.sub(pattern, replacement, content) with open(file_path, "w", encoding="utf-8") as file: file.write(content) def run_command(command: str) -> str: """Run a shell command and return its output.""" try: print("COMMAND>", command) result = subprocess.run(command, shell=True, capture_output=True, text=True) result.check_returncode() print(result.stderr) return result.stdout except Exception as err: print("Output:", err.stdout) print("Errors:", err.stderr) raise err def get_conan_path() -> pathlib.Path: home_path = pathlib.Path.home() conan_paths = [home_path / ".conan2", home_path / ".conan"] for path in conan_paths: if path.exists() and path.is_dir(): return path raise FileNotFoundError("Conan path could not be detected in the home directory.") def copy_qt_libraries(frameworks_path: pathlib.Path): conan_path = get_conan_path() print(f"Detected Conan path: {conan_path}") qt_libs = run_command(f'dir /S /B "{conan_path}"\\*.dll') qt_lib_paths = qt_libs.splitlines() frameworks_qt_plugins_path = frameworks_path / "plugins" for lib in qt_lib_paths: lib_path = pathlib.Path(lib) lib_name = lib_path.name for lib in qt_lib_paths: lib_path = pathlib.Path(lib) lib_name = lib_path.name if r"qtbase/bin/" in str(lib_path.as_posix()): if r".6.7.3.dll" in lib_name: lib_name = lib_name.replace(".6.7.3.dll", ".6.dll") print(f"Copy {lib} -> {frameworks_path / lib_name}") shutil.copy(lib, frameworks_path / lib_name) elif "qtbase/plugins" in str(lib_path.as_posix()): separated = str(lib_path.as_posix()).split("qtbase/plugins/") plugin_folder = frameworks_qt_plugins_path / separated[1].replace(lib_name, "") os.makedirs(plugin_folder, exist_ok=True) print(f"Copy {lib_path} -> {plugin_folder / lib_name}") shutil.copy(lib, plugin_folder / lib_name) def compile_inno_setup_script(script_path: pathlib.Path, output_dir: pathlib.Path) -> bool: command = [INNOSETUP_DEFAULT_PATH, script_path] # if output_dir: # command.append(f"/O{output_dir}") try: result = subprocess.run( command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) print("Inno Setup compiled successfully.") output = result.stdout if len(output) > 0: print(output) warnings = result.stderr if len(warnings) > 0: print("Warnings:", result.stderr) return True except Exception as err: print("Failed to compile Inno Setup script: ", err) print("Output:", err.stdout) print("Errors:", err.stderr) raise err if __name__ == "__main__": parser = argparse.ArgumentParser(description="A script to manage the build process for FPTN Client.") parser.add_argument( "--wintun-dll", type=pathlib.Path, required=True, help="The path to the wintun dll", ) parser.add_argument( "--fptn-client", type=pathlib.Path, required=True, help="The path to the FPTN client executable.", ) parser.add_argument( "--fptn-client-cli", type=pathlib.Path, required=True, help="The path to the FPTN client CLI executable.", ) parser.add_argument("--version", required=True, help="Version") parser.add_argument("--output-folder", required=True, help="output-folder") args = parser.parse_args() if is_arm_64(): download_file("https://aka.ms/vs/17/release/vc_redist.arm64.exe", DEPENDS_VC_REDIST_PATH) elif is_windows_x86_64(): download_file("https://aka.ms/vs/17/release/vc_redist.x64.exe", DEPENDS_VC_REDIST_PATH) else: raise EnvironmentError("Unsuported system!") # copy wintun dll shutil.copy(args.wintun_dll, DEPENDS_WINTUN_DLL_PATH) # copy clients shutil.copy(args.fptn_client, APP_FPTN_CLIENT) shutil.copy(args.fptn_client_cli, APP_FPTN_CLIENT_CLI) # copy SNI files from deploy/sni to depends/sni sni_source = REPOSITORY_FOLDER / "deploy" / "sni" sni_dest = DEPENDS_DIR / "sni" if sni_source.exists(): print(f"Copying SNI files from {sni_source} to {sni_dest}") shutil.copytree(sni_source, sni_dest, dirs_exist_ok=True) else: print(f"Warning: SNI source folder not found: {sni_source}") # prepare innosetup INNOSETUP_SCRIPT_PATH = INSTALLER_DIR / "fptn-installer.iss" PREPARED_INNOSETUP_SCRIPT_PATH = INSTALLER_DIR / "tmp.iss" shutil.copy(INNOSETUP_SCRIPT_PATH, PREPARED_INNOSETUP_SCRIPT_PATH) arch = is_arm_64() replace_values_in_innosetupfile( PREPARED_INNOSETUP_SCRIPT_PATH, { "APP_VERSION_NAME": args.version, "APP_VERSION_NUMBER": datetime.datetime.now().strftime("%Y.%m.%d.%H%M"), "APP_COPYRIGHT_YEAR": str(datetime.datetime.now().year), }, ) # prepare qt conan_path = get_conan_path() copy_qt_libraries(DEPENDS_QT_DIR) # change dir os.chdir(INSTALLER_DIR.resolve().as_posix()) # change resources TOOLS_RC_EDIT = TMP_DIR / "rcedit.exe" download_file( "https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe", TOOLS_RC_EDIT, ) ICON_FILE = INSTALLER_DIR / "resources" / "icons" / "app.ico" run_command( f'"{TOOLS_RC_EDIT.as_posix()}" "{APP_FPTN_CLIENT.as_posix()}" --set-product-version "{args.version}" --set-icon "{ICON_FILE.as_posix()}"' ) # change permissions manifest = INSTALLER_DIR / "app.manifest" run_command(f'mt.exe -manifest "{manifest.as_posix()}" -outputresource:"{APP_FPTN_CLIENT.as_posix()}";1') run_command(f'mt.exe -manifest "{manifest.as_posix()}" -outputresource:"{APP_FPTN_CLIENT_CLI.as_posix()}";1') compile_inno_setup_script(PREPARED_INNOSETUP_SCRIPT_PATH, OUTPUT_DIR) arch = "arm64" if is_arm_64() else "x64_x86" output_file = pathlib.Path(args.output_folder) / f"FptnClientInstaller-{args.version}-windows-{arch}.exe" shutil.copy(INSTALLER_DIR / "Output" / "FptnClientInstaller.exe", output_file) ================================================ FILE: deploy/windows/installer/app.manifest ================================================ ================================================ FILE: deploy/windows/installer/fptn-installer.iss ================================================ // replacement automatically #define APP_VERSION_NAME "1.0.0" // replacement automatically #define APP_VERSION_NUMBER "1.0.0" // replacement automatically #define APP_COPYRIGHT_YEAR "2024" #define APP_ID "{D2D2C1f8-5F5F-5f79-9C5F-7E2B9F1C39A4}" #define APP_URL "fptn.org" #define APP_NAME "FPTN Client" #define APP_PUBLISHER "fptn.org" [Setup] AppId={{#APP_ID} AppName={#APP_NAME} AppVersion={#APP_VERSION_NAME} MinVersion=10.0.10240 AppPublisher={#APP_PUBLISHER} AppPublisherURL={#APP_URL} AppSupportURL={#APP_URL} AppUpdatesURL={#APP_URL} DefaultDirName={autopf}\{#APP_NAME} DefaultGroupName={#APP_NAME} OutputDir=Output OutputBaseFilename=FptnClientInstaller SetupIconFile=resources\icons\app.ico Compression=lzma SolidCompression=yes DisableDirPage=no // # LicenseFile=installer\license\LicenseFile.rtf VersionInfoVersion={#APP_VERSION_NUMBER} WizardStyle=modern UninstallLogMode=overwrite AppCopyright=Copyright (C) {#APP_COPYRIGHT_YEAR} {#APP_PUBLISHER}. PrivilegesRequired=admin AppendDefaultDirName=yes ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible SetupLogging=Yes [Dirs] Name: "{app}\"; Name: "{app}\logs"; Name: "{app}\plugins"; [Files] Source: "depends/qt/*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall Source: "depends/wintun.dll"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall Source: "depends/fptn-client.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall Source: "depends/fptn-client-cli.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall Source: "depends/vc_redist.exe"; DestDir: "{tmp}"; AfterInstall: InstallVCRedist(); Flags: ignoreversion recursesubdirs createallsubdirs // ------- generate bat files ------- Source: "depends/wintun.dll"; DestDir: "{app}"; AfterInstall: GenerateBatFile('{app}\fptn-client.exe','{app}\FptnClient.bat'); Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall // ------- copy SNI files ------- Source: "depends/sni/*"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; // --- Name: "startup"; Description: {cm:AutoStartProgram,{#APP_NAME}}; [Run] // Filename: "cmd.exe"; Parameters: "/c reg add ""HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters"" /v IPEnableRouter /t REG_DWORD /d 1 /f"; Flags: runhidden Filename: "{app}\FptnClient.bat"; Description: "{cm:LaunchProgram,{#APP_NAME}}"; Flags: nowait postinstall skipifdoesntexist [UninstallRun] Filename: "taskkill"; Parameters: "/F /IM fptn-client.exe"; Flags: runhidden waituntilterminated Filename: "taskkill"; Parameters: "/F /IM fptn-client-cli.exe"; Flags: runhidden waituntilterminated [Icons] Name: "{group}\{#APP_NAME}"; Filename: "{app}\fptn-client.exe" Name: "{group}\{cm:UninstallProgram,{#APP_NAME}}"; Filename: "{uninstallexe}"; Name: "{commondesktop}\{#APP_NAME}"; Filename: "{app}\fptn-client.exe"; Tasks: desktopicon // --- Name: "{userstartup}\{#APP_NAME}"; Filename: "{app}\fptn-client.exe"; Tasks: startup // --- Name: "{userstartup}\{#APP_NAME}"; Filename: "{app}\FptnClient.bat"; Tasks: startup [InstallDelete] Type: filesandordirs; Name: "{app}\qt" Type: filesandordirs; Name: "{app}\bin" Type: filesandordirs; Name: "{app}\SNI" Type: filesandordirs; Name: "{app}\logs" Type: filesandordirs; Name: "{app}\plugins" Type: files; Name: "{app}\*" [UninstallDelete] Type: filesandordirs; Name: "{app}\qt" Type: filesandordirs; Name: "{app}\bin" Type: filesandordirs; Name: "{app}\SNI" Type: filesandordirs; Name: "{app}\logs" Type: filesandordirs; Name: "{app}\plugins" Type: files; Name: "{app}\*" [Languages] Name: "en"; MessagesFile: "compiler:Default.isl" // "English" seems to always break the default language detection with unknown reasons. Name: "polish"; MessagesFile: "compiler:Languages\Polish.isl" Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" Name: "ukrainian"; MessagesFile: "compiler:Languages\Ukrainian.isl" [Code] function cmd(command: string): integer; begin exec('cmd.exe', '/c ' + command, ExpandConstant('{app}'), SW_HIDE, ewwaituntilterminated, result ) end; procedure InstallVCRedist(); var ExitCode: Integer; begin ExitCode := cmd(ExpandConstant(CurrentFileName) + ' /install /quiet /norestart '); if ExitCode <> 0 then begin ExitCode := cmd(ExpandConstant(CurrentFileName) + ' /repair /quiet /norestart '); //MsgBox('Failed to install Visual C++ Redistributable.', mbError, MB_OK); end; end; procedure GenerateBatFile(programPath: String; batFilePath: String); var content: String; begin content := '@echo off' + #13#10 + ExpandConstant('cd "{app}"') + #13#10 + 'Net session >nul 2>&1 || (PowerShell start -verb runas ' + #39 + '%~0' + #39 +' &exit /b)' + #13#10 + 'cmd /c start "" "' + ExpandConstant(programPath) + ' ' + #13#10 + 'exit /s'; SaveStringToFile(ExpandConstant(batFilePath), content, False); end; procedure CurStepChanged(CurStep: TSetupStep); begin if CurStep = ssPostInstall then begin //if MsgBox('The installation has completed. For changes to take effect, please restart your computer. Do you want to restart now?', mbConfirmation, MB_YESNO) = IDYES then //begin // cmd('shutdown /r /t 0'); //end; end; end; ================================================ FILE: docker-compose/.gitignore ================================================ .env fptn-server-data/ logs/ ================================================ FILE: docker-compose/README.md ================================================ ================================================ FILE: docker-compose/docker-compose.yml ================================================ services: fptn-server: restart: unless-stopped image: fptnvpn/fptn-vpn-server:latest cap_add: - NET_ADMIN - SYS_MODULE - NET_RAW - SYS_ADMIN sysctls: net.ipv4.ip_forward: "1" net.ipv4.conf.all.src_valid_mark: "1" net.ipv6.conf.all.disable_ipv6: "0" net.ipv6.conf.all.forwarding: "1" net.ipv6.conf.default.forwarding: "1" net.ipv4.conf.all.rp_filter: "0" net.ipv4.conf.default.rp_filter: "0" net.ipv4.ip_local_port_range: "1024 65535" net.core.somaxconn: "65535" ulimits: nproc: soft: 524288 hard: 524288 nofile: soft: 524288 hard: 524288 memlock: soft: 524288 hard: 524288 devices: - /dev/net/tun:/dev/net/tun ports: - "${FPTN_PORT}:443/tcp" volumes: - ./fptn-server-data:/etc/fptn environment: - ENABLE_DETECT_PROBING=${ENABLE_DETECT_PROBING} - DEFAULT_PROXY_DOMAIN=${DEFAULT_PROXY_DOMAIN} - ALLOWED_SNI_LIST=${ALLOWED_SNI_LIST} - DISABLE_BITTORRENT=${DISABLE_BITTORRENT} - PROMETHEUS_SECRET_ACCESS_KEY=${PROMETHEUS_SECRET_ACCESS_KEY} - USE_REMOTE_SERVER_AUTH=${USE_REMOTE_SERVER_AUTH} - REMOTE_SERVER_AUTH_HOST=${REMOTE_SERVER_AUTH_HOST} - REMOTE_SERVER_AUTH_PORT=${REMOTE_SERVER_AUTH_PORT} - MAX_ACTIVE_SESSIONS_PER_USER=${MAX_ACTIVE_SESSIONS_PER_USER} - SERVER_EXTERNAL_IPS=${SERVER_EXTERNAL_IPS} - USING_DNS_SERVER=${USING_DNS_SERVER:-dnsmasq} - DNS_IPV6_ENABLE=${DNS_IPV6_ENABLE:-false} - DNS_IPV4_PRIMARY=${DNS_IPV4_PRIMARY:-8.8.8.8} - DNS_IPV4_SECONDARY=${DNS_IPV4_SECONDARY:-8.8.4.4} - DNS_IPV6_PRIMARY=${DNS_IPV6_PRIMARY:-2001:4860:4860::8888} - DNS_IPV6_SECONDARY=${DNS_IPV6_SECONDARY:-2001:4860:4860::8844} healthcheck: test: ["CMD", "sh", "-c", "pgrep dnsmasq && pgrep fptn-server"] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - fptn-network networks: fptn-network: driver: bridge enable_ipv6: true ipam: config: - subnet: dead:beef:cafe::/48 gateway: dead:beef:cafe::1 - subnet: 192.168.200.0/24 gateway: 192.168.200.1 ================================================ FILE: docs/CNAME ================================================ fptn.org ================================================ FILE: docs/index-ru.html ================================================ FPTN Project FPTN Project

FPTN Project


FPTN is a fully custom-built VPN technology — developed from scratch, including the core protocol, server implementation, and cross-platform clients. It is a non-commercial, open-source project developed by volunteers and designed to bypass censorship.

The VPN-client is a mobile application or a compact desktop application with an interface located in the system tray. No registration process is required! We will send the access token via the bot on Telegram.

Application
Application

FPTN operates over the HTTPS protocol, effectively masking traffic and enabling users to bypass censorship restrictions. The project's source code is available on GitHub.

FPTN Project


FPTN — это полностью разработанная с нуля технология VPN, включая собственный протокол, сервер и кроссплатформенные клиенты. Это некоммерческий проект с открытым исходным кодом, развиваемый волонтерами и предназначенный для обхода цензуры.

VPN-клиент — мобильное приложение или миниатюрная программа для компьютера, интерфейс которой размещается в трее. Здесь нет процесса регистрации! Мы пришлем токен доступа через бота в Telegram

Application
Application

FPTN работает через протокол HTTPS, эффективно маскируя трафик и позволяя обходить ограничения цензуры. Исходный код проекта доступен на Github.


Download FPTN

Скачать FPTN

Google play Google play

Download Android app from the site Скачать Android приложение с сайта

Установка и настройка


Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings
Вставьте токен в соответствующее поле.

Application
Войдите в приложение, используя полученный токен.

Application
Выберите VPN-сервер из списка доступных (Режим "Авто" подберет наиболее оптимальный сервер или можете выбрать сервер из списка доступных). Для включения VPN-сервиса нажмите на кнопку в центре экрана, которая активирует подключение к выбранному серверу.

Application
Готово!

Application

Installation and Setup


Go to the Telegram bot and generate your token by sending the /token command. Then copy the token. Settings
Insert the token into the appropriate field.

Application
Log into the application using the obtained token.

Application
Select a VPN server from the list of available servers (the "Auto" mode will choose the most optimal server, or you can manually select a server from the list). To enable the VPN service, press the button in the center of the screen, which will activate the connection to the selected server.

Application
Done!

Application

MacOS client (Apple Silicon) Клиент для MacOS (Apple Silicon)

Установка и настройка


При установке клиента на MacOS вам может понадобиться более детальная инструкция из-за проблем с драйверами

Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings

После запуска приложения нажмите на иконку в системном трее чтобы открыть контекстное меню. Application

Вставьте сюда токен из Telegram-бота Settings

Сохраните токен. Settings

Закройте окно настроек и попробуйте подключиться Settings

Installation and Setup


When installing the client on macOS, you may need a more detailed guide due to driver-related issues.

Go to the Telegram bot and generate your token by sending the /token command. Then copy the token. Settings

After launching the app, click on the tray icon to open the context menu. Application

Paste the token you received from the Telegram bot into the token field. Settings

Save the token. Settings

Close the settings window and try connecting. Settings

Download Linux client Скачать Linux клиент

Установка и настройка


Скачайте .deb пакет FPTN-клиента и установите его вместе со всеми зависимостями, выполнив следующую команду (не забудьте заменить путь до пакета):

sudo apt install -f ~/Downloads/path-to-deb

Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings

Запустите FPTN-клиент. Application

Нажмите на иконку FPTN-клиента в системном трее, чтобы открыть контекстное меню. Application

Откройте настройки FPTN-клиента и нажмите кнопку "Добавить токен". Settings

Вставьте сюда токен из Telegram-бота и сохраните Settings

Закройте окно настроек Settings

В системном трее кликните по иконке приложения и выберите "Умное подключение". Эта функция автоматически подберет наиболее оптимальный сервер. Settings

Готово! Settings

Installation and Setup


Download the .deb package for the FPTN client and install it along with all dependencies by running the following command (make sure to replace the package path):

sudo apt install -f ~/Downloads/path-to-deb

Go to the Telegram bot and generate a token by sending the /token command, then copy it. Settings

Launch the FPTN client. Application

Click on the FPTN client icon in the system tray to open the context menu. Application

Open the FPTN client settings and click the "Add token" button. Settings

Paste the token from the Telegram bot here and save it. Settings

Close the settings window. Settings

In the system tray, click on the application icon and select "Smart Connection". This feature will automatically select the most optimal server. Settings

Done! Settings

Download the client for Windows (supports Windows 10 and Windows 11, x86_64 architecture) Скачать клиент для Windows (поддерживаются Windows 10 и Windows 11, архитектура x86_64)

Установка и настройка


Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings

Скачайте и запустите установщик. При запуске установщика, Windows покажет окно с предупреждением, нажмите "подробнее". Application И нажмите на кнопку "Запустить в любом случае". Application После чего подтвердите запуск установщика. Application После завершения установки нажмите кнопку завершить и приложение запустится автоматически. Application После запуска клиента в системном трее появится иконка приложения. Кликните по иконке и откройте настройки. Application
Нажмите кнопку "Добавить токен".
Application
Вставьте сюда токен из Telegram-бота и сохраните

Application
После открытия появится список доступных серверов, далее нужно просто закрыть окно настроек.

Application
В системном трее кликните по иконке приложения и выберите "Умное подключение". "Умное подключение" подберет наиболее оптимальный сервер.

Application
Готово!

Application

Installation and Setup


Go to the Telegram bot and generate a token by sending the /token command, then copy it. Settings

Application Then click "Run anyway". Application Confirm the installer launch. Application After the installation is complete, click the finish button, and the application will launch automatically. Application After the client launches, the application icon will appear in the system tray. Click on the icon and open the settings. Application
Click the "Add Token" button.
Application
Paste the token from the Telegram bot and save it.

Application
After saving, a list of available servers will appear. Simply close the settings window.

Application
In the system tray, click on the application icon and select "Smart Connection". This will choose the most optimal server for you.

Application
Done!

Application

А еще, можно легко превратить RaspberryPi/OrangePi в WiFi-точку доступа и установить на неё FPTN-клиент. В этом случае все устройства, подключённые к WiFi, получат доступ к интернету, обходя любые ограничения. Информация тут Settings

Любую помощь можно получить в нашем Telegram чате!

You can also easily turn a Raspberry Pi or Orange Pi into a WiFi access point and install the FPTN client on it. In this case, all devices connected to the WiFi will have unrestricted internet access, bypassing any limitations. More information here Settings

For any assistance, feel free to join our Telegram chat!

FPTN Project 2024
================================================ FILE: docs/index.html ================================================ FPTN Project FPTN Project

FPTN Project


FPTN is a fully custom-built VPN technology — developed from scratch, including the core protocol, server implementation, and cross-platform clients. It is a non-commercial, open-source project developed by volunteers and designed to bypass censorship.

The VPN-client is a mobile application or a compact desktop application with an interface located in the system tray. No registration process is required! We will send the access token via the bot on Telegram.

Application
Application

FPTN operates over the HTTPS protocol, effectively masking traffic and enabling users to bypass censorship restrictions. The project's source code is available on GitHub.

FPTN Project


FPTN — это полностью разработанная с нуля технология VPN, включая собственный протокол, сервер и кроссплатформенные клиенты. Это некоммерческий проект с открытым исходным кодом, развиваемый волонтерами и предназначенный для обхода цензуры.

VPN-клиент — мобильное приложение или миниатюрная программа для компьютера, интерфейс которой размещается в трее. Здесь нет процесса регистрации! Мы пришлем токен доступа через бота в Telegram

Application
Application

FPTN работает через протокол HTTPS, эффективно маскируя трафик и позволяя обходить ограничения цензуры. Исходный код проекта доступен на Github.


Download FPTN

Скачать FPTN

Google play Google play

Download Android app from the site Скачать Android приложение с сайта

Установка и настройка


Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings
Вставьте токен в соответствующее поле.

Application
Войдите в приложение, используя полученный токен.

Application
Выберите VPN-сервер из списка доступных (Режим "Авто" подберет наиболее оптимальный сервер или можете выбрать сервер из списка доступных). Для включения VPN-сервиса нажмите на кнопку в центре экрана, которая активирует подключение к выбранному серверу.

Application
Готово!

Application

Installation and Setup


Go to the Telegram bot and generate your token by sending the /token command. Then copy the token. Settings
Insert the token into the appropriate field.

Application
Log into the application using the obtained token.

Application
Select a VPN server from the list of available servers (the "Auto" mode will choose the most optimal server, or you can manually select a server from the list). To enable the VPN service, press the button in the center of the screen, which will activate the connection to the selected server.

Application
Done!

Application

MacOS client (Apple Silicon) Клиент для MacOS (Apple Silicon)

Установка и настройка


При установке клиента на MacOS вам может понадобиться более детальная инструкция из-за проблем с драйверами

Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings

После запуска приложения нажмите на иконку в системном трее чтобы открыть контекстное меню. Application

Вставьте сюда токен из Telegram-бота Settings

Сохраните токен. Settings

Закройте окно настроек и попробуйте подключиться Settings

Installation and Setup


When installing the client on macOS, you may need a more detailed guide due to driver-related issues.

Go to the Telegram bot and generate your token by sending the /token command. Then copy the token. Settings

After launching the app, click on the tray icon to open the context menu. Application

Paste the token you received from the Telegram bot into the token field. Settings

Save the token. Settings

Close the settings window and try connecting. Settings

Download Linux client Скачать Linux клиент

Установка и настройка


Скачайте .deb пакет FPTN-клиента и установите его вместе со всеми зависимостями, выполнив следующую команду (не забудьте заменить путь до пакета):

sudo apt install -f ~/Downloads/path-to-deb

Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings

Запустите FPTN-клиент. Application

Нажмите на иконку FPTN-клиента в системном трее, чтобы открыть контекстное меню. Application

Откройте настройки FPTN-клиента и нажмите кнопку "Добавить токен". Settings

Вставьте сюда токен из Telegram-бота и сохраните Settings

Закройте окно настроек Settings

В системном трее кликните по иконке приложения и выберите "Умное подключение". Эта функция автоматически подберет наиболее оптимальный сервер. Settings

Готово! Settings

Installation and Setup


Download the .deb package for the FPTN client and install it along with all dependencies by running the following command (make sure to replace the package path):

sudo apt install -f ~/Downloads/path-to-deb

Go to the Telegram bot and generate a token by sending the /token command, then copy it. Settings

Launch the FPTN client. Application

Click on the FPTN client icon in the system tray to open the context menu. Application

Open the FPTN client settings and click the "Add token" button. Settings

Paste the token from the Telegram bot here and save it. Settings

Close the settings window. Settings

In the system tray, click on the application icon and select "Smart Connection". This feature will automatically select the most optimal server. Settings

Done! Settings

Download the client for Windows (supports Windows 10 and Windows 11, x86_64 architecture) Скачать клиент для Windows (поддерживаются Windows 10 и Windows 11, архитектура x86_64)

Установка и настройка


Перейдите в Telegram-бот и сгенерируйте токен командой /token, затем скопируйте его. Settings

Скачайте и запустите установщик. При запуске установщика, Windows покажет окно с предупреждением, нажмите "подробнее". Application И нажмите на кнопку "Запустить в любом случае". Application После чего подтвердите запуск установщика. Application После завершения установки нажмите кнопку завершить и приложение запустится автоматически. Application После запуска клиента в системном трее появится иконка приложения. Кликните по иконке и откройте настройки. Application
Нажмите кнопку "Добавить токен".
Application
Вставьте сюда токен из Telegram-бота и сохраните

Application
После открытия появится список доступных серверов, далее нужно просто закрыть окно настроек.

Application
В системном трее кликните по иконке приложения и выберите "Умное подключение". "Умное подключение" подберет наиболее оптимальный сервер.

Application
Готово!

Application

Installation and Setup


Go to the Telegram bot and generate a token by sending the /token command, then copy it. Settings

Application Then click "Run anyway". Application Confirm the installer launch. Application After the installation is complete, click the finish button, and the application will launch automatically. Application After the client launches, the application icon will appear in the system tray. Click on the icon and open the settings. Application
Click the "Add Token" button.
Application
Paste the token from the Telegram bot and save it.

Application
After saving, a list of available servers will appear. Simply close the settings window.

Application
In the system tray, click on the application icon and select "Smart Connection". This will choose the most optimal server for you.

Application
Done!

Application

А еще, можно легко превратить RaspberryPi/OrangePi в WiFi-точку доступа и установить на неё FPTN-клиент. В этом случае все устройства, подключённые к WiFi, получат доступ к интернету, обходя любые ограничения. Информация тут Settings

Любую помощь можно получить в нашем Telegram чате!

You can also easily turn a Raspberry Pi or Orange Pi into a WiFi access point and install the FPTN client on it. In this case, all devices connected to the WiFi will have unrestricted internet access, bypassing any limitations. More information here Settings

For any assistance, feel free to join our Telegram chat!

FPTN Project 2024
================================================ FILE: pyproject.toml ================================================ [tool.black] line-length = 120 extend-exclude = ''' cpplint\.py ''' ================================================ FILE: renovate.json ================================================ { "extends": [ "config:recommended", ":dependencyDashboard" ], "enabledManagers": [ "conan" ], "conan": { "enabled": true, "managerFilePatterns": [ "/conanfile.txt/", "/conanfile.py/" ] } } ================================================ FILE: src/common/client_id.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include namespace fptn { using ClientID = std::uint64_t; #define MAX_CLIENT_ID (UINT64_MAX) } // namespace fptn ================================================ FILE: src/common/data/channel.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include #include "common/network/ip_packet.h" namespace fptn::common::data { class Channel { public: // Increase default capacity for high throughput explicit Channel(std::size_t max_capacity = 8192) { buffer_.set_capacity(max_capacity); } void Push(network::IPPacketPtr pkt) noexcept { { const std::unique_lock lock(mutex_); // mutex if (buffer_.size() < buffer_.capacity()) { buffer_.push_back(std::move(pkt)); } // If buffer full, drop packet (better than unbounded growth or blocking) } condvar_.notify_one(); } network::IPPacketPtr WaitForPacket( const std::chrono::milliseconds& duration) noexcept { std::unique_lock lock(mutex_); // mutex // exists if (!buffer_.empty()) { auto pkt = std::move(buffer_.front()); buffer_.pop_front(); return pkt; } // wait for data or timeout if (condvar_.wait_for( lock, duration, [this] { return !buffer_.empty(); })) { auto pkt = std::move(buffer_.front()); buffer_.pop_front(); return pkt; } return nullptr; } protected: mutable std::mutex mutex_; std::condition_variable condvar_; boost::circular_buffer_space_optimized buffer_; }; using ChannelPtr = std::unique_ptr; } // namespace fptn::common::data ================================================ FILE: src/common/data/channel_async.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/network/ip_packet.h" namespace fptn::common::data { namespace this_coro = boost::asio::this_coro; using boost::asio::use_awaitable; class ChannelAsync { public: explicit ChannelAsync( boost::asio::io_context& ioc, const std::size_t max_capacity = 8192) : ioc_(ioc), notify_timer_(ioc) { buffer_.set_capacity(max_capacity); notify_timer_.expires_at(std::chrono::steady_clock::time_point::max()); } void Push(network::IPPacketPtr pkt) { { const std::unique_lock lock(mutex_); if (buffer_.size() < buffer_.capacity()) { buffer_.push_back(std::move(pkt)); } } try { condvar_.notify_one(); notify_timer_.cancel(); // Trigger async waiter if any } catch (...) { SPDLOG_WARN("ChannelAsync::Push unexpected exception: "); } } network::IPPacketPtr WaitForPacket( const std::chrono::milliseconds& duration) { std::unique_lock lock(mutex_); // mutex // exists if (!buffer_.empty()) { auto pkt = std::move(buffer_.front()); buffer_.pop_front(); return pkt; } // wait for data or timeout if (condvar_.wait_for( lock, duration, [this] { return !buffer_.empty(); })) { auto pkt = std::move(buffer_.front()); buffer_.pop_front(); return pkt; } return nullptr; } boost::asio::awaitable> WaitForPacketAsync(std::chrono::milliseconds timeout) { { const std::lock_guard lock(mutex_); // mutex if (!buffer_.empty()) { auto pkt = std::move(buffer_.front()); buffer_.pop_front(); co_return pkt; } } boost::asio::steady_timer timeout_timer(ioc_); timeout_timer.expires_after(timeout); boost::asio::steady_timer local_notify_timer(ioc_); local_notify_timer.expires_at(std::chrono::steady_clock::time_point::max()); { std::unique_lock lock(mutex_); // mutex if (!buffer_.empty()) { auto pkt = std::move(buffer_.front()); buffer_.pop_front(); co_return pkt; } lock.unlock(); co_await boost::asio::experimental::make_parallel_group( timeout_timer.async_wait(boost::asio::deferred), local_notify_timer.async_wait(boost::asio::deferred)) .async_wait( boost::asio::experimental::wait_for_one(), boost::asio::deferred); } { const std::lock_guard lock(mutex_); // mutex if (!buffer_.empty()) { auto pkt = std::move(buffer_.front()); buffer_.pop_front(); co_return pkt; } } co_return std::nullopt; } protected: boost::asio::io_context& ioc_; mutable boost::asio::steady_timer notify_timer_; mutable std::mutex mutex_; std::condition_variable condvar_; boost::circular_buffer_space_optimized buffer_; }; using ChannelAsyncPtr = std::unique_ptr; } // namespace fptn::common::data ================================================ FILE: src/common/jwt_token/token_manager.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) namespace fptn::common::jwt_token { class TokenManager { public: TokenManager( const std::string& server_crt_path, const std::string& server_key_path) : server_crt_path_(server_crt_path), server_key_path_(server_key_path), server_crt_(ReadFromFile(server_crt_path_)), server_key_(ReadFromFile(server_key_path_)) {} [[nodiscard]] std::pair Generate( const std::string& username, int bandwidth_bit) const noexcept { const auto now = std::chrono::system_clock::now(); // CHECK JWT const auto access_token = jwt::create() .set_issuer("auth0") .set_type("JWT") .set_id("fptn") .set_issued_at(now) .set_expires_at(now + std::chrono::seconds{36000}) .set_payload_claim("username", username) .set_payload_claim("bandwidth_bit", bandwidth_bit) .sign(jwt::algorithm::rs256("", server_key_, "", "")); return std::make_pair(access_token, ""); } bool Validate(const std::string& token, std::string& username, std::size_t& bandwidth_bit) const noexcept { // CHECK IT try { auto decoded = jwt::decode(token); username = decoded.get_payload_claim("username").as_string(); bandwidth_bit = decoded.get_payload_claim("bandwidth_bit").as_integer(); auto verifier = jwt::verify( jwt::default_clock()) .allow_algorithm(jwt::algorithm::rs256("", server_key_, "", "")) .with_issuer("auth0"); return true; } catch (const jwt::error::invalid_json_exception& e) { SPDLOG_ERROR("Token parsing error: {}", e.what()); } catch (const jwt::error::token_verification_exception& e) { SPDLOG_ERROR("Unauthorized: Invalid token: {}", e.what()); } catch (const std::exception& e) { SPDLOG_ERROR("Handle other standard exceptions: {}", e.what()); } catch (...) { SPDLOG_ERROR("Undefined error"); } return false; } const std::string& ServerCrtPath() const noexcept { return server_crt_path_; } const std::string& ServerKeyPath() const noexcept { return server_key_path_; } private: std::string ReadFromFile(const std::string& path) noexcept { std::ifstream is(path, std::ios::binary); if (!is) { SPDLOG_ERROR("Failed to open file: {}", path); return {}; } std::string contents( (std::istreambuf_iterator(is)), std::istreambuf_iterator()); return contents; } private: const std::string server_crt_path_; const std::string server_key_path_; const std::string server_crt_; const std::string server_key_; }; using TokenManagerSPtr = std::shared_ptr; } // namespace fptn::common::jwt_token ================================================ FILE: src/common/logger/logger.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #ifdef __ANDROID__ #include // NOLINT(build/include_order) #elif __APPLE__ #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #elif __linux__ #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #elif _WIN32 #include #endif #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) namespace fptn::logger { inline bool init(const std::string& app_name) { // Set locale #ifdef _WIN32 SetConsoleOutputCP(CP_UTF8); SetConsoleCP(CP_UTF8); std::ios::sync_with_stdio(false); std::wcout.imbue(std::locale(".UTF-8")); #endif std::locale::global(std::locale::classic()); setlocale(LC_ALL, "en_US.UTF-8"); try { #ifdef __ANDROID__ auto logger = spdlog::android_logger_mt("android", app_name); #elif TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR // iOS specific logging - use console only since filesystem access is // restricted auto console_sink = std::make_shared(); auto logger = std::make_shared(app_name, console_sink); logger->flush_on(spdlog::level::debug); spdlog::flush_every(std::chrono::seconds(3)); #else #ifdef __linux__ const std::filesystem::path log_dir = "/var/log/fptn/"; #elif defined(__APPLE__) && TARGET_OS_MAC const std::filesystem::path log_dir = []() { if (const char* home = getenv("HOME")) { return std::filesystem::path(home) / "Library/Logs/fptn"; } struct passwd pwd = {}; struct passwd* result = nullptr; char buffer[1024] = {}; if (getpwuid_r(getuid(), &pwd, buffer, sizeof(buffer), &result) != 0 || !result) { throw std::runtime_error("Failed to get user home directory"); } return std::filesystem::path(pwd.pw_dir) / "Library/Logs/fptn"; }(); #elif _WIN32 const std::filesystem::path log_dir = "./logs/"; #endif const std::filesystem::path log_file = log_dir / (app_name + ".log"); if (!std::filesystem::exists(log_dir)) { try { std::filesystem::create_directories(log_dir); } catch (const std::filesystem::filesystem_error& e) { std::cerr << "Failed to create log directory: " << e.what() << "\n"; return false; } } auto console_sink = std::make_shared(); auto file_sink = std::make_shared( log_file.string(), 12 * 1024 * 1024, 3, true); auto logger = std::make_shared( app_name, spdlog::sinks_init_list{console_sink, file_sink}); logger->flush_on(spdlog::level::debug); spdlog::flush_every(std::chrono::seconds(3)); #endif spdlog::set_default_logger(logger); spdlog::set_level(spdlog::level::info); spdlog::set_pattern("[%Y-%m-%d %H:%M:%S] [%^%l%$] [%s:%#] %v"); #if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR SPDLOG_INFO("Logger initialized for iOS - console output only"); #elif __ANDROID__ SPDLOG_INFO("Logger inited"); #else SPDLOG_INFO("Logging to file: {}", log_file.string()); SPDLOG_INFO("FPTN version: {}", FPTN_VERSION); #endif return true; } catch (const spdlog::spdlog_ex& ex) { #ifdef __ANDROID__ __android_log_print(ANDROID_LOG_ERROR, "FPTN", "Logger initialization failed: %s", ex.what()); #else std::cerr << "Logger initialization failed: " << ex.what() << "\n"; #endif } catch (...) { std::cerr << "Unhandled exception caught in logger\n"; } return false; } } // namespace fptn::logger ================================================ FILE: src/common/network/ip_address.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP #include #else #include #endif namespace fptn::common::network { #ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP template class IPAddress { public: // Default constructor IPAddress() = default; explicit IPAddress(const std::string& ip) : ip_(ip) { if (!ip.empty()) { try { ip_impl_ = boost::asio::ip::make_address(ip); } catch (const boost::system::system_error&) { // Invalid address, leave ip_impl_ as default constructed ip_impl_ = T(); } } } T Get() const noexcept { return ip_impl_; } bool IsEmpty() const { return ip_.empty() || ip_impl_ == T(); } bool IsValid() const { return !ip_.empty() && ip_impl_ != T(); } const std::string& ToString() const { return ip_; } // Add copy and move constructors/assignment for base class IPAddress(const IPAddress& other) : ip_(other.ip_), ip_impl_(other.ip_impl_) {} IPAddress(IPAddress&& other) noexcept : ip_(std::move(other.ip_)), ip_impl_(std::move(other.ip_impl_)) {} IPAddress& operator=(const IPAddress& other) { if (this != &other) { ip_ = other.ip_; ip_impl_ = other.ip_impl_; } return *this; } IPAddress& operator=(IPAddress&& other) noexcept { if (this != &other) { ip_ = std::move(other.ip_); ip_impl_ = std::move(other.ip_impl_); } return *this; } bool operator!=(const IPAddress& other) const noexcept { return ip_ != other.ip_ || ip_impl_ != other.ip_impl_; } bool operator==(const IPAddress& other) const noexcept { return ip_ == other.ip_ && ip_impl_ == other.ip_impl_; } std::uint32_t ToInt() const { if (ip_impl_.is_v4()) { return ip_impl_.to_v4().to_uint(); } return 0; // For IPv6, return 0 or handle differently } protected: std::string ip_; T ip_impl_; }; class IPv4Address : public IPAddress { public: // Default constructor IPv4Address() = default; // Constructor from string explicit IPv4Address(const std::string& ip) : IPAddress(ip) { // Additional validation for IPv4 if (!ip_.empty() && !ip_impl_.is_v4()) { ip_impl_ = boost::asio::ip::address(); } } // Constructor from boost::asio::ip::address object explicit IPv4Address(const boost::asio::ip::address& ip_addr) : IPAddress(ip_addr.to_string()) { if (!ip_addr.is_v4()) { ip_impl_ = boost::asio::ip::address(); } } // Copy constructor IPv4Address(const IPv4Address& other) : IPAddress(other) {} // Move constructor IPv4Address(IPv4Address&& other) noexcept : IPAddress(std::move(other)) {} // Copy assignment operator IPv4Address& operator=(const IPv4Address& other) { if (this != &other) { IPAddress::operator=(other); } return *this; } // Move assignment operator IPv4Address& operator=(IPv4Address&& other) noexcept { if (this != &other) { IPAddress::operator=(std::move(other)); } return *this; } static IPv4Address Create(const std::string& ip) { return IPv4Address(ip); } static IPv4Address Create(const boost::asio::ip::address& ip_addr) { return IPv4Address(ip_addr); } static IPv4Address Create(const IPv4Address& ip_addr) { return IPv4Address(ip_addr); } // Additional IPv4-specific methods virtual bool IsValid() const { return IPAddress::IsValid() && ip_impl_.is_v4(); } virtual std::uint32_t ToInt() const { if (ip_impl_.is_v4()) { return ip_impl_.to_v4().to_uint(); } return 0; } }; class IPv6Address : public IPAddress { public: // Default constructor IPv6Address() = default; // Constructor from string explicit IPv6Address(const std::string& ip) : IPAddress(ip) { // Additional validation for IPv6 if (!ip_.empty() && !ip_impl_.is_v6()) { ip_impl_ = boost::asio::ip::address(); } } // Constructor from boost::asio::ip::address object explicit IPv6Address(const boost::asio::ip::address& ip_addr) : IPAddress(ip_addr.to_string()) { if (!ip_addr.is_v6()) { ip_impl_ = boost::asio::ip::address(); } } // Copy constructor IPv6Address(const IPv6Address& other) : IPAddress(other) {} // Move constructor IPv6Address(IPv6Address&& other) noexcept : IPAddress(std::move(other)) {} // Copy assignment operator IPv6Address& operator=(const IPv6Address& other) { if (this != &other) { IPAddress::operator=(other); } return *this; } // Move assignment operator IPv6Address& operator=(IPv6Address&& other) noexcept { if (this != &other) { IPAddress::operator=(std::move(other)); } return *this; } static IPv6Address Create(const std::string& ip) { return IPv6Address(ip); } static IPv6Address Create(const boost::asio::ip::address& ip_addr) { return IPv6Address(ip_addr); } static IPv6Address Create(const IPv6Address& ip_addr) { return IPv6Address(ip_addr); } // Additional IPv6-specific methods virtual bool IsValid() const { return IPAddress::IsValid() && ip_impl_.is_v6(); } }; #else template class IPAddress { public: // Default constructor IPAddress() = default; explicit IPAddress(const std::string& ip) : ip_(ip) { try { ip_impl_ = T(ip); } catch (...) { ip_impl_ = T(); } } T Get() const noexcept { return ip_impl_; } bool IsEmpty() const { return ip_.empty() || ip_impl_ == T(); } const std::string& ToString() const { return ip_; } // Add copy and move constructors/assignment for base class IPAddress(const IPAddress& other) : ip_(other.ip_), ip_impl_(other.ip_impl_) {} IPAddress(IPAddress&& other) noexcept : ip_(std::move(other.ip_)), ip_impl_(std::move(other.ip_impl_)) {} IPAddress& operator=(const IPAddress& other) { if (this != &other) { ip_ = other.ip_; ip_impl_ = other.ip_impl_; } return *this; } IPAddress& operator=(IPAddress&& other) noexcept { if (this != &other) { ip_ = std::move(other.ip_); ip_impl_ = std::move(other.ip_impl_); } return *this; } bool operator!=(const IPAddress& other) const noexcept { return ip_ != other.ip_ || ip_impl_ != other.ip_impl_; } bool operator==(const IPAddress& other) const noexcept { return ip_ == other.ip_ && ip_impl_ == other.ip_impl_; } std::uint32_t ToInt() const { return ip_impl_.toInt(); } private: std::string ip_; T ip_impl_; }; class IPv4Address : public IPAddress { public: // Default constructor - explicitly calls base class default constructor IPv4Address() = default; // Constructor from string - explicitly calls base class constructor explicit IPv4Address(std::string ip) : IPAddress(std::move(ip)) {} // Constructor from pcpp::IPv4Address object - explicitly calls base class // constructor explicit IPv4Address(const pcpp::IPv4Address& ip_addr) : IPAddress(ip_addr.toString()) {} // Copy constructor - explicitly calls base class copy constructor IPv4Address(const IPv4Address& other) : IPAddress(other) {} // Move constructor - explicitly calls base class move constructor IPv4Address(IPv4Address&& other) noexcept : IPAddress(std::move(other)) {} // Copy assignment operator IPv4Address& operator=(const IPv4Address& other) { if (this != &other) { IPAddress::operator=(other); } return *this; } // Move assignment operator IPv4Address& operator=(IPv4Address&& other) noexcept { if (this != &other) { IPAddress::operator=(std::move(other)); } return *this; } static IPv4Address Create(std::string ip) { return IPv4Address(std::move(ip)); } static IPv4Address Create(const pcpp::IPv4Address& ip_addr) { return IPv4Address(ip_addr); } static IPv4Address Create(const IPv4Address& ip_addr) { return IPv4Address(ip_addr); } }; class IPv6Address : public IPAddress { public: // Default constructor - explicitly calls base class default constructor IPv6Address() = default; // Constructor from string - explicitly calls base class constructor explicit IPv6Address(std::string ip) : IPAddress(std::move(ip)) {} // Constructor from pcpp::IPv6Address object - explicitly calls base class // constructor explicit IPv6Address(const pcpp::IPv6Address& ip_addr) : IPAddress(ip_addr.toString()) {} // Copy constructor - explicitly calls base class copy constructor IPv6Address(const IPv6Address& other) : IPAddress(other) {} // Move constructor - explicitly calls base class move constructor IPv6Address(IPv6Address&& other) noexcept : IPAddress(std::move(other)) {} // Copy assignment operator IPv6Address& operator=(const IPv6Address& other) { if (this != &other) { IPAddress::operator=(other); } return *this; } // Move assignment operator IPv6Address& operator=(IPv6Address&& other) noexcept { if (this != &other) { IPAddress::operator=(std::move(other)); } return *this; } static IPv6Address Create(std::string ip) { return IPv6Address(std::move(ip)); } static IPv6Address Create(const pcpp::IPv6Address& ip_addr) { return IPv6Address(ip_addr); } static IPv6Address Create(const IPv6Address& ip_addr) { return IPv6Address(ip_addr); } }; #endif } // namespace fptn::common::network ================================================ FILE: src/common/network/ip_packet.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include "common/utils/utils.h" #if _WIN32 #pragma warning(disable : 4996) #endif #if _WIN32 #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #else #include #endif #include #ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP #include "common/network/ip_address.h" #else #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #ifdef TCPOPT_CC #undef TCPOPT_CC #endif // TCPOPT_CC #ifdef TCPOPT_CCNEW #undef TCPOPT_CCNEW #endif // TCPOPT_CCNEW #ifdef TCPOPT_CCECHO #undef TCPOPT_CCECHO #endif // TCPOPT_CCECHO #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #if _WIN32 #pragma warning(default : 4996) #endif #endif #ifdef FPTN_WITH_LIBIDN2 #include #endif #include // NOLINT(build/include_order) #include "common/client_id.h" #include "common/network/ip_address.h" namespace fptn::common::network { using IPPacketData = std::vector; #define FPTN_PACKET_UNDEFINED_CLIENT_ID MAX_CLIENT_ID #ifndef FPTN_IP_ADDRESS_WITHOUT_PCAP inline bool CheckIPv4(const IPPacketData& buffer) { return (static_cast(buffer[0]) >> 4) == 4; } inline bool CheckIPv6(const IPPacketData& buffer) { return (static_cast(buffer[0]) >> 4) == 6; } #ifdef FPTN_WITH_LIBIDN2 inline bool IsPunycode(const std::string& str) { return str.find("xn--") != std::string::npos; } inline std::string ConvertDomainToUnicode(const std::string& domain) { if (domain.empty()) { return domain; } char* result = nullptr; const int ret = idn2_to_unicode_8z8z(domain.c_str(), &result, 0); if (ret == IDN2_OK && result != nullptr) { std::string unicode_result = result; free(result); return unicode_result; } if (result != nullptr) { free(result); } return domain; } #endif class IPPacket { public: static std::unique_ptr Parse(IPPacketData buffer, fptn::ClientID client_id = FPTN_PACKET_UNDEFINED_CLIENT_ID) { // Minimum IPv4 header size if (buffer.empty() || buffer.size() < 20) { return nullptr; } if (CheckIPv4(buffer)) { auto packet = std::make_unique( std::move(buffer), client_id, pcpp::LINKTYPE_IPV4); if (nullptr != packet->IPv4Layer()) { return packet; } } else if (CheckIPv6(buffer)) { auto packet = std::make_unique( std::move(buffer), client_id, pcpp::LINKTYPE_IPV6); if (nullptr != packet->IPv6Layer()) { return packet; } } return nullptr; } public: IPPacket(IPPacketData data, fptn::ClientID client_id, const pcpp::LinkLayerType& ip_type) : packet_data_(std::move(data)), client_id_(client_id) { try { raw_packet_ = pcpp::RawPacket( reinterpret_cast(packet_data_.data()), static_cast(packet_data_.size()), timeval{0, 0}, false, ip_type); parsed_packet_ = pcpp::Packet(&raw_packet_, false); if (pcpp::LINKTYPE_IPV4 == ip_type) { ipv4_layer_ = parsed_packet_.getLayerOfType(); } else if (pcpp::LINKTYPE_IPV6 == ip_type) { ipv6_layer_ = parsed_packet_.getLayerOfType(); } } catch (const std::exception& e) { SPDLOG_WARN( "IP Packet parsing exception (client {}): {}", client_id_, e.what()); } catch (...) { SPDLOG_WARN("Unknown error while parsing IP Packet"); } } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) void ComputeCalculateFields() noexcept { auto* tcp_layer = parsed_packet_.getLayerOfType(); if (tcp_layer) { tcp_layer->computeCalculateFields(); } else { auto* udp_layer = parsed_packet_.getLayerOfType(); if (udp_layer) { udp_layer->computeCalculateFields(); } } if (ipv4_layer_) { ipv4_layer_->computeCalculateFields(); } else if (ipv6_layer_) { auto* icmp_layer = parsed_packet_.getLayerOfType(); if (icmp_layer) { icmp_layer->computeCalculateFields(); } ipv6_layer_->computeCalculateFields(); } } void SetClientId(fptn::ClientID client_id) noexcept { client_id_ = client_id; } void SetDstIPv4Address(const pcpp::IPv4Address& dst) noexcept { if (ipv4_layer_) { ipv4_layer_->getIPv4Header()->timeToLive -= 1; ipv4_layer_->setDstIPv4Address(dst); } } void SetSrcIPv4Address(const pcpp::IPv4Address& src) noexcept { if (ipv4_layer_) { ipv4_layer_->getIPv4Header()->timeToLive -= 1; ipv4_layer_->setSrcIPv4Address(src); } } void SetDstIPv6Address(const pcpp::IPv6Address& dst) noexcept { if (ipv6_layer_) { ipv6_layer_->setDstIPv6Address(dst); } } void SetSrcIPv6Address(const pcpp::IPv6Address& src) noexcept { if (ipv6_layer_) { ipv6_layer_->setSrcIPv6Address(src); } } bool IsICMPv4() const { const auto* icmp_v4 = parsed_packet_.getLayerOfType(); return icmp_v4 != nullptr; } bool IsICMPv6() const { const auto* icmp_v6 = parsed_packet_.getLayerOfType(); return icmp_v6 != nullptr; } bool IsDns() const { const auto* udp = parsed_packet_.getLayerOfType(); if (udp && (udp->getDstPort() == 53 || udp->getSrcPort() == 53)) { return GetDnsLayer() != nullptr; } const auto* tcp = parsed_packet_.getLayerOfType(); if (tcp && (tcp->getDstPort() == 53 || tcp->getSrcPort() == 53)) { return GetDnsLayer() != nullptr; } return false; } std::optional GetDnsDomain() const { const auto* dns_layer = GetDnsLayer(); if (dns_layer) { const auto* query = dns_layer->getFirstQuery(); if (query) { std::string domain_name = query->getName(); #ifdef FPTN_WITH_LIBIDN2 if (IsPunycode(domain_name)) { return ConvertDomainToUnicode(domain_name); } #endif return domain_name; } } return std::nullopt; } std::vector GetDnsIPv4Addresses() const { const auto* dns_layer = GetDnsLayer(); if (!dns_layer) { return {}; } if (dns_layer->getDnsHeader()->queryOrResponse != 1) { return {}; } std::vector ipv4_addresses; auto* answer = dns_layer->getFirstAnswer(); while (answer != nullptr) { if (answer->getDnsType() == pcpp::DNS_TYPE_A) { try { const std::size_t data_offset = answer->getDataOffset(); const std::size_t data_length = answer->getDataLength(); if (data_length == 4) { const std::uint8_t* dns_raw_data = dns_layer->getData(); const std::size_t dns_data_len = dns_layer->getDataLen(); if (data_offset + 4 <= dns_data_len) { const uint8_t* ip_bytes = dns_raw_data + data_offset; const std::uint32_t ip_int = (static_cast(ip_bytes[0]) << 24) | (static_cast(ip_bytes[1]) << 16) | (static_cast(ip_bytes[2]) << 8) | static_cast(ip_bytes[3]); const auto addr = boost::asio::ip::make_address_v4(ip_int); ipv4_addresses.emplace_back(addr.to_string()); } } } catch (const std::exception& e) { SPDLOG_WARN("Failed to parse IPv4 from DNS answer '{}': {}", answer->getName(), e.what()); } } answer = dns_layer->getNextAnswer(answer); } return ipv4_addresses; } std::vector GetDnsIPv6Addresses() const { const auto* dns_layer = GetDnsLayer(); if (!dns_layer) { return {}; } if (dns_layer->getDnsHeader()->queryOrResponse != 1) { return {}; } std::vector ipv6_addresses; auto* answer = dns_layer->getFirstAnswer(); while (answer != nullptr) { if (answer->getDnsType() == pcpp::DNS_TYPE_AAAA) { try { const std::size_t data_offset = answer->getDataOffset(); const std::size_t data_length = answer->getDataLength(); if (data_length == 16) { const std::uint8_t* dns_raw_data = dns_layer->getData(); const std::size_t dns_data_len = dns_layer->getDataLen(); if (data_offset + 16 <= dns_data_len) { const uint8_t* ip_bytes = dns_raw_data + data_offset; std::array bytes; std::memcpy(bytes.data(), ip_bytes, 16); const auto addr = boost::asio::ip::make_address_v6(bytes); ipv6_addresses.emplace_back(addr.to_string()); } } } catch (const std::exception& e) { SPDLOG_WARN("Failed to parse IPv6 from DNS answer '{}': {}", answer->getName(), e.what()); } } answer = dns_layer->getNextAnswer(answer); } return ipv6_addresses; } fptn::ClientID ClientId() const noexcept { return client_id_; } pcpp::Packet& Pkt() noexcept { return parsed_packet_; } std::size_t Size() const noexcept { return packet_data_.size(); } const pcpp::RawPacket* GetRawPacket() const noexcept { return parsed_packet_.getRawPacket(); } protected: pcpp::DnsLayer* GetDnsLayer() const { try { auto* udp_layer = parsed_packet_.getLayerOfType(); if (udp_layer) { return parsed_packet_.getLayerOfType(); } auto* tcp_layer = parsed_packet_.getLayerOfType(); if (tcp_layer && tcp_layer->getLayerPayloadSize() >= 2) { return parsed_packet_.getLayerOfType(); } } catch (...) { // ignore } return nullptr; } public: // TODO(stas): Remove virtual functions in the future for better performance. // TODO(stas): Anti-scan tests are currently inadequate and need improvement. virtual ~IPPacket() = default; virtual bool IsIPv4() const noexcept { return ipv4_layer_ != nullptr; } virtual bool IsIPv6() const noexcept { return ipv6_layer_ != nullptr; } virtual pcpp::IPv4Layer* IPv4Layer() noexcept { return ipv4_layer_; } virtual pcpp::IPv6Layer* IPv6Layer() noexcept { return ipv6_layer_; } protected: // for tests only IPPacket() : client_id_(FPTN_PACKET_UNDEFINED_CLIENT_ID) {} private: std::vector packet_data_; fptn::ClientID client_id_; pcpp::RawPacket raw_packet_; pcpp::Packet parsed_packet_; pcpp::IPv4Layer* ipv4_layer_ = nullptr; pcpp::IPv6Layer* ipv6_layer_ = nullptr; }; using IPPacketPtr = std::unique_ptr; #else /** * Specific lightweight container for IPv4 packets. * Wraps raw IP packet data with basic access methods. */ class LightIPv4Packet { public: static std::unique_ptr Parse(IPPacketData buffer, std::uint64_t client_id = FPTN_PACKET_UNDEFINED_CLIENT_ID) { if (buffer.empty() || buffer.size() < 20) { // Minimum IPv4 header size return nullptr; } return std::make_unique(std::move(buffer), client_id); } LightIPv4Packet(IPPacketData buffer, const fptn::ClientID client_id) : ip_packet_(std::move(buffer)) { (void)client_id; } std::size_t Size() const { return ip_packet_.size(); } // specific methods to have general interface with IPPacket const LightIPv4Packet* GetRawPacket() const { return this; } std::size_t getRawDataLen() const { return ip_packet_.size(); } const void* getRawData() const { return ip_packet_.data(); } private: const IPPacketData ip_packet_; }; using IPPacket = LightIPv4Packet; using IPPacketPtr = std::unique_ptr; #endif } // namespace fptn::common::network ================================================ FILE: src/common/network/ipv4_generator.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #if _WIN32 #pragma warning(disable : 4996) #endif #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ip_address.h" #if _WIN32 #pragma warning(default : 4996) #endif namespace fptn::common::network { class IPv4AddressGenerator { public: IPv4AddressGenerator(const fptn::common::network::IPv4Address& netAddress, std::uint32_t subnet_mask) : ip_(boost::asio::ip::make_address_v4(netAddress.ToString())), net_addr_(boost::asio::ip::make_address_v4(netAddress.ToString())) { // cppcheck-suppress useInitializationList netmask_ = boost::asio::ip::address_v4( (subnet_mask == 0) ? 0 : (~static_cast(0) << (32 - subnet_mask))); const uint32_t ip_num = ip_.to_uint(); const uint32_t netmask_num = netmask_.to_uint(); const uint32_t network_address = ip_num & netmask_num; broadcast_ = boost::asio::ip::address_v4(network_address | ~netmask_.to_uint()); num_available_addresses_ = (1U << (32 - subnet_mask)) - 2; } std::uint32_t NumAvailableAddresses() const noexcept { return num_available_addresses_; } fptn::common::network::IPv4Address GetNextAddress() noexcept { const std::unique_lock lock(mutex_); // mutex const std::uint32_t new_ip = ip_.to_uint() + 1; if (new_ip < broadcast_.to_uint()) { ip_ = boost::asio::ip::address_v4(new_ip); } else { ip_ = boost::asio::ip::address_v4(net_addr_.to_uint() + 1); } return fptn::common::network::IPv4Address(ip_.to_string()); } private: mutable std::mutex mutex_; boost::asio::ip::address_v4 ip_; boost::asio::ip::address_v4 net_addr_; boost::asio::ip::address_v4 netmask_; boost::asio::ip::address_v4 broadcast_; std::uint32_t num_available_addresses_; }; using IPv4AddressGeneratorSPtr = std::shared_ptr; } // namespace fptn::common::network ================================================ FILE: src/common/network/ipv6_generator.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #if _WIN32 #pragma warning(disable : 4996) #endif #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ip_address.h" #if _WIN32 #pragma warning(default : 4996) #endif #include "common/network/ipv6_utils.h" namespace fptn::common::network { class IPv6AddressGenerator { public: IPv6AddressGenerator(const fptn::common::network::IPv6Address& net_address, std::uint32_t subnet_mask) { const auto net_address_boost = boost::asio::ip::make_address_v6(net_address.ToString()); net_addr_ = ipv6::toUInt128(net_address_boost); max_addr_ = net_addr_ | ((boost::multiprecision::uint128_t(1) << (128 - subnet_mask)) - 1); current_addr_ = net_addr_; } fptn::common::network::IPv6Address GetNextAddress() noexcept { const std::unique_lock lock(mutex_); // mutex const auto new_ip = current_addr_ + 1; if (new_ip < max_addr_) { current_addr_ = new_ip; } else { current_addr_ = net_addr_ + 1; } return fptn::common::network::IPv6Address(ipv6::toString(current_addr_)); } boost::multiprecision::uint128_t NumAvailableAddresses() const { return max_addr_ - net_addr_ - 1; } private: mutable std::mutex mutex_; boost::multiprecision::uint128_t net_addr_; boost::multiprecision::uint128_t max_addr_; boost::multiprecision::uint128_t current_addr_; }; using IPv6AddressGeneratorSPtr = std::shared_ptr; } // namespace fptn::common::network ================================================ FILE: src/common/network/ipv6_utils.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include // NOLINT(build/include_order) namespace fptn::common::network::ipv6 { inline boost::multiprecision::uint128_t toUInt128( const boost::asio::ip::address_v6& address) { boost::multiprecision::uint128_t result{}; for (uint8_t b : address.to_bytes()) { (result <<= 8) |= b; } return result; } inline boost::multiprecision::uint128_t toUInt128( const pcpp::IPv6Address& address) { return toUInt128(boost::asio::ip::make_address_v6(address.toString())); } inline std::string toString(const boost::multiprecision::uint128_t& val) { const std::uint64_t high = static_cast(val >> 64); // High 64 bits const std::uint64_t low = static_cast(val & 0xFFFFFFFFFFFFFFFF); // Build 16-byte network-order address from the two 64-bit halves const boost::asio::ip::address_v6::bytes_type bytes = {{ static_cast(high >> 56), static_cast(high >> 48), static_cast(high >> 40), static_cast(high >> 32), static_cast(high >> 24), static_cast(high >> 16), static_cast(high >> 8), static_cast(high), static_cast(low >> 56), static_cast(low >> 48), static_cast(low >> 40), static_cast(low >> 32), static_cast(low >> 24), static_cast(low >> 16), static_cast(low >> 8), static_cast(low), }}; // Produces canonical compressed format (matches pcpp/inet_ntop) return boost::asio::ip::make_address_v6(bytes).to_string(); } } // namespace fptn::common::network::ipv6 ================================================ FILE: src/common/network/net_interface.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Copyright (c) 2024-2026 Pavel Shpilev Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "common/data/channel.h" #include "common/network/ip_address.h" #include "common/network/ip_packet.h" namespace fptn::common::network { class DataRateCalculator { public: explicit DataRateCalculator( std::chrono::milliseconds interval = std::chrono::milliseconds(1000)) : interval_(interval), bytes_(0), lastUpdateTime_(std::chrono::steady_clock::now()), rate_(0) {} void Update(std::size_t len) noexcept { const std::scoped_lock lock(mutex_); // mutex auto now = std::chrono::steady_clock::now(); std::chrono::duration elapsed = now - lastUpdateTime_; bytes_ += len; if (elapsed >= interval_) { rate_ = static_cast(bytes_ / elapsed.count()); lastUpdateTime_ = now; bytes_ = 0; } } std::size_t GetRateForSecond() const noexcept { const std::scoped_lock lock(mutex_); // mutex const auto interval_count = interval_.count(); if (interval_count) { return static_cast(rate_ / (1000 / interval_.count())); } return 0; } private: mutable std::mutex mutex_; std::chrono::milliseconds interval_; std::atomic bytes_; std::chrono::steady_clock::time_point lastUpdateTime_; std::atomic rate_; }; using NewIPPacketCallback = std::function; /** * @brief Base network interface class using CRTP (Curiously Recurring Template * Pattern) * * This template provides common interface functionality while delegating * platform-specific operations to the derived implementation class. * * CRTP Benefits: * - Static polymorphism (no virtual function overhead) * - Clear separation of interface and implementation * - Compile-time enforcement of interface contracts * * Derived classes must implement: * - StartImpl() - Initialize the interface * - StopImpl() - Shutdown the interface * - SendImpl() - Packet transmission * - GetSendRateImpl() - Outbound rate monitoring * - GetReceiveRateImpl() - Inbound rate monitoring */ template class BaseNetInterface { public: friend Implementation; // Network configuration struct Config { std::string name; fptn::common::network::IPv4Address ipv4_addr; int ipv4_netmask; fptn::common::network::IPv6Address ipv6_addr; int ipv6_netmask; }; bool Start() { return impl()->StartImpl(); } bool Stop() { return impl()->StopImpl(); } bool Send(IPPacketPtr packet) { return impl()->SendImpl(std::move(packet)); } std::size_t GetSendRate() { return impl()->GetSendRateImpl(); } std::size_t GetReceiveRate() { return impl()->GetReceiveRateImpl(); } private: explicit BaseNetInterface(Config config) : config_(std::move(config)), recv_ip_packet_callback_(nullptr) {} Implementation* impl() { return static_cast(this); } public: [[nodiscard]] const std::string& Name() const noexcept { return config_.name; } [[nodiscard]] const fptn::common::network::IPv4Address& IPv4Addr() const noexcept { return config_.ipv4_addr; } [[nodiscard]] int IPv4Netmask() const noexcept { return config_.ipv4_netmask; } [[nodiscard]] const fptn::common::network::IPv6Address& IPv6Addr() const noexcept { return config_.ipv6_addr; } int IPv6Netmask() const noexcept { return config_.ipv6_netmask; } void SetRecvIPPacketCallback(const NewIPPacketCallback& callback) noexcept { recv_ip_packet_callback_ = callback; } [[nodiscard]] NewIPPacketCallback GetRecvIPPacketCallback() const { return recv_ip_packet_callback_; } private: Config config_; NewIPPacketCallback recv_ip_packet_callback_; }; /** * @brief Generic TUN interface parameterized by a platform-specific Device. * * The Device type must satisfy the following concept (duck-typed): * bool Open(const std::string& name); * void Close(); * std::string GetName() const; * bool ConfigureIPv4(const std::string& addr, int mask); * bool ConfigureIPv6(const std::string& addr, int mask); * void SetNonBlocking(bool enabled); * void SetMTU(int mtu); * void BringUp(); * int Read(void* buffer, int size); * int Write(const void* data, int size); * void SetStopFlag(const std::atomic* running); * * Platform-specific devices: LinuxTunDevice, DarwinTunDevice, WinTunDevice */ template class GenericTunInterface final : public BaseNetInterface> { public: using Base = BaseNetInterface>; friend Base; using Config = typename Base::Config; explicit GenericTunInterface(Config config) : Base(std::move(config)), mtu_(FPTN_MTU_SIZE), running_(false) {} ~GenericTunInterface() { StopImpl(); } protected: bool StartImpl() noexcept { const std::scoped_lock lock(mutex_); try { // cppcheck-suppress knownConditionTrueFalse if (!device_.Open(this->Name())) { SPDLOG_ERROR("Failed to open TUN device"); return false; } // Update name to actual device name (may differ, e.g., utun on macOS) this->config_.name = device_.GetName(); /* set IPv6 */ // cppcheck-suppress knownConditionTrueFalse if (!device_.ConfigureIPv6( this->IPv6Addr().ToString(), this->IPv6Netmask())) { SPDLOG_WARN("IPv6 configuration failed, continuing with IPv4 only"); } /* set IPv4 */ // cppcheck-suppress knownConditionTrueFalse if (!device_.ConfigureIPv4( this->IPv4Addr().ToString(), this->IPv4Netmask())) { SPDLOG_ERROR("IPv4 configuration failed"); device_.Close(); return false; } device_.SetNonBlocking(true); device_.SetMTU(mtu_); device_.BringUp(); running_ = true; device_.SetStopFlag(&running_); thread_ = std::thread(&GenericTunInterface::Run, this); return thread_.joinable(); } catch (const std::exception& ex) { SPDLOG_ERROR("Error start: {}", ex.what()); } return false; } bool StopImpl() noexcept { if (!running_) { return false; } const std::scoped_lock lock(mutex_); // cppcheck-suppress identicalConditionAfterEarlyExit if (!running_) { // Double-check after acquiring lock return false; } if (thread_.joinable()) { running_ = false; thread_.join(); device_.Close(); } return true; } bool SendImpl(IPPacketPtr packet) noexcept { if (!running_ || !packet || !packet->Size()) { return false; } try { const std::scoped_lock lock(mutex_); if (running_) { const auto* raw_packet = packet->GetRawPacket(); if (!raw_packet) { return false; } const auto* data = raw_packet->getRawData(); const auto len = raw_packet->getRawDataLen(); const int bytes_written = device_.Write(data, static_cast(len)); send_rate_calculator_.Update(bytes_written); return bytes_written == len; } } catch (const std::exception& ex) { SPDLOG_ERROR("SendImpl error: {}", ex.what()); } return false; } std::size_t GetSendRateImpl() const noexcept { return send_rate_calculator_.GetRateForSecond(); } std::size_t GetReceiveRateImpl() const noexcept { return receive_rate_calculator_.GetRateForSecond(); } private: void Run() { const auto callback = this->GetRecvIPPacketCallback(); while (running_) { std::vector buffer(mtu_); const int size = device_.Read(buffer.data(), mtu_); if (size > 0 && running_) { buffer.resize(size); auto packet = IPPacket::Parse(buffer); if (running_ && packet != nullptr && callback) { receive_rate_calculator_.Update(packet->Size()); // calculate rate callback(std::move(packet)); } } else { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } } } mutable std::mutex mutex_; const std::uint16_t mtu_; std::atomic running_; std::thread thread_; Device device_; DataRateCalculator send_rate_calculator_; DataRateCalculator receive_rate_calculator_; }; } // namespace fptn::common::network // Platform-specific TUN device includes (outside namespace to avoid nesting) #if defined(__APPLE__) #include "common/network/tun/darwin_tun_device.h" #elif defined(__linux__) #include "common/network/tun/linux_tun_device.h" #elif defined(_WIN32) #include "common/network/tun/win_tun_device.h" #endif namespace fptn::common::network { // Platform-specific TUN interface aliases #if defined(__APPLE__) using TunInterface = GenericTunInterface; #elif defined(__linux__) using TunInterface = GenericTunInterface; #elif defined(_WIN32) using TunInterface = GenericTunInterface; #endif using TunInterfacePtr = std::unique_ptr; } // namespace fptn::common::network ================================================ FILE: src/common/network/resolv.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include // NOLINT(build/include_order) namespace fptn::common::network { inline bool IsIpAddress(const std::string& host) { boost::system::error_code ec; boost::asio::ip::make_address(host, ec); return !ec; } struct ResolveResult { boost::system::error_code error; boost::asio::ip::tcp::resolver::results_type results; bool success() const { return !error; } explicit operator bool() const { return success(); } }; inline ResolveResult ResolveWithTimeout(boost::asio::io_context& ioc, const std::string& host, const std::string& port, int timeout_seconds) { boost::asio::ip::tcp::resolver resolver(ioc); ResolveResult result; if (IsIpAddress(host)) { boost::system::error_code ec; auto address = boost::asio::ip::make_address(host, ec); if (ec) { result.error = ec; SPDLOG_WARN( "DNS resolution - Invalid IP address {}: {}", host, ec.message()); return result; } std::uint16_t port_num = 0; try { port_num = static_cast(std::stoi(port)); } catch (const std::exception& e) { result.error = boost::system::error_code(boost::system::errc::invalid_argument, boost::system::system_category()); SPDLOG_WARN( "DNS resolution - Invalid port number {}: {}", port, e.what()); return result; } boost::asio::ip::tcp::endpoint endpoint(address, port_num); result.results = boost::asio::ip::tcp::resolver::results_type::create( endpoint, host, port); return result; } boost::asio::steady_timer timer(ioc); timer.expires_after(std::chrono::seconds(timeout_seconds)); bool operation_completed = false; // FIXME IPv4 only! resolver.async_resolve(boost::asio::ip::tcp::v4(), host, port, [&](const boost::system::error_code& ec, boost::asio::ip::tcp::resolver::results_type results) { if (!operation_completed) { result.error = ec; if (!ec) { result.results = std::move(results); } else { SPDLOG_WARN("DNS resolution - Failed for {}:{}: {}", host, port, ec.message()); } operation_completed = true; timer.cancel(); } }); timer.async_wait([&](const boost::system::error_code& ec) { if (!ec && !operation_completed) { SPDLOG_WARN("DNS resolution - Timeout for {}:{} after {}s", host, port, timeout_seconds); resolver.cancel(); result.error = boost::asio::error::timed_out; operation_completed = true; } }); ioc.restart(); while (!operation_completed) { ioc.run_one(); } return result; } } // namespace fptn::common::network ================================================ FILE: src/common/network/tun/darwin_tun_device.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Pavel Shpilev Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include // NOLINT(build/include_order) #include #include #include #include // NOLINT(build/include_order) #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) namespace fptn::common::network { class DarwinTunDevice { public: DarwinTunDevice() : fd_(-1) {} ~DarwinTunDevice() { Close(); } DarwinTunDevice(const DarwinTunDevice&) = delete; DarwinTunDevice& operator=(const DarwinTunDevice&) = delete; bool Open(const std::string& /*requested_name*/) { fd_ = socket(PF_SYSTEM, SOCK_DGRAM, SYSPROTO_CONTROL); if (fd_ < 0) { SPDLOG_ERROR( "DarwinTunDevice: socket(PF_SYSTEM) failed: {}", strerror(errno)); return false; } struct ctl_info info = {}; strncpy(info.ctl_name, UTUN_CONTROL_NAME, sizeof(info.ctl_name) - 1); if (ioctl(fd_, CTLIOCGINFO, &info) < 0) { SPDLOG_ERROR( "DarwinTunDevice: ioctl(CTLIOCGINFO) failed: {}", strerror(errno)); close(fd_); fd_ = -1; return false; } // Try utun numbers starting from 0 until we find an available one constexpr int kMaxUtunAttempts = 256; bool connected = false; for (int unit = 0; unit < kMaxUtunAttempts; ++unit) { struct sockaddr_ctl addr = {}; addr.sc_len = sizeof(addr); addr.sc_family = AF_SYSTEM; addr.ss_sysaddr = AF_SYS_CONTROL; addr.sc_id = info.ctl_id; addr.sc_unit = unit + 1; // sc_unit is 1-based (utun0 = unit 1) if (connect(fd_, reinterpret_cast(&addr), sizeof(addr)) == 0) { connected = true; break; } } if (!connected) { SPDLOG_ERROR("DarwinTunDevice: failed to connect to any utun device"); close(fd_); fd_ = -1; return false; } // Get the assigned interface name char ifname[IFNAMSIZ] = {}; socklen_t ifname_len = sizeof(ifname); if (getsockopt( fd_, SYSPROTO_CONTROL, UTUN_OPT_IFNAME, ifname, &ifname_len) < 0) { SPDLOG_ERROR("DarwinTunDevice: getsockopt(UTUN_OPT_IFNAME) failed: {}", strerror(errno)); close(fd_); fd_ = -1; return false; } name_ = ifname; SPDLOG_INFO("DarwinTunDevice: opened {}", name_); return true; } void Close() { if (fd_ >= 0) { close(fd_); fd_ = -1; } } // For unit tests: inject a pre-created fd (e.g., from socketpair) bool OpenWithFd(int fd, const std::string& name) { Close(); fd_ = fd; name_ = name; return true; } [[nodiscard]] const std::string& GetName() const { return name_; } bool ConfigureIPv4(const std::string& addr, int mask) { // Use ifconfig to set IPv4 address const std::string cmd = "ifconfig " + name_ + " inet " + addr + "/" + std::to_string(mask) + " " + addr; SPDLOG_DEBUG("DarwinTunDevice: {}", cmd); return system(cmd.c_str()) == 0; // NOLINT(cert-env33-c) } bool ConfigureIPv6(const std::string& addr, int mask) { const std::string cmd = "ifconfig " + name_ + " inet6 " + addr + "/" + std::to_string(mask); SPDLOG_DEBUG("DarwinTunDevice: {}", cmd); return system(cmd.c_str()) == 0; // NOLINT(cert-env33-c) } void SetNonBlocking(bool enabled) { int flags = fcntl(fd_, F_GETFL, 0); if (flags < 0) { return; } if (enabled) { flags |= O_NONBLOCK; } else { flags &= ~O_NONBLOCK; } fcntl(fd_, F_SETFL, flags); } void SetMTU(int mtu) { const std::string cmd = "ifconfig " + name_ + " mtu " + std::to_string(mtu); system(cmd.c_str()); // NOLINT(cert-env33-c) } void BringUp() { const std::string cmd = "ifconfig " + name_ + " up"; system(cmd.c_str()); // NOLINT(cert-env33-c) } int Read(void* buffer, int size) { // macOS utun prepends a 4-byte protocol family header constexpr int kAfHeaderSize = 4; const int total_size = size + kAfHeaderSize; EnsureReadBuffer(total_size); const ssize_t n = ::read(fd_, read_buf_.get(), total_size); if (n <= kAfHeaderSize) { return 0; } const int payload_size = static_cast(n) - kAfHeaderSize; std::memcpy(buffer, read_buf_.get() + kAfHeaderSize, payload_size); return payload_size; } int Write(const void* data, int size) { // Determine address family from IP version nibble constexpr int kAfHeaderSize = 4; const auto* pkt = static_cast(data); const std::uint8_t version = (pkt[0] >> 4) & 0x0F; std::uint32_t af_header = 0; if (version == 4) { af_header = htonl(AF_INET); } else if (version == 6) { af_header = htonl(AF_INET6); } else { return 0; } const int total_size = kAfHeaderSize + size; EnsureWriteBuffer(total_size); std::memcpy(write_buf_.get(), &af_header, kAfHeaderSize); std::memcpy(write_buf_.get() + kAfHeaderSize, data, size); const ssize_t written = ::write(fd_, write_buf_.get(), total_size); if (written <= kAfHeaderSize) { return 0; } return static_cast(written) - kAfHeaderSize; } // cppcheck-suppress functionStatic void SetStopFlag(const std::atomic* /*running*/) {} private: void EnsureReadBuffer(int size) { if (read_buf_size_ < size) { read_buf_ = std::make_unique(size); read_buf_size_ = size; } } void EnsureWriteBuffer(int size) { if (write_buf_size_ < size) { write_buf_ = std::make_unique(size); write_buf_size_ = size; } } int fd_; std::string name_; std::unique_ptr read_buf_; int read_buf_size_ = 0; std::unique_ptr write_buf_; int write_buf_size_ = 0; }; } // namespace fptn::common::network ================================================ FILE: src/common/network/tun/linux_tun_device.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Pavel Shpilev Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include // NOLINT(build/include_order) namespace fptn::common::network { class LinuxTunDevice { public: bool Open(const std::string& name) { tun_ = std::make_unique(); tun_->name(name); name_ = tun_->name(); return true; } void Close() { tun_.reset(); } [[nodiscard]] const std::string& GetName() const { return name_; } bool ConfigureIPv4(const std::string& addr, int mask) { tun_->ip(addr, mask); return true; } bool ConfigureIPv6(const std::string& addr, int mask) { tun_->ip(addr, mask); return true; } void SetNonBlocking(bool enabled) { tun_->nonblocking(enabled); } void SetMTU(int mtu) { tun_->mtu(mtu); } void BringUp() { tun_->up(); } int Read(void* buffer, int size) { return tun_->read(buffer, static_cast(size)); } int Write(const void* data, int size) { return tun_->write(const_cast(data), static_cast(size)); } // cppcheck-suppress functionStatic void SetStopFlag(const std::atomic* /*running*/) {} private: std::unique_ptr tun_; std::string name_; }; } // namespace fptn::common::network ================================================ FILE: src/common/network/tun/win_tun_device.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Pavel Shpilev Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include // clang-format off #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) // clang-format on #include // NOLINT(build/include_order) #include "common/network/ip_address.h" namespace fptn::common::network { class WinTunDevice { public: WinTunDevice() : wintun_(nullptr), adapter_(nullptr), session_(nullptr), running_(nullptr) { wintun_ = InitializeWintun(); UuidCreate(&guid_); } ~WinTunDevice() { Close(); } WinTunDevice(const WinTunDevice&) = delete; WinTunDevice& operator=(const WinTunDevice&) = delete; bool Open(const std::string& name) { if (!wintun_) { return false; } SPDLOG_INFO("WINTUN: {} version loaded", ParseWinTunVersion(WintunGetRunningDriverVersion())); name_ = name; const std::wstring interface_name = ToWString(name); adapter_ = WintunCreateAdapter( interface_name.c_str(), interface_name.c_str(), &guid_); if (!adapter_) { SPDLOG_ERROR("Network adapter wasn't created!"); return false; } return true; } void Close() { if (session_) { WintunEndSession(session_); session_ = nullptr; } if (adapter_) { WintunCloseAdapter(adapter_); adapter_ = nullptr; } if (wintun_) { WintunDeleteDriver(); wintun_ = nullptr; } } [[nodiscard]] const std::string& GetName() const { return name_; } bool ConfigureIPv4(const std::string& addr, int mask) { return SetIPAddressEntry(AF_INET, addr, mask); } bool ConfigureIPv6(const std::string& addr, int mask) { return SetIPAddressEntry(AF_INET6, addr, mask); } // cppcheck-suppress functionStatic void SetNonBlocking(bool /*enabled*/) { // Wintun uses event-based I/O, no-op } // cppcheck-suppress functionStatic void SetMTU(int /*mtu*/) { // Wintun handles MTU internally, no-op } void BringUp() { constexpr int kSessionCapacity = 0x20000; session_ = WintunStartSession(adapter_, kSessionCapacity); if (!session_) { SPDLOG_ERROR("Open session error"); } } int Read(void* buffer, int size) { if (!session_) { return 0; } constexpr std::size_t kRetryAmount = 20; while (running_ && *running_) { for (std::size_t i = 0; i < kRetryAmount; ++i) { DWORD packet_size = 0; BYTE* packet = WintunReceivePacket(session_, &packet_size); if (packet && running_ && *running_) { const int copy_size = (static_cast(packet_size) < size) ? static_cast(packet_size) : size; std::memcpy(buffer, packet, copy_size); WintunReleaseReceivePacket(session_, packet); return copy_size; } if (GetLastError() == ERROR_NO_MORE_ITEMS) { continue; } return 0; } WaitForSingleObject(WintunGetReadWaitEvent(session_), 10); } return 0; } int Write(const void* data, int size) { if (!session_ || !data || size <= 0) { return 0; } BYTE* send_buffer = WintunAllocateSendPacket(session_, static_cast(size)); if (!send_buffer) { return 0; } std::memcpy(send_buffer, data, size); WintunSendPacket(session_, send_buffer); return size; } void SetStopFlag(const std::atomic* running) { running_ = running; } private: bool SetIPAddressEntry(int family, const std::string& addr, int mask) { MIB_UNICASTIPADDRESS_ROW address_row; InitializeUnicastIpAddressEntry(&address_row); WintunGetAdapterLUID(adapter_, &address_row.InterfaceLuid); if (family == AF_INET) { address_row.Address.Ipv4.sin_family = AF_INET; if (1 != inet_pton(AF_INET, addr.c_str(), &(address_row.Address.Ipv4.sin_addr))) { SPDLOG_ERROR("Wrong IPv4 address"); return false; } } else { address_row.Address.Ipv6.sin6_family = AF_INET6; if (1 != inet_pton(AF_INET6, addr.c_str(), &(address_row.Address.Ipv6.sin6_addr))) { SPDLOG_ERROR("Wrong IPv6 address"); return false; } } address_row.OnLinkPrefixLength = static_cast(mask); address_row.DadState = IpDadStatePreferred; const auto res = CreateUnicastIpAddressEntry(&address_row); if (res != ERROR_SUCCESS && res != ERROR_OBJECT_ALREADY_EXISTS) { SPDLOG_ERROR("Failed to set {} address", addr); return false; } return true; } static std::wstring ToWString(const std::string& s) { return {s.begin(), s.end()}; } static std::string ParseWinTunVersion(DWORD version_number) { return std::to_string((version_number >> 16) & 0xff) + "." + std::to_string((version_number >> 0) & 0xff); } HMODULE InitializeWintun() { HMODULE wintun_lib = LoadLibraryExW(L"wintun.dll", nullptr, LOAD_LIBRARY_SEARCH_APPLICATION_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32); if (!wintun_lib) { SPDLOG_ERROR("WINTUN NOT FOUND!"); return nullptr; } #define X(Name) \ ((*(reinterpret_cast(&Name)) = \ GetProcAddress(wintun_lib, #Name)) == nullptr) if (X(WintunCreateAdapter) || X(WintunCloseAdapter) || X(WintunOpenAdapter) || X(WintunGetAdapterLUID) || X(WintunGetRunningDriverVersion) || X(WintunDeleteDriver) || X(WintunSetLogger) || X(WintunStartSession) || X(WintunEndSession) || X(WintunGetReadWaitEvent) || X(WintunReceivePacket) || X(WintunReleaseReceivePacket) || X(WintunAllocateSendPacket) || X(WintunSendPacket)) { DWORD last_error = GetLastError(); FreeLibrary(wintun_lib); SetLastError(last_error); SPDLOG_ERROR("Error whilst loading the lib: {}", last_error); return nullptr; } #undef X SPDLOG_INFO("Wintun initialization successful"); return wintun_lib; } // Wintun function pointers WINTUN_CREATE_ADAPTER_FUNC* WintunCreateAdapter = nullptr; WINTUN_CLOSE_ADAPTER_FUNC* WintunCloseAdapter = nullptr; WINTUN_OPEN_ADAPTER_FUNC* WintunOpenAdapter = nullptr; WINTUN_GET_ADAPTER_LUID_FUNC* WintunGetAdapterLUID = nullptr; WINTUN_GET_RUNNING_DRIVER_VERSION_FUNC* WintunGetRunningDriverVersion = nullptr; WINTUN_DELETE_DRIVER_FUNC* WintunDeleteDriver = nullptr; WINTUN_SET_LOGGER_FUNC* WintunSetLogger = nullptr; WINTUN_START_SESSION_FUNC* WintunStartSession = nullptr; WINTUN_END_SESSION_FUNC* WintunEndSession = nullptr; WINTUN_GET_READ_WAIT_EVENT_FUNC* WintunGetReadWaitEvent = nullptr; WINTUN_RECEIVE_PACKET_FUNC* WintunReceivePacket = nullptr; WINTUN_RELEASE_RECEIVE_PACKET_FUNC* WintunReleaseReceivePacket = nullptr; WINTUN_ALLOCATE_SEND_PACKET_FUNC* WintunAllocateSendPacket = nullptr; WINTUN_SEND_PACKET_FUNC* WintunSendPacket = nullptr; GUID guid_; HMODULE wintun_; WINTUN_ADAPTER_HANDLE adapter_; WINTUN_SESSION_HANDLE session_; std::string name_; const std::atomic* running_; }; } // namespace fptn::common::network ================================================ FILE: src/common/network/utils.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #ifndef __ANDROID__ #include "common/system/command.h" #endif namespace fptn::common::network { #ifndef __ANDROID__ inline std::vector GetServerIpAddresses() { std::vector cmd_stdout; fptn::common::system::command::run( "ip -o addr show | awk '{print $4}' | cut -d'/' -f1", cmd_stdout); return cmd_stdout; } #endif inline void CleanSocket(boost::asio::ip::tcp::socket& socket) { try { while (socket.available() != 0) { boost::system::error_code ec; std::array buffer{}; const std::size_t bytes = socket.read_some(boost::asio::buffer(buffer), ec); (void)bytes; if (ec == boost::asio::error::eof) { break; } if (ec) { SPDLOG_ERROR("Socket error: {}", ec.message()); break; } } } catch (const std::exception& e) { SPDLOG_ERROR("Exception: {}", e.what()); } } inline bool CleanSsl(const SSL* ssl) { if (ssl == nullptr) { return false; } if (BIO* rb = SSL_get_rbio(ssl)) { BIO_flush(rb); char buf[4096] = {}; while (BIO_pending(rb) > 0) { BIO_read(rb, buf, sizeof(buf)); } } return true; } inline bool IsServerHelloComplete(const std::vector& data) { if (data.size() < 5) { SPDLOG_INFO( "IsServerHelloComplete: data too small ({} bytes)", data.size()); return false; } std::size_t pos = 0; bool found_server_hello = false; bool is_tls13 = false; bool handshake_done = false; while (pos + 5 <= data.size()) { const std::uint8_t content_type = data[pos]; const std::uint16_t record_len = (data[pos + 3] << 8) | data[pos + 4]; if (pos + 5 + record_len > data.size()) { return false; } if (content_type == 22) { // Handshake std::size_t hpos = pos + 5; std::size_t hend = hpos + record_len; while (hpos + 4 <= hend) { const std::uint8_t msg_type = data[hpos]; const std::uint32_t msg_len = (data[hpos + 1] << 16) | (data[hpos + 2] << 8) | data[hpos + 3]; if (hpos + 4 + msg_len > hend) { SPDLOG_INFO( "IsServerHelloComplete: incomplete handshake message at pos {}", hpos); return false; } if (msg_type == 2) { // ServerHello found_server_hello = true; if (hpos + 4 + 2 <= data.size()) { const std::uint16_t version = (data[hpos + 4 + 2] << 8) | data[hpos + 4 + 3]; is_tls13 = (version != 0x0303); SPDLOG_INFO( "IsServerHelloComplete: ServerHello found, TLS version " "0x{:04x}, is_tls13={}", version, is_tls13); } else { SPDLOG_INFO( "IsServerHelloComplete: ServerHello found but version field " "missing"); } } // TLS 1.2: ServerHelloDone if (found_server_hello && !is_tls13 && msg_type == 14) { handshake_done = true; SPDLOG_INFO( "IsServerHelloComplete: ServerHelloDone (TLS 1.2) detected, " "handshake complete"); } // TLS 1.3: Finished if (found_server_hello && is_tls13 && msg_type == 20) { handshake_done = true; SPDLOG_INFO( "IsServerHelloComplete: Finished (TLS 1.3) detected, handshake " "complete"); } hpos += 4 + msg_len; } } // TLS 1.3: Application Data (23) или ChangeCipherSpec (20) // Finished if (found_server_hello && is_tls13 && !handshake_done && (content_type == 20 || content_type == 23)) { if (pos + 5 + record_len >= data.size()) { handshake_done = true; SPDLOG_INFO( "IsServerHelloComplete: TLS 1.3 {} record ends the handshake", content_type == 20 ? "ChangeCipherSpec" : "ApplicationData"); } } pos += 5 + record_len; } if (found_server_hello && handshake_done) { SPDLOG_INFO("IsServerHelloComplete: handshake complete, total size={}", data.size()); } else if (found_server_hello && !handshake_done) { SPDLOG_INFO( "IsServerHelloComplete: ServerHello found but handshake not yet done, " "size={}", data.size()); } else { SPDLOG_INFO( "IsServerHelloComplete: ServerHello not found, size={}", data.size()); } return found_server_hello && handshake_done; } inline bool IsClientHelloComplete(const std::vector& data) { if (data.size() < 5) { return false; } std::size_t pos = 0; while (pos + 5 <= data.size()) { const std::uint8_t content_type = data[pos]; const std::uint16_t record_length = (data[pos + 3] << 8) | data[pos + 4]; if (pos + 5 + record_length > data.size()) { return false; } if (content_type == 22) { // Handshake std::size_t handshake_pos = pos + 5; std::size_t handshake_end = handshake_pos + record_length; while (handshake_pos + 4 <= handshake_end) { const std::uint8_t msg_type = data[handshake_pos]; const std::uint32_t msg_length = (data[handshake_pos + 1] << 16) | (data[handshake_pos + 2] << 8) | data[handshake_pos + 3]; if (handshake_pos + 4 + msg_length > handshake_end) { return false; } if (msg_type == 1) { // ClientHello return true; } handshake_pos += 4 + msg_length; } } pos += 5 + record_length; } return false; } using TlsData = std::optional>; inline TlsData WaitForServerTlsHello(boost::asio::ip::tcp::socket& socket, const std::chrono::milliseconds drain_timeout = std::chrono::milliseconds( 5000)) { std::vector data; data.reserve(65536); const auto start_time = std::chrono::steady_clock::now(); try { boost::system::error_code ec; std::array buffer{}; while (std::chrono::steady_clock::now() - start_time < drain_timeout) { if (socket.available() == 0) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); continue; } const std::size_t bytes = socket.read_some(boost::asio::buffer(buffer), ec); if (bytes) { data.insert(data.end(), buffer.begin(), buffer.begin() + bytes); if (IsServerHelloComplete(data)) { return data; } } if (ec == boost::asio::error::eof) { break; } if (ec) { SPDLOG_ERROR("Socket error: {}", ec.message()); break; } } SPDLOG_WARN( "Timeout waiting for server hello, total data: {} bytes", data.size()); } catch (const std::exception& e) { SPDLOG_ERROR("Exception: {}", e.what()); } return std::nullopt; } inline boost::asio::awaitable WaitForServerTlsHelloAsync( boost::asio::ip::tcp::socket& socket, const std::chrono::milliseconds drain_timeout = std::chrono::milliseconds( 5000)) { std::vector data; data.reserve(65536); try { boost::system::error_code ec; std::array buffer{}; const auto start_time = std::chrono::steady_clock::now(); int packet_count = 0; while (std::chrono::steady_clock::now() - start_time < drain_timeout) { if (socket.available() == 0) { boost::asio::steady_timer timer( co_await boost::asio::this_coro::executor, std::chrono::milliseconds(100)); co_await timer.async_wait(boost::asio::use_awaitable); continue; } const std::size_t bytes = co_await socket.async_read_some(boost::asio::buffer(buffer), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); packet_count++; if (bytes) { data.insert(data.end(), buffer.begin(), buffer.begin() + bytes); if (IsServerHelloComplete(data)) { SPDLOG_INFO( "Server hello complete after {} packets, total size: {} bytes", packet_count, data.size()); co_return data; } } if (ec == boost::asio::error::eof) { SPDLOG_INFO( "Connection closed by server, total data: {} bytes", data.size()); break; } if (ec) { SPDLOG_ERROR("Socket error: {}", ec.message()); break; } } SPDLOG_WARN( "Timeout waiting for server hello, total data: {} bytes", data.size()); } catch (const std::exception& e) { SPDLOG_ERROR("Exception: {}", e.what()); } co_return std::nullopt; } inline boost::asio::awaitable WaitForClientChangeCipherSpec( boost::asio::ip::tcp::socket& socket, const std::chrono::milliseconds drain_timeout = std::chrono::milliseconds( 5000)) { const auto target_ccs = protocol::https::utils::MakeClientChangeCipherSpec(); const std::size_t target_size = target_ccs.size(); std::vector buffer(target_size); try { boost::system::error_code ec; const auto start_time = std::chrono::steady_clock::now(); std::size_t total_read = 0; while (std::chrono::steady_clock::now() - start_time < drain_timeout) { if (socket.available() == 0) { boost::asio::steady_timer timer( co_await boost::asio::this_coro::executor, std::chrono::milliseconds(100)); co_await timer.async_wait(boost::asio::use_awaitable); continue; } const std::size_t bytes_read = co_await socket.async_read_some( boost::asio::buffer( buffer.data() + total_read, target_size - total_read), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); total_read += bytes_read; if (total_read == target_size) { co_return buffer == target_ccs; } if (ec == boost::asio::error::eof) { break; } if (ec) { SPDLOG_ERROR("Socket error during drain: {}", ec.message()); break; } } } catch (const std::exception& e) { SPDLOG_ERROR("Exception during socket drain: {}", e.what()); } co_return false; } inline boost::asio::awaitable WaitForClientTlsHelloAsync( boost::asio::ip::tcp::socket& socket, const std::chrono::milliseconds drain_timeout = std::chrono::milliseconds( 5000)) { std::vector data; data.reserve(65536); try { boost::system::error_code ec; std::array buffer{}; const auto start_time = std::chrono::steady_clock::now(); while (std::chrono::steady_clock::now() - start_time < drain_timeout) { if (socket.available() == 0) { boost::asio::steady_timer timer( co_await boost::asio::this_coro::executor, std::chrono::milliseconds(100)); co_await timer.async_wait(boost::asio::use_awaitable); continue; } const std::size_t bytes = co_await socket.async_read_some(boost::asio::buffer(buffer), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (bytes) { data.insert(data.end(), buffer.begin(), buffer.begin() + bytes); if (IsClientHelloComplete(data)) { co_return data; } } if (ec == boost::asio::error::eof) { break; } if (ec) { SPDLOG_ERROR("Socket error during drain: {}", ec.message()); break; } } } catch (const std::exception& e) { SPDLOG_ERROR("Exception during socket drain: {}", e.what()); } co_return std::nullopt; } } // namespace fptn::common::network ================================================ FILE: src/common/system/command.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #if _WIN32 #include // NOLINT(build/include_order) #endif #include #include #include #include #include #include #include // NOLINT(build/include_order) #if _WIN32 #include #endif namespace fptn::common::system::command { inline bool run(const std::string& command) { try { SPDLOG_INFO("Running: {}", command); #ifdef _WIN32 boost::process::v1::child child(command, boost::process::v1::std_out > stdout, boost::process::v1::std_err > stderr, ::boost::process::v1::windows::hide); #elif defined(__linux__) || defined(__APPLE__) boost::process::v1::child child(command, boost::process::v1::std_out > stdout, boost::process::v1::std_err > stderr); #endif child.wait(); return child.exit_code() == 0; } catch (const std::exception& e) { const std::string msg = e.what(); SPDLOG_ERROR("Command error: {} CMD: '{}' ", msg, command); } catch (...) { SPDLOG_ERROR("Command error: undefined error CMD: '{}' ", command); } return false; } inline bool run( const std::string& command, std::vector& std_output) { try { boost::process::v1::ipstream pipe; #ifdef _WIN32 boost::process::v1::child child(command, boost::process::v1::std_out > pipe, ::boost::process::v1::windows::hide); #elif defined(__linux__) || defined(__APPLE__) boost::process::v1::child child(boost::process::v1::search_path("bash"), "-c", command, boost::process::v1::std_out > pipe); #endif std::string line; while (std::getline(pipe, line)) { std_output.emplace_back(line); } child.wait(); return child.exit_code() == 0; } catch (const std::exception& ex) { SPDLOG_ERROR( "Error: failed to run command '{}'. Error: {}", command, ex.what()); } return false; } } // namespace fptn::common::system::command ================================================ FILE: src/common/user/common_user_manager.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) namespace fptn::common::user { class CommonUserManager final { public: explicit CommonUserManager(std::string file_path) : file_path_(std::move(file_path)) { CreateFileIfNotExists(file_path_); LoadUsers(); } bool AddUser( const std::string& username, const std::string& password, int bandwidth) { const std::scoped_lock lock(mutex_); // mutex if (!ValidateUsername(username)) { std::cerr << "Invalid username." << std::endl; return false; } if (bandwidth < 0) { std::cerr << "Invalid bandwidth value. It should be a positive number." << std::endl; return false; } if (users_.find(username) != users_.end()) { std::cout << "User " << username << " already exists." << std::endl; return false; } std::string hash = HashPassword(password); users_[username] = {hash, bandwidth}; SaveUsers(); std::cout << "User " << username << " added with bandwidth " << bandwidth << " MB." << std::endl; return true; } bool DeleteUser(const std::string& username) { const std::scoped_lock lock(mutex_); // mutex if (users_.find(username) == users_.end()) { return false; } users_.erase(username); SaveUsers(); return true; } void ListUsers() const { const std::scoped_lock lock(mutex_); // mutex for (const auto& user_entry : users_) { const auto& username = user_entry.first; const auto& [hash_password, max_speed] = user_entry.second; std::cout << username << " " << std::string(hash_password.length(), 'X') << " " << max_speed << " MB" << std::endl; } } bool Authenticate(const std::string& username, const std::string& password) { const std::scoped_lock lock(mutex_); // mutex LoadUsers(); auto it = users_.find(username); if (it != users_.end()) { std::string hash = HashPassword(password); return it->second.first == hash; } return false; } int GetUserBandwidthBit(const std::string& username) const { const std::scoped_lock lock(mutex_); // mutex auto it = users_.find(username); if (it != users_.end()) { return it->second.second * 1024 * 1024; } return 0; } int GetUserBandwidth(const std::string& username) const { const std::scoped_lock lock(mutex_); // mutex auto it = users_.find(username); if (it != users_.end()) { return it->second.second; } return 0; } protected: void LoadUsers() { // FIXIT // update users_.clear(); std::ifstream file(file_path_); if (file.is_open()) { std::string line; while (std::getline(file, line)) { std::string username; std::string passwordHash; int bandwidth; std::istringstream iss(line); if (iss >> username >> passwordHash >> bandwidth) { users_[username] = {passwordHash, bandwidth}; } else { std::cerr << "Skipping invalid line: " << line << std::endl; } } } else { std::cerr << "Unable to open file: " << file_path_ << std::endl; } } void SaveUsers() const { std::ofstream file(file_path_); if (file.is_open()) { for (const auto& user_entry : users_) { const auto& username = user_entry.first; const auto& [hash_password, max_speed] = user_entry.second; // NOLINT file << username << " " << hash_password << " " << max_speed << "\n"; } } else { std::cerr << "Unable to open file: " << file_path_ << std::endl; } } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) std::string HashPassword(const std::string& password) const { unsigned int length = 0; unsigned char hash[EVP_MAX_MD_SIZE] = {0}; EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); if (!mdctx) { std::cerr << "Failed to create EVP_MD_CTX" << std::endl; return ""; } if (1 != EVP_DigestInit_ex(mdctx, EVP_sha256(), nullptr)) { std::cerr << "Failed to initialize digest" << std::endl; EVP_MD_CTX_free(mdctx); return ""; } if (1 != EVP_DigestUpdate(mdctx, password.c_str(), password.size())) { std::cerr << "Failed to update digest" << std::endl; EVP_MD_CTX_free(mdctx); return ""; } if (1 != EVP_DigestFinal_ex(mdctx, hash, &length)) { std::cerr << "Failed to finalize digest" << std::endl; EVP_MD_CTX_free(mdctx); return ""; } EVP_MD_CTX_free(mdctx); std::ostringstream oss; for (unsigned int i = 0; i < length; ++i) { oss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return oss.str(); } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) bool ValidateUsername(const std::string& username) const { return !username.empty() && std::all_of(username.begin(), username.end(), ::isalnum); } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) void CreateFileIfNotExists(const std::string& file_path) { const std::filesystem::path path(file_path); const std::filesystem::path directory_path = path.parent_path(); if (!directory_path.empty() && !std::filesystem::exists(directory_path)) { std::error_code ec; if (!std::filesystem::create_directories(directory_path, ec)) { std::cerr << "Failed to create directories: " << ec.message() << std::endl; return; } } if (!std::filesystem::exists(file_path)) { std::ofstream file(file_path); if (!file.is_open()) { std::cerr << "Failed to create file: " << file_path << std::endl; } } } private: std::unordered_map> users_; mutable std::mutex mutex_; std::string file_path_; }; using CommonUserManagerPtr = std::unique_ptr; } // namespace fptn::common::user ================================================ FILE: src/common/utils/base64.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include namespace fptn::common::utils::base64 { inline std::string decode(const std::string& s) { // If the input string's length is not a multiple of 4, // it appends '=' to make the length valid for base64 decoding. std::string additional; if (s.size() % 4 != 0) { const std::size_t padding = 4 - (s.size() % 4); for (std::size_t i = 0; i < padding; i++) { additional += "="; } } return ::base64::from_base64(s + additional); } } // namespace fptn::common::utils::base64 ================================================ FILE: src/common/utils/utils.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include namespace fptn::common::utils { inline void GenerateRandomBytes(std::uint8_t* buffer, std::size_t length) { thread_local std::mt19937 gen{std::random_device {}()}; std::uniform_int_distribution dist; std::size_t i = 0; for (; i + 8 <= length; i += 8) { const std::uint64_t rand_val = dist(gen); std::memcpy(buffer + i, &rand_val, 8); } if (i < length) { const std::uint64_t rand_val = dist(gen); std::memcpy(buffer + i, &rand_val, length - i); } } inline std::string GenerateRandomString(const int length) { static constexpr char kCharacters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; static constexpr std::size_t kCharCount = sizeof(kCharacters) - 1; thread_local std::mt19937 gen{std::random_device {}()}; thread_local std::uniform_int_distribution dist( 0, kCharCount - 1); std::string result(length, '\0'); for (int i = 0; i < length; ++i) { result[i] = kCharacters[dist(gen)]; } return result; } inline std::string RemoveSubstring( std::string input, const std::vector& strs) { for (const auto& substr : strs) { boost::algorithm::erase_all(input, substr); } return input; } inline std::string Trim(const std::string& str) { auto is_space = [](const unsigned char c) { return std::isspace(c); }; const auto start = std::ranges::find_if_not(str, is_space); const auto end = std::ranges::find_if_not(str | std::views::reverse, is_space).base(); return (start < end) ? std::string(start, end) : std::string(); } inline std::vector SplitCommaSeparated(const std::string& input) { std::vector result; std::stringstream ss(input); std::string item; while (std::getline(ss, item, ',')) { const std::string trimmed = Trim(item); if (!trimmed.empty()) { result.push_back(trimmed); } } return result; } inline std::string ToLowerCase(const std::string& str) { try { boost::locale::generator gen; std::locale loc = gen(""); return boost::locale::to_lower(str, loc); } catch (...) { return str; } return str; } } // namespace fptn::common::utils ================================================ FILE: src/fptn-client/CMakeLists.txt ================================================ project(fptn-client) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) if(APPLE OR UNIX) set(TUNTAP_LIB tuntap++) else() set(TUNTAP_LIB Wintun rpcrt4 iphlpapi Kernel32.lib) endif() find_package(OpenSSL REQUIRED) find_package(argparse REQUIRED) find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(PcapPlusPlus REQUIRED) find_package(Boost REQUIRED COMPONENTS random filesystem process locale) find_package(ZLIB REQUIRED) find_package(httplib REQUIRED) find_package(libidn2 REQUIRED) find_package(re2 REQUIRED) find_package(nlohmann_json REQUIRED) include_directories(${nlohmann_json_INCLUDE_DIRS}) ## brotli find_package(brotli REQUIRED) find_path(BROTLI_INCLUDE_DIR "brotli/decode.h" REQUIRED) include_directories(${BROTLI_INCLUDE_DIR}) find_library(BROTLI_DEC_LIB NAMES brotlidec REQUIRED) find_library(BROTLI_COMMON_LIB NAMES brotlicommon REQUIRED) find_library(BROTLI_ENC_LIB NAMES brotlienc REQUIRED) # fptn-client-cli add_executable( "${PROJECT_NAME}-cli" fptn-client-cli.cpp vpn/vpn_client.h vpn/vpn_client.cpp vpn/http/client.h vpn/http/client.cpp routing/route_manager.h routing/route_manager.cpp config/config_file.cpp config/config_file.h plugins/split/tunneling.h plugins/split/tunneling.cpp plugins/blacklist/domain_blacklist.h plugins/blacklist/domain_blacklist.cpp utils/speed_estimator/speed_estimator.cpp utils/speed_estimator/speed_estimator.h utils/speed_estimator/server_info.h) target_link_libraries( "${PROJECT_NAME}-cli" PRIVATE ZLIB::ZLIB Boost::boost Boost::random Boost::filesystem Boost::process Boost::locale OpenSSL::SSL OpenSSL::Crypto argparse::argparse nlohmann_json::nlohmann_json spdlog::spdlog fmt::fmt PcapPlusPlus::PcapPlusPlus libidn2::libidn2 re2::re2 fptn-protocol-lib_static "${TUNTAP_LIB}" "${BROTLI_DEC_LIB}" "${BROTLI_COMMON_LIB}") # fptn-client-gui if("${FPTN_BUILD_WITH_GUI_CLIENT}") # --- disable clang-tidy for QT generated code --- set_source_files_properties($ PROPERTIES SKIP_CLANG_TIDY ON) set(DISABLE_TIDY_DIR "${CMAKE_BINARY_DIR}/src/fptn-client") file(MAKE_DIRECTORY "${DISABLE_TIDY_DIR}") file( WRITE "${DISABLE_TIDY_DIR}/.clang-tidy" "Checks: '-*,readability-inconsistent-declaration-parameter-name' WarningsAsErrors: '' ") find_package( Qt6 REQUIRED COMPONENTS Core Gui Widgets Network LinguistTools Concurrent CONFIG REQUIRED) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_INCLUDE_CURRENT_DIR ON) qt_standard_project_setup() set(QRC_FILES gui/resources/resources.qrc) qt6_add_resources(PROJECT_RESOURCES ${QRC_FILES}) set(QT_ADDITIONAL_PARAMS "") if(WIN32) set(QT_ADDITIONAL_PARAMS WIN32) endif() set(PLATFORM_SPECIFIC_LIBS "") if(APPLE) set(PLATFORM_SPECIFIC_LIBS "-framework Security") endif() # translations file(GLOB TRANSLATIONS gui/resources/translations/*.ts) set_source_files_properties(${TRANSLATIONS} PROPERTIES OUTPUT_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/gui/resources/translations/") qt6_add_translation(TRANSLATIONS_OUT ${TRANSLATIONS}) add_executable( "${PROJECT_NAME}-gui" ${QT_ADDITIONAL_PARAMS} fptn-client-gui.cpp vpn/vpn_client.h vpn/vpn_client.cpp vpn/http/client.h vpn/http/client.cpp routing/route_manager.h routing/route_manager.cpp config/config_file.cpp config/config_file.h gui/tray/tray.h gui/tray/tray.cpp gui/speedwidget/speedwidget.h gui/speedwidget/speedwidget.cpp gui/settingswidget/settings.h gui/settingswidget/settings.cpp gui/settingsmodel/settingsmodel.h gui/settingsmodel/settingsmodel.cpp gui/tokendialog/tokendialog.h gui/tokendialog/tokendialog.cpp gui/translations/translations.h gui/translations/translations.cpp gui/sni_manager/sni_manager.h gui/sni_manager/sni_manager.cpp gui/sni_autoscan_dialog/sni_autoscan_dialog.h gui/sni_autoscan_dialog/sni_autoscan_dialog.cpp gui/server_menu_item_widget/server_menu_item_widget.h gui/server_menu_item_widget/server_menu_item_widget.cpp gui/style/style.h gui/style/style.cpp gui/autostart/autostart.h gui/autoupdate/autoupdate.h plugins/split/tunneling.h plugins/split/tunneling.cpp plugins/blacklist/domain_blacklist.h plugins/blacklist/domain_blacklist.cpp utils/windows/vpn_conflict.h utils/macos/admin.h utils/speed_estimator/speed_estimator.cpp utils/speed_estimator/speed_estimator.h utils/speed_estimator/server_info.h ${PROJECT_RESOURCES} ${TRANSLATIONS_OUT}) target_link_libraries( "${PROJECT_NAME}-gui" PRIVATE ZLIB::ZLIB Boost::boost Boost::random Boost::filesystem Boost::process Boost::locale OpenSSL::SSL OpenSSL::Crypto argparse::argparse spdlog::spdlog fmt::fmt PcapPlusPlus::PcapPlusPlus Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Network Qt6::Concurrent libidn2::libidn2 re2::re2 fptn-protocol-lib_static httplib::httplib "${TUNTAP_LIB}" "${BROTLI_DEC_LIB}" "${BROTLI_COMMON_LIB}" "${PLATFORM_SPECIFIC_LIBS}") set_target_properties("${PROJECT_NAME}-gui" PROPERTIES AUTOMOC ON) endif() ================================================ FILE: src/fptn-client/config/config_file.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "config/config_file.h" #include #include #include #include #include "common/utils/base64.h" #include "common/utils/utils.h" #include "utils/brotli/brotli.h" using fptn::utils::speed_estimator::ServerInfo; namespace fptn::config { ConfigFile::ConfigFile(std::string sni, fptn::protocol::https::CensorshipStrategy censorship_strategy) : sni_(std::move(sni)), censorship_strategy_(censorship_strategy), version_(0) {} // NOLINT(whitespace/indent_namespace) ConfigFile::ConfigFile(std::string token, std::string sni, fptn::protocol::https::CensorshipStrategy censorship_strategy) : token_(std::move(token)), sni_(std::move(sni)), censorship_strategy_(censorship_strategy), version_(0) {} // NOLINT(whitespace/indent_namespace) bool ConfigFile::AddServer(const ServerInfo& s) { servers_.push_back(s); return true; } std::optional ConfigFile::GetServer( const std::string& server_name) const { const std::string prepared_server_name = fptn::common::utils::Trim(fptn::common::utils::ToLowerCase(server_name)); if (server_name.empty()) { return std::nullopt; } auto it = std::ranges::find_if( servers_, [&prepared_server_name]( const fptn::utils::speed_estimator::ServerInfo& server) { return fptn::common::utils::Trim(fptn::common::utils::ToLowerCase( server.name)) == prepared_server_name; }); if (it != servers_.end()) { return *it; } return std::nullopt; } bool ConfigFile::Parse() { try { if (token_.empty()) { throw std::runtime_error("Token is empty"); } std::string token_data; if (token_.starts_with("fptnb:") || token_.starts_with("fptnb//")) { // Brotli const std::string compressed_data = fptn::common::utils::base64::decode( fptn::common::utils::RemoveSubstring( token_, {"fptnb:", "fptnb//", " ", "\n", "\r", "\t", "="})); token_data = fptn::utils::brotli::Decompress(compressed_data); } else { const std::string sanitized_token = fptn::common::utils::RemoveSubstring( token_, {"fptn://", "fptn:", " ", "\n", "\r", "\t", "="}); token_data = fptn::common::utils::base64::decode(sanitized_token); } const auto config = nlohmann::json::parse(token_data); version_ = config.at("version").get(); service_name_ = config.at("service_name").get(); username_ = config.at("username").get(); password_ = config.at("password").get(); for (const auto& server : config.at("servers")) { ServerInfo s(server.at("name").get(), server.at("host").get(), server.at("port").get(), server.at("md5_fingerprint").get()); servers_.push_back(s); } if (servers_.empty()) { throw std::runtime_error("Server list is empty!"); } return true; } catch (const nlohmann::json::exception& e) { throw std::runtime_error(std::string("JSON parsing error: ") + e.what() + ". Try to update your token"); } catch (const std::exception& e) { throw std::runtime_error(std::string("Token parsing error: ") + e.what()); } } ServerInfo ConfigFile::FindFastestServer(int timeout_sec) const { return fptn::utils::speed_estimator::FindFastestServer( sni_, servers_, censorship_strategy_, timeout_sec); } std::uint64_t ConfigFile::GetDownloadTimeMs(const ServerInfo& server, const std::string& sni, int timeout, const std::string& md5_fingerprint) { return fptn::utils::speed_estimator::GetDownloadTimeMs( server, sni, timeout, md5_fingerprint, censorship_strategy_); } int ConfigFile::GetVersion() const noexcept { return version_; } const std::string& ConfigFile::GetServiceName() const noexcept { return service_name_; } const std::string& ConfigFile::GetUsername() const noexcept { return username_; } const std::string& ConfigFile::GetPassword() const noexcept { return password_; } const std::vector& ConfigFile::GetServers() const noexcept { return servers_; } } // namespace fptn::config ================================================ FILE: src/fptn-client/config/config_file.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include "fptn-client/utils/speed_estimator/server_info.h" #include "fptn-client/utils/speed_estimator/speed_estimator.h" #include "fptn-protocol-lib/https/censorship_strategy.h" namespace fptn::config { class ConfigFile final { public: explicit ConfigFile(std::string sni, fptn::protocol::https::CensorshipStrategy censorship_strategy); explicit ConfigFile(std::string token, std::string sni, fptn::protocol::https::CensorshipStrategy censorship_strategy); bool Parse(); fptn::utils::speed_estimator::ServerInfo FindFastestServer( int timeout_sec) const; std::uint64_t GetDownloadTimeMs( const fptn::utils::speed_estimator::ServerInfo& server, const std::string& sni, int timeout, const std::string& md5_fingerprint); bool AddServer(const fptn::utils::speed_estimator::ServerInfo& s); std::optional GetServer( const std::string& server_name) const; int GetVersion() const noexcept; const std::string& GetServiceName() const noexcept; const std::string& GetUsername() const noexcept; const std::string& GetPassword() const noexcept; const std::vector& GetServers() const noexcept; private: const std::string token_; const std::string sni_; const fptn::protocol::https::CensorshipStrategy censorship_strategy_; int version_; std::string service_name_; std::string username_; std::string password_; std::vector servers_; }; } // namespace fptn::config ================================================ FILE: src/fptn-client/fptn-client-cli.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #if defined(__linux__) || defined(__APPLE__) #include // NOLINT(build/include_order) #endif #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/logger/logger.h" #include "common/network/ip_address.h" #include "common/network/net_interface.h" #include "config/config_file.h" #include "fptn-protocol-lib/https/obfuscator/methods/detector.h" #include "fptn-protocol-lib/time/time_provider.h" #include "plugins/blacklist/domain_blacklist.h" #include "routing/route_manager.h" #include "utils/signal/main_loop.h" #include "vpn/vpn_client.h" int main(int argc, char* argv[]) { #if defined(__linux__) || defined(__APPLE__) if (geteuid() != 0) { std::cerr << "You must be root to run this program." << std::endl; return EXIT_FAILURE; } #endif try { const std::set bypass_methods = {"obfuscation", /* chrome */ "sni-reality-chrome147", "sni-reality-chrome146", "sni-reality-chrome145", /* Firefox */ "sni-reality-firefox149", /* Yandex */ "sni-reality-yandex26", "sni-reality-yandex25", "sni-reality-yandex24", /* Safari */ "sni-reality-safari26"}; const std::set tunnel_modes = {"exclude", "include"}; using fptn::protocol::https::obfuscator::GetObfuscatorByName; using fptn::protocol::https::obfuscator::GetObfuscatorNames; argparse::ArgumentParser args("fptn-client", FPTN_VERSION); // Required arguments args.add_argument("--access-token").required().help("Access token"); // Optional arguments args.add_argument("--out-network-interface") .default_value("") .help("Network out interface"); args.add_argument("--gateway-ip") .default_value("") .help("Your default gateway IPv4 address"); args.add_argument("--gateway-ipv6") .default_value("") .help("Your default gateway IPv6 address"); args.add_argument("--preferred-server") .default_value("") .help("Preferred server name (case-insensitive)"); args.add_argument("--tun-interface-name") .default_value("tun0") .help("Network interface name"); args.add_argument("--tun-interface-ip") .default_value(FPTN_CLIENT_DEFAULT_ADDRESS_IP4) .help("Network interface IPv4 address"); args.add_argument("--tun-interface-ipv6") .default_value(FPTN_CLIENT_DEFAULT_ADDRESS_IP6) .help("Network interface IPv6 address"); args.add_argument("--sni") .default_value(FPTN_DEFAULT_SNI) .help( "Domain name for SNI in TLS handshake (used to obfuscate VPN " "traffic)"); args.add_argument("--blacklist-domains") .default_value(FPTN_CLIENT_DEFAULT_BLACKLIST_DOMAINS) .help( "Completely block access to the main domain AND all its " "subdomains\n" "Format: domain:example.com,domain:sub.site.org\n" "Example: domain:ria.ru blocks ria.ru and all *.ria.ru sites"); // Method to bypass censorship args.add_argument("--bypass-method") .default_value("sni-reality-yandex25") .help( "Method to bypass censorship:\n" " obfuscation - TLS obfuscation\n" " sni-reality-chrome147 - SNI reality with Chrome 146 handshake\n" " sni-reality-chrome146 - SNI reality with Chrome 146 handshake\n" " sni-reality-chrome145 - SNI reality with Chrome 145 handshake\n" " sni-reality-firefox149 - SNI reality with Firefox 149 " "handshake\n" " sni-reality-yandex26 - SNI reality with Yandex 26 handshake\n" " sni-reality-yandex25 - SNI reality with Yandex 25 handshake\n" " sni-reality-yandex24 - SNI reality with Yandex 24 handshake\n" " sni-reality-safari26 - SNI reality with Safari 26 handshake\n") .action([&bypass_methods](const std::string& v) { if (!bypass_methods.contains(v)) { throw std::runtime_error( fmt::format("Invalid bypass method '{}'. Choose from: {}", v, fmt::join(bypass_methods, ", "))); } return v; }); // networks args.add_argument("--exclude-tunnel-networks") .default_value(FPTN_CLIENT_DEFAULT_EXCLUDE_NETWORKS) .help( "Networks that always bypass VPN tunnel\n" "Traffic to these networks goes directly, never through VPN\n" "Format: CIDR notation or IP addresses, comma-separated\n" "Example: 10.0.0.0/8,192.168.0.0/16"); args.add_argument("--include-tunnel-networks") .default_value("") .help( "Networks that always use VPN tunnel\n" "Traffic to these networks always goes through VPN\n" "Format: CIDR notation or IP addresses, comma-separated\n" "Example: 172.16.0.0/12,192.168.99.0/24"); // Split-tunneling arguments args.add_argument("--enable-split-tunnel") .help( "Enable split tunneling - allows different traffic routing for " "different sites.\n" "When enabled, you can configure which sites use VPN and which go" "directly.\n" "Use with --split-tunnel-mode and --split-tunnel-domains for " "configuration.") .default_value(false) .nargs(1) .action([](const std::string& value) { if (value.empty()) { return true; } if (fptn::common::utils::ToLowerCase(value) == "true") { return true; } if (fptn::common::utils::ToLowerCase(value) == "false") { return false; } throw std::runtime_error("Value must be true/false"); }); args.add_argument("--split-tunnel-mode") .default_value("exclude") .help( "Defines traffic routing strategy for split tunneling.\n" "Modes:\n" " exclude - Bypass VPN for specified domains, route all other " "traffic through VPN.\n" " include - Route only specified domains through VPN, bypass VPN " "for all other traffic.\n") .action([&tunnel_modes](const std::string& v) { if (!tunnel_modes.contains(v)) { throw std::runtime_error( fmt::format("Invalid tunnel mode '{}'. Choose from: {}", v, fmt::join(tunnel_modes, ", "))); } return v; }); args.add_argument("--split-tunnel-domains") .default_value(FPTN_CLIENT_DEFAULT_SPLIT_TUNNEL_DOMAINS) .help( "List websites that should either use or bypass VPN\n" "\n" "How it works:\n" " If --tunnel-mode=exclude: VPN skips these sites\n" " If --tunnel-mode=include: VPN only for these sites\n" "Format: domain:com,domain:another.com,domain:sub.domainname.com"); // parse cmd arguments try { args.parse_args(argc, argv); } catch (const std::runtime_error& err) { std::cerr << err.what() << std::endl; std::cerr << args; return EXIT_FAILURE; } if (fptn::logger::init("fptn-client-cli")) { SPDLOG_INFO("Application started successfully."); } else { std::cerr << "Logger initialization failed. Exiting application." << std::endl; return EXIT_FAILURE; } /* parse cmd args */ const auto out_network_interface_name = args.get("--out-network-interface"); const auto param_gateway_ip = args.get("--gateway-ip"); const auto gateway_ip = fptn::common::network::IPv4Address::Create(param_gateway_ip); const auto param_gateway_ipv6 = args.get("--gateway-ipv6"); const auto gateway_ipv6 = fptn::common::network::IPv6Address::Create(param_gateway_ipv6); const auto preferred_server = args.get("--preferred-server"); const auto tun_interface_name = args.get("--tun-interface-name"); const auto tun_interface_address_ipv4 = fptn::common::network::IPv4Address::Create( args.get("--tun-interface-ip")); const auto tun_interface_address_ipv6 = fptn::common::network::IPv6Address::Create( args.get("--tun-interface-ipv6")); const auto sni = args.get("--sni"); /* check gateway address */ const auto using_gateway_ip = gateway_ip.IsEmpty() ? fptn::routing::GetDefaultGatewayIPAddress() : fptn::common::network::IPv4Address::Create(gateway_ip); const auto using_gateway_ipv6 = gateway_ipv6.IsEmpty() ? fptn::routing::GetDefaultGatewayIPv6Address() : fptn::common::network::IPv6Address::Create(gateway_ipv6); if (using_gateway_ip.IsEmpty()) { SPDLOG_ERROR( "Unable to find the default gateway IP address. " "Please check your connection and make sure no other VPN is active. " "If the error persists, specify the gateway address in the FPTN " "settings using your router's IP " "address with the \"--gateway-ip\" option. If the issue " "remains unresolved, please contact the developer via Telegram " "@fptn_chat."); return EXIT_FAILURE; } const auto bypass_method = args.get("--bypass-method"); fptn::protocol::https::CensorshipStrategy censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex25; if (bypass_method == "obfuscation") { censorship_strategy = fptn::protocol::https::CensorshipStrategy::kTlsObfuscator; } /* Chrome */ else if (bypass_method == "sni-reality-chrome147") { SPDLOG_INFO("Using Chrome 147 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeChrome147; } else if (bypass_method == "sni-reality-chrome146") { SPDLOG_INFO("Using Chrome 146 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeChrome146; } else if (bypass_method == "sni-reality-chrome145") { SPDLOG_INFO("Using Chrome 144 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeChrome145; } /* Firefox */ else if (bypass_method == "sni-reality-firefox149") { SPDLOG_INFO("Using Firefox 149 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeFirefox149; } /* Yandex */ else if (bypass_method == "sni-reality-yandex26") { SPDLOG_INFO("Using Yandex 26 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex26; } else if (bypass_method == "sni-reality-yandex25") { SPDLOG_INFO("Using Yandex 25 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex25; } else if (bypass_method == "sni-reality-yandex24") { SPDLOG_INFO("Using Yandex 25 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex25; } /* Safari */ else if (bypass_method == "sni-reality-safari26") { SPDLOG_INFO("Using Safari 26 handshake for censorship bypass"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeSafari26; } /* parse network lists */ const auto exclude_networks_str = args.get("--exclude-tunnel-networks"); const auto include_networks_str = args.get("--include-tunnel-networks"); const std::vector exclude_networks = fptn::common::utils::SplitCommaSeparated(exclude_networks_str); const std::vector include_networks = fptn::common::utils::SplitCommaSeparated(include_networks_str); /* parse split-tunneling parameters */ const bool enable_split_tunnel = args.get("--enable-split-tunnel"); const auto tunnel_mode = args.get("--split-tunnel-mode"); const auto split_domains_str = args.get("--split-tunnel-domains"); const auto blacklist_domains_str = args.get("--blacklist-domains"); const std::vector split_domains = fptn::common::utils::SplitCommaSeparated(split_domains_str); const std::vector blacklist_domains = fptn::common::utils::SplitCommaSeparated(blacklist_domains_str); /* check config */ const auto access_token = args.get("--access-token"); fptn::config::ConfigFile config(access_token, sni, censorship_strategy); fptn::utils::speed_estimator::ServerInfo selected_server; try { config.Parse(); if (!preferred_server.empty()) { auto server_opt = config.GetServer(preferred_server); if (server_opt.has_value()) { selected_server = std::move(*server_opt); } else { SPDLOG_WARN("Server '{}' does not exist! Check your token!", preferred_server); selected_server = config.FindFastestServer(15); } } else { selected_server = config.FindFastestServer(15); } } catch (const std::runtime_error& err) { SPDLOG_ERROR("Config error: {}", err.what()); return EXIT_FAILURE; } const auto server_ip = fptn::routing::ResolveDomain(selected_server.host); if (server_ip.IsEmpty()) { SPDLOG_ERROR("DNS resolve error: {}", selected_server.host); return EXIT_FAILURE; } SPDLOG_INFO( "\n--- Starting client ---\n" "VERSION: {}\n" "SELECTED SERVER: {}\n" "SNI: {}\n" "GATEWAY IP: {}\n" "NETWORK INTERFACE: {}\n" "VPN SERVER NAME: {}\n" "VPN SERVER IP: {}\n" "VPN SERVER PORT: {}\n" "TUN INTERFACE IPv4: {}\n" "TUN INTERFACE IPv6: {}\n" "BYPASS-METHOD: {}\n" "EXCLUDE NETWORKS: {}\n" "INCLUDE NETWORKS: {}\n" "SPLIT TUNNEL: {}\n" "TUNNEL MODE: {}\n" "TUNNEL DOMAINS: {}\n" "BLACKLIST DOMAINS: {}\n", FPTN_VERSION, selected_server.name, sni, using_gateway_ip.ToString(), out_network_interface_name, selected_server.name, selected_server.host, selected_server.port, tun_interface_address_ipv4.ToString(), tun_interface_address_ipv6.ToString(), bypass_method, exclude_networks_str, include_networks_str, enable_split_tunnel ? "enabled" : "disabled", tunnel_mode, split_domains_str, blacklist_domains_str); /* auth & dns */ auto http_client = std::make_unique(server_ip, selected_server.port, tun_interface_address_ipv4, tun_interface_address_ipv6, sni, selected_server.md5_fingerprint, censorship_strategy); const bool status = http_client->Login(config.GetUsername(), config.GetPassword()); if (!status) { SPDLOG_ERROR("The username or password you entered is incorrect"); return EXIT_FAILURE; } const auto [dnsServerIPv4, dnsServerIPv6] = http_client->GetDns(); if (dnsServerIPv4.IsEmpty() || dnsServerIPv6.IsEmpty()) { SPDLOG_ERROR("DNS server error! Check your connection!"); return EXIT_FAILURE; } /* tun interface */ auto virtual_network_interface = std::make_unique( fptn::common::network::TunInterface::Config{ .name = tun_interface_name, .ipv4_addr = tun_interface_address_ipv4, .ipv4_netmask = 30, // IPv4 netmask .ipv6_addr = tun_interface_address_ipv6, .ipv6_netmask = 126 // IPv6 netmask }); /* route manager */ auto route_manager = std::make_shared( out_network_interface_name, tun_interface_name, server_ip, dnsServerIPv4, dnsServerIPv6, using_gateway_ip, using_gateway_ipv6, tun_interface_address_ipv4, tun_interface_address_ipv6 #if _WIN32 , false #endif ); // NOLINT /* plugins */ std::vector client_plugins; if (!blacklist_domains.empty()) { auto blacklist_plugin = std::make_unique( blacklist_domains, route_manager); client_plugins.push_back(std::move(blacklist_plugin)); } if (enable_split_tunnel) { const auto policy = tunnel_mode == "exclude" ? fptn::routing::RoutingPolicy::kExcludeFromVpn : fptn::routing::RoutingPolicy::kIncludeInVpn; auto split_tunnel_plugin = std::make_unique( split_domains, route_manager, policy); client_plugins.push_back(std::move(split_tunnel_plugin)); } /* vpn client */ fptn::vpn::VpnClient vpn_client(std::move(http_client), std::move(virtual_network_interface), dnsServerIPv4, dnsServerIPv6, std::move(client_plugins)); vpn_client.Start(); // Update tun name to actual device name (may differ on macOS) route_manager->UpdateTunInterfaceName(vpn_client.GetInterfaceName()); // Wait for the WebSocket tunnel to establish constexpr std::chrono::seconds kTimeout(10); const auto start = std::chrono::steady_clock::now(); while (!vpn_client.IsStarted()) { if (std::chrono::steady_clock::now() - start > kTimeout) { SPDLOG_ERROR("Couldn't open websocket tunnel!"); return EXIT_FAILURE; } std::this_thread::sleep_for(std::chrono::microseconds(200)); } /* apply mandatory network routes */ route_manager->Apply(); if (!exclude_networks.empty()) { route_manager->AddExcludeNetworks(exclude_networks); } if (!include_networks.empty()) { route_manager->AddIncludeNetworks(include_networks); } /* start event loop */ fptn::utils::WaitForSignal(vpn_client); /* clean */ route_manager->Clean(); vpn_client.Stop(); spdlog::shutdown(); return EXIT_SUCCESS; } catch (const std::exception& ex) { SPDLOG_ERROR("An error occurred: {}. Exiting...", ex.what()); } catch (...) { SPDLOG_ERROR("An unknown error occurred. Exiting..."); } return EXIT_FAILURE; } ================================================ FILE: src/fptn-client/fptn-client-gui.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #if defined(__linux__) || defined(__APPLE__) #include #endif #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/logger/logger.h" #include "gui/tray/tray.h" #ifdef __APPLE__ #include "utils/macos/admin.h" #endif namespace { #if defined(__linux__) || defined(__APPLE__) void SignalHandler(int) { QApplication::quit(); } #elif defined(_WIN32) BOOL WINAPI SignalHandler(DWORD ctrlType) { if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_BREAK_EVENT) { qApp->quit(); return TRUE; } return FALSE; } #else #error "Unsupported platform" #endif } // namespace int main(int argc, char* argv[]) { try { #ifdef __linux__ if (geteuid() != 0) { std::cerr << "You must be root to run this program.\n"; return EXIT_FAILURE; } #elif defined(__APPLE__) if (!fptn::utils::macos::RestartApplicationWithAdminRights()) { return EXIT_FAILURE; // Failed to get admin rights } #endif // Initialize logger if (fptn::logger::init("fptn-client-gui")) { SPDLOG_INFO("Application started successfully."); } else { std::cerr << "Logger initialization failed. Exiting application." << std::endl; return EXIT_FAILURE; } // Setup signal handler #if defined(__APPLE__) || defined(__linux__) std::signal(SIGINT, SignalHandler); std::signal(SIGHUP, SignalHandler); std::signal(SIGTERM, SignalHandler); std::signal(SIGQUIT, SignalHandler); #if __linux__ std::signal(SIGPWR, SignalHandler); #endif #elif defined(_WIN32) SetConsoleCtrlHandler(SignalHandler, TRUE); #endif // Initialize GUI app QApplication::setDesktopSettingsAware(true); QApplication::setQuitOnLastWindowClosed(false); #if __APPLE__ QCoreApplication::setSetuidAllowed(true); QApplication::setAttribute(Qt::AA_MacDontSwapCtrlAndMeta, false); #elif defined(_WIN32) QApplication::setStyle(QStyleFactory::create("windowsvista")); #endif QApplication app(argc, argv); const auto settings = std::make_shared( QMap{{"en", "English"}, {"ru", "Русский"}}); // Start GUI app fptn::gui::TrayApp tray(settings); // NOLINTNEXTLINE(readability-static-accessed-through-instance) const int code = app.exec(); // Clean resources tray.stop(); spdlog::shutdown(); return code; } catch (const std::exception& ex) { SPDLOG_ERROR("An error occurred: {}. Exiting...", ex.what()); } catch (...) { SPDLOG_ERROR("An unknown error occurred. Exiting..."); } return EXIT_FAILURE; } ================================================ FILE: src/fptn-client/gui/autostart/autostart.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include // NOLINT(build/include_order) #include "common/system/command.h" #if _WIN32 #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #endif namespace fptn::gui::autostart { #if __APPLE__ inline std::string GetMacOsPlistPath() { const char* home_env = std::getenv("HOME"); if (nullptr == home_env) { return {}; } const std::string home = home_env; const auto path = std::filesystem::path(home) / "Library" / "LaunchAgents" / "org.fptn.vpn"; return path.string(); } #elif __linux__ inline std::string getLinuxDesktopEntryPath() { return "/etc/xdg/autostart/fptn-autostart.desktop"; } #elif _WIN32 inline std::string getWindowsFullPath() { wchar_t fptn_path[MAX_PATH] = {}; if (GetModuleFileNameW(nullptr, fptn_path, MAX_PATH) == 0) { const DWORD code = GetLastError(); SPDLOG_ERROR("Failed to retrieve the path. Error code: {}", code); return {}; } const std::filesystem::path fptnExe(fptn_path); const auto batPath = fptnExe.parent_path() / "FptnClient.bat"; return batPath.string(); } inline std::string getWindowsStartupFolder() { wchar_t path[MAX_PATH] = {}; if (!SUCCEEDED(SHGetFolderPathW(nullptr, CSIDL_STARTUP, nullptr, 0, path))) { const DWORD code = GetLastError(); SPDLOG_ERROR( "Failed to retrieve the startup folder path. Error code: {}", code); return {}; } // Convert wide string to UTF-8 using Windows API int utf8_size = WideCharToMultiByte(CP_UTF8, 0, path, -1, nullptr, 0, nullptr, nullptr); if (utf8_size == 0) { const DWORD code = GetLastError(); SPDLOG_ERROR("Failed to convert path to UTF-8. Error code: {}", code); return {}; } std::string utf8_path(utf8_size, '\0'); WideCharToMultiByte( CP_UTF8, 0, path, -1, &utf8_path[0], utf8_size, nullptr, nullptr); utf8_path.resize(utf8_size - 1); // Remove null terminator return utf8_path; } #endif inline bool enable() { #if __APPLE__ // const std::string autostart_template = // R"PLIST( // // // // Label // org.fptn.vpn // // AssociatedBundleIdentifiers // // org.fptn.vpn // // // DisplayName // FptnClient // // ProgramArguments // // {} // // // UserName // root // // RunAtLoad // // // KeepAlive> // // // SessionCreate // // // EnableTransactions // // // // )PLIST"; // const auto script_path = std::filesystem::current_path() / "Contents" / // "MacOS" / "fptn-client-gui-wrapper.sh"; // const auto plist = fmt::format(autostart_template, script_path.u8string()); // const std::string plist_path = GetMacOsPlistPath(); // if (plist_path.empty()) { // SPDLOG_ERROR("Failed to get the macOS plist path."); // return false; // } // SPDLOG_INFO("Plist path: {}", plist_path); // std::ofstream file(plist_path); // if (file.is_open()) { // file << plist; // file.close(); // SPDLOG_INFO("Plist file written successfully at {}", plist_path); // } else { // SPDLOG_ERROR("Unable to write to plist file at {}", plist_path); // return false; // } // const std::string command = // fmt::format(R"(launchctl load "{}" )", plist_path); // if (!fptn::common::system::command::run(command)) { // SPDLOG_ERROR("Failed to load plist using launchctl. // Command: {}", command); // return false; // } #elif __linux__ const std::string entry = R"PLIST([Desktop Entry] Name=FptnClient Terminal=false Exec=/usr/bin/fptn-client Type=Application Icon=/path/icon.png Categories=Utility; )PLIST"; const auto path = std::filesystem::path(getLinuxDesktopEntryPath()); if (path.empty()) { SPDLOG_ERROR("Failed to get the macOS plist path."); return false; } std::ofstream file(path); if (file.is_open()) { file << entry; file.close(); } else { return false; } #elif _WIN32 // const std::string fptn_path = getWindowsFullPath(); // const std::string windowsStartupFolder = getWindowsStartupFolder(); // if (fptn_path.empty() || windowsStartupFolder.empty()) { // return false; // } // // SET REG // const std::string command = fmt::format( // R"(reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v // "FptnClient" /t REG_SZ /d "{}" /f )", fptn_path // ); // if (!fptn::common::system::command::run(command)) { // SPDLOG_ERROR("Error running command: {}", command); // return false; // } // SET SHORTCUT // const std::filesystem::path shortcutPath = // std::filesystem::path(windowsStartupFolder) / "FptnClient.lnk"; const // std::string powershellCommand = fmt::format( // R"(powershell -Command "$ws = New-Object -ComObject WScript.Shell; $s = // $ws.CreateShortcut('{}'); $s.TargetPath = '{}'; $s.Save();")", // shortcutPath.u8string(), fptn_path // ); // if (!fptn::common::system::command::run(powershellCommand)) { // SPDLOG_ERROR("Failed to create shortcut: {}", powershellCommand); // return false; // } // SPDLOG_INFO("Shortcut created successfully at: {}", // shortcutPath.u8string()); #endif SPDLOG_INFO("Autostart successfully enabled"); return true; } inline bool disable() { #if __APPLE__ const std::string plist_path = GetMacOsPlistPath(); if (plist_path.empty()) { SPDLOG_ERROR("Failed to get the macOS plist path."); return false; } if (std::filesystem::exists(plist_path)) { const std::string command = fmt::format(R"(launchctl unload "{}" )", plist_path); if (!fptn::common::system::command::run(command)) { return false; } if (!std::filesystem::remove(plist_path)) { return false; } } #elif __linux__ if (std::filesystem::exists(getLinuxDesktopEntryPath())) { if (!std::filesystem::remove(getLinuxDesktopEntryPath())) { return false; } } #elif _WIN32 // delete reg // const std::string command = R"(reg delete // "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "FptnClient" /f )"; // if (!fptn::common::system::command::run(command)) { // SPDLOG_ERROR("Error running command: {}", command); // } // delete shortcut // const std::string windowsStartupFolder = getWindowsStartupFolder(); // const std::filesystem::path shortcutPath = // std::filesystem::path(windowsStartupFolder) / "FptnClient.lnk"; if // (std::filesystem::exists(shortcutPath) && // std::filesystem::remove(shortcutPath)) { // SPDLOG_INFO("Shortcut deleted successfully: {}", // shortcutPath.u8string()); // } else { // SPDLOG_INFO("No shortcut found to delete at: {}", // shortcutPath.u8string()); // } #endif SPDLOG_INFO("Disable autostart"); return true; } } // namespace fptn::gui::autostart ================================================ FILE: src/fptn-client/gui/autoupdate/autoupdate.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #define CPPHTTPLIB_OPENSSL_SUPPORT #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include #include // NOLINT(build/include_order) #include "fptn-protocol-lib/https/api_client/api_client.h" namespace fptn::gui::autoupdate { namespace version { inline std::vector ParseVersion(const std::string& version) { std::vector parsed; std::stringstream ss(version); std::string segment; while (std::getline(ss, segment, '.')) { parsed.push_back(std::stoi(segment)); } return parsed; } inline int compare(const std::string& version1, const std::string& version2) { std::vector v1 = ParseVersion(version1); std::vector v2 = ParseVersion(version2); const std::size_t max_length = (std::max)(v1.size(), v2.size()); v1.resize(max_length, 0); v2.resize(max_length, 0); for (size_t i = 0; i < max_length; ++i) { if (v1[i] < v2[i]) return -1; // version1 is less than version2 if (v1[i] > v2[i]) return 1; // version1 is greater than version2 } return 0; } } // namespace version #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline std::pair Check() { const auto url = fmt::format("/repos/{}/{}/releases/latest", FPTN_GITHUB_USERNAME, FPTN_GITHUB_REPOSITORY); try { httplib::SSLClient cli("api.github.com", 443); { cli.enable_server_certificate_verification(false); // NEED TO FIX cli.set_connection_timeout(5, 0); // 5 seconds cli.set_read_timeout(5, 0); // 5 seconds cli.set_write_timeout(5, 0); // 5 seconds } if (auto resp = cli.Get(url)) { try { const auto msg = nlohmann::json::parse(resp->body); if (msg.contains("draft") && msg.contains("name")) { const bool draft = msg["draft"]; const std::string version_name = msg["name"]; if (!draft && version::compare(FPTN_VERSION, version_name) == -1) { return {true, version_name}; } return {false, version_name}; } } catch (const nlohmann::json::parse_error& e) { SPDLOG_ERROR("autoupdate:check Error parsing JSON response: {} {}", e.what(), resp->body); } } } catch (...) { SPDLOG_ERROR("unhandled exception"); } return {false, {}}; } #else inline std::pair Check() { fptn::protocol::https::HttpsClient cli("api.github.com", 443); const auto url = fmt::format("/repos/{}/{}/releases/latest", FPTN_GITHUB_USERNAME, FPTN_GITHUB_REPOSITORY); const auto resp = cli.Get(url); if (resp.code == 200) { try { const auto msg = resp.Json(); if (msg.contains("draft") && msg.contains("name")) { const bool draft = msg["draft"]; const std::string version_name = msg["name"]; if (!draft && version::compare(FPTN_VERSION, version_name) == -1) { return {true, version_name}; } return {false, version_name}; } } catch (const nlohmann::json::parse_error& e) { SPDLOG_ERROR("autoupdate:check Error parsing JSON response: {} Body: {}", e.what(), resp.body); } } else { SPDLOG_WARN("autoupdate:check error: {}", resp.errmsg); } return {false, {}}; } #endif } // namespace fptn::gui::autoupdate ================================================ FILE: src/fptn-client/gui/resources/resources.qrc ================================================ icons/app.ico icons/active.ico icons/inactive.ico icons/ping_green_circle.png icons/ping_orange_circle.png icons/ping_red_circle.png icons/ping_yellow_circle.png icons/menu_server_list.png icons/menu_settings.png icons/menu_new_version_download.png icons/menu_exit.png icons/menu_disconnect.png icons/menu_connection.png translations/fptn_ru.qm translations/fptn_en.qm ================================================ FILE: src/fptn-client/gui/resources/translations/.gitignore ================================================ *.qm ================================================ FILE: src/fptn-client/gui/resources/translations/fptn_en.ts ================================================ QObject Connect Connect Settings Settings Quit Quit Connecting... Connecting... Disconnecting... Disconnecting... Disconnect Disconnect Upload speed Upload speed Download speed Download speed Unable to find the default gateway IP address. Please check your connection and make sure no other VPN is active. If the error persists, specify the gateway address in the FPTN settings using your router's IP address, and ensure that an active internet interface (adapter) is selected. If the issue remains unresolved, please contact the developer via Telegram @fptn_chat. Unable to find the default gateway IP address. Please check your connection and make sure no other VPN is active. If the error persists, specify the gateway address in the FPTN settings using your router's IP address, and ensure that an active internet interface (adapter) is selected. If the issue remains unresolved, please contact the developer via Telegram @fptn_chat. Configuration error Configuration error DNS resolution error DNS resolution error Connection error Connection error Unable to connect to the server. Please use the Telegram bot to generate a new TOKEN with your personal settings, then try again. Unable to connect to the server. Please use the Telegram bot to generate a new TOKEN with your personal settings, then try again. DNS server error! Check your connection! DNS server error! Check your connection! Network Interface (adapter) Network Interface (adapter) Gateway IP Address (typically your router's address) Gateway IP Address (typically your router's address) Name Name User User Servers Servers Action Action Add token Add token Save Save About About Delete Delete Version Version Save Successful Save Successful Data has been successfully saved. Data has been successfully saved. Save Failed Save Failed An error occurred while saving the data. An error occurred while saving the data. Delete Successful Delete Successful The data has been successfully removed The data has been successfully removed The data has been successfully updated The data has been successfully updated Language Language Smart Connect Smart Connect Connection Error Connection Error The server is unavailable. Please select another server or use Auto-connect to find the best available server. The server is unavailable. Please select another server or use Auto-connect to find the best available server. No servers No servers Paste your token Paste your token Token Token OK OK Cancel Cancel Validation Error Validation Error Token cannot be empty Token cannot be empty Wrong token Wrong token Close Close Autostart Autostart Auto Auto New version available New version available Fake domain to bypass blocking Fake domain to bypass blocking FPTN_DESCRIPTION FPTN is a fully custom-built VPN technology — developed from scratch, including the core protocol, server implementation, and cross-platform clients. It is a non-commercial, open-source project developed by volunteers and designed to bypass censorship. The project's source code is available on GitHub. FPTN_WEBSITE_DESCRIPTION https://storage.googleapis.com/fptn.org/index.html ]]> FPTN_TELEGRAM_DESCRIPTION - English Community
- Russian Community
- Persian Community ]]>
Limited access servers Limited access servers Missing required fields in configuration. Generate and apply a new token. Missing required fields in configuration. Generate and apply a new token. Failed to connect to the server! Failed to connect to the server! The VPN connection was unexpectedly closed. The VPN connection was unexpectedly closed. FPTN Connection Error FPTN Connection Error VPN Conflict Detected VPN Conflict Detected A conflicting VPN connection is currently active on your system: %1 This may cause network connectivity issues or prevent proper operation of FPTN. A conflicting VPN connection is currently active on your system: %1 This may cause network connectivity issues or prevent proper operation of FPTN. Bypass blocking method Bypass blocking method SNI Domain spoofing (SNI) OBFUSCATION Traffic masking (obfuscation) SNI-REALITY (Generic) Domain spoofing SNI-REALITY (Chrome 147) Domain spoofing (Chrome 147) SNI-REALITY (Chrome 146) Domain spoofing (Chrome 146) SNI-REALITY (Chrome 145) Domain spoofing (Chrome 145) SNI-REALITY (Firefox 149) Domain spoofing (Firefox 149) SNI-REALITY (Yandex 26) Domain spoofing (Yandex 26) SNI-REALITY (Yandex 25) Domain spoofing (Yandex 25) SNI-REALITY (Yandex 24) Domain spoofing (Yandex 24) SNI-REALITY (Safari 26) Domain spoofing (Safari 26) Support the project on Support the project on Project Sponsors Project Sponsors Autoscan SNI Autoscan SNI All All Start Start Scan completed Scan completed Working SNI found: %1 Working SNI found: %1 No working SNI found. No working SNI found. Error Error No SNI available for scanning. No SNI available for scanning. No servers available for scanning. No servers available for scanning. TLS Handshake: %1 TLS Handshake: %1 HTTP Request: %1 HTTP Request: %1 Import SNI file Import SNI file Select SNI file Select SNI file SNI files (*.sni);;All files (*) SNI files (*.sni);;All files (*) File exists File exists File "%1" already exists. Overwrite? File "%1" already exists. Overwrite? Success Success SNI file imported successfully SNI file imported successfully Failed to import SNI file Failed to import SNI file Delete this file Delete this file No SNI files imported No SNI files imported Routing Routing Blacklist domains Blacklist domains Completely block access to the main domain AND all its subdomains. Format: domain:example.com (one per line) Completely block access to the main domain AND all its subdomains. Format: domain:example.com (one per line) Exclude tunnel networks Exclude tunnel networks Networks that always bypass VPN tunnel. Traffic to these networks goes directly, never through VPN Networks that always bypass VPN tunnel. Traffic to these networks goes directly, never through VPN Include tunnel networks Include tunnel networks Networks that always use VPN tunnel. Traffic to these networks always goes through VPN Networks that always use VPN tunnel. Traffic to these networks always goes through VPN Enable split tunnel Enable split tunneling (experimental) When enabled, you can configure which sites use VPN and which go directly. When enabled, you can configure which sites use VPN and which go directly. Split tunnel mode Split tunnel mode Defines traffic routing strategy for split tunneling. Defines traffic routing strategy for split tunneling. Exclude Exclude Include Include Domains to route through VPN Domains to route through VPN Domains to bypass VPN Domains to bypass VPN List domains that should bypass VPN tunnel. Only these domains will go through VPN, all other traffic bypasses VPN List domains that should bypass VPN tunnel. Only these domains will go through VPN, all other traffic bypasses VPN List websites that should bypass VPN tunnel. These domains will go directly, all other traffic uses VPN List websites that should bypass VPN tunnel. These domains will go directly, all other traffic uses VPN Enable advanced DNS management Enable advanced DNS management (experimental) Enables advanced DNS configuration to prevent leaks. Recommended when using split tunneling. Use with caution! Enables advanced DNS configuration to prevent leaks. Recommended when using split tunneling. Use with caution!
================================================ FILE: src/fptn-client/gui/resources/translations/fptn_ru.ts ================================================ QObject Connect Подключиться Settings Настройки Quit Выход Connecting... Подключение... Disconnecting... Отключение... Disconnect Отключить Upload speed Скорость выгрузки Download speed Скорость зарузки Unable to find the default gateway IP address. Please check your connection and make sure no other VPN is active. If the error persists, specify the gateway address in the FPTN settings using your router's IP address, and ensure that an active internet interface (adapter) is selected. If the issue remains unresolved, please contact the developer via Telegram @fptn_chat. Не удалось найти IP-адрес по умолчанию для шлюза. Пожалуйста, проверьте ваше соединение и убедитесь, что у вас не активен другой VPN. Если ошибка сохраняется, укажите адрес шлюза в настройках FPTN, используя IP-адрес вашего маршрутизатора, и убедитесь, что выбран активный интернет-интерфейс (адаптер). Если проблема не устранена, обратитесь к разработчику через Telegram @fptn_chat. Configuration error Ошибка конфигурации DNS resolution error Ошибка разрешения DNS Connection error Ошибка подключения Unable to connect to the server. Please use the Telegram bot to generate a new TOKEN with your personal settings, then try again. Невозможно подключиться к серверу. Пожалуйста, сгенерируйте новый TOKEN с вашими персональными настройками через Telegram-бота и повторите попытку. DNS server error! Check your connection! Ошибка DNS сервера! Проверьте ваше соединение! Network Interface (adapter) Сетевой интерфейс (адаптер) Gateway IP Address (typically your router's address) IP-адрес шлюза (обычно адрес роутера) Name Имя User Пользователь Servers Серверы Action Действие Add token Добавить токен Save Сохранить About О программе Delete Удалить Version Версия Save Successful Сохранение прошло успешно Data has been successfully saved. Данные успешно сохранены. Save Failed Ошибка сохранения An error occurred while saving the data. Произошла ошибка при сохранении данных. Delete Successful Успешное удаление The data has been successfully removed Данные были успешно удалены The data has been successfully updated Данные были успешно обновлены Language Язык Smart Connect Умное подключение Connection Error Ошибка подключения The server is unavailable. Please select another server or use Auto-connect to find the best available server. Сервер недоступен. Пожалуйста, выберите другой сервер или используйте функцию автоподключения для выбора лучшего доступного сервера. No servers Нет серверов Paste your token Вставьте ваш токен Token Токен OK OK Cancel Отмена Validation Error Ошибка валидации Token cannot be empty Токен не может быть пустым Wrong token Неправильный токен Close Закрыть Autostart Автостарт Auto Автоматически New version available Доступна новая версия Fake domain to bypass blocking Фейковый домен для обхода блокировок FPTN_DESCRIPTION FPTN — это полностью разработанная с нуля технология VPN, включая собственный протокол, сервер и кроссплатформенные клиенты. Это некоммерческий проект с открытым исходным кодом, развиваемый волонтерами и предназначенный для обхода цензуры. Исходный код проекта доступен на Github. FPTN_WEBSITE_DESCRIPTION fptn.org ]]> FPTN_TELEGRAM_DESCRIPTION - Русскоязычное сообщество
- Англоязычное сообщество
- Персидское сообщество ]]>
Limited access servers Серверы с ограниченным доступом Missing required fields in configuration. Generate and apply a new token. Конфиг не содержит необходимых полей. Пожалуйста, сгенерируйте и используйте новый токен. Failed to connect to the server! Не удалось подключиться к серверу! The VPN connection was unexpectedly closed. VPN-соединение было неожиданно разорвано. FPTN Connection Error FPTN Ошибка подключения VPN Conflict Detected Обнаружен конфликт VPN A conflicting VPN connection is currently active on your system: %1 This may cause network connectivity issues or prevent proper operation of FPTN. Обнаружено конфликтующее VPN-соединение: %1 Это может вызвать проблемы с подключением к сети или нарушить работу FPTN. Bypass blocking method Метод обхода блокировок SNI Подмена домена (SNI) OBFUSCATION Маскировка трафика (Обфускация) SNI-REALITY (Generic) Подмена домена SNI-REALITY (Chrome 147) Подмена домена (Chrome 147) SNI-REALITY (Chrome 146) Подмена домена (Chrome 146) SNI-REALITY (Chrome 145) Подмена домена (Chrome 145) SNI-REALITY (Firefox 149) Подмена домена (Firefox 149) SNI-REALITY (Yandex 26) Подмена домена (Яндекс 26) SNI-REALITY (Yandex 25) Подмена домена (Яндекс 25) SNI-REALITY (Yandex 24) Подмена домена (Яндекс 24) SNI-REALITY (Safari 26) Подмена домена (Safari 26) Support the project on Поддержать проект можно на Project Sponsors Спонсоры проекта Autoscan SNI Автосканирование SNI All Все Start Начать Scan completed Сканирование завершено Working SNI found: %1 Рабочий SNI найден: %1 No working SNI found. Рабочий SNI не найден. Error Ошибка No SNI available for scanning. Нет доступных SNI для сканирования. No servers available for scanning. Нет доступных серверов для сканирования. TLS Handshake: %1 TLS Handshake: %1 HTTP Request: %1 HTTP Запрос: %1 Import SNI file Импорт SNI файла Select SNI file Выберите SNI файл SNI files (*.sni);;All files (*) SNI файлы (*.sni);;Все файлы (*) File exists Файл существует File "%1" already exists. Overwrite? Файл "%1" уже существует. Перезаписать? Success Успех SNI file imported successfully SNI файл успешно импортирован Failed to import SNI file Не удалось импортировать SNI файл Delete this file Удалить этот файл No SNI files imported Нет импортированных SNI файлов Routing Маршрутизация Blacklist domains Черный список доменов Completely block access to the main domain AND all its subdomains. Format: domain:example.com (one per line) Полностью блокировать доступ к основному домену и всем его поддоменам. Формат: domain:example.com (по одному в строке) Exclude tunnel networks Исключить сети из туннеля Networks that always bypass VPN tunnel. Traffic to these networks goes directly, never through VPN Сети, которые всегда обходят VPN-туннель. Трафик к этим сетям идет напрямую, никогда через VPN Include tunnel networks Включить сети в туннель Networks that always use VPN tunnel. Traffic to these networks always goes through VPN Сети, которые всегда используют VPN-туннель. Трафик к этим сетям всегда идет через VPN Enable split tunnel Раздельное туннелирование (экспериментально) When enabled, you can configure which sites use VPN and which go directly. Когда включено, вы можете настроить, какие сайты используют VPN, а какие идут напрямую. Split tunnel mode Режим раздельного туннеля Defines traffic routing strategy for split tunneling. Определяет стратегию маршрутизации трафика для раздельного туннелирования. Exclude Исключить Include Включить Domains to route through VPN Домены для маршрутизации через VPN Domains to bypass VPN Домены для обхода VPN List domains that should use VPN tunnel. Only these domains will go through VPN, all other traffic bypasses VPN Список доменов, которые должны использовать VPN-туннель. Только эти домены будут проходить через VPN, весь остальной трафик мимо VPN List domains that should bypass VPN tunnel. These domains will go directly, all other traffic uses VPN Список доменов, которые не пойдут через VPN-туннель. Эти домены будут идти напрямую, весь остальной трафик пойдет в VPN Enable advanced DNS management Разрешить расширенное управление DNS (экспериментально) Enables advanced DNS configuration to prevent leaks. Recommended when using split tunneling. Use with caution! Включает расширенную конфигурацию DNS для защиты от утечек. Рекомендуется активировать при использовании раздельного туннелирования. Используйте с осторожностью!
================================================ FILE: src/fptn-client/gui/server_menu_item_widget/server_menu_item_widget.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/server_menu_item_widget/server_menu_item_widget.h" #include #include // NOLINT(build/include_order) namespace { const QPixmap& GetPingIcon(const int ping_ms) { static const QPixmap kRedCircle(":/icons/ping_red_circle.png"); static const QPixmap kGreenCircle(":/icons/ping_green_circle.png"); static const QPixmap kYellowCircle(":/icons/ping_yellow_circle.png"); static const QPixmap kOrangeCircle(":/icons/ping_orange_circle.png"); if (ping_ms == -1) { return kRedCircle; } if (ping_ms < 200) { return kGreenCircle; } if (ping_ms < 300) { return kYellowCircle; } if (ping_ms < 500) { return kOrangeCircle; } return kRedCircle; } } // namespace namespace fptn::gui { #ifdef __APPLE__ ServerMenuItemWidget::ServerMenuItemWidget( QString name, int ping_ms, QObject* parent) : QAction(parent), name_(std::move(name)) { setIconVisibleInMenu(true); UpdatePing(ping_ms); } // macos void ServerMenuItemWidget::UpdatePing(int ping_ms) { const QString ping = (ping_ms == -1) ? " " : QString("%1ms").arg(ping_ms); QString result = name_; const QFontMetrics kfm(this->font()); const int target_width = kfm.horizontalAdvance("A") * 25; int current_width = kfm.horizontalAdvance(name_); while (current_width < target_width) { result.append(' '); current_width = kfm.horizontalAdvance(result); } result.append(ping); setText(result); setIcon(GetPingIcon(ping_ms)); } #else ServerMenuItemWidget::ServerMenuItemWidget( QString name, int ping_ms, QObject* parent) : QWidgetAction(parent), name_(std::move(name)) { auto* widget = new QWidget(); widget->setAttribute(Qt::WA_Hover); widget->setMouseTracking(true); widget->setStyleSheet(R"( QWidget:hover { background-color: #e0e0e0; } )"); auto* layout = new QHBoxLayout(widget); layout->setContentsMargins(2, 2, 2, 2); layout->setSpacing(2); icon_label_ = new QLabel(); icon_label_->setFixedSize(16, 16); icon_label_->setScaledContents(false); icon_label_->setAlignment(Qt::AlignCenter); icon_label_->setStyleSheet("background-color: transparent;"); name_label_ = new QLabel(name_); name_label_->setStyleSheet( "background-color: transparent;padding-right: 10px;"); name_label_->setSizePolicy( QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); name_label_->setMinimumWidth( QFontMetrics(name_label_->font()).horizontalAdvance(name_) + 40); ping_label_ = new QLabel(); ping_label_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred); ping_label_->setAlignment(Qt::AlignRight); ping_label_->setStyleSheet("background-color: transparent;"); layout->addWidget(icon_label_); layout->addWidget(name_label_); layout->addWidget(ping_label_); widget->setLayout(layout); setDefaultWidget(widget); UpdatePing(ping_ms); } void ServerMenuItemWidget::UpdatePing(int ping_ms) { const QString ping = (ping_ms == -1) ? "" : QString("%1ms").arg(ping_ms); ping_label_->setText(ping); icon_label_->setPixmap(GetPingIcon(ping_ms)); } #endif QString ServerMenuItemWidget::ServerName() const { return name_; } } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/server_menu_item_widget/server_menu_item_widget.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) namespace fptn::gui { #ifdef __APPLE__ // QWidgetAction doesn't work fopr macos class ServerMenuItemWidget : public QAction { #else class ServerMenuItemWidget : public QWidgetAction { #endif Q_OBJECT public: explicit ServerMenuItemWidget( QString name, int ping_ms, QObject* parent = nullptr); void UpdatePing(int ping_ms); QString ServerName() const; private: QString name_; QLabel* icon_label_{nullptr}; QLabel* name_label_{nullptr}; QLabel* ping_label_{nullptr}; }; } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/settingsmodel/settingsmodel.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/settingsmodel/settingsmodel.h" #if _WIN32 #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #elif defined(__linux__) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #endif #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "routing//route_manager.h" #include "utils/brotli/brotli.h" using fptn::gui::ServerConfig; using fptn::gui::ServiceConfig; using fptn::gui::SettingsModel; namespace { QVector ParseServers(const QJsonArray& servers_array) { QVector servers; for (const auto& server_value : servers_array) { const QJsonObject server_obj = server_value.toObject(); bool status = false; auto server = ServerConfig::parse(server_obj, status); if (status) { servers.push_back(std::move(server)); } else { QString error = QObject::tr( "Missing required fields in configuration. Generate and apply a new " "token."); throw std::runtime_error(error.toStdString()); } } return servers; } QVector SplitStringToVector(const QString& str) { QVector result; if (str.isEmpty()) { return result; } const auto parts = str.split(',', Qt::SkipEmptyParts); for (const auto& part : parts) { result.append(part.trimmed()); } return result; } QString JoinVectorToString(const QVector& vec) { return vec.join(','); } }; // namespace SettingsModel::SettingsModel(const QMap& languages, const QString& default_language, std::size_t ping_thread_pool_size, QObject* parent) : QObject(parent), languages_(languages), default_language_(default_language), selected_language_(default_language), ping_thread_pool_(ping_thread_pool_size), ping_timer_(this), #if _WIN32 enable_advanced_dns_management_(false), #endif client_autostart_(false), enable_split_tunnel_(false) { #if _WIN32 wchar_t exe_path[MAX_PATH] = {}; if (GetModuleFileNameW(nullptr, exe_path, MAX_PATH) != 0) { std::filesystem::path exe_dir = std::filesystem::path(exe_path).parent_path(); std::string sni_folder = (exe_dir / "SNI").string(); sni_manager_ = std::make_shared(sni_folder); } else { const auto settings_folder = GetSettingsFolderPath(); const std::string sni_folder = settings_folder.toStdString() + "/" + "SNI"; sni_manager_ = std::make_shared(sni_folder); } #elif __linux__ char exe_path[PATH_MAX] = {}; ssize_t count = readlink("/proc/self/exe", exe_path, PATH_MAX); if (count != -1) { exe_path[count] = '\0'; std::filesystem::path exe_dir = std::filesystem::path(exe_path).parent_path(); std::string sni_folder = (exe_dir / "SNI").string(); sni_manager_ = std::make_shared(sni_folder); } else { const auto settings_folder = GetSettingsFolderPath(); const std::string sni_folder = settings_folder.toStdString() + "/" + "SNI"; sni_manager_ = std::make_shared(sni_folder); } #else const auto settings_folder = GetSettingsFolderPath(); const std::string sni_folder = settings_folder.toStdString() + "/" + "SNI"; sni_manager_ = std::make_shared(sni_folder); #endif Load(true); } SettingsModel::~SettingsModel() { StopPingMonitoring(); ping_thread_pool_.stop(); ping_thread_pool_.join(); } QString SettingsModel::GetSettingsFilePath() const { const QString directory = GetSettingsFolderPath(); return directory + "/fptn-settings-4.json"; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) QString SettingsModel::GetSettingsFolderPath() const { #ifdef __APPLE__ const QString directory = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); #else const QString directory = QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); #endif QDir dir(directory); if (!dir.exists()) { dir.mkpath(directory); } return directory; } void SettingsModel::Load(bool dont_load_server) { const std::unique_lock lock(mutex_); // mutex services_.clear(); const QString file_path = GetSettingsFilePath(); QFile file(file_path); if (!file.open(QIODevice::ReadOnly)) { SPDLOG_WARN("Failed to open file for reading: {}", file_path.toStdString()); return; } const QByteArray data = file.readAll(); file.close(); const QJsonDocument document = QJsonDocument::fromJson(data); const QJsonObject service_obj = document.object(); if (service_obj.contains("services")) { QJsonArray services_array = service_obj["services"].toArray(); for (const auto& service_value : services_array) { QJsonObject jsonservice_obj = service_value.toObject(); ServiceConfig service; service.service_name = jsonservice_obj["service_name"].toString(); service.username = jsonservice_obj["username"].toString(); service.password = jsonservice_obj["password"].toString(); if (!dont_load_server) { service.servers = ParseServers(jsonservice_obj["servers"].toArray()); if (jsonservice_obj.contains("censored_zone_servers")) { service.censored_zone_servers = ParseServers(jsonservice_obj["censored_zone_servers"].toArray()); } } services_.push_back(service); } } if (service_obj.contains("network_interface")) { network_interface_ = service_obj["network_interface"].toString(); } if (network_interface_.isEmpty()) { network_interface_ = "auto"; } if (service_obj.contains("language")) { selected_language_ = service_obj["language"].toString(); } if (service_obj.contains("autostart")) { client_autostart_ = service_obj["autostart"].toBool(); } #if _WIN32 if (service_obj.contains("enable_advanced_dns_management")) { enable_advanced_dns_management_ = service_obj["enable_advanced_dns_management"].toBool(); } #endif if (service_obj.contains("gateway_ip")) { gateway_ip_ = service_obj["gateway_ip"].toString(); } if (gateway_ip_.isEmpty()) { gateway_ip_ = "auto"; } if (service_obj.contains("sni")) { sni_ = service_obj["sni"].toString(); } if (sni_.isEmpty()) { sni_ = FPTN_DEFAULT_SNI; } if (service_obj.contains("bypass_method")) { bypass_method_ = service_obj["bypass_method"].toString(); } /* Replace DEPRECATED METHODS */ if (bypass_method_ == kBypassMethodSni || bypass_method_ == kBypassMethodSniReality) { bypass_method_ = kBypassMethodSniRealityYandex25; } if (bypass_method_.isEmpty() || (bypass_method_ != kBypassMethodSni && bypass_method_ != kBypassMethodObfuscation && bypass_method_ != kBypassMethodSniReality && /* Chrome */ bypass_method_ != kBypassMethodSniRealityChrome147 && bypass_method_ != kBypassMethodSniRealityChrome146 && bypass_method_ != kBypassMethodSniRealityChrome145 && /* Firefox */ bypass_method_ != kBypassMethodSniRealityFirefox149 && /* Yandex Browser */ bypass_method_ != kBypassMethodSniRealityYandex26 && bypass_method_ != kBypassMethodSniRealityYandex25 && bypass_method_ != kBypassMethodSniRealityYandex24 && /* Safari */ bypass_method_ != kBypassMethodSniRealitySafari26)) { bypass_method_ = kBypassMethodSniRealityYandex25; // BYDEFAULT } if (service_obj.contains("blacklist_domains")) { blacklist_domains_ = service_obj["blacklist_domains"].toString(); } if (blacklist_domains_.isEmpty()) { blacklist_domains_ = "domain:solovev-live.ru,domain:ria.ru,domain:tass.ru,domain:1tv.ru," "domain:ntv.ru,domain:rt.com"; } if (service_obj.contains("exclude_tunnel_networks")) { exclude_tunnel_networks_ = service_obj["exclude_tunnel_networks"].toString(); } if (exclude_tunnel_networks_.isEmpty()) { exclude_tunnel_networks_ = "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16"; } if (service_obj.contains("include_tunnel_networks")) { include_tunnel_networks_ = service_obj["include_tunnel_networks"].toString(); } if (service_obj.contains("enable_split_tunnel")) { enable_split_tunnel_ = service_obj["enable_split_tunnel"].toBool(); } if (service_obj.contains("split_tunnel_mode")) { split_tunnel_mode_ = service_obj["split_tunnel_mode"].toString(); } if (split_tunnel_mode_.isEmpty() || (split_tunnel_mode_ != kSplitTunnelModeExclude && split_tunnel_mode_ != kSplitTunnelModeInclude)) { split_tunnel_mode_ = kSplitTunnelModeExclude; } if (service_obj.contains("split_tunnel_domains")) { split_tunnel_domains_ = service_obj["split_tunnel_domains"].toString(); } if (split_tunnel_domains_.isEmpty()) { split_tunnel_domains_ = "domain:ru,domain:su,domain:рф,domain:vk.com,domain:yandex.com," "domain:userapi.com,domain:yandex.net,domain:clstorage.net"; } } QString SettingsModel::LanguageName() const { for (auto it = languages_.begin(); it != languages_.end(); ++it) { if (it.key() == selected_language_) { return it.value(); } } return "English"; } const QString& SettingsModel::LanguageCode() const { return selected_language_; } const QString& SettingsModel::DefaultLanguageCode() const { return default_language_; } void SettingsModel::SetLanguage(const QString& language_name) { for (auto it = languages_.begin(); it != languages_.end(); ++it) { if (language_name == it.value()) { selected_language_ = it.key(); } } Save(); } void SettingsModel::SetLanguageCode(const QString& language_code) { for (auto it = languages_.begin(); it != languages_.end(); ++it) { if (language_code == it.key()) { selected_language_ = language_code; } } Save(); } QVector SettingsModel::GetLanguages() const { QVector languages; for (auto it = languages_.begin(); it != languages_.end(); ++it) { languages.push_back(it.value()); } return languages; } bool SettingsModel::ExistsTranslation(const QString& language_code) const { return languages_.contains(language_code); } bool SettingsModel::Save() { const std::unique_lock lock(mutex_); // mutex QString file_path = GetSettingsFilePath(); QFile file(file_path); if (!file.open(QIODevice::WriteOnly)) { SPDLOG_ERROR( "Failed to open file for writing: {}", file_path.toStdString()); return false; } QJsonObject json_object; QJsonArray services_array; for (const auto& service : services_) { QJsonObject service_obj; service_obj["service_name"] = service.service_name; service_obj["username"] = service.username; service_obj["password"] = service.password; QJsonArray servers_array; for (const auto& server : service.servers) { QJsonObject server_obj; server_obj["name"] = server.name; server_obj["host"] = server.host; server_obj["port"] = server.port; server_obj["is_using"] = server.is_using; server_obj["md5_fingerprint"] = server.md5_fingerprint; servers_array.append(server_obj); } service_obj["servers"] = servers_array; QJsonArray censored_zone_servers; for (const auto& server : service.censored_zone_servers) { QJsonObject server_obj; server_obj["name"] = server.name; server_obj["host"] = server.host; server_obj["port"] = server.port; server_obj["is_using"] = server.is_using; server_obj["md5_fingerprint"] = server.md5_fingerprint; censored_zone_servers.append(server_obj); } service_obj["censored_zone_servers"] = censored_zone_servers; services_array.append(service_obj); } json_object["language"] = selected_language_; json_object["services"] = services_array; json_object["network_interface"] = network_interface_; json_object["gateway_ip"] = gateway_ip_; json_object["autostart"] = client_autostart_ ? 1 : 0; json_object["sni"] = sni_; json_object["bypass_method"] = bypass_method_; #if _WIN32 json_object["enable_advanced_dns_management"] = enable_advanced_dns_management_; #endif json_object["blacklist_domains"] = blacklist_domains_; json_object["exclude_tunnel_networks"] = exclude_tunnel_networks_; json_object["include_tunnel_networks"] = include_tunnel_networks_; json_object["enable_split_tunnel"] = enable_split_tunnel_; json_object["split_tunnel_mode"] = split_tunnel_mode_; json_object["split_tunnel_domains"] = split_tunnel_domains_; QJsonDocument document(json_object); auto len = file.write(document.toJson()); file.close(); emit dataChanged(); return len > 0; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) ServiceConfig SettingsModel::ParseToken(const QString& token) { QJsonParseError parse_error; const QByteArray token_data = token.toUtf8(); QJsonDocument json_doc = QJsonDocument::fromJson(token_data, &parse_error); if (parse_error.error != QJsonParseError::NoError) { throw std::runtime_error( "JSON parsing error: " + parse_error.errorString().toStdString()); } QJsonObject json_object = json_doc.object(); if (!json_object.contains("service_name") || !json_object.contains("username") || !json_object.contains("password") || !json_object.contains("servers")) { throw std::runtime_error("Missing required fields in JSON."); } ServiceConfig service; service.service_name = json_object["service_name"].toString(); service.username = json_object["username"].toString(); service.password = json_object["password"].toString(); service.servers = ParseServers(json_object["servers"].toArray()); if (json_object.contains("censored_zone_servers")) { service.censored_zone_servers = ParseServers(json_object["censored_zone_servers"].toArray()); } return service; } QString SettingsModel::UsingNetworkInterface() const { return network_interface_; } void SettingsModel::SetUsingNetworkInterface(const QString& iface) { network_interface_ = (iface.isEmpty() ? "auto" : iface); } QString SettingsModel::GatewayIp() const { return gateway_ip_.isEmpty() ? "auto" : gateway_ip_; } void SettingsModel::SetGatewayIp(const QString& ip) { gateway_ip_ = ip.isEmpty() ? "auto" : ip; Save(); } QString SettingsModel::SNI() const { return sni_.isEmpty() ? FPTN_DEFAULT_SNI : sni_; } void SettingsModel::SetSNI(const QString& sni) { sni_ = sni; Save(); } bool SettingsModel::Autostart() const { return client_autostart_; } void SettingsModel::SetAutostart(bool value) { client_autostart_ = value; Save(); } const QVector& SettingsModel::Services() const { return services_; } void SettingsModel::AddService(const ServiceConfig& server) { const std::unique_lock lock(mutex_); // mutex services_.append(server); } void SettingsModel::RemoveServer(int index) { const std::unique_lock lock(mutex_); // mutex if (index >= 0 && index < services_.size()) { services_.removeAt(index); } } void SettingsModel::Clear() { services_.clear(); } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) QVector SettingsModel::GetNetworkInterfaces() const { QVector interfaces; interfaces.append("auto"); QList network_interfaces = QNetworkInterface::allInterfaces(); for (const QNetworkInterface& network_interface : network_interfaces) { if (!network_interface.flags().testFlag(QNetworkInterface::IsLoopBack)) { const QString iface_name = network_interface.humanReadableName(); if (!iface_name.isEmpty()) { interfaces.append(iface_name); } } } return interfaces; } int SettingsModel::GetExistServiceIndex(const QString& name) const { for (int i = 0; i < services_.size(); i++) { if (services_[i].service_name == name) { return i; } } return -1; } QString SettingsModel::BypassMethod() const { return bypass_method_.isEmpty() ? kBypassMethodSni : bypass_method_; } void SettingsModel::SetBypassMethod(const QString& method) { bypass_method_ = method; Save(); } fptn::gui::SNIManagerSPtr SettingsModel::SniManager() const { return sni_manager_; } QVector SettingsModel::BlacklistDomains() const { if (blacklist_domains_.isEmpty()) { return SplitStringToVector(FPTN_CLIENT_DEFAULT_BLACKLIST_DOMAINS); } return SplitStringToVector(blacklist_domains_); } void SettingsModel::SetBlacklistDomains(const QVector& domains) { blacklist_domains_ = JoinVectorToString(domains); Save(); } QVector SettingsModel::ExcludeTunnelNetworks() const { if (exclude_tunnel_networks_.isEmpty()) { return SplitStringToVector(FPTN_CLIENT_DEFAULT_EXCLUDE_NETWORKS); } return SplitStringToVector(exclude_tunnel_networks_); } void SettingsModel::SetExcludeTunnelNetworks(const QVector& networks) { exclude_tunnel_networks_ = JoinVectorToString(networks); Save(); } QVector SettingsModel::IncludeTunnelNetworks() const { return SplitStringToVector(include_tunnel_networks_); } void SettingsModel::SetIncludeTunnelNetworks(const QVector& networks) { include_tunnel_networks_ = JoinVectorToString(networks); Save(); } bool SettingsModel::EnableSplitTunnel() const { return enable_split_tunnel_; } void SettingsModel::SetEnableSplitTunnel(bool enable) { enable_split_tunnel_ = enable; Save(); } QString SettingsModel::SplitTunnelMode() const { return split_tunnel_mode_.isEmpty() ? kSplitTunnelModeExclude : split_tunnel_mode_; } void SettingsModel::SetSplitTunnelMode(const QString& mode) { split_tunnel_mode_ = mode; Save(); } QVector SettingsModel::SplitTunnelDomains() { const std::unique_lock lock(mutex_); // mutex if (split_tunnel_domains_.isEmpty()) { return SplitStringToVector(FPTN_CLIENT_DEFAULT_SPLIT_TUNNEL_DOMAINS); } return SplitStringToVector(split_tunnel_domains_); } void SettingsModel::SetSplitTunnelDomains(const QVector& domains) { split_tunnel_domains_ = JoinVectorToString(domains); Save(); } #if _WIN32 bool SettingsModel::EnableAdvancedDnsManagement() const { return enable_advanced_dns_management_; } void SettingsModel::SetEnableAdvancedDnsManagement(const bool enable) { enable_advanced_dns_management_ = enable; } #endif void SettingsModel::StartPingMonitoring() { const std::unique_lock lock(mutex_); if (start_pinging_) { return; } start_pinging_ = true; connect(&ping_timer_, &QTimer::timeout, [this]() { if (!start_pinging_ || pending_pings_ > 0) { return; } QSet> servers_to_check; { const std::unique_lock lock(mutex_); for (const auto& service : services_) { for (const auto& server : service.servers) { servers_to_check.insert({server.host, server.port}); } for (const auto& server : service.censored_zone_servers) { servers_to_check.insert({server.host, server.port}); } } } pending_pings_ = servers_to_check.size(); for (const auto& [host, port] : servers_to_check) { boost::asio::post(ping_thread_pool_, [this, host, port]() { PingServer(host, port); pending_pings_--; }); } }); if (!ping_timer_.isActive()) { ping_timer_.start(1000); } } void SettingsModel::StopPingMonitoring() { if (!start_pinging_) { return; } { const std::unique_lock lock(mutex_); // cppcheck-suppress identicalConditionAfterEarlyExit if (!start_pinging_) { return; } start_pinging_ = false; } } void SettingsModel::PingServer(const QString& host, int port) { std::vector results; for (int i = 0; start_pinging_ && i < 3; ++i) { const auto start_time = std::chrono::steady_clock::now(); int ping_ms = -1; QTcpSocket socket; socket.connectToHost(host, port); if (socket.waitForConnected(2000)) { const auto end_time = std::chrono::steady_clock::now(); ping_ms = std::chrono::duration_cast( end_time - start_time) .count(); } socket.close(); results.push_back(ping_ms); } const bool has_error = std::ranges::any_of(results, [](int ping) { return ping == -1; }); int final_ping_ms = -1; if (!has_error && !results.empty()) { const int sum = std::accumulate(results.begin(), results.end(), 0); final_ping_ms = sum / static_cast(results.size()); } const std::unique_lock lock(mutex_); if (start_pinging_) { for (auto& service : services_) { for (auto& server : service.servers) { if (server.host == host && server.port == port) { server.ping_ms = final_ping_ms; } } for (auto& server : service.censored_zone_servers) { if (server.host == host && server.port == port) { server.ping_ms = final_ping_ms; } } } } } ================================================ FILE: src/fptn-client/gui/settingsmodel/settingsmodel.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "gui/sni_manager/sni_manager.h" namespace fptn::gui { /* { "gateway_ip": "auto", "language": "en", "network_interface": "auto", "services": [ { "version": 2, "service_name": "FPTN.ONLINE", "username": "test", "password": "test", "servers": [ { "name": "pq1", "host": "74.119.195.151", "md5_fingerprint": "5c903603cbcfbf0601193c4cc859292c", "port": 443 } ], "censored_zone_servers": [ { "name": "Server1", "host": "127.0.0.1", "port": 443, "md5_fingerprint": "5c903603cbcfbf0601193c4cc859292c" } ] } } ], "blacklist_domains": "domain:solovev-live.ru,domain:ria.ru,domain:tass.ru,domain:1tv.ru,domain:ntv.ru,domain:rt.com", "exclude_tunnel_networks": "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16", "include_tunnel_networks": "", "enable_split_tunnel": true, "split_tunnel_mode": "exclude", "split_tunnel_domains": "domain:ru,domain:su,domain:рф,domain:vk.com,domain:yandex.com,domain:userapi.com,domain:yandex.net,domain:clstorage.net" } */ struct ServerConfig { QString name; QString host; int port; bool is_using; QString md5_fingerprint; int ping_ms = -1; static ServerConfig parse(const QJsonObject& server_obj, bool& status) { status = false; if (!server_obj.contains("name") || !server_obj.contains("host") || !server_obj.contains("port") || !server_obj.contains("md5_fingerprint")) { return {}; } ServerConfig server = {}; server.name = server_obj["name"].toString(); server.host = server_obj["host"].toString(); server.port = server_obj["port"].toInt(); server.md5_fingerprint = server_obj["md5_fingerprint"].toString(); server.is_using = true; server.ping_ms = -1; status = true; return server; } }; struct ServiceConfig { QString service_name; QString username; QString password; QVector servers; QVector censored_zone_servers; QString language; }; class SettingsModel : public QObject { Q_OBJECT public: static constexpr const char* kSplitTunnelModeExclude = "exclude"; static constexpr const char* kSplitTunnelModeInclude = "include"; // DEPRECATED static constexpr const char* kBypassMethodSni = "SNI"; static constexpr const char* kBypassMethodObfuscation = "OBFUSCATION"; // DEPRECATED static constexpr const char* kBypassMethodSniReality = "SNI-REALITY"; /* chrome */ static constexpr const char* kBypassMethodSniRealityChrome147 = "SNI-REALITY-CHROME-147"; static constexpr const char* kBypassMethodSniRealityChrome146 = "SNI-REALITY-CHROME-146"; static constexpr const char* kBypassMethodSniRealityChrome145 = "SNI-REALITY-CHROME-145"; /* Firefox */ static constexpr const char* kBypassMethodSniRealityFirefox149 = "SNI-REALITY-FIREFOX-149"; /* Yandex Browser */ static constexpr const char* kBypassMethodSniRealityYandex26 = "SNI-REALITY-YANDEX-26"; static constexpr const char* kBypassMethodSniRealityYandex25 = "SNI-REALITY-YANDEX-25"; static constexpr const char* kBypassMethodSniRealityYandex24 = "SNI-REALITY-YANDEX-24"; /* Safari */ static constexpr const char* kBypassMethodSniRealitySafari26 = "SNI-REALITY-SAFARI-26"; public: explicit SettingsModel(const QMap& languages, const QString& default_language = "en", std::size_t ping_thread_pool_size = 4, QObject* parent = nullptr); ~SettingsModel() override; void Load(bool dont_load_server = false); bool Save(); void StartPingMonitoring(); void StopPingMonitoring(); QString UsingNetworkInterface() const; void SetUsingNetworkInterface(const QString&); QString GatewayIp() const; void SetGatewayIp(const QString& ip); QString SNI() const; void SetSNI(const QString& sni); QVector GetNetworkInterfaces() const; const QVector& Services() const; void AddService(const ServiceConfig& server); void RemoveServer(int index); int GetExistServiceIndex(const QString& name) const; ServiceConfig ParseToken(const QString& token); void Clear(); QString LanguageName() const; void SetLanguage(const QString& language); void SetLanguageCode(const QString& language_code); QVector GetLanguages() const; const QString& DefaultLanguageCode() const; const QString& LanguageCode() const; bool ExistsTranslation(const QString& language_code) const; bool Autostart() const; void SetAutostart(bool value); QString GetSettingsFilePath() const; QString GetSettingsFolderPath() const; QString BypassMethod() const; void SetBypassMethod(const QString& method); SNIManagerSPtr SniManager() const; QVector BlacklistDomains() const; void SetBlacklistDomains(const QVector& domains); QVector ExcludeTunnelNetworks() const; void SetExcludeTunnelNetworks(const QVector& networks); QVector IncludeTunnelNetworks() const; void SetIncludeTunnelNetworks(const QVector& networks); bool EnableSplitTunnel() const; void SetEnableSplitTunnel(bool enable); QString SplitTunnelMode() const; void SetSplitTunnelMode(const QString& mode); QVector SplitTunnelDomains(); void SetSplitTunnelDomains(const QVector& domains); #if _WIN32 bool EnableAdvancedDnsManagement() const; void SetEnableAdvancedDnsManagement(bool enable); #endif protected: void PingServer(const QString& host, int port); signals: void dataChanged(); private: std::mutex mutex_; QMap languages_; QString default_language_; QString selected_language_; boost::asio::thread_pool ping_thread_pool_; QTimer ping_timer_; std::atomic start_pinging_{false}; std::atomic pending_pings_{0}; QVector services_; QString network_interface_; QString gateway_ip_; QString sni_; #if _WIN32 bool enable_advanced_dns_management_; #endif bool client_autostart_; QString bypass_method_; QString blacklist_domains_; QString exclude_tunnel_networks_; QString include_tunnel_networks_; bool enable_split_tunnel_; QString split_tunnel_mode_; QString split_tunnel_domains_; SNIManagerSPtr sni_manager_; }; using SettingsModelPtr = std::shared_ptr; } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/settingswidget/settings.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/settingswidget/settings.h" #include "gui/sni_autoscan_dialog/sni_autoscan_dialog.h" #if _WIN32 #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #endif #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "gui/autostart/autostart.h" #include "gui/tokendialog/tokendialog.h" #include "gui/translations/translations.h" namespace { QString CleanDomain(const QString& domain) { if (domain.isEmpty()) { return domain; } QString cleaned; cleaned.reserve(domain.length()); static QRegularExpression valid_chars("[a-zA-Z0-9.-]"); for (int i = 0; i < domain.length(); ++i) { QChar ch = domain[i]; if (valid_chars.match(ch).hasMatch()) { cleaned.append(ch.toLower()); } } return cleaned; } QString VectorToText(const QVector& items) { return items.join('\n'); } QVector TextToVector(const QString& text) { QVector result; const auto lines = text.split('\n', Qt::SkipEmptyParts); for (const auto& line : lines) { result.append(line.trimmed()); } return result; } } // namespace using fptn::gui::SettingsWidget; SettingsWidget::SettingsWidget(SettingsModelPtr settings, QWidget* parent) : QDialog(parent), settings_(std::move(settings)) { SetupUi(); setWindowIcon(QIcon(":/icons/app.ico")); // show on top setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); setModal(true); show(); activateWindow(); raise(); setWindowTitle(QObject::tr("Settings")); } void SettingsWidget::SetupUi() { tab_widget_ = new QTabWidget(this); tab_widget_->setContextMenuPolicy(Qt::ActionsContextMenu); // ==================== Settings Tab ==================== settings_tab_ = new QWidget(); auto* main_settings_layout = new QVBoxLayout(settings_tab_); main_settings_layout->setContentsMargins(0, 0, 0, 0); auto* settings_scroll_area = new QScrollArea(this); settings_scroll_area->setWidgetResizable(true); settings_scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); settings_scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); settings_scroll_area->setFrameShape(QFrame::NoFrame); auto* settings_content_widget = new QWidget(); settings_content_widget->setMinimumWidth(600); auto* settings_layout = new QVBoxLayout(settings_content_widget); settings_layout->setContentsMargins(5, 5, 5, 5); grid_layout_ = new QGridLayout(); grid_layout_->setContentsMargins(0, 0, 0, 0); grid_layout_->setHorizontalSpacing(10); grid_layout_->setVerticalSpacing(10); grid_layout_->setColumnStretch(0, 1); grid_layout_->setColumnStretch(1, 3); grid_layout_->setColumnMinimumWidth(0, 380); #ifdef __linux__ autostart_label_ = new QLabel(QObject::tr("Autostart"), this); autostart_checkbox_ = new QCheckBox(" ", this); autostart_checkbox_->setChecked(settings_->Autostart()); connect(autostart_checkbox_, &QCheckBox::toggled, this, &SettingsWidget::onAutostartChanged); grid_layout_->addWidget(autostart_label_, 0, 0, Qt::AlignLeft); grid_layout_->addWidget(autostart_checkbox_, 0, 1, Qt::AlignLeft); #endif language_label_ = new QLabel(QObject::tr("Language"), this); language_combo_box_ = new QComboBox(this); language_combo_box_->addItems(settings_->GetLanguages()); language_combo_box_->setCurrentText(settings_->LanguageName()); connect(language_combo_box_, &QComboBox::currentTextChanged, this, &SettingsWidget::onLanguageChanged); grid_layout_->addWidget(language_label_, 1, 0, Qt::AlignLeft); grid_layout_->addWidget(language_combo_box_, 1, 1, Qt::AlignLeft); interface_label_ = new QLabel(QObject::tr("Network Interface (adapter)"), this); interface_combo_box_ = new QComboBox(this); interface_combo_box_->addItems(settings_->GetNetworkInterfaces()); interface_combo_box_->setCurrentText(settings_->UsingNetworkInterface()); connect(interface_combo_box_, &QComboBox::currentTextChanged, this, &SettingsWidget::onInterfaceChanged); grid_layout_->addWidget(interface_label_, 2, 0, Qt::AlignLeft); grid_layout_->addWidget(interface_combo_box_, 2, 1, Qt::AlignLeft); gateway_label_ = new QLabel( QObject::tr("Gateway IP Address (typically your router's address)"), this); gateway_auto_checkbox_ = new QCheckBox(QObject::tr("Auto"), this); gateway_line_edit_ = new QLineEdit(this); if (settings_->GatewayIp().toLower() != "auto") { gateway_auto_checkbox_->setChecked(false); gateway_line_edit_->setText(settings_->GatewayIp()); gateway_line_edit_->setEnabled(true); } else { gateway_auto_checkbox_->setChecked(true); gateway_line_edit_->setDisabled(true); } connect(gateway_auto_checkbox_, &QCheckBox::toggled, this, &SettingsWidget::onAutoGatewayChanged); auto* gateway_layout = new QHBoxLayout(); gateway_layout->addWidget(gateway_auto_checkbox_); gateway_layout->addWidget(gateway_line_edit_); gateway_layout->setStretch(0, 0); gateway_layout->setStretch(1, 1); grid_layout_->addWidget(gateway_label_, 3, 0, Qt::AlignLeft); grid_layout_->addLayout(gateway_layout, 3, 1); bypass_method_label_ = new QLabel(QObject::tr("Bypass blocking method"), this); bypass_method_combo_box_ = new QComboBox(this); bypass_method_combo_box_->addItem( QObject::tr("OBFUSCATION"), SettingsModel::kBypassMethodObfuscation); /* Chrome */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Chrome 147)"), SettingsModel::kBypassMethodSniRealityChrome147); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Chrome 146)"), SettingsModel::kBypassMethodSniRealityChrome146); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Chrome 145)"), SettingsModel::kBypassMethodSniRealityChrome145); /* Firefox */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Firefox 149)"), SettingsModel::kBypassMethodSniRealityFirefox149); /* Yandex */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Yandex 26)"), SettingsModel::kBypassMethodSniRealityYandex26); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Yandex 25)"), SettingsModel::kBypassMethodSniRealityYandex25); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Yandex 24)"), SettingsModel::kBypassMethodSniRealityYandex24); /* Safari */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Safari 26)"), SettingsModel::kBypassMethodSniRealitySafari26); bypass_method_combo_box_->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed); bypass_method_combo_box_->setMinimumWidth(200); const QString current_method = settings_->BypassMethod(); if (current_method == SettingsModel::kBypassMethodObfuscation) { bypass_method_combo_box_->setCurrentText(QObject::tr("OBFUSCATION")); } /* Chrome */ else if (current_method == SettingsModel::kBypassMethodSniRealityChrome147) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Chrome 147)")); } else if (current_method == SettingsModel::kBypassMethodSniRealityChrome146) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Chrome 146)")); } else if (current_method == SettingsModel::kBypassMethodSniRealityChrome145) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Chrome 145)")); } /* Firefox */ else if (current_method == SettingsModel::kBypassMethodSniRealityFirefox149) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Firefox 149)")); } /* Yandex */ else if (current_method == SettingsModel::kBypassMethodSniRealityYandex26) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 26)")); } else if ( current_method == SettingsModel:: kBypassMethodSniRealityYandex25) { // NOLINT(bugprone-branch-clone) bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 25)")); } else if (current_method == SettingsModel::kBypassMethodSniRealityYandex24) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 24)")); } /* Safari */ else if (current_method == SettingsModel::kBypassMethodSniRealitySafari26) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Safari 26)")); } /* Default */ else { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 25)")); } connect(bypass_method_combo_box_, &QComboBox::currentTextChanged, this, &SettingsWidget::onBypassMethodChanged); grid_layout_->addWidget(bypass_method_label_, 4, 0, Qt::AlignLeft); grid_layout_->addWidget(bypass_method_combo_box_, 4, 1); sni_label_ = new QLabel(this); if (settings_->BypassMethod() == SettingsModel::kBypassMethodSniReality) { sni_label_->setText(QObject::tr("Fake domain to bypass blocking")); } sni_label_->setMinimumHeight(40); sni_label_->setWordWrap(true); sni_line_edit_ = new QLineEdit(this); sni_line_edit_->setText(settings_->SNI()); sni_line_edit_->setMinimumWidth(200); sni_label_->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Preferred); sni_line_edit_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); connect(sni_line_edit_, &QLineEdit::textChanged, this, [this](const QString& text) { if (text.isEmpty()) { settings_->SetSNI(FPTN_DEFAULT_SNI); return; } QString normalized = CleanDomain(text.toLower()); if (normalized != text) { sni_line_edit_->blockSignals(true); sni_line_edit_->setText(normalized); sni_line_edit_->blockSignals(false); } settings_->SetSNI(normalized); }); grid_layout_->addWidget(sni_label_, 5, 0, Qt::AlignLeft | Qt::AlignVCenter); grid_layout_->addWidget(sni_line_edit_, 5, 1); sni_files_list_widget_ = new QListWidget(this); sni_files_list_widget_->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded); sni_files_list_widget_->setMaximumHeight(80); sni_buttons_layout_ = new QHBoxLayout(); sni_autoscan_button_ = new QPushButton(QObject::tr("Autoscan SNI"), this); sni_import_button_ = new QPushButton(QObject::tr("Import SNI file"), this); sni_buttons_layout_->addWidget(sni_autoscan_button_); sni_buttons_layout_->addSpacing(10); sni_buttons_layout_->addWidget(sni_import_button_); sni_buttons_layout_->addStretch(1); sni_autoscan_button_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); sni_import_button_->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); grid_layout_->addLayout(sni_buttons_layout_, 7, 0, 1, 2); grid_layout_->addWidget(sni_files_list_widget_, 8, 0, 1, 2); connect(sni_autoscan_button_, &QPushButton::clicked, this, &SettingsWidget::onAutoscanClicked); connect(sni_import_button_, &QPushButton::clicked, this, &SettingsWidget::onImportSniFile); settings_layout->addLayout(grid_layout_); server_table_ = new QTableWidget(0, 4, this); server_table_->setHorizontalHeaderLabels({QObject::tr("Name"), QObject::tr("User"), QObject::tr("Servers"), QObject::tr("Action")}); server_table_->horizontalHeader()->setStretchLastSection(false); server_table_->horizontalHeader()->setSectionResizeMode( 0, QHeaderView::Stretch); server_table_->horizontalHeader()->setSectionResizeMode( 1, QHeaderView::Stretch); server_table_->horizontalHeader()->setSectionResizeMode( 2, QHeaderView::Stretch); server_table_->horizontalHeader()->setSectionResizeMode( 3, QHeaderView::ResizeToContents); server_table_->setEditTriggers(QAbstractItemView::NoEditTriggers); server_table_->setSelectionBehavior(QAbstractItemView::SelectRows); server_table_->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); server_table_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); server_table_->verticalHeader()->setSectionResizeMode( QHeaderView::ResizeToContents); server_table_->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); server_table_->setSizeAdjustPolicy(QAbstractScrollArea::AdjustToContents); settings_layout->addWidget(server_table_); settings_layout->addStretch(1); settings_scroll_area->setWidget(settings_content_widget); main_settings_layout->addWidget(settings_scroll_area); tab_widget_->addTab(settings_tab_, QObject::tr("Settings")); // ==================== Routing Tab ==================== routing_tab_ = new QWidget(); auto* main_routing_layout = new QVBoxLayout(routing_tab_); main_routing_layout->setContentsMargins(0, 0, 0, 0); auto* routing_scroll_area = new QScrollArea(this); routing_scroll_area->setWidgetResizable(true); routing_scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); routing_scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); routing_scroll_area->setFrameShape(QFrame::NoFrame); auto* routing_content_widget = new QWidget(); auto* routing_layout = new QVBoxLayout(routing_content_widget); routing_layout->setContentsMargins(10, 10, 10, 10); routing_layout->setSpacing(5); routing_grid_layout_ = new QGridLayout(); routing_grid_layout_->setContentsMargins(0, 0, 0, 0); routing_grid_layout_->setHorizontalSpacing(10); routing_grid_layout_->setVerticalSpacing(5); routing_grid_layout_->setColumnStretch(0, 1); routing_grid_layout_->setColumnStretch(1, 2); int current_row = 0; #ifdef _WIN32 constexpr char kInfoLabelStyle[] = "color: #111111; font-size: 7pt;"; #elif defined(__APPLE__) constexpr char kInfoLabelStyle[] = "color: #888888; font-size: 9pt;"; #elif defined(__linux__) constexpr char kInfoLabelStyle[] = "color: #111111; font-size: 8pt;"; #endif // Routing #if _WIN32 // DNS Management enable_dns_management_label_ = new QLabel(QObject::tr("Enable advanced DNS management"), this); enable_dns_management_info_label_ = new QLabel( QObject::tr("Enables advanced DNS configuration to prevent leaks. " "Recommended when using split tunneling. Use with caution!"), this); enable_dns_management_info_label_->setWordWrap(true); enable_dns_management_info_label_->setStyleSheet(kInfoLabelStyle); enable_dns_management_info_label_->setMinimumHeight(70); auto* dns_label_container = new QWidget(this); auto* dns_label_layout = new QVBoxLayout(dns_label_container); dns_label_layout->setContentsMargins(0, 0, 0, 0); dns_label_layout->addWidget( enable_dns_management_label_, 0, Qt::AlignLeft | Qt::AlignTop); dns_label_layout->addWidget( enable_dns_management_info_label_, 0, Qt::AlignLeft | Qt::AlignTop); dns_label_layout->addStretch(1); enable_dns_management_checkbox_ = new QCheckBox(" ", this); enable_dns_management_checkbox_->setChecked( settings_->EnableAdvancedDnsManagement()); connect(enable_dns_management_checkbox_, &QCheckBox::toggled, this, [this](bool checked) { settings_->SetEnableAdvancedDnsManagement(checked); }); routing_grid_layout_->addWidget( dns_label_container, current_row, 0, Qt::AlignLeft | Qt::AlignTop); routing_grid_layout_->addWidget(enable_dns_management_checkbox_, current_row, 1, Qt::AlignLeft | Qt::AlignTop); current_row++; #endif blacklist_domains_label_ = new QLabel(QObject::tr("Blacklist domains"), this); blacklist_domains_info_label_ = new QLabel( QObject::tr("Completely block access to the main domain AND all its " "subdomains. Format: domain:example.com (one per line)"), this); blacklist_domains_info_label_->setWordWrap(true); blacklist_domains_info_label_->setMinimumHeight(60); blacklist_domains_info_label_->setStyleSheet(kInfoLabelStyle); auto* blacklist_label_container = new QWidget(this); auto* blacklist_label_layout = new QVBoxLayout(blacklist_label_container); blacklist_label_layout->setContentsMargins(0, 0, 0, 0); blacklist_label_layout->addWidget( blacklist_domains_label_, 0, Qt::AlignLeft | Qt::AlignTop); blacklist_label_layout->addWidget( blacklist_domains_info_label_, 0, Qt::AlignLeft | Qt::AlignTop); blacklist_domains_text_edit_ = new QTextEdit(this); blacklist_domains_text_edit_->setPlainText( VectorToText(settings_->BlacklistDomains())); blacklist_domains_text_edit_->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed); blacklist_domains_text_edit_->setMaximumHeight(60); connect( blacklist_domains_text_edit_, &QTextEdit::textChanged, this, [this]() { settings_->SetBlacklistDomains( TextToVector(blacklist_domains_text_edit_->toPlainText())); }); routing_grid_layout_->addWidget( blacklist_label_container, current_row, 0, Qt::AlignLeft | Qt::AlignTop); routing_grid_layout_->addWidget(blacklist_domains_text_edit_, current_row, 1); routing_grid_layout_->setRowStretch(current_row, 1); current_row++; exclude_tunnel_networks_label_ = new QLabel(QObject::tr("Exclude tunnel networks"), this); exclude_tunnel_networks_info_label_ = new QLabel( QObject::tr("Networks that always bypass VPN tunnel. " "Traffic to these networks goes directly, never through VPN"), this); exclude_tunnel_networks_info_label_->setWordWrap(true); exclude_tunnel_networks_info_label_->setMinimumHeight(60); exclude_tunnel_networks_info_label_->setStyleSheet(kInfoLabelStyle); auto* exclude_label_container = new QWidget(this); auto* exclude_label_layout = new QVBoxLayout(exclude_label_container); exclude_label_layout->setContentsMargins(0, 0, 0, 0); exclude_label_layout->addWidget( exclude_tunnel_networks_label_, 0, Qt::AlignLeft | Qt::AlignTop); exclude_label_layout->addWidget( exclude_tunnel_networks_info_label_, 0, Qt::AlignLeft | Qt::AlignTop); exclude_tunnel_networks_text_edit_ = new QTextEdit(this); exclude_tunnel_networks_text_edit_->setPlainText( VectorToText(settings_->ExcludeTunnelNetworks())); exclude_tunnel_networks_text_edit_->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed); exclude_tunnel_networks_text_edit_->setMaximumHeight(60); connect(exclude_tunnel_networks_text_edit_, &QTextEdit::textChanged, this, [this]() { settings_->SetExcludeTunnelNetworks( TextToVector(exclude_tunnel_networks_text_edit_->toPlainText())); }); routing_grid_layout_->addWidget( exclude_label_container, current_row, 0, Qt::AlignLeft | Qt::AlignTop); routing_grid_layout_->addWidget( exclude_tunnel_networks_text_edit_, current_row, 1); current_row++; include_tunnel_networks_label_ = new QLabel(QObject::tr("Include tunnel networks"), this); include_tunnel_networks_info_label_ = new QLabel( QObject::tr("Networks that always use VPN tunnel. " "Traffic to these networks always goes through VPN"), this); include_tunnel_networks_info_label_->setWordWrap(true); include_tunnel_networks_info_label_->setMinimumHeight(60); include_tunnel_networks_info_label_->setStyleSheet(kInfoLabelStyle); auto* include_label_container = new QWidget(this); auto* include_label_layout = new QVBoxLayout(include_label_container); include_label_layout->setContentsMargins(0, 0, 0, 0); include_label_layout->addWidget( include_tunnel_networks_label_, 0, Qt::AlignLeft | Qt::AlignTop); include_label_layout->addWidget( include_tunnel_networks_info_label_, 0, Qt::AlignLeft | Qt::AlignTop); include_tunnel_networks_text_edit_ = new QTextEdit(this); include_tunnel_networks_text_edit_->setPlainText( VectorToText(settings_->IncludeTunnelNetworks())); include_tunnel_networks_text_edit_->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed); include_tunnel_networks_text_edit_->setMaximumHeight(60); include_tunnel_networks_text_edit_->setPlaceholderText( QObject::tr("192.168.99.0/24")); connect(include_tunnel_networks_text_edit_, &QTextEdit::textChanged, this, [this]() { settings_->SetIncludeTunnelNetworks( TextToVector(include_tunnel_networks_text_edit_->toPlainText())); }); routing_grid_layout_->addWidget( include_label_container, current_row, 0, Qt::AlignLeft | Qt::AlignTop); routing_grid_layout_->addWidget( include_tunnel_networks_text_edit_, current_row, 1); current_row++; enable_split_tunnel_label_ = new QLabel(QObject::tr("Enable split tunnel"), this); enable_split_tunnel_info_label_ = new QLabel(QObject::tr("When enabled, you can configure which sites use " "VPN and which go directly."), this); enable_split_tunnel_info_label_->setWordWrap(true); enable_split_tunnel_info_label_->setMinimumHeight(60); enable_split_tunnel_info_label_->setStyleSheet(kInfoLabelStyle); enable_split_tunnel_info_label_->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed); enable_split_tunnel_info_label_->setFixedHeight(40); auto* enable_split_label_container = new QWidget(this); auto* enable_split_label_layout = new QVBoxLayout(enable_split_label_container); enable_split_label_layout->setContentsMargins(0, 0, 0, 0); enable_split_label_layout->addWidget( enable_split_tunnel_label_, 0, Qt::AlignLeft | Qt::AlignTop); enable_split_label_layout->addWidget( enable_split_tunnel_info_label_, 0, Qt::AlignLeft | Qt::AlignTop); enable_split_label_layout->addStretch(1); enable_split_tunnel_checkbox_ = new QCheckBox(" ", this); enable_split_tunnel_checkbox_->setChecked(settings_->EnableSplitTunnel()); connect(enable_split_tunnel_checkbox_, &QCheckBox::toggled, this, [this](bool checked) { split_tunnel_mode_label_->setVisible(checked); split_tunnel_mode_info_label_->setVisible(checked); split_tunnel_mode_combo_box_->setVisible(checked); split_tunnel_domains_label_->setVisible(checked); split_tunnel_domains_info_label_->setVisible(checked); split_tunnel_domains_text_edit_->setVisible(checked); settings_->SetEnableSplitTunnel(checked); }); routing_grid_layout_->addWidget(enable_split_label_container, current_row, 0, Qt::AlignLeft | Qt::AlignTop); routing_grid_layout_->addWidget(enable_split_tunnel_checkbox_, current_row, 1, Qt::AlignLeft | Qt::AlignTop); current_row++; split_tunnel_mode_label_ = new QLabel(QObject::tr("Split tunnel mode"), this); split_tunnel_mode_info_label_ = new QLabel( QObject::tr("Defines traffic routing strategy for split tunneling."), this); split_tunnel_mode_info_label_->setWordWrap(true); split_tunnel_mode_info_label_->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed); split_tunnel_mode_info_label_->setFixedHeight(40); split_tunnel_mode_info_label_->setStyleSheet(kInfoLabelStyle); auto* split_mode_label_container = new QWidget(this); auto* split_mode_label_layout = new QVBoxLayout(split_mode_label_container); split_mode_label_layout->setContentsMargins(0, 0, 0, 0); split_mode_label_layout->addWidget( split_tunnel_mode_label_, 0, Qt::AlignLeft | Qt::AlignTop); split_mode_label_layout->addWidget( split_tunnel_mode_info_label_, 0, Qt::AlignLeft | Qt::AlignTop); split_mode_label_layout->addStretch(1); split_tunnel_mode_combo_box_ = new QComboBox(this); split_tunnel_mode_combo_box_->addItem( QObject::tr("Exclude"), SettingsModel::kSplitTunnelModeExclude); split_tunnel_mode_combo_box_->addItem( QObject::tr("Include"), SettingsModel::kSplitTunnelModeInclude); split_tunnel_mode_combo_box_->setCurrentText( settings_->SplitTunnelMode() == SettingsModel::kSplitTunnelModeInclude ? QObject::tr("Include") : QObject::tr("Exclude")); connect(split_tunnel_mode_combo_box_, &QComboBox::currentTextChanged, this, [this](const QString& mode) { if (mode == QObject::tr("Include") || mode == SettingsModel::kSplitTunnelModeInclude) { settings_->SetSplitTunnelMode(SettingsModel::kSplitTunnelModeInclude); split_tunnel_domains_label_->setText( QObject::tr("Domains to route through VPN")); split_tunnel_domains_info_label_->setText( QObject::tr("List domains that should use VPN tunnel. " "Only these domains will go through VPN, " "all other traffic bypasses VPN")); } else { settings_->SetSplitTunnelMode(SettingsModel::kSplitTunnelModeExclude); split_tunnel_domains_label_->setText( QObject::tr("Domains to bypass VPN")); split_tunnel_domains_info_label_->setText( QObject::tr("List domains that should bypass VPN tunnel. " "These domains will go directly, " "all other traffic uses VPN")); } }); routing_grid_layout_->addWidget( split_mode_label_container, current_row, 0, Qt::AlignLeft | Qt::AlignTop); routing_grid_layout_->addWidget(split_tunnel_mode_combo_box_, current_row, 1, Qt::AlignLeft | Qt::AlignTop); current_row++; split_tunnel_domains_label_ = new QLabel(this); if (settings_->SplitTunnelMode() == SettingsModel::kSplitTunnelModeInclude) { split_tunnel_domains_label_->setText( QObject::tr("Domains to route through VPN")); } else { split_tunnel_domains_label_->setText(QObject::tr("Domains to bypass VPN")); } split_tunnel_domains_info_label_ = new QLabel(this); if (settings_->SplitTunnelMode() == SettingsModel::kSplitTunnelModeInclude) { split_tunnel_domains_info_label_->setText(QObject::tr( "List domains that should use VPN tunnel. Only these domains will go " "through VPN, all other traffic bypasses VPN")); } else { split_tunnel_domains_info_label_->setText( QObject::tr("List domains that should bypass VPN tunnel. These domains " "will go directly, all other traffic uses VPN")); } split_tunnel_domains_info_label_->setWordWrap(true); split_tunnel_domains_info_label_->setStyleSheet(kInfoLabelStyle); auto* split_domains_label_container = new QWidget(this); auto* split_domains_label_layout = new QVBoxLayout(split_domains_label_container); split_domains_label_layout->setContentsMargins(0, 0, 0, 0); split_domains_label_layout->addWidget( split_tunnel_domains_label_, 0, Qt::AlignLeft | Qt::AlignTop); split_domains_label_layout->addWidget( split_tunnel_domains_info_label_, 0, Qt::AlignLeft | Qt::AlignTop); split_tunnel_domains_text_edit_ = new QTextEdit(this); split_tunnel_domains_text_edit_->setPlainText( VectorToText(settings_->SplitTunnelDomains())); split_tunnel_domains_text_edit_->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding); split_tunnel_domains_text_edit_->setMinimumHeight(80); split_tunnel_domains_text_edit_->setPlaceholderText( QObject::tr("domain:com\ndomain:another.com\ndomain:sub.domainname.com")); connect( split_tunnel_domains_text_edit_, &QTextEdit::textChanged, this, [this]() { settings_->SetSplitTunnelDomains( TextToVector(split_tunnel_domains_text_edit_->toPlainText())); }); routing_grid_layout_->addWidget(split_domains_label_container, current_row, 0, Qt::AlignLeft | Qt::AlignTop); routing_grid_layout_->addWidget( split_tunnel_domains_text_edit_, current_row, 1); routing_grid_layout_->setRowStretch(current_row, 1); current_row++; bool split_enabled = settings_->EnableSplitTunnel(); split_tunnel_mode_label_->setVisible(split_enabled); split_tunnel_mode_info_label_->setVisible(split_enabled); split_tunnel_mode_combo_box_->setVisible(split_enabled); split_tunnel_domains_label_->setVisible(split_enabled); split_tunnel_domains_info_label_->setVisible(split_enabled); split_tunnel_domains_text_edit_->setVisible(split_enabled); routing_layout->addLayout(routing_grid_layout_); routing_layout->addStretch(1); routing_scroll_area->setWidget(routing_content_widget); main_routing_layout->addWidget(routing_scroll_area); tab_widget_->addTab(routing_tab_, QObject::tr("Routing")); // ==================== About Tab ==================== about_tab_ = new QWidget(); auto* main_about_layout = new QVBoxLayout(about_tab_); main_about_layout->setContentsMargins(0, 0, 0, 0); auto* about_scroll_area = new QScrollArea(this); about_scroll_area->setWidgetResizable(true); about_scroll_area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); about_scroll_area->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); about_scroll_area->setFrameShape(QFrame::NoFrame); auto* about_content_widget = new QWidget(); auto* about_layout = new QVBoxLayout(about_content_widget); about_layout->setContentsMargins(5, 5, 5, 5); about_layout->setSpacing(10); auto* fptn_label = new QLabel("FPTN", this); fptn_label->setAlignment(Qt::AlignCenter); about_layout->addWidget(fptn_label); version_label_ = new QLabel( QString(QObject::tr("Version") + ": %1").arg(FPTN_VERSION), this); version_label_->setAlignment(Qt::AlignCenter); about_layout->addWidget(version_label_); project_info_label_ = new QLabel(QObject::tr("FPTN_DESCRIPTION"), this); project_info_label_->setWordWrap(true); project_info_label_->setAlignment(Qt::AlignJustify); about_layout->addWidget(project_info_label_); website_link_label_ = new QLabel(QObject::tr("FPTN_WEBSITE_DESCRIPTION"), this); website_link_label_->setOpenExternalLinks(true); about_layout->addWidget(website_link_label_); telegram_group_label_ = new QLabel(QObject::tr("FPTN_TELEGRAM_DESCRIPTION"), this); telegram_group_label_->setTextFormat(Qt::RichText); telegram_group_label_->setTextInteractionFlags(Qt::TextBrowserInteraction); telegram_group_label_->setOpenExternalLinks(true); about_layout->addWidget(telegram_group_label_); boosty_link_label_ = new QLabel(QObject::tr("Support the project on") + " Boosty", this); boosty_link_label_->setOpenExternalLinks(true); boosty_link_label_->setAlignment(Qt::AlignLeft); about_layout->addWidget(boosty_link_label_); sponsors_label_ = new QLabel(QObject::tr("Project Sponsors"), this); sponsors_label_->setAlignment(Qt::AlignLeft); about_layout->addWidget(sponsors_label_); const QString sponsors_list = " - Brebor
" " - miklefox
" " - usrbb
" " - Secret_Agent_001
" " - ragdollmaster
" " - slimefrozik
" " - HooLigaN
" " - Dima
" " - Kori
" " - DrowASD
" " - GΣG 5952
" " - NikVas
" " - Сергей
" " - Frizgy
" " - Tired Smi1e
" " - Teya Aster
" " - loftynite
" " - vlz78
" " - Erranted
" " - Kotishqua
" " - Neiros
" " - Кабан из AYAYANII
" " - Stasyan
" " - Daptepik
" " - Cu6ic
" " - FC
" " - Temikin
" " - Azeasy
" " - NekoDark
" " - Alin Zoberg
" " - zsergey
" " - OlegPaRe
" " - Norelax
" " - Alligator
" " - vblll
" " - Ayuemin
" " - Semchuk
" " - Barzas153
" " - machinefactor
" " - mahovmm
" " - KOTleta
" " - Arelost
" " - Хмурый
" " - 弐式炎雷
" " - ☸️Bhagos☸️
" " - Pavel
" " - zoomzoom
" " - Kovalskij
" " - Yumarictx
" " - ББ
" " - huko
" " - @UNBREAKABLE189
" " - TheChosenOne
"; sponsors_names_label_ = new QLabel(sponsors_list, this); sponsors_names_label_->setAlignment(Qt::AlignLeft); sponsors_names_label_->setTextInteractionFlags(Qt::TextSelectableByMouse); sponsors_names_label_->setWordWrap(true); about_layout->addWidget(sponsors_names_label_); about_layout->addStretch(1); about_scroll_area->setWidget(about_content_widget); main_about_layout->addWidget(about_scroll_area); tab_widget_->addTab(about_tab_, QObject::tr("About")); // Setup tabs auto* main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(5, 5, 5, 5); main_layout->addWidget(tab_widget_); auto* button_layout = new QHBoxLayout(); button_layout->addStretch(); load_new_token_button_ = new QPushButton(" " + QObject::tr("Add token") + " ", this); connect(load_new_token_button_, &QPushButton::clicked, this, &SettingsWidget::onLoadNewConfig); button_layout->addWidget(load_new_token_button_); exit_button_ = new QPushButton(" " + QObject::tr("Close") + " ", this); connect(exit_button_, &QPushButton::clicked, this, &SettingsWidget::onExit); button_layout->addWidget(exit_button_); main_layout->addLayout(button_layout); setLayout(main_layout); const QVector& services = settings_->Services(); if (server_table_) { server_table_->setRowCount(services.size()); for (int i = 0; i < services.size(); ++i) { const ServiceConfig& service = services[i]; server_table_->setItem(i, 0, new QTableWidgetItem(service.service_name)); server_table_->setItem(i, 1, new QTableWidgetItem(service.username)); QString servers_text_list = ""; for (const auto& s : service.servers) { servers_text_list += QString("%1\n").arg(s.name); } auto* item = new QTableWidgetItem(servers_text_list); item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); item->setFlags(item->flags() | Qt::ItemIsEnabled); item->setData(Qt::DisplayRole, servers_text_list); server_table_->setItem(i, 2, item); auto* delete_button = new QPushButton(QObject::tr("X"), this); delete_button->setFixedSize(24, 24); delete_button->setStyleSheet(R"( QPushButton { background-color: #444444; color: white; border: none; border-radius: 12px; font-weight: bold; padding: 0px; } QPushButton:hover { background-color: #cc0000; } QPushButton:pressed { background-color: #990000; } )"); delete_button->setToolTip(QObject::tr("Delete")); connect(delete_button, &QPushButton::clicked, [this, i]() { onRemoveServer(i); }); auto* button_container = new QWidget(this); auto* action_layout = new QHBoxLayout(button_container); action_layout->setContentsMargins(0, 0, 0, 0); action_layout->setAlignment(Qt::AlignCenter); action_layout->addWidget(delete_button); server_table_->setCellWidget(i, 3, button_container); } } onBypassMethodChanged(bypass_method_combo_box_->currentText()); UpdateSniFilesList(); resize(680, 500); if (tab_widget_) { tab_widget_->setCurrentIndex(0); } UpdateServerTableVisibility(); } // NOLINTNEXTLINE(bugprone-branch-clone) void SettingsWidget::onExit() { settings_->SetUsingNetworkInterface(interface_combo_box_->currentText()); settings_->SetLanguage(language_combo_box_->currentText()); settings_->SetGatewayIp(gateway_line_edit_->text()); settings_->SetSNI(sni_line_edit_->text()); const QString current_method = bypass_method_combo_box_->currentText(); if (current_method == QObject::tr("OBFUSCATION") || current_method == SettingsModel::kBypassMethodObfuscation) { settings_->SetBypassMethod(SettingsModel::kBypassMethodObfuscation); } /* Chrome */ else if (current_method == QObject::tr("SNI-REALITY (Chrome 147)") || current_method == SettingsModel::kBypassMethodSniRealityChrome147) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityChrome147); } else if (current_method == QObject::tr("SNI-REALITY (Chrome 146)") || current_method == SettingsModel::kBypassMethodSniRealityChrome146) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityChrome146); } else if (current_method == QObject::tr("SNI-REALITY (Chrome 145)") || current_method == SettingsModel::kBypassMethodSniRealityChrome145) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityChrome145); } /* Firefox */ else if (current_method == QObject::tr("SNI-REALITY (Firefox 149)") || current_method == SettingsModel::kBypassMethodSniRealityFirefox149) { settings_->SetBypassMethod( SettingsModel::kBypassMethodSniRealityFirefox149); } /* Yandex */ else if (current_method == QObject::tr("SNI-REALITY (Yandex 26)") || current_method == SettingsModel::kBypassMethodSniRealityYandex26) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex26); } /* default else if (current_method == QObject::tr("SNI-REALITY (Yandex 25)") || current_method == SettingsModel::kBypassMethodSniRealityYandex25) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex25); } */ else if (current_method == QObject::tr("SNI-REALITY (Yandex 24)") || current_method == SettingsModel::kBypassMethodSniRealityYandex24) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex24); } /* Safari */ else if (current_method == QObject::tr("SNI-REALITY (Safari 26)") || current_method == SettingsModel::kBypassMethodSniRealitySafari26) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealitySafari26); } /* Default */ else { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex25); } if (!settings_->Save()) { QMessageBox::critical(this, QObject::tr("Save Failed"), QObject::tr("An error occurred while saving the data.")); } this->close(); } void SettingsWidget::onLoadNewConfig() { TokenDialog dialog(this); dialog.exec(); const QString token = dialog.Token(); // show on top show(); activateWindow(); raise(); if (!token.isEmpty()) { try { ServiceConfig config = settings_->ParseToken(token); int exists_index = settings_->GetExistServiceIndex(config.service_name); if (exists_index != -1 && server_table_ != nullptr) { // remove previous settings settings_->RemoveServer(exists_index); server_table_->removeRow(exists_index); } settings_->AddService(config); const bool saving_status = settings_->Save(); if (saving_status && server_table_ != nullptr) { // Insert a new row into the server table const int new_row = server_table_->rowCount(); server_table_->insertRow(new_row); server_table_->setItem( new_row, 0, new QTableWidgetItem(config.service_name)); server_table_->setItem( new_row, 1, new QTableWidgetItem(config.username)); QString servers_text_list = ""; for (const auto& s : config.servers) { servers_text_list += QString("%1\n").arg(s.name); } for (const auto& s : config.censored_zone_servers) { servers_text_list += QString("* %1\n").arg(s.name); } auto* item = new QTableWidgetItem(servers_text_list); item->setTextAlignment(Qt::AlignLeft | Qt::AlignVCenter); item->setFlags(item->flags() | Qt::ItemIsEnabled); item->setData(Qt::DisplayRole, servers_text_list); server_table_->setItem(new_row, 2, item); auto* delete_button = new QPushButton(QObject::tr("X"), this); delete_button->setFixedSize(24, 24); delete_button->setStyleSheet(R"( QPushButton { background-color: #444444; color: white; border: none; border-radius: 12px; font-weight: bold; padding: 0px; } QPushButton:hover { background-color: #cc0000; } QPushButton:pressed { background-color: #990000; } )"); delete_button->setToolTip(QObject::tr("Delete")); auto* button_container = new QWidget(); auto* button_layout = new QHBoxLayout(button_container); button_layout->setContentsMargins(0, 0, 0, 0); button_layout->setAlignment(Qt::AlignCenter); button_layout->addWidget(delete_button); server_table_->setCellWidget(new_row, 3, button_container); QMessageBox::information(this, QObject::tr("Save Successful"), QObject::tr("Data has been successfully saved.")); } else { QMessageBox::critical(this, QObject::tr("Save Failed"), QObject::tr("An error occurred while saving the data.")); } } catch (const std::exception& err) { QMessageBox::critical(this, QObject::tr("Error!"), err.what()); } } UpdateServerTableVisibility(); } void SettingsWidget::onRemoveServer(int row) { if (row >= 0 && row < server_table_->rowCount() && server_table_ != nullptr) { server_table_->removeRow(row); settings_->RemoveServer(row); QMessageBox::information(this, QObject::tr("Delete Successful"), QObject::tr("The data has been successfully removed")); } UpdateServerTableVisibility(); } void SettingsWidget::closeEvent(QCloseEvent* event) { onExit(); event->accept(); // Accept the event to proceed with the closing } void SettingsWidget::onLanguageChanged(const QString&) { // set language settings_->SetLanguage(language_combo_box_->currentText()); fptn::gui::SetTranslation(settings_->LanguageCode()); if (!settings_->Save()) { QMessageBox::critical(this, QObject::tr("Save Failed"), QObject::tr("An error occurred while saving the data.")); } setWindowTitle(QObject::tr("Settings")); if (tab_widget_) { tab_widget_->setTabText(0, QObject::tr("Settings")); tab_widget_->setTabText(1, QObject::tr("Routing")); tab_widget_->setTabText(2, QObject::tr("About")); } if (language_label_) { language_label_->setText(QObject::tr("Language")); } if (interface_label_) { interface_label_->setText(QObject::tr("Network Interface (adapter)")); } if (gateway_label_) { gateway_label_->setText( QObject::tr("Gateway IP Address (typically your router's address)")); } if (server_table_) { server_table_->setHorizontalHeaderLabels({QObject::tr("Name"), QObject::tr("User"), QObject::tr("Servers"), QObject::tr("Action")}); } if (load_new_token_button_) { load_new_token_button_->setText(" " + QObject::tr("Add token") + " "); } if (exit_button_) { exit_button_->setText(" " + QObject::tr("Close") + " "); } if (gateway_auto_checkbox_) { gateway_auto_checkbox_->setText(QObject::tr("Auto")); } // AUTOSTART (show only for Linux) #ifdef __linux__ if (autostart_label_) { autostart_label_->setText(QObject::tr("Autostart")); } #endif // Bypass blocking if (bypass_method_label_) { bypass_method_label_->setText(QObject::tr("Bypass blocking method")); } const auto current_method = settings_->BypassMethod(); if (bypass_method_combo_box_) { bypass_method_combo_box_->clear(); // bypass_method_combo_box_->addItem( // QObject::tr("SNI"), SettingsModel::kBypassMethodSni); bypass_method_combo_box_->addItem( QObject::tr("OBFUSCATION"), SettingsModel::kBypassMethodObfuscation); // bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Generic)"), // SettingsModel::kBypassMethodSniReality); /* Chrome */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Chrome 147)"), SettingsModel::kBypassMethodSniRealityChrome147); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Chrome 146)"), SettingsModel::kBypassMethodSniRealityChrome146); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Chrome 145)"), SettingsModel::kBypassMethodSniRealityChrome145); /* Firefox */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Firefox 149)"), SettingsModel::kBypassMethodSniRealityFirefox149); /* Yandex */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Yandex 26)"), SettingsModel::kBypassMethodSniRealityYandex26); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Yandex 25)"), SettingsModel::kBypassMethodSniRealityYandex25); bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Yandex 24)"), SettingsModel::kBypassMethodSniRealityYandex24); /* Safari */ bypass_method_combo_box_->addItem(QObject::tr("SNI-REALITY (Safari 26)"), SettingsModel::kBypassMethodSniRealitySafari26); /* Chrome */ if (current_method == SettingsModel::kBypassMethodSniRealityChrome147) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Chrome 147)")); } else if (current_method == SettingsModel::kBypassMethodSniRealityChrome146) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Chrome 146)")); } else if (current_method == SettingsModel::kBypassMethodSniRealityChrome145) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Chrome 145)")); } /* Firefox */ else if (current_method == SettingsModel::kBypassMethodSniRealityFirefox149) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Firefox 149)")); } /* Yandex */ else if (current_method == SettingsModel::kBypassMethodSniRealityYandex26) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 26)")); } else if ( current_method == SettingsModel:: kBypassMethodSniRealityYandex25) { // NOLINT(bugprone-branch-clone) bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 25)")); } else if (current_method == SettingsModel::kBypassMethodSniRealityYandex24) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 24)")); } /* Safari */ else if (current_method == SettingsModel::kBypassMethodSniRealitySafari26) { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Safari 26)")); } /* Default */ else { bypass_method_combo_box_->setCurrentText( QObject::tr("SNI-REALITY (Yandex 25)")); } } if (sni_label_) { if (settings_->BypassMethod() == SettingsModel::kBypassMethodSniReality) { sni_label_->setText(QObject::tr("Fake domain to bypass blocking")); } } if (sni_autoscan_button_) { sni_autoscan_button_->setText(QObject::tr("Autoscan SNI")); } if (sni_import_button_) { sni_import_button_->setText(QObject::tr("Import SNI file")); } // Routing tab if (enable_dns_management_label_) { enable_dns_management_label_->setText( QObject::tr("Enable advanced DNS management")); } if (enable_dns_management_info_label_) { enable_dns_management_info_label_->setText(QObject::tr( "Enables advanced DNS configuration to prevent leaks. " "Recommended when using split tunneling. Use with caution!")); } if (blacklist_domains_label_) { blacklist_domains_label_->setText(QObject::tr("Blacklist domains")); } if (blacklist_domains_info_label_) { blacklist_domains_info_label_->setText( QObject::tr("Completely block access to the main domain AND all its " "subdomains. Format: domain:example.com (one per line)")); } if (blacklist_domains_text_edit_) { blacklist_domains_text_edit_->setPlaceholderText( QObject::tr("domain:example.com\ndomain:another.com")); } if (exclude_tunnel_networks_label_) { exclude_tunnel_networks_label_->setText( QObject::tr("Exclude tunnel networks")); } if (exclude_tunnel_networks_info_label_) { exclude_tunnel_networks_info_label_->setText(QObject::tr( "Networks that always bypass VPN tunnel. " "Traffic to these networks goes directly, never through VPN")); } if (include_tunnel_networks_label_) { include_tunnel_networks_label_->setText( QObject::tr("Include tunnel networks")); } if (include_tunnel_networks_info_label_) { include_tunnel_networks_info_label_->setText( QObject::tr("Networks that always use VPN tunnel. " "Traffic to these networks always goes through VPN")); } if (enable_split_tunnel_label_) { enable_split_tunnel_label_->setText(QObject::tr("Enable split tunnel")); } if (enable_split_tunnel_info_label_) { enable_split_tunnel_info_label_->setText( QObject::tr("When enabled, you can configure which sites use VPN and " "which go directly.")); } if (split_tunnel_mode_label_) { split_tunnel_mode_label_->setText(QObject::tr("Split tunnel mode")); } if (split_tunnel_mode_info_label_) { split_tunnel_mode_info_label_->setText( QObject::tr("Defines traffic routing strategy for split tunneling.")); } if (split_tunnel_mode_combo_box_) { QString current_mode = split_tunnel_mode_combo_box_->currentText(); split_tunnel_mode_combo_box_->clear(); split_tunnel_mode_combo_box_->addItem( QObject::tr("Exclude"), SettingsModel::kSplitTunnelModeExclude); split_tunnel_mode_combo_box_->addItem( QObject::tr("Include"), SettingsModel::kSplitTunnelModeInclude); if (current_mode == QObject::tr("Include") || current_mode == SettingsModel::kSplitTunnelModeInclude) { split_tunnel_mode_combo_box_->setCurrentText(QObject::tr("Include")); } else { split_tunnel_mode_combo_box_->setCurrentText(QObject::tr("Exclude")); } } if (split_tunnel_domains_label_ && split_tunnel_domains_info_label_) { if (settings_->SplitTunnelMode() == SettingsModel::kSplitTunnelModeInclude) { split_tunnel_domains_label_->setText( QObject::tr("Domains to route through VPN")); split_tunnel_domains_info_label_->setText(QObject::tr( "List domains that should use VPN tunnel. Only these domains will " "go through VPN, all other traffic bypasses VPN")); } else { split_tunnel_domains_label_->setText( QObject::tr("Domains to bypass VPN")); split_tunnel_domains_info_label_->setText( QObject::tr("List domains that should bypass VPN tunnel. These " "domains will go directly, all other traffic uses VPN")); } } if (split_tunnel_domains_text_edit_) { split_tunnel_domains_text_edit_->setPlaceholderText(QObject::tr( "domain:com\ndomain:another.com\ndomain:sub.domainname.com")); } // about if (version_label_) { version_label_->setText( QString(QObject::tr("Version") + ": %1").arg(FPTN_VERSION)); } if (project_info_label_) { project_info_label_->setText(QObject::tr("FPTN_DESCRIPTION")); } if (website_link_label_) { website_link_label_->setText(QObject::tr("FPTN_WEBSITE_DESCRIPTION")); } if (telegram_group_label_) { telegram_group_label_->setText(QObject::tr("FPTN_TELEGRAM_DESCRIPTION")); } // sponsors section if (boosty_link_label_) { boosty_link_label_->setText( QObject::tr("Support the project on") + " Boosty"); } if (sponsors_label_) { sponsors_label_->setText(QObject::tr("Project Sponsors")); } } void SettingsWidget::onInterfaceChanged(const QString&) { settings_->SetUsingNetworkInterface(interface_combo_box_->currentText()); if (!settings_->Save()) { QMessageBox::critical(this, QObject::tr("Save Failed"), QObject::tr("An error occurred while saving the data.")); } } void SettingsWidget::onAutostartChanged(bool checked) { if (checked) { fptn::gui::autostart::enable(); settings_->SetAutostart(true); } else { fptn::gui::autostart::disable(); settings_->SetAutostart(false); } } void SettingsWidget::onAutoGatewayChanged(bool checked) { if (checked) { gateway_line_edit_->setDisabled(true); gateway_line_edit_->setText(""); settings_->SetGatewayIp("auto"); } else { gateway_line_edit_->setEnabled(true); } } void SettingsWidget::onBypassMethodChanged(const QString& method) { const bool is_reality_mode = /* Chrome */ method == QObject::tr("SNI-REALITY (Chrome 147)") || method == SettingsModel::kBypassMethodSniRealityChrome147 || method == QObject::tr("SNI-REALITY (Chrome 146)") || method == SettingsModel::kBypassMethodSniRealityChrome146 || method == QObject::tr("SNI-REALITY (Chrome 145)") || method == SettingsModel::kBypassMethodSniRealityChrome145 || /* Firefox */ method == QObject::tr("SNI-REALITY (Firefox 149)") || /* Yandex */ method == SettingsModel::kBypassMethodSniRealityFirefox149 || method == QObject::tr("SNI-REALITY (Yandex 26)") || method == SettingsModel::kBypassMethodSniRealityYandex26 || method == QObject::tr("SNI-REALITY (Yandex 25)") || method == SettingsModel::kBypassMethodSniRealityYandex25 || method == QObject::tr("SNI-REALITY (Yandex 24)") || method == SettingsModel::kBypassMethodSniRealityYandex24 || /* Safari */ method == QObject::tr("SNI-REALITY (Safari 26)") || method == SettingsModel::kBypassMethodSniRealitySafari26; // Show/hide SNI field sni_label_->setVisible(is_reality_mode); sni_line_edit_->setVisible(is_reality_mode); // Show/hide SNI files section based on mode sni_files_list_widget_->setVisible(is_reality_mode); sni_autoscan_button_->setVisible(is_reality_mode); sni_import_button_->setVisible(is_reality_mode); if (is_reality_mode) { grid_layout_->addWidget(sni_label_, 5, 0, Qt::AlignLeft | Qt::AlignVCenter); grid_layout_->addWidget(sni_line_edit_, 5, 1, 1, 2); grid_layout_->addLayout(sni_buttons_layout_, 7, 0); grid_layout_->addWidget(sni_files_list_widget_, 7, 1, 1, 2); } else { grid_layout_->removeWidget(sni_label_); grid_layout_->removeWidget(sni_line_edit_); grid_layout_->removeItem(sni_buttons_layout_); grid_layout_->removeWidget(sni_files_list_widget_); } // Set the bypass method if (method == QObject::tr("OBFUSCATION") || method == SettingsModel::kBypassMethodObfuscation) { settings_->SetBypassMethod(SettingsModel::kBypassMethodObfuscation); } /* Chrome */ else if (method == QObject::tr("SNI-REALITY (Chrome 147)") || method == SettingsModel::kBypassMethodSniRealityChrome147) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityChrome147); } else if (method == QObject::tr("SNI-REALITY (Chrome 146)") || method == SettingsModel::kBypassMethodSniRealityChrome146) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityChrome146); } else if (method == QObject::tr("SNI-REALITY (Chrome 145)") || method == SettingsModel::kBypassMethodSniRealityChrome145) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityChrome145); } /* Firefox */ else if (method == QObject::tr("SNI-REALITY (Firefox 149)") || method == SettingsModel::kBypassMethodSniRealityFirefox149) { settings_->SetBypassMethod( SettingsModel::kBypassMethodSniRealityFirefox149); } /* Yandex */ else if (method == QObject::tr("SNI-REALITY (Yandex 26)") || method == SettingsModel::kBypassMethodSniRealityYandex26) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex26); } /* * default else if (method == QObject::tr("SNI-REALITY (Yandex 25)") || method == SettingsModel::kBypassMethodSniRealityYandex25) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex25); } */ else if (method == QObject::tr("SNI-REALITY (Yandex 24)") || method == SettingsModel::kBypassMethodSniRealityYandex24) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex24); } /* Safari */ else if (method == QObject::tr("SNI-REALITY (Safari 26)") || method == SettingsModel::kBypassMethodSniRealitySafari26) { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealitySafari26); } /* Default */ else { settings_->SetBypassMethod(SettingsModel::kBypassMethodSniRealityYandex25); } if (sni_label_) { if (settings_->BypassMethod() != SettingsModel::kBypassMethodSni) { sni_label_->setText(QObject::tr("Fake domain to bypass blocking")); } } } void SettingsWidget::UpdateSniFilesList() { sni_files_list_widget_->clear(); auto files = settings_->SniManager()->SniFileList(); for (const auto& file : files) { QString file_name = QString::fromStdString(file); auto* item_widget = new QWidget(this); auto* layout = new QHBoxLayout(item_widget); layout->setContentsMargins(10, 5, 10, 5); layout->setSpacing(10); auto* name_label = new QLabel(file_name); name_label->setStyleSheet("QLabel { font-weight: bold; }"); name_label->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); layout->addWidget(name_label); item_widget->setLayout(layout); auto* item = new QListWidgetItem(sni_files_list_widget_); item->setSizeHint(item_widget->sizeHint()); sni_files_list_widget_->setItemWidget(item, item_widget); } if (files.empty()) { auto* empty_item = new QListWidgetItem( QObject::tr("No SNI files imported"), sni_files_list_widget_); empty_item->setFlags(empty_item->flags() & ~Qt::ItemIsEnabled); empty_item->setTextAlignment(Qt::AlignCenter); } } void SettingsWidget::onImportSniFile() { QString file_path = QFileDialog::getOpenFileName(this, QObject::tr("Select SNI file"), "", QObject::tr("SNI files (*.sni);;All files (*)")); if (!file_path.isEmpty()) { QFileInfo file_info(file_path); QString file_name = file_info.fileName(); auto existing_files = settings_->SniManager()->SniFileList(); bool file_exists = false; for (const auto& existing_file : existing_files) { if (QString::fromStdString(existing_file) == file_name) { file_exists = true; break; } } if (file_exists) { QMessageBox::StandardButton reply = QMessageBox::question(this, QObject::tr("File exists"), QObject::tr("File \"%1\" already exists. Overwrite?").arg(file_name), QMessageBox::Yes | QMessageBox::No); if (reply != QMessageBox::Yes) { return; } } if (settings_->SniManager()->AddSniFile(file_path.toStdString())) { UpdateSniFilesList(); QMessageBox::information(this, QObject::tr("Success"), QObject::tr("SNI file imported successfully")); } else { QMessageBox::warning( this, QObject::tr("Error"), QObject::tr("Failed to import SNI file")); } } } void SettingsWidget::onAutoscanClicked() { SniAutoscanDialog dialog(settings_, this); dialog.exec(); sni_line_edit_->setText(settings_->SNI()); } void SettingsWidget::UpdateServerTableVisibility() { if (server_table_) { const bool has_data = (server_table_->rowCount() > 0); server_table_->setVisible(has_data); } } ================================================ FILE: src/fptn-client/gui/settingswidget/settings.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include "gui/settingsmodel/settingsmodel.h" namespace fptn::gui { class SettingsWidget : public QDialog { Q_OBJECT public: explicit SettingsWidget(SettingsModelPtr settings, QWidget* parent = nullptr); protected: void closeEvent(QCloseEvent* event) override; protected: void SetupUi(); void UpdateSniFilesList(); void UpdateServerTableVisibility(); // cppcheck-suppress unknownMacro private slots: void onExit(); void onLoadNewConfig(); void onRemoveServer(int row); void onLanguageChanged(const QString& new_language); void onInterfaceChanged(const QString& new_language); void onAutostartChanged(bool checked); void onAutoGatewayChanged(bool checked); void onBypassMethodChanged(const QString& method); // cppcheck-suppress unknownMacro private slots: void onImportSniFile(); void onAutoscanClicked(); private: SettingsModelPtr settings_; QTabWidget* tab_widget_ = nullptr; QWidget* settings_tab_ = nullptr; QWidget* routing_tab_ = nullptr; QWidget* about_tab_ = nullptr; QTableWidget* server_table_ = nullptr; // AUTOSTART (show only for Linux) #ifdef __linux__ QLabel* autostart_label_ = nullptr; QCheckBox* autostart_checkbox_ = nullptr; #endif QLabel* language_label_ = nullptr; QComboBox* language_combo_box_ = nullptr; QGridLayout* grid_layout_ = nullptr; QGridLayout* routing_grid_layout_ = nullptr; QComboBox* interface_combo_box_ = nullptr; QLabel* interface_label_ = nullptr; QLineEdit* gateway_line_edit_ = nullptr; QCheckBox* gateway_auto_checkbox_ = nullptr; QLabel* gateway_label_ = nullptr; QLabel* bypass_method_label_ = nullptr; QComboBox* bypass_method_combo_box_ = nullptr; QHBoxLayout* sni_buttons_layout_ = nullptr; QLabel* sni_label_ = nullptr; QLineEdit* sni_line_edit_ = nullptr; QListWidget* sni_files_list_widget_ = nullptr; QPushButton* sni_import_button_ = nullptr; QPushButton* sni_autoscan_button_ = nullptr; // New fields widgets for routing tab QLabel* enable_dns_management_label_ = nullptr; QLabel* enable_dns_management_info_label_ = nullptr; QCheckBox* enable_dns_management_checkbox_ = nullptr; QLabel* blacklist_domains_label_ = nullptr; QLabel* blacklist_domains_info_label_ = nullptr; QTextEdit* blacklist_domains_text_edit_ = nullptr; QLabel* exclude_tunnel_networks_label_ = nullptr; QLabel* exclude_tunnel_networks_info_label_ = nullptr; QTextEdit* exclude_tunnel_networks_text_edit_ = nullptr; QLabel* include_tunnel_networks_label_ = nullptr; QLabel* include_tunnel_networks_info_label_ = nullptr; QTextEdit* include_tunnel_networks_text_edit_ = nullptr; QLabel* enable_split_tunnel_label_ = nullptr; QLabel* enable_split_tunnel_info_label_ = nullptr; QCheckBox* enable_split_tunnel_checkbox_ = nullptr; QLabel* split_tunnel_mode_label_ = nullptr; QLabel* split_tunnel_mode_info_label_ = nullptr; QComboBox* split_tunnel_mode_combo_box_ = nullptr; QLabel* split_tunnel_domains_label_ = nullptr; QLabel* split_tunnel_domains_info_label_ = nullptr; QTextEdit* split_tunnel_domains_text_edit_ = nullptr; QPushButton* load_new_token_button_ = nullptr; QPushButton* exit_button_ = nullptr; QLabel* version_label_ = nullptr; QLabel* project_info_label_ = nullptr; QLabel* website_link_label_ = nullptr; QLabel* telegram_group_label_ = nullptr; QLabel* boosty_link_label_ = nullptr; QLabel* sponsors_label_ = nullptr; QLabel* sponsors_names_label_ = nullptr; }; } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/sni_autoscan_dialog/sni_autoscan_dialog.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/sni_autoscan_dialog/sni_autoscan_dialog.h" #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "fptn-protocol-lib/https/api_client/api_client.h" namespace fptn::gui { SniAutoscanDialog::SniAutoscanDialog(SettingsModelPtr settings, QWidget* parent) : QDialog(parent), settings_(std::move(settings)) { SetupUi(); } SniAutoscanDialog::~SniAutoscanDialog() { StopScanning(); } void SniAutoscanDialog::SetupUi() { setMinimumSize(650, 400); setWindowTitle(QObject::tr("Autoscan SNI")); setModal(true); auto* main_layout = new QVBoxLayout(this); main_layout->setSpacing(3); main_layout->setContentsMargins(3, 3, 3, 3); auto* top_layout = new QHBoxLayout(this); top_layout->setAlignment(Qt::AlignVCenter); server_combo_box_ = new QComboBox(this); server_combo_box_->addItem(QObject::tr("All")); const QVector& services = settings_->Services(); for (const auto& service : services) { for (const auto& server : service.servers) { server_combo_box_->addItem(server.name); } for (const auto& server : service.censored_zone_servers) { server_combo_box_->addItem("* " + server.name); } } sni_file_combo_box_ = new QComboBox(this); sni_file_combo_box_->addItem(QObject::tr("All")); auto sni_files = settings_->SniManager()->SniFileList(); for (const auto& file : sni_files) { sni_file_combo_box_->addItem(QString::fromStdString(file)); } progress_label_ = new QLabel("0/0", this); progress_label_->setMinimumWidth(80); progress_label_->setAlignment(Qt::AlignCenter); start_stop_button_ = new QPushButton(QObject::tr("Start"), this); connect(start_stop_button_, &QPushButton::clicked, this, &SniAutoscanDialog::onStartStopClicked); close_button_ = new QPushButton(QObject::tr("Close"), this); connect(close_button_, &QPushButton::clicked, this, [this]() { if (is_scanning_) { StopScanning(); } reject(); }); top_layout->addWidget(server_combo_box_); top_layout->addWidget(sni_file_combo_box_); top_layout->addWidget(progress_label_); top_layout->addWidget(start_stop_button_); top_layout->addWidget(close_button_); log_text_edit_ = new QTextEdit(this); log_text_edit_->setReadOnly(true); log_text_edit_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); QFont log_font = QFontDatabase::systemFont(QFontDatabase::FixedFont); log_font.setPointSize(8); log_text_edit_->setFont(log_font); connect(log_text_edit_->verticalScrollBar(), &QScrollBar::rangeChanged, this, [this](int min, int max) { (void)min; if (auto_scroll_ && is_scanning_) { log_text_edit_->verticalScrollBar()->setValue(max); } }); connect(log_text_edit_->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) { auto_scroll_ = (value == log_text_edit_->verticalScrollBar()->maximum()); }); main_layout->addLayout(top_layout); main_layout->addWidget(log_text_edit_, 1); progress_timer_ = new QTimer(this); connect(progress_timer_, &QTimer::timeout, this, &SniAutoscanDialog::onUpdateProgress); } void SniAutoscanDialog::onStartStopClicked() { if (!is_scanning_) { StartScanning(); } else { StopScanning(); } } void SniAutoscanDialog::onUpdateProgress() { if (!is_scanning_) { return; } const auto total = sni_vector_.size(); const auto tested = static_cast(tested_count_); if (total > 0) { progress_label_->setText(QString("%1/%2").arg(tested).arg(total)); } else { progress_label_->setText("0/0"); } if (working_sni_found_ > 0 && !found_working_sni_.empty()) { StopScanning(); settings_->SetSNI(QString::fromStdString(found_working_sni_)); settings_->Save(); QMessageBox::information(this, QObject::tr("Scan completed"), QObject::tr("Working SNI found: %1") .arg(QString::fromStdString(found_working_sni_))); return; } if (static_cast(tested_count_) >= sni_vector_.size()) { StopScanning(); if (working_sni_found_ == 0) { QMessageBox::information(this, QObject::tr("Scan completed"), QObject::tr("No working SNI found.")); } } } void SniAutoscanDialog::StartScanning() { if (sni_file_combo_box_->currentText() == QObject::tr("All")) { sni_vector_ = CollectAllSni(); } else { sni_vector_ = CollectSniFromSelectedFile(); } if (sni_vector_.empty()) { QMessageBox::warning(this, QObject::tr("Error"), QObject::tr("No SNI available for scanning.")); return; } target_servers_ = CollectTargetServers(); if (target_servers_.isEmpty()) { QMessageBox::warning(this, QObject::tr("Error"), QObject::tr("No servers available for scanning.")); return; } std::random_device rd; std::mt19937 g(rd()); std::ranges::shuffle(sni_vector_, g); is_scanning_ = true; stop_requested_ = false; tested_count_ = 0; working_sni_found_ = 0; current_sni_index_ = 0; found_working_sni_.clear(); start_stop_button_->setText(QObject::tr("Cancel")); server_combo_box_->setEnabled(false); sni_file_combo_box_->setEnabled(false); progress_label_->setText("0/0"); constexpr int kThreadCount = 8; for (int i = 0; i < kThreadCount; ++i) { worker_threads_.emplace_back(&SniAutoscanDialog::WorkerThread, this, i); } progress_timer_->start(100); } void SniAutoscanDialog::StopScanning() { if (!is_scanning_) return; stop_requested_ = true; for (auto& thread : worker_threads_) { if (thread.joinable()) { thread.join(); } } worker_threads_.clear(); is_scanning_ = false; progress_timer_->stop(); start_stop_button_->setText(QObject::tr("Start")); server_combo_box_->setEnabled(true); sni_file_combo_box_->setEnabled(true); } std::string SniAutoscanDialog::GetNextSni() { const std::unique_lock lock(mutex_); if (current_sni_index_ >= sni_vector_.size()) { return std::string(); } return sni_vector_[current_sni_index_++]; } void SniAutoscanDialog::WorkerThread(int thread_id) { (void)thread_id; while (!stop_requested_) { std::string sni = GetNextSni(); if (sni.empty()) { break; } bool sni_works = false; for (const auto& server : target_servers_) { if (stop_requested_) { break; } bool handshake_ok = false; bool http_ok = false; constexpr int kHandshakeTimeout = 2; fptn::protocol::https::ApiClient client(server.host.toStdString(), server.port, sni, server.md5_fingerprint.toStdString(), protocol::https::CensorshipStrategy::kSni); handshake_ok = client.TestHandshake(kHandshakeTimeout); if (handshake_ok) { constexpr int kHttpTimeout = 5; const auto response = client.Get("/api/v1/dns", kHttpTimeout); http_ok = (response.code == 200); if (http_ok) { sni_works = true; } } AddLogEntry( server.name, QString::fromStdString(sni), handshake_ok, http_ok); if (sni_works) { break; } } ++tested_count_; if (sni_works) { found_working_sni_ = sni; ++working_sni_found_; break; } } } std::vector SniAutoscanDialog::CollectAllSni() const { std::vector all_sni; auto files = settings_->SniManager()->SniFileList(); for (const auto& file : files) { auto sni_list = settings_->SniManager()->GetSniList(file); for (const auto& sni : sni_list) { all_sni.push_back(sni); } } return all_sni; } std::vector SniAutoscanDialog::CollectSniFromSelectedFile() const { std::vector sni_list; QString selected_file = sni_file_combo_box_->currentText(); if (selected_file != QObject::tr("All")) { sni_list = settings_->SniManager()->GetSniList(selected_file.toStdString()); } return sni_list; } QVector SniAutoscanDialog::CollectTargetServers() const { QVector servers; QString selected = server_combo_box_->currentText(); const QVector& services = settings_->Services(); if (selected == QObject::tr("All")) { for (const auto& service : services) { for (const auto& server : service.servers) { servers.append(server); } for (const auto& server : service.censored_zone_servers) { servers.append(server); } } } else { for (const auto& service : services) { for (const auto& server : service.servers) { if (server.name == selected) { servers.append(server); break; } } // specific servers QString clean_name = selected; if (clean_name.startsWith("* ")) { clean_name = clean_name.mid(2); } for (const auto& server : service.censored_zone_servers) { if (server.name == clean_name) { servers.append(server); break; } } } } return servers; } void SniAutoscanDialog::AddLogEntry(const QString& server, const QString& sni, bool handshake_ok, bool http_ok) { QMetaObject::invokeMethod(this, [this, server, sni, handshake_ok, http_ok]() { const QString handshake_status = handshake_ok ? QString("YES") : QString("NO"); const QString http_status = http_ok ? QString("YES") : QString("NO"); const QString log_entry = QString(R"(
%1 %2 Handshake: %3 HTTP: %4
)") .arg(server.toHtmlEscaped()) .arg(sni.toHtmlEscaped()) .arg(handshake_status) .arg(http_status); { const std::unique_lock lock(mutex_); constexpr int kMaxLogSize = 2048; if (log_text_edit_->document()->lineCount() > kMaxLogSize) { QTextCursor cursor(log_text_edit_->document()); cursor.movePosition(QTextCursor::Start); cursor.select(QTextCursor::LineUnderCursor); cursor.removeSelectedText(); } log_text_edit_->moveCursor(QTextCursor::End); log_text_edit_->insertHtml(log_entry); QTextCursor cursor(log_text_edit_->textCursor()); cursor.movePosition(QTextCursor::End); log_text_edit_->setTextCursor(cursor); } }); } } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/sni_autoscan_dialog/sni_autoscan_dialog.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h" #include "gui/settingsmodel/settingsmodel.h" namespace fptn::gui { class SniAutoscanDialog : public QDialog { Q_OBJECT public: explicit SniAutoscanDialog( SettingsModelPtr settings, QWidget* parent = nullptr); ~SniAutoscanDialog() override; // cppcheck-suppress unknownMacro public slots: void onStartStopClicked(); void onUpdateProgress(); protected: void SetupUi(); void StartScanning(); void StopScanning(); void WorkerThread(int thread_id); std::vector CollectAllSni() const; std::vector CollectSniFromSelectedFile() const; QVector CollectTargetServers() const; std::string GetNextSni(); void AddLogEntry(const QString& server, const QString& sni, bool handshake_ok, bool http_ok); private: mutable std::mutex mutex_; std::vector sni_vector_; std::size_t current_sni_index_ = 0; std::atomic is_scanning_{false}; std::atomic stop_requested_{false}; std::atomic tested_count_{0}; std::atomic working_sni_found_{0}; std::string found_working_sni_; QVector target_servers_; std::vector worker_threads_; SettingsModelPtr settings_; QComboBox* server_combo_box_ = nullptr; QComboBox* sni_file_combo_box_ = nullptr; QLabel* progress_label_ = nullptr; QPushButton* start_stop_button_ = nullptr; QPushButton* close_button_ = nullptr; QTextEdit* log_text_edit_ = nullptr; QTimer* progress_timer_ = nullptr; bool auto_scroll_ = true; }; } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/sni_manager/sni_manager.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/sni_manager/sni_manager.h" #include #include #include #include #include #include namespace fptn::gui { SNIManager::SNIManager(std::string sni_folder) : sni_folder_(std::move(sni_folder)) { std::error_code ec; std::filesystem::create_directories(sni_folder_, ec); } std::vector SNIManager::SniFileList() const { std::vector result; std::error_code ec; auto dir_iter = std::filesystem::directory_iterator(sni_folder_, ec); if (ec) { return {}; } for (const auto& entry : dir_iter) { if (entry.is_regular_file() && entry.path().extension() == ".sni") { result.push_back(entry.path().filename().string()); } } std::ranges::sort(result); return result; } bool SNIManager::AddSniFile(const std::string& path) { std::error_code ec; if (!std::filesystem::exists(path, ec) || ec) { return false; } std::filesystem::path source(path); std::filesystem::path dest = sni_folder_ + "/" + source.filename().string(); std::filesystem::copy_file( source, dest, std::filesystem::copy_options::overwrite_existing, ec); return !ec; } bool SNIManager::RemoveFile(const std::string& file_name) { std::filesystem::path file_path = sni_folder_ + "/" + file_name; std::error_code ec; return std::filesystem::remove(file_path, ec) && !ec; } std::vector SNIManager::GetSniList( const std::string& file_name) const { std::vector result; std::filesystem::path file_path = sni_folder_ + "/" + file_name; std::ifstream file(file_path); if (!file.is_open()) { return result; } std::string line; while (std::getline(file, line)) { if (!line.empty()) { result.push_back(line); } } return result; } } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/sni_manager/sni_manager.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include namespace fptn::gui { class SNIManager { public: explicit SNIManager(std::string sni_folder); std::vector SniFileList() const; bool AddSniFile(const std::string& path); bool RemoveFile(const std::string& file_name); std::vector GetSniList(const std::string& file_name) const; private: const std::string sni_folder_; }; using SNIManagerSPtr = std::shared_ptr; } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/speedwidget/speedwidget.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/speedwidget/speedwidget.h" using fptn::gui::SpeedWidget; namespace { QString FormatSpeed(std::size_t bytes_per_sec) { QString speed_str; double bits_per_sec = bytes_per_sec * 8.0; if (bits_per_sec >= 1e9) { speed_str = QString::asprintf("%.2f Gbps", bits_per_sec / 1e9); } else if (bits_per_sec >= 1e6) { speed_str = QString::asprintf("%.2f Mbps", bits_per_sec / 1e6); } else if (bits_per_sec >= 1e3) { speed_str = QString::asprintf("%.2f Kbps", bits_per_sec / 1e3); } else { speed_str = QString::asprintf("%.2f bps", bits_per_sec); } if (speed_str.size() >= 20) { return speed_str; } return speed_str.leftJustified(25); } QString FormatSpeedLabel(const QString& text, std::size_t speed) { return " " + text + ": " + FormatSpeed(speed); } } // namespace SpeedWidget::SpeedWidget(QWidget* parent) : QWidget(parent), upload_speed_label_( new QLabel(FormatSpeedLabel(QObject::tr("Upload speed"), 0), this)), download_speed_label_(new QLabel( FormatSpeedLabel(QObject::tr("Download speed"), 0), this)) { auto* layout = new QVBoxLayout(); layout->setContentsMargins(4, 4, 4, 4); layout->addWidget(download_speed_label_); layout->addWidget(upload_speed_label_); setLayout(layout); } void SpeedWidget::UpdateSpeed( std::size_t upload_speed, std::size_t download_speed) { upload_speed_label_->setText( FormatSpeedLabel(QObject::tr("Upload speed"), upload_speed)); download_speed_label_->setText( FormatSpeedLabel(QObject::tr("Download speed"), download_speed)); } ================================================ FILE: src/fptn-client/gui/speedwidget/speedwidget.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) namespace fptn::gui { class SpeedWidget : public QWidget { Q_OBJECT public: explicit SpeedWidget(QWidget* parent = nullptr); void UpdateSpeed(std::size_t upload_speed, std::size_t download_speed); private: QLabel* upload_speed_label_; QLabel* download_speed_label_; }; } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/style/style.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/style/style.h" namespace fptn::gui { QString GetMacStyleSheet() { static const QString kStyleSheet = R"( QMenu { background-color: #333; color: #fff; border: 1px solid #555; } QMenu::item { background-color: #333; color: #fff; padding: 5px 5px; } QMenu::item:selected { background-color: #555; color: #fff; } QMenu::icon { margin-right: 4px; } QAction { padding: 2px 2px; color: #fff; } QWidgetAction { padding: 5px; } )"; return kStyleSheet; } QString GetUbuntuStyleSheet() { static const QString kStyleSheet = R"( QMenu { background-color: #ffffff; color: #333333; border: 1px solid #d0d0d0; border-radius: 8px; padding: 5px; } QMenu::item { background-color: #ffffff; color: #333333; padding: 2px 3px; border-radius: 4px; } QMenu::item:selected { background-color: #e0e0e0; color: #333333; } QMenu::item:hover { background-color: #e0e0e0; } QMenu::icon { margin-right: 4px; } QAction { color: #333333; } QWidgetAction { padding: 2px 4px; } QWidget { font-family: 'Ubuntu', 'Segoe UI', Tahoma, Verdana, Arial, sans-serif; font-size: 10pt; color: #333333; background-color: #f0f0f0; } QPushButton { background-color: #ffffff; color: #333333; border: 1px solid #d0d0d0; border-radius: 4px; padding: 6px 12px; } QPushButton:hover { background-color: #e0e0e0; } QPushButton:pressed { background-color: #d0d0d0; } QLineEdit, QTextEdit { background-color: #ffffff; color: #333333; border: 1px solid #d0d0d0; border-radius: 4px; padding: 4px 8px; } QCheckBox, QRadioButton { color: #333333; } QSlider::groove:horizontal { border: 1px solid #d0d0d0; height: 8px; background: #ffffff; border-radius: 4px; } QSlider::handle:horizontal { background: #333333; border: 1px solid #d0d0d0; width: 16px; border-radius: 4px; } QScrollBar:vertical { border: 1px solid #d0d0d0; background: #ffffff; width: 16px; } QScrollBar::handle:vertical { background: #c0c0c0; min-height: 20px; border-radius: 8px; } QTabBar::tab { background: #e0e0e0; color: #333333; padding: 6px 12px; border: 1px solid #d0d0d0; border-bottom: 1px solid #ffffff; border-radius: 4px 4px 0 0; } QTabBar::tab:selected { background: #ffffff; color: #333333; border: 1px solid #d0d0d0; border-bottom: 1px solid #ffffff; border-radius: 4px 4px 0 0; font-weight: bold; } QTabBar::tab:!selected { background: #f0f0f0; } QTabWidget::pane { border: 1px solid #d0d0d0; border-radius: 4px; background: #ffffff; } QMenu::item:disabled { background-color: #ffffff; color: #a0a0a0; } QAction:disabled { color: #a0a0a0; } )"; return kStyleSheet; } QString GetWindowsStyleSheet() { static const QString kStyleSheet = R"( QMenu { background-color: #ffffff; color: #000000; border: 1px solid #bfbfbf; border-radius: 4px; padding: 5px; } QPushButton { padding: 6px 12px; } QMenu::item { background-color: #ffffff; color: #000000; padding: 2px 1px; border-radius: 3px; } QMenu::item:selected { background-color: #e0e0e0; color: #000000; } QMenu::item:hover { background-color: #e0e0e0; } QMenu::icon { margin-right: 4px; } QAction { color: #000000; } QMenu QWidget { background-color: #ffffff; color: #000000; border: none; padding: 1px 4px; } QMenu::item:disabled { background-color: #ffffff; color: #a0a0a0; } QAction:disabled { color: #a0a0a0; } )"; return kStyleSheet; } } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/style/style.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include namespace fptn::gui { QString GetMacStyleSheet(); QString GetUbuntuStyleSheet(); QString GetWindowsStyleSheet(); } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/tokendialog/tokendialog.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/tokendialog/tokendialog.h" #include #include #include // NOLINT(build/include_order) #include "common/utils/base64.h" #include "common/utils/utils.h" #include "utils/brotli/brotli.h" #include "utils/utils.h" using fptn::gui::TokenDialog; TokenDialog::TokenDialog(QWidget* parent) : QDialog(parent) { setWindowTitle("Token"); label_ = new QLabel(QObject::tr("Paste your token") + ": ", this); token_field_ = new QLineEdit(this); token_field_->setPlaceholderText(QObject::tr("Token") + "..."); token_field_->setMinimumWidth(350); token_layout_ = new QHBoxLayout(); token_layout_->addWidget(label_); token_layout_->addWidget(token_field_); save_button_ = new QPushButton(QObject::tr("Save"), this); cancel_button_ = new QPushButton(QObject::tr("Cancel"), this); // Layout for buttons button_layout_ = new QHBoxLayout(); button_layout_->addStretch(); button_layout_->addWidget(save_button_); button_layout_->addWidget(cancel_button_); // Main layout main_layout_ = new QVBoxLayout(this); main_layout_->addLayout(token_layout_); main_layout_->addLayout(button_layout_); setLayout(main_layout_); connect(cancel_button_, &QPushButton::clicked, this, &QDialog::reject); connect(save_button_, &QPushButton::clicked, this, &TokenDialog::onOkClicked); // show on top setWindowFlags(Qt::Window | Qt::WindowStaysOnTopHint); setModal(true); show(); activateWindow(); raise(); setWindowModality(Qt::ApplicationModal); } const QString& TokenDialog::Token() const { return token_; } void TokenDialog::onOkClicked() { try { const std::string entered_token = token_field_->text().trimmed().toStdString(); const std::string token = fptn::common::utils::RemoveSubstring( entered_token, {" ", "\n", "\r", "\t"}); std::string decoded_token; if (token.starts_with("fptnb:") || token.starts_with("fptnb//")) { const std::string clean_token = common::utils::RemoveSubstring(token, {"fptnb:", "fptnb//"}); decoded_token = fptn::utils::brotli::Decompress( fptn::common::utils::base64::decode(clean_token)); } else { const std::string clean_token = fptn::common::utils::RemoveSubstring( entered_token, {"fptn:", "fptn://"}); decoded_token = fptn::common::utils::base64::decode(clean_token); } const QString t = QString::fromStdString(decoded_token); if (t.isEmpty()) { QMessageBox::warning(this, QObject::tr("Validation Error"), QObject::tr("Token cannot be empty") + "!"); } else { token_ = t; accept(); } } catch (const std::runtime_error& err) { SPDLOG_WARN("Wrong token: {}", err.what()); QMessageBox::warning(this, QObject::tr("Wrong token"), QObject::tr("Wrong token") + ": " + err.what()); } } ================================================ FILE: src/fptn-client/gui/tokendialog/tokendialog.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) namespace fptn::gui { class TokenDialog final : public QDialog { Q_OBJECT public: explicit TokenDialog(QWidget* parent = nullptr); const QString& Token() const; // cppcheck-suppress unknownMacro private slots: void onOkClicked(); private: QLabel* label_; QLineEdit* token_field_; QHBoxLayout* token_layout_; QPushButton* save_button_; QPushButton* cancel_button_; QHBoxLayout* button_layout_; QVBoxLayout* main_layout_; QString token_; }; } // namespace fptn::gui ================================================ FILE: src/fptn-client/gui/translations/translations.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/translations/translations.h" #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/logger/logger.h" namespace { QTranslator& getTranslator() { static QTranslator translator; return translator; } } // namespace bool fptn::gui::SetTranslation(const QString& language_code) { const QString translation_file = QString("fptn_%1.qm").arg(language_code); QTranslator& translator = getTranslator(); qApp->removeTranslator(&translator); if (translator.load(translation_file, ":/translations")) { if (!qApp->installTranslator(&translator)) { SPDLOG_WARN("Failed to install translator for language: {}", language_code.toStdString()); } else { return true; } } else { SPDLOG_WARN( "Translation file not found: {}", translation_file.toStdString()); } return false; } ================================================ FILE: src/fptn-client/gui/translations/translations.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include // NOLINT(build/include_order) namespace fptn::gui { bool SetTranslation(const QString& language_code); } ================================================ FILE: src/fptn-client/gui/tray/tray.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "gui/tray/tray.h" #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ip_packet.h" #include "common/system/command.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h" #include "fptn-protocol-lib/time/time_provider.h" #include "gui/autoupdate/autoupdate.h" #include "gui/server_menu_item_widget/server_menu_item_widget.h" #include "gui/style/style.h" #include "gui/translations/translations.h" #include "plugins/blacklist/domain_blacklist.h" #ifdef _WIN32 #include "utils/windows/vpn_conflict.h" #endif using fptn::gui::TrayApp; namespace { QPixmap LoadIcon(const QString& icon_path, int size = 12) { QPixmap pixmap(icon_path); if (pixmap.isNull()) { return QPixmap(); } return pixmap.scaled( size, size, Qt::KeepAspectRatio, Qt::SmoothTransformation); } void ShowError(const QString& title, const QString& msg) { QMessageBox msg_box; msg_box.setWindowIcon(QIcon(":/icons/app.ico")); msg_box.setIcon(QMessageBox::Critical); msg_box.setWindowTitle(title); #ifdef _WIN32 msg_box.setText(msg.toUtf8()); #else msg_box.setText(msg); #endif msg_box.exec(); } #ifdef _WIN32 void ShowWarning(const QString& title, const QString& msg) { QMessageBox msg_box; msg_box.setWindowIcon(QIcon(":/icons/app.ico")); msg_box.setIcon(QMessageBox::Warning); msg_box.setWindowTitle(title); msg_box.setText(msg); msg_box.exec(); } #endif } // namespace TrayApp::TrayApp(const SettingsModelPtr& settings, QObject* parent) : settings_(settings), tray_icon_(new QSystemTrayIcon(this)), tray_menu_(new QMenu(this)), connect_menu_(new QMenu(QObject::tr("Connect") + " ", tray_menu_)), speed_widget_(new SpeedWidget(tray_menu_)), update_timer_(new QTimer(this)), active_icon_path_(":/icons/active.ico"), inactive_icon_path_(":/icons/inactive.ico") { (void)parent; #ifdef __linux__ qApp->setStyleSheet(fptn::gui::GetUbuntuStyleSheet()); #elif __APPLE__ qApp->setStyleSheet(fptn::gui::GetMacStyleSheet()); #elif _WIN32 qApp->setStyleSheet(fptn::gui::GetWindowsStyleSheet()); #else #error "Unsupported system!" #endif #if __linux__ connect(tray_icon_, &QSystemTrayIcon::activated, [this](QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Context) { tray_menu_->popup(tray_icon_->geometry().bottomLeft()); } else { tray_menu_->close(); } }); #elif _WIN32 connect(tray_icon_, &QSystemTrayIcon::activated, [this](QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Context) { tray_menu_->show(); tray_menu_->exec(QCursor::pos()); } else { tray_menu_->close(); } }); #endif const QString selected_language = settings->LanguageCode(); if (selected_language.isEmpty()) { // save default language for first start const QString system_language = GetSystemLanguageCode(); if (settings->ExistsTranslation(system_language)) { settings->SetLanguageCode(system_language); } else { settings->SetLanguageCode(settings->DefaultLanguageCode()); } } else { fptn::gui::SetTranslation(selected_language); } // State connect(this, &TrayApp::defaultState, this, &TrayApp::handleDefaultState); connect(this, &TrayApp::connecting, this, &TrayApp::handleConnecting); connect(this, &TrayApp::connected, this, &TrayApp::handleConnected); connect(this, &TrayApp::disconnecting, this, &TrayApp::handleDisconnecting); connect(this, &TrayApp::vpnStarted, this, &TrayApp::handleVpnStarted); // Show connection... label connecting_label_action_ = new QAction(QObject::tr("Connecting..."), this); connecting_label_action_->setIcon(LoadIcon(":/icons/menu_connection.png")); connect(connecting_label_action_, &QAction::triggered, this, &TrayApp::handleDefaultState); // Show Disconnecting... label disconnecting_label_action_ = new QAction(QObject::tr("Disconnecting..."), this); // Disconect disconnect_action_ = new QAction(QObject::tr("Disconnect"), this); disconnect_action_->setIcon(LoadIcon(":/icons/menu_disconnect.png")); connect(disconnect_action_, &QAction::triggered, this, &TrayApp::onDisconnectFromServer); speed_widget_action_ = new QWidgetAction(this); speed_widget_action_->setDefaultWidget(speed_widget_); // Settings connect(settings_.get(), &SettingsModel::dataChanged, this, &TrayApp::UpdateTrayMenu); connect(update_timer_, &QTimer::timeout, this, &TrayApp::handleTimer); update_timer_->start(1000); // Settings settings_action_ = new QAction(QObject::tr("Settings"), this); settings_action_->setIcon(LoadIcon(":/icons/menu_settings.png")); connect( settings_action_, &QAction::triggered, this, &TrayApp::onShowSettings); // Autoupdate auto_update_action_ = new QAction( QObject::tr("New version available") + " " + auto_available_version_, this); auto_update_action_->setIcon( LoadIcon(":/icons/menu_new_version_download.png")); connect(auto_update_action_, &QAction::triggered, this, [this] { OpenWebBrowser(FPTN_GITHUB_PAGE_LINK); }); auto_update_action_->setVisible(false); // Quit quit_action_ = new QAction(QObject::tr("Quit"), this); quit_action_->setIcon(LoadIcon(":/icons/menu_exit.png")); connect(quit_action_, &QAction::triggered, this, &QCoreApplication::quit); // Show menu // tray_menu_->addAction(connecting_action_); tray_menu_->addAction(disconnect_action_); tray_menu_->addAction(connecting_label_action_); tray_menu_->addAction(disconnecting_label_action_); tray_menu_->addAction(speed_widget_action_); tray_menu_->addSeparator(); tray_menu_->addAction(settings_action_); tray_menu_->addSeparator(); tray_menu_->addAction(auto_update_action_); tray_menu_->addSeparator(); tray_menu_->addAction(quit_action_); tray_icon_->setContextMenu(tray_menu_); tray_icon_->show(); // check update CheckForUpdatesAsync(); try { settings_->Load(false); // use this to show notification about change // structure v1 and v2 config } catch (std::runtime_error& err) { ShowError(QObject::tr("Settings"), err.what()); } UpdateTrayMenu(); #ifdef _WIN32 std::string found_adapters; if (fptn::utils::windows::HasVpnConflicts(found_adapters)) { SPDLOG_WARN( "Detected conflicting VPN network adapters: {}", found_adapters); const QString message = QObject::tr( "A conflicting VPN connection is currently active on your system: %1\n" "This may cause network connectivity issues or prevent proper " "operation of FPTN.") .arg(QString::fromStdString(found_adapters)); ShowWarning(QObject::tr("VPN Conflict Detected"), message); } #endif // start pinging settings_->StartPingMonitoring(); // Show pings ping_update_timer_ = new QTimer(this); connect(ping_update_timer_, &QTimer::timeout, [this]() { UpdatePings(); }); ping_update_timer_->start(1000); } void TrayApp::CheckForUpdatesAsync() { (void)QtConcurrent::run([this]() { try { SPDLOG_DEBUG("Checking for updates in background thread"); const auto update_result = fptn::gui::autoupdate::Check(); const bool is_available = update_result.first; const std::string version_name = update_result.second; SPDLOG_INFO("Update check completed: available={}, version={}", is_available, version_name); if (is_available && !version_name.empty()) { QMetaObject::invokeMethod( this, // NOLINTNEXTLINE(bugprone-exception-escape) [this, version_name]() noexcept { try { auto_available_version_ = QString::fromStdString(version_name); auto_update_action_->setText( QObject::tr("New version available") + " " + auto_available_version_); auto_update_action_->setVisible(true); RetranslateUi(); } catch (...) { SPDLOG_WARN("Failed to update UI with new version info"); } }, Qt::QueuedConnection); } else { SPDLOG_DEBUG("No updates available or version name is empty"); } } catch (const std::exception& e) { SPDLOG_WARN("Failed to check for updates: {}", e.what()); } catch (...) { SPDLOG_WARN("Unknown error during update check"); } }); } void TrayApp::UpdateTrayMenu() { if (limited_zone_connect_menu_) { limited_zone_connect_menu_->clear(); } if (connect_menu_) { connect_menu_->clear(); } if (tray_menu_ && connect_menu_) { tray_menu_->removeAction(connect_menu_->menuAction()); smart_connect_action_ = nullptr; empty_configuration_action_ = nullptr; limited_zone_connect_menu_ = nullptr; } switch (connection_state_) { case ConnectionState::None: { tray_icon_->setIcon(QIcon(inactive_icon_path_)); const auto& services = settings_->Services(); // calculate services const std::size_t servers_number = std::accumulate(services.begin(), services.end(), std::size_t{0}, [](std::size_t sum, const auto& service) { return sum + service.servers.size(); }); if (0 != servers_number) { smart_connect_action_ = new QAction(QObject::tr("Smart Connect"), connect_menu_); smart_connect_action_->setIcon(QIcon(":/icons/ping_green_circle.png")); connect(smart_connect_action_, &QAction::triggered, [this]() { smart_connect_ = true; onConnectToServer(); }); connect_menu_->addAction(smart_connect_action_); connect_menu_->addSeparator(); // servers for (const auto& service : services) { // usual servers for (const auto& server : service.servers) { auto* action = new ServerMenuItemWidget( server.name, server.ping_ms, connect_menu_); // auto* widget_action = new QWidgetAction(connect_menu_); // widget_action->setDefaultWidget(widget); connect_menu_->addAction(action); // FIXME connect(action, &QAction::triggered, [this, server, service]() { smart_connect_ = false; fptn::utils::speed_estimator::ServerInfo cfg_server; { cfg_server.name = server.name.toStdString(); cfg_server.host = server.host.toStdString(); cfg_server.port = server.port; cfg_server.is_using = server.is_using; cfg_server.service_name = service.service_name.toStdString(); cfg_server.username = service.username.toStdString(); cfg_server.password = service.password.toStdString(); cfg_server.md5_fingerprint = server.md5_fingerprint.toStdString(); } selected_server_ = cfg_server; onConnectToServer(); }); } // Censored zone servers for (const auto& server : service.censored_zone_servers) { if (!limited_zone_connect_menu_) { limited_zone_connect_menu_ = new QMenu( QObject::tr("Limited access servers") + " ", connect_menu_); connect_menu_->setIcon(LoadIcon(":/icons/menu_server_list.png")); connect_menu_->addMenu(limited_zone_connect_menu_); } auto* server_connect = new QAction(server.name, limited_zone_connect_menu_); limited_zone_connect_menu_->addAction(server_connect); // FIXME connect( server_connect, &QAction::triggered, [this, server, service]() { smart_connect_ = false; fptn::utils::speed_estimator::ServerInfo cfg_server; { cfg_server.name = server.name.toStdString(); cfg_server.host = server.host.toStdString(); cfg_server.port = server.port; cfg_server.is_using = server.is_using; cfg_server.service_name = service.service_name.toStdString(); cfg_server.username = service.username.toStdString(); cfg_server.password = service.password.toStdString(); cfg_server.md5_fingerprint = server.md5_fingerprint.toStdString(); } selected_server_ = cfg_server; onConnectToServer(); }); } } } else { empty_configuration_action_ = new QAction(QObject::tr("No servers"), connect_menu_); connect_menu_->addAction(empty_configuration_action_); empty_configuration_action_->setEnabled(false); } tray_menu_->insertMenu(settings_action_, connect_menu_); if (connect_menu_) { connect_menu_->setVisible(false); } if (disconnect_action_) { disconnect_action_->setVisible(false); } if (speed_widget_action_) { speed_widget_action_->setVisible(false); } if (settings_action_) { settings_action_->setEnabled(true); } if (speed_widget_) { speed_widget_->setVisible(false); } if (connecting_label_action_) { connecting_label_action_->setVisible(false); } if (disconnecting_label_action_) { disconnecting_label_action_->setVisible(false); } if (quit_action_) { quit_action_->setEnabled(true); } break; } case ConnectionState::Connecting: { tray_icon_->setIcon(QIcon(inactive_icon_path_)); if (connecting_label_action_) { connecting_label_action_->setVisible(true); } if (disconnecting_label_action_) { disconnecting_label_action_->setVisible(false); } if (speed_widget_action_) { speed_widget_action_->setVisible(false); } if (settings_action_) { settings_action_->setEnabled(false); } if (disconnect_action_) { disconnect_action_->setVisible(false); } if (quit_action_) { quit_action_->setEnabled(false); } break; } case ConnectionState::Connected: { tray_icon_->setIcon(QIcon(active_icon_path_)); if (disconnect_action_) { disconnect_action_->setText(QString(QObject::tr("Disconnect") + ": %1") .arg(QString::fromStdString(selected_server_.name))); disconnect_action_->setVisible(true); } if (speed_widget_) { speed_widget_->setVisible(true); } if (settings_action_) { settings_action_->setEnabled(false); } if (speed_widget_action_) { speed_widget_action_->setVisible(true); } if (connecting_label_action_) { connecting_label_action_->setVisible(false); } if (disconnecting_label_action_) { disconnecting_label_action_->setVisible(false); } if (quit_action_) { quit_action_->setEnabled(true); } break; } case ConnectionState::Disconnecting: { tray_icon_->setIcon(QIcon(inactive_icon_path_)); if (disconnect_action_) { disconnect_action_->setVisible(false); } if (speed_widget_action_) { speed_widget_action_->setVisible(false); } if (settings_action_) { settings_action_->setEnabled(false); } if (connecting_label_action_) { connecting_label_action_->setVisible(false); } if (disconnecting_label_action_) { disconnecting_label_action_->setVisible(true); } if (quit_action_) { quit_action_->setEnabled(false); } break; } } // Apply the language translation based on the user's settings const QString selected_language = settings_->LanguageCode(); if (!selected_language.isEmpty()) { fptn::gui::SetTranslation(selected_language); } RetranslateUi(); } void TrayApp::onConnectToServer() { SPDLOG_INFO("Signal: connecting to server"); { const std::unique_lock lock(mutex_); // mutex connection_state_ = ConnectionState::Connecting; UpdateTrayMenu(); } emit connecting(); } void TrayApp::onDisconnectFromServer() { SPDLOG_INFO("Signal: disconnected from server"); const std::unique_lock lock(mutex_); // mutex connection_state_ = ConnectionState::None; if (route_manager_) { route_manager_->Clean(); route_manager_.reset(); } if (vpn_client_) { vpn_client_->Stop(); vpn_client_.reset(); } settings_->StartPingMonitoring(); UpdateTrayMenu(); } void TrayApp::onShowSettings() { auto dialog = std::make_unique(settings_); QMetaObject::invokeMethod(dialog.get(), "setFocus", Qt::QueuedConnection); dialog->exec(); } void TrayApp::handleDefaultState() { SPDLOG_INFO("Signal: entering default state"); { const std::unique_lock lock(mutex_); // mutex cancel_connecting_ = true; settings_->StartPingMonitoring(); connection_state_ = ConnectionState::None; if (route_manager_) { route_manager_->Clean(); route_manager_.reset(); } if (vpn_client_) { vpn_client_->Stop(); vpn_client_.reset(); } } UpdateTrayMenu(); } void TrayApp::handleConnecting() { SPDLOG_INFO("Signal: connecting to server"); const std::unique_lock lock(mutex_); // mutex settings_->StopPingMonitoring(); connection_state_ = ConnectionState::Connecting; UpdateTrayMenu(); if (!connecting_in_progress_) { // only once! cancel_connecting_ = false; connecting_in_progress_ = true; QFuture> future = QtConcurrent::run([this]() { QString err_msg; const auto status = startVpn(err_msg); return std::make_tuple(status, std::move(err_msg)); }); auto* watcher = new QFutureWatcher>(this); connect(watcher, &QFutureWatcher>::finished, this, [this, watcher]() { const std::tuple result = watcher->result(); watcher->deleteLater(); if (!cancel_connecting_) { const bool status = std::get<0>(result); const QString err_msg = std::get<1>(result); emit this->vpnStarted(status, err_msg); } connecting_in_progress_ = false; }); watcher->setFuture(future); } } void TrayApp::handleConnected() { SPDLOG_INFO("Signal: connected to server"); { const std::unique_lock lock(mutex_); // mutex connection_state_ = ConnectionState::Connected; } UpdateTrayMenu(); } void TrayApp::handleDisconnecting() { { const std::unique_lock lock(mutex_); // mutex settings_->StartPingMonitoring(); connection_state_ = ConnectionState::None; UpdateTrayMenu(); stopVpn(); } emit defaultState(); } void TrayApp::handleTimer() { static bool reconnection_in_progress = false; static auto last_reconnection_time = std::chrono::steady_clock::now(); // check connection state bool is_disconnected = false; if (connection_state_ == ConnectionState::Connected && vpn_client_) { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress identicalConditionAfterEarlyExit if (connection_state_ == ConnectionState::Connected && vpn_client_) { if (!vpn_client_->IsStarted()) { // check reconnection auto now = std::chrono::steady_clock::now(); auto time_since_last = std::chrono::duration_cast( now - last_reconnection_time); if (!reconnection_in_progress && time_since_last > std::chrono::seconds(3)) { reconnection_in_progress = true; last_reconnection_time = now; is_disconnected = true; } } else if (speed_widget_) { speed_widget_->UpdateSpeed( vpn_client_->GetReceiveRate(), vpn_client_->GetSendRate()); } } } if (is_disconnected) { // show error ShowError(QObject::tr("FPTN Connection Error"), QObject::tr("The VPN connection was unexpectedly closed.")); SPDLOG_INFO("FPTN Connection Error"); emit disconnecting(); } } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) QString TrayApp::GetSystemLanguageCode() const { const QLocale locale; const QString locale_name = locale.name(); if (locale_name.contains('_')) { const QString language_code = locale.name().split('_').first(); return language_code; } return "en"; } void TrayApp::RetranslateUi() { if (connect_menu_) { connect_menu_->setTitle(QObject::tr("Connect") + " "); } if (settings_action_) { settings_action_->setText(QObject::tr("Settings")); } if (quit_action_) { quit_action_->setText(QObject::tr("Quit")); } if (connecting_label_action_) { connecting_label_action_->setText(QObject::tr("Connecting...")); } if (empty_configuration_action_) { empty_configuration_action_->setText(QObject::tr("No servers")); } if (smart_connect_action_) { smart_connect_action_->setText(QObject::tr("Smart Connect")); } if (limited_zone_connect_menu_) { limited_zone_connect_menu_->setTitle( QObject::tr("Limited access servers") + " "); } if (connecting_label_action_) { connecting_label_action_->setText(QObject::tr("Connecting...")); } if (disconnecting_label_action_) { disconnecting_label_action_->setText(QObject::tr("Disconnecting...")); } if (disconnect_action_) { const QString disconnect_text = QString(QObject::tr("Disconnect") + ": %1") .arg(QString::fromStdString(selected_server_.name)); disconnect_action_->setText(disconnect_text); } if (auto_update_action_) { auto_update_action_->setText( QObject::tr("New version available") + " " + auto_available_version_); } } void TrayApp::stop() { SPDLOG_INFO("Stopping TrayApp"); settings_->StopPingMonitoring(); if (route_manager_) { route_manager_->Clean(); route_manager_.reset(); } if (vpn_client_) { vpn_client_->Stop(); vpn_client_.reset(); } } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) void TrayApp::OpenWebBrowser(const std::string& url) { #if __APPLE__ QDesktopServices::openUrl(QString::fromStdString(url)); #elif __linux__ const std::string command = fmt::format( R"(bash -c "xhost +SI:localuser:root && (xdg-open \"{0}\" || sensible-browser \"{0}\" || x-www-browser \"{0}\" || gnome-open \"{0}\" ) " )", url); fptn::common::system::command::run(command); #elif _WIN32 const std::string command = fmt::format(R"(explorer "{}" )", url); fptn::common::system::command::run(command); #endif } bool TrayApp::startVpn(QString& err_msg) { SPDLOG_DEBUG("Handling connecting state"); const fptn::common::network::IPv4Address tun_interface_address_ipv4( FPTN_CLIENT_DEFAULT_ADDRESS_IP4); const fptn::common::network::IPv6Address tun_interface_address_ipv6( FPTN_CLIENT_DEFAULT_ADDRESS_IP6); const std::string tun_interface_name = "tun0"; /* check gateway address */ const auto gateway_ip = (settings_->GatewayIp() == "auto" ? fptn::routing::GetDefaultGatewayIPAddress() : fptn::common::network::IPv4Address( settings_->GatewayIp().toStdString())); const auto gateway_ipv6 = fptn::routing::GetDefaultGatewayIPv6Address(); if (gateway_ip.IsEmpty()) { err_msg = QObject::tr( "Unable to find the default gateway IP address. " "Please check your connection and make sure no other VPN " "is active. " "If the error persists, specify the gateway address in the " "FPTN settings using your router's IP address, " "and ensure that an active internet interface (adapter) is " "selected. If the issue remains unresolved, " "please contact the developer via Telegram @fptn_chat."); return false; } /* config */ const std::string network_interface = (settings_->UsingNetworkInterface() == "auto" ? "" : settings_->UsingNetworkInterface().toStdString()); const std::string sni = !settings_->SNI().isEmpty() ? settings_->SNI().toStdString() : FPTN_DEFAULT_SNI; fptn::protocol::https::CensorshipStrategy censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex25; const auto& bypass_method = settings_->BypassMethod(); if (bypass_method == SettingsModel::kBypassMethodObfuscation) { SPDLOG_INFO("Using obfuscation to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kTlsObfuscator; } else if (bypass_method == SettingsModel::kBypassMethodSniReality) { // DEPRECATED SPDLOG_INFO("Using generic reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityMode; } /* chrome */ else if (bypass_method == SettingsModel::kBypassMethodSniRealityChrome147) { SPDLOG_INFO("Using Chrome 147 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeChrome147; } else if (bypass_method == SettingsModel::kBypassMethodSniRealityChrome146) { SPDLOG_INFO("Using Chrome 146 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeChrome146; } else if (bypass_method == SettingsModel::kBypassMethodSniRealityChrome145) { SPDLOG_INFO("Using Chrome 145 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeChrome145; } /* firefox */ else if (bypass_method == SettingsModel::kBypassMethodSniRealityFirefox149) { SPDLOG_INFO("Using Firefox 149 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeFirefox149; } /* Yandex */ else if (bypass_method == SettingsModel::kBypassMethodSniRealityYandex26) { SPDLOG_INFO("Using Yandex 26 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex26; } else if (bypass_method == SettingsModel::kBypassMethodSniRealityYandex25) { SPDLOG_INFO("Using Yandex 25 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex25; } else if (bypass_method == SettingsModel::kBypassMethodSniRealityYandex24) { SPDLOG_INFO("Using Yandex 24 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex24; } /* Safari */ else if (bypass_method == SettingsModel::kBypassMethodSniRealitySafari26) { SPDLOG_INFO("Using Safari 26 reality mode to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeSafari26; } /* Default */ else { SPDLOG_INFO("Using default SNI spoofing to bypass censorship"); censorship_strategy = fptn::protocol::https::CensorshipStrategy::kSniRealityModeYandex25; } if (cancel_connecting_) { return false; } fptn::config::ConfigFile config(sni, censorship_strategy); // SET SNI if (smart_connect_) { // find the best server for (const auto& service : settings_->Services()) { for (const auto& s : service.servers) { fptn::utils::speed_estimator::ServerInfo cfg_server; { cfg_server.name = s.name.toStdString(); cfg_server.host = s.host.toStdString(); cfg_server.port = s.port; cfg_server.is_using = s.is_using; cfg_server.service_name = service.service_name.toStdString(); cfg_server.username = service.username.toStdString(); cfg_server.password = service.password.toStdString(); cfg_server.md5_fingerprint = s.md5_fingerprint.toStdString(); } config.AddServer(cfg_server); } } try { selected_server_ = config.FindFastestServer(30); } catch (std::runtime_error& err) { err_msg = QObject::tr("Config error: ") + err.what(); return false; } } /* else { // check connection to selected server const std::uint64_t time = config.GetDownloadTimeMs( selected_server_, sni, 30, selected_server_.md5_fingerprint); if (time == UINT64_MAX) { err_msg = QObject::tr( "The server is unavailable. Please select another server " "or use Auto-connect to find the best available server."); return false; } } */ const auto server_ip = fptn::routing::ResolveDomain(selected_server_.host); if (server_ip == fptn::common::network::IPv4Address()) { err_msg = QObject::tr( "The server is unavailable. Please select another server " "or use Auto-connect to find the best available server."); return false; } if (cancel_connecting_) { return false; } auto http_client = std::make_unique(server_ip, selected_server_.port, tun_interface_address_ipv4, tun_interface_address_ipv6, sni, selected_server_.md5_fingerprint, censorship_strategy); // login bool login_status = http_client->Login(selected_server_.username, selected_server_.password); if (!login_status) { const std::string error = http_client->LatestError(); err_msg = QObject::tr( "Unable to connect to the server. Please use the Telegram " "bot to generate a new TOKEN with your personal settings, " "then try again.") + "\n\n" + QObject::tr("Error message: ") + QString::fromStdString(error); return false; } if (cancel_connecting_) { return false; } // get dns const auto [dns_server_ipv4, dns_server_ipv6] = http_client->GetDns(); if (dns_server_ipv4.IsEmpty() || dns_server_ipv6.IsEmpty()) { const std::string error = http_client->LatestError(); err_msg = QObject::tr("DNS server error! Check your connection!") + "\n\n" + QObject::tr("Error message: ") + QString::fromStdString(error); return false; } if (cancel_connecting_) { return false; } const auto blacklist_domains = settings_->BlacklistDomains(); const auto exclude_networks = settings_->ExcludeTunnelNetworks(); const auto include_networks = settings_->IncludeTunnelNetworks(); const bool enable_split_tunnel = settings_->EnableSplitTunnel(); const QString split_tunnel_mode = settings_->SplitTunnelMode(); const auto split_tunnel_domains = settings_->SplitTunnelDomains(); // route manager route_manager_ = std::make_unique( network_interface, tun_interface_name, server_ip, dns_server_ipv4, dns_server_ipv6, gateway_ip, gateway_ipv6, tun_interface_address_ipv4, tun_interface_address_ipv6 #if _WIN32 , settings_->EnableAdvancedDnsManagement() #endif ); // NOLINT if (cancel_connecting_) { return false; } // setup plugins std::vector client_plugins; if (!blacklist_domains.empty()) { std::vector blacklist_domains_std; for (const auto& domain : blacklist_domains) { blacklist_domains_std.push_back(domain.toStdString()); } auto blacklist_plugin = std::make_unique( blacklist_domains_std, route_manager_); client_plugins.push_back(std::move(blacklist_plugin)); } if (enable_split_tunnel) { std::vector split_domains_std; for (const auto& domain : split_tunnel_domains) { split_domains_std.push_back(domain.toStdString()); } const auto policy = (split_tunnel_mode == "exclude") ? fptn::routing::RoutingPolicy::kExcludeFromVpn : fptn::routing::RoutingPolicy::kIncludeInVpn; auto split_tunnel_plugin = std::make_unique( split_domains_std, route_manager_, policy); client_plugins.push_back(std::move(split_tunnel_plugin)); } if (cancel_connecting_) { return false; } // setup tun interface auto virtual_network_interface = std::make_unique( fptn::common::network::TunInterface::Config{ .name = tun_interface_name, .ipv4_addr = tun_interface_address_ipv4, .ipv4_netmask = 30, // IPv4 netmask .ipv6_addr = tun_interface_address_ipv6, .ipv6_netmask = 126 // IPv6 netmask }); // setup vpn client vpn_client_ = std::make_unique(std::move(http_client), std::move(virtual_network_interface), dns_server_ipv4, dns_server_ipv6, std::move(client_plugins)); if (cancel_connecting_) { return false; } // Wait for the WebSocket tunnel to establish vpn_client_->Start(); if (cancel_connecting_) { return false; } // Update tun name to actual device name (may differ on macOS) route_manager_->UpdateTunInterfaceName(vpn_client_->GetInterfaceName()); constexpr auto kTimeout = std::chrono::seconds(10); const auto start = std::chrono::steady_clock::now(); while (!vpn_client_->IsStarted()) { if (std::chrono::steady_clock::now() - start > kTimeout) { err_msg = QObject::tr("Failed to connect to the server!"); return false; } std::this_thread::sleep_for(std::chrono::microseconds(300)); } if (cancel_connecting_) { return false; } route_manager_->Apply(); if (!exclude_networks.empty()) { std::vector exclude_networks_std; for (const auto& network : exclude_networks) { exclude_networks_std.push_back(network.toStdString()); } route_manager_->AddExcludeNetworks(exclude_networks_std); } if (!include_networks.empty()) { std::vector include_networks_std; for (const auto& network : include_networks) { include_networks_std.push_back(network.toStdString()); } route_manager_->AddIncludeNetworks(include_networks_std); } if (cancel_connecting_) { return false; } return true; } bool TrayApp::stopVpn() { SPDLOG_INFO("Stopping vpn"); if (route_manager_) { route_manager_->Clean(); route_manager_.reset(); } if (vpn_client_) { vpn_client_->Stop(); vpn_client_.reset(); } return true; } void TrayApp::handleVpnStarted(bool success, const QString& err_msg) { cancel_connecting_ = false; if (success) { emit connected(); } else { ShowError(QObject::tr("FPTN Connection Error"), err_msg); emit disconnecting(); settings_->StartPingMonitoring(); } } void TrayApp::UpdatePings() { const std::unique_lock lock(mutex_); // mutex if (connection_state_ == ConnectionState::Connected || connection_state_ == ConnectionState::Connecting || connection_state_ == ConnectionState::Disconnecting) { return; } for (auto* action : connect_menu_->actions()) { if (auto* server_action = qobject_cast(action)) { for (const auto& service : settings_->Services()) { for (const auto& server : service.servers) { if (server.name == server_action->ServerName()) { server_action->UpdatePing(server.ping_ms); break; } } } } } } ================================================ FILE: src/fptn-client/gui/tray/tray.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/data/channel.h" #include "common/network/ip_address.h" #include "common/network/net_interface.h" #include "config/config_file.h" #include "gui/settingsmodel/settingsmodel.h" #include "gui/settingswidget/settings.h" #include "gui/speedwidget/speedwidget.h" #include "gui/tray/tray.h" #include "routing/route_manager.h" #include "utils/speed_estimator/server_info.h" #include "vpn/http/client.h" #include "vpn/vpn_client.h" namespace fptn::gui { class TrayApp : public QWidget { Q_OBJECT protected: enum class ConnectionState { None, Connecting, Connected, Disconnecting }; public: explicit TrayApp(const SettingsModelPtr& settings, QObject* parent = nullptr); void stop(); protected: QString GetSystemLanguageCode() const; void RetranslateUi(); signals: void defaultState(); void connecting(); void connected(); void disconnecting(); void vpnStarted(bool success, const QString& err_msg); // cppcheck-suppress unknownMacro protected slots: void onConnectToServer(); void onDisconnectFromServer(); void onShowSettings(); // cppcheck-suppress unknownMacro protected slots: void handleDefaultState(); void handleConnecting(); void handleConnected(); void handleDisconnecting(); void handleTimer(); void handleVpnStarted(bool success, const QString& err_msg); protected: void UpdateTrayMenu(); void UpdatePings(); void OpenWebBrowser(const std::string& url); protected: bool startVpn(QString& err_msg); bool stopVpn(); void CheckForUpdatesAsync(); private: mutable std::mutex mutex_; bool smart_connect_ = false; fptn::utils::speed_estimator::ServerInfo selected_server_; SettingsModelPtr settings_; QSystemTrayIcon* tray_icon_ = nullptr; QMenu* tray_menu_ = nullptr; QMenu* connect_menu_ = nullptr; QAction* smart_connect_action_ = nullptr; QMenu* limited_zone_connect_menu_ = nullptr; QAction* empty_configuration_action_ = nullptr; QAction* disconnect_action_ = nullptr; // QAction* connecting_action_ = nullptr; QAction* settings_action_ = nullptr; QAction* auto_update_action_ = nullptr; QString auto_available_version_; QAction* quit_action_ = nullptr; QAction* connecting_label_action_ = nullptr; QAction* disconnecting_label_action_ = nullptr; QWidgetAction* speed_widget_action_ = nullptr; SpeedWidget* speed_widget_ = nullptr; QTimer* update_timer_ = nullptr; ConnectionState connection_state_ = ConnectionState::None; QString connected_server_address_; QString active_icon_path_; QString inactive_icon_path_; fptn::vpn::VpnClientPtr vpn_client_; fptn::routing::RouteManagerSPtr route_manager_; // connecting std::atomic connecting_in_progress_{false}; QTimer* ping_update_timer_{nullptr}; std::atomic cancel_connecting_{false}; }; } // namespace fptn::gui ================================================ FILE: src/fptn-client/plugins/base_plugin.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include "common/network/ip_packet.h" namespace fptn::plugin { class BasePlugin { public: virtual ~BasePlugin() = default; virtual std::pair HandlePacket( fptn::common::network::IPPacketPtr packet) = 0; }; using BasePluginPtr = std::unique_ptr; using PluginList = std::vector; } // namespace fptn::plugin ================================================ FILE: src/fptn-client/plugins/blacklist/domain_blacklist.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "plugins/blacklist/domain_blacklist.h" #include #include #include #include #include #include "common/utils/utils.h" #include "utils/utils.h" namespace fptn::plugin { DomainBlacklist::DomainBlacklist(const std::vector& rules, routing::RouteManagerSPtr route_manager) : route_manager_(std::move(route_manager)) { RE2::Options re_options; re_options.set_case_sensitive(false); re_options.set_log_errors(false); for (const auto& rule : rules) { const std::string regex_pattern = fptn::utils::DomainToRegex(rule); if (!regex_pattern.empty()) { auto re = std::make_unique(regex_pattern, re_options); if (re->ok()) { SPDLOG_INFO("Added blacklist rule: '{}' -> '{}'", rule, regex_pattern); rules_.push_back(std::move(re)); } else { SPDLOG_WARN("Invalid regex pattern: {}, item={}", re->error(), rule); } } else { SPDLOG_WARN("Wrong pattern {}", rule); } } } std::pair DomainBlacklist::HandlePacket(fptn::common::network::IPPacketPtr packet) { bool triggered = false; if (packet->IsDns()) { const auto domain_opt = packet->GetDnsDomain(); if (domain_opt.has_value()) { const std::string& domain = domain_opt.value(); if (std::ranges::any_of(rules_, [&domain](const auto& re) { return RE2::PartialMatch(domain, *re); })) { SPDLOG_INFO("Domain {} is blacklisted", domain); const auto ipv4_addresses = packet->GetDnsIPv4Addresses(); const auto ipv6_addresses = packet->GetDnsIPv6Addresses(); // save const std::unique_lock lock(mutex_); // mutex { for (const auto& ipv4_address : ipv4_addresses) { if (!ipv4_addresses_.contains(ipv4_address.ToInt())) { SPDLOG_INFO( "Added IPv4 to blacklist: {}", ipv4_address.ToString()); ipv4_addresses_.insert(ipv4_address.ToInt()); } } for (const auto& ipv6_address : ipv6_addresses) { if (ipv6_addresses_.contains(ipv6_address.ToString())) { SPDLOG_INFO( "Added IPv6 to blacklist: {}", ipv6_address.ToString()); ipv6_addresses_.insert(ipv6_address.ToString()); } } } triggered = true; } } } else if (packet->IsIPv4()) { const std::uint32_t src_ipv4 = packet->IPv4Layer()->getSrcIPAddress().getIPv4().toInt(); const std::unique_lock lock(mutex_); // mutex if (ipv4_addresses_.contains(src_ipv4)) { SPDLOG_INFO("Blocked IPv4 packet from {}", packet->IPv4Layer()->getSrcIPAddress().getIPv4().toString()); return {nullptr, true}; } } else if (packet->IsIPv6()) { const std::string src_ipv6 = packet->IPv6Layer()->getSrcIPAddress().getIPv6().toString(); const std::unique_lock lock(mutex_); // mutex if (ipv6_addresses_.contains(src_ipv6)) { SPDLOG_INFO("Blocked IPv6 packet from {}", src_ipv6); return {nullptr, true}; } } return {std::move(packet), triggered}; } } // namespace fptn::plugin ================================================ FILE: src/fptn-client/plugins/blacklist/domain_blacklist.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "plugins/base_plugin.h" #include "routing/route_manager.h" namespace fptn::plugin { class DomainBlacklist final : public BasePlugin { public: explicit DomainBlacklist(const std::vector& rules, routing::RouteManagerSPtr route_manager); ~DomainBlacklist() override = default; std::pair HandlePacket( fptn::common::network::IPPacketPtr packet) override; private: mutable std::mutex mutex_; const routing::RouteManagerSPtr route_manager_; std::vector> rules_; std::unordered_set ipv4_addresses_; std::unordered_set ipv6_addresses_; }; using DomainBlacklistPtr = std::unique_ptr; } // namespace fptn::plugin ================================================ FILE: src/fptn-client/plugins/split/tunneling.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "plugins/split/tunneling.h" #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "utils/utils.h" namespace fptn::plugin { Tunneling::Tunneling(const std::vector& rules, routing::RouteManagerSPtr route_manager, fptn::routing::RoutingPolicy policy) : route_manager_(std::move(route_manager)), policy_(policy) { RE2::Options re_options; re_options.set_case_sensitive(false); re_options.set_log_errors(false); for (const auto& rule : rules) { const std::string regex_pattern = fptn::utils::DomainToRegex(rule); if (!regex_pattern.empty()) { auto re = std::make_unique(regex_pattern, re_options); if (re->ok()) { SPDLOG_INFO("Added tunneling rule: '{}' -> '{}'", rule, regex_pattern); rules_.push_back(std::move(re)); } else { SPDLOG_WARN("Invalid regex pattern: {}, item={}", re->error(), rule); } } else { SPDLOG_WARN("Wrong pattern {}", rule); } } } std::pair Tunneling::HandlePacket( fptn::common::network::IPPacketPtr packet) { bool triggered = false; if (packet->IsDns()) { const auto domain_opt = packet->GetDnsDomain(); if (domain_opt.has_value()) { const std::string& domain = domain_opt.value(); const bool domain_matched = std::ranges::any_of(rules_, [&domain](const auto& re) { return RE2::PartialMatch(domain, *re); }); const auto ipv4_addresses = packet->GetDnsIPv4Addresses(); if (policy_ == routing::RoutingPolicy::kIncludeInVpn) { if (!domain_matched) { triggered = true; route_manager_->AddDnsRoutesIPv4( ipv4_addresses, routing::RoutingPolicy::kExcludeFromVpn); SPDLOG_INFO( "Domain '{}' -> EXCLUDE from VPN (policy: INCLUDE only selected)", domain); } } else if (policy_ == routing::RoutingPolicy::kExcludeFromVpn) { if (domain_matched) { triggered = true; route_manager_->AddDnsRoutesIPv4( ipv4_addresses, routing::RoutingPolicy::kExcludeFromVpn); SPDLOG_INFO( "Domain '{}' -> EXCLUDE from VPN (policy: EXCLUDE selected)", domain); } } #ifndef __APPLE__ const auto ipv6_addresses = packet->GetDnsIPv6Addresses(); if (!ipv6_addresses.empty()) { if (policy_ == routing::RoutingPolicy::kIncludeInVpn) { if (!domain_matched) { triggered = true; route_manager_->AddDnsRoutesIPv6( ipv6_addresses, routing::RoutingPolicy::kExcludeFromVpn); SPDLOG_INFO( "Domain '{}' -> EXCLUDE from VPN (policy: INCLUDE only " "selected)", domain); } } else if (policy_ == routing::RoutingPolicy::kExcludeFromVpn) { if (domain_matched) { triggered = true; route_manager_->AddDnsRoutesIPv6( ipv6_addresses, routing::RoutingPolicy::kExcludeFromVpn); SPDLOG_INFO( "Domain '{}' -> EXCLUDE from VPN (policy: EXCLUDE selected)", domain); } } } #endif } } return {std::move(packet), triggered}; } } // namespace fptn::plugin ================================================ FILE: src/fptn-client/plugins/split/tunneling.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include // NOLINT(build/include_order) #include "common/network/ip_packet.h" #include "plugins/base_plugin.h" #include "routing/route_manager.h" namespace fptn::plugin { class Tunneling final : public BasePlugin { public: explicit Tunneling(const std::vector& rules, routing::RouteManagerSPtr route_manager, fptn::routing::RoutingPolicy policy); ~Tunneling() override = default; std::pair HandlePacket( fptn::common::network::IPPacketPtr packet) override; private: const routing::RouteManagerSPtr route_manager_; const fptn::routing::RoutingPolicy policy_; std::vector> rules_; }; using TunnelingPtr = std::unique_ptr; } // namespace fptn::plugin ================================================ FILE: src/fptn-client/routing/route_manager.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "routing/route_manager.h" #include #include #include #include #include #include #include #include "common/network/net_interface.h" #include "common/system/command.h" namespace { #ifdef _WIN32 std::string GetWindowsInterfaceNumber(const std::string& interface_name) { if (interface_name.empty()) { return {}; } ULONG out_buf_len = 15000; ULONG flags = GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_GATEWAYS; std::unique_ptr adapter_addresses; DWORD ret = ERROR_BUFFER_OVERFLOW; for (int i = 0; i < 3 && ret == ERROR_BUFFER_OVERFLOW; i++) { adapter_addresses = std::make_unique( out_buf_len / sizeof(IP_ADAPTER_ADDRESSES) + 1); ret = GetAdaptersAddresses( AF_UNSPEC, flags, nullptr, adapter_addresses.get(), &out_buf_len); if (ret == ERROR_BUFFER_OVERFLOW) { adapter_addresses.reset(); } } if (ret == NO_ERROR && adapter_addresses) { PIP_ADAPTER_ADDRESSES current = adapter_addresses.get(); DWORD if_index = 0; while (current) { std::string adapter_name = current->AdapterName; int size_need = WideCharToMultiByte( CP_UTF8, 0, current->FriendlyName, -1, nullptr, 0, nullptr, nullptr); std::string friendly_name(size_need - 1, 0); WideCharToMultiByte(CP_UTF8, 0, current->FriendlyName, -1, &friendly_name[0], size_need, nullptr, nullptr); size_need = WideCharToMultiByte( CP_UTF8, 0, current->Description, -1, nullptr, 0, nullptr, nullptr); std::string description(size_need - 1, 0); WideCharToMultiByte(CP_UTF8, 0, current->Description, -1, &description[0], size_need, nullptr, nullptr); if (interface_name == adapter_name || interface_name == friendly_name || interface_name == description) { if_index = current->IfIndex; break; } current = current->Next; } return if_index > 0 ? std::to_string(if_index) : std::string(); } return {}; } std::pair ParseIPv4CIDR(const std::string& network) { std::string ip = network; std::string mask = "255.255.255.255"; std::size_t slash_pos = network.find('/'); if (slash_pos != std::string::npos) { ip = network.substr(0, slash_pos); std::string cidr_str = network.substr(slash_pos + 1); try { int cidr = std::stoi(cidr_str); if (cidr >= 0 && cidr <= 32) { std::uint32_t mask_value = 0; if (cidr > 0) { mask_value = ~0u << (32 - cidr); } mask = fmt::format("{}.{}.{}.{}", (mask_value >> 24) & 0xFF, (mask_value >> 16) & 0xFF, (mask_value >> 8) & 0xFF, mask_value & 0xFF); } } catch (...) { SPDLOG_WARN("Warning: Failed to parse CIDR: {}", network); } } return {ip, mask}; } std::pair ParseIPv6CIDR(const std::string& network) { std::string ip = network; int prefix = 128; size_t slash_pos = network.find('/'); if (slash_pos != std::string::npos) { ip = network.substr(0, slash_pos); std::string cidr_str = network.substr(slash_pos + 1); try { int cidr = std::stoi(cidr_str); if (cidr >= 0 && cidr <= 128) { prefix = cidr; } } catch (...) { SPDLOG_WARN("Warning: Failed to parse IPv6 CIDR: {}", network); } } return {ip, prefix}; } #elif __linux__ std::vector GetLinuxDnsServers(const std::string& interface) { std::vector dns_servers; const std::string command = fmt::format( "resolvectl status {} | grep 'DNS Servers:' | awk -F': ' '{{print $2}}'", interface); std::vector output; fptn::common::system::command::run(command, output); if (!output.empty() && !output[0].empty()) { std::istringstream iss(output[0]); std::string server; while (iss >> server) { if (!server.empty()) { dns_servers.push_back(server); } } } return dns_servers; } #endif bool AddIPv4RouteToSystem(const std::string& destination, const std::string& gateway_ip, const std::string& out_interface) { (void)gateway_ip; (void)out_interface; try { #ifdef __linux__ const std::string command = fmt::format(R"(ip route add {} via "{}" dev "{}" )", destination, gateway_ip, out_interface); #elif __APPLE__ const std::string command = fmt::format("route add -net {} {}", destination, gateway_ip); #elif _WIN32 auto [ip, mask] = ParseIPv4CIDR(destination); std::string interface_param = ""; if (!out_interface.empty()) { std::string interface_number = GetWindowsInterfaceNumber(out_interface); if (!interface_number.empty()) { interface_param = "if " + interface_number; } } const std::string command = fmt::format("route add {} mask {} {} METRIC 2 {}", ip, mask, gateway_ip, interface_param); #else return false; #endif fptn::common::system::command::run(command); return true; } catch (const std::exception& e) { SPDLOG_ERROR("Failed to add IPv4 route {}: {}", destination, e.what()); return false; } catch (...) { SPDLOG_ERROR("Unknown error adding IPv4 route: {}", destination); return false; } } bool AddIPv6RouteToSystem(const std::string& destination, const std::string& gateway_ip, const std::string& out_interface) { (void)gateway_ip; (void)out_interface; try { #ifdef __linux__ const std::string command = fmt::format(R"(ip -6 route add {} via "{}" dev "{}" )", destination, gateway_ip, out_interface); #elif __APPLE__ const std::string command = fmt::format("route add -inet6 {} {}", destination, gateway_ip); #elif _WIN32 auto [ip, prefix] = ParseIPv6CIDR(destination); std::string interface_name = out_interface; if (interface_name.empty()) { SPDLOG_ERROR("Interface name required for IPv6 route on Windows"); return false; } const std::string command = fmt::format("netsh interface ipv6 add route {}/{} \"{}\" {}", ip, prefix, interface_name, gateway_ip); #else return false; #endif fptn::common::system::command::run(command); return true; } catch (const std::exception& e) { SPDLOG_ERROR("Failed to add IPv6 route {}: {}", destination, e.what()); return false; } catch (...) { SPDLOG_ERROR("Unknown error adding IPv6 route: {}", destination); return false; } } bool RemoveIPv4RouteFromSystem(const std::string& destination, const std::string& gateway_ip, const std::string& out_interface) { (void)gateway_ip; (void)out_interface; try { #ifdef __linux__ const std::string command = fmt::format("ip route del {} via {} dev {}", destination, gateway_ip, out_interface); #elif __APPLE__ const std::string command = fmt::format("route delete -net {} {}", destination, gateway_ip); #elif _WIN32 auto [ip, mask] = ParseIPv4CIDR(destination); std::string interface_param = ""; if (!out_interface.empty()) { std::string interface_number = GetWindowsInterfaceNumber(out_interface); if (!interface_number.empty()) { interface_param = "if " + interface_number; } } const std::string command = fmt::format( "route delete {} mask {} {} {}", ip, mask, gateway_ip, interface_param); #else return false; #endif fptn::common::system::command::run(command); return true; } catch (const std::exception& e) { SPDLOG_ERROR("Failed to remove IPv4 route {}: {}", destination, e.what()); return false; } catch (...) { SPDLOG_ERROR("Unknown error removing IPv4 route: {}", destination); return false; } } bool RemoveIPv6RouteFromSystem(const std::string& destination, const std::string& gateway_ip, const std::string& out_interface) { (void)gateway_ip; (void)out_interface; try { #ifdef __linux__ const std::string command = fmt::format("ip -6 route del {} via {} dev {}", destination, gateway_ip, out_interface); #elif __APPLE__ const std::string command = fmt::format("route delete -inet6 {} {}", destination, gateway_ip); #elif _WIN32 auto [ip, prefix] = ParseIPv6CIDR(destination); std::string interface_name = out_interface; if (interface_name.empty()) { SPDLOG_ERROR("Interface name required for IPv6 route removal on Windows"); return false; } const std::string command = fmt::format("netsh interface ipv6 delete route {}/{} \"{}\"", ip, prefix, interface_name); #else return false; #endif fptn::common::system::command::run(command); return true; } catch (const std::exception& e) { SPDLOG_ERROR("Failed to remove IPv6 route {}: {}", destination, e.what()); return false; } catch (...) { SPDLOG_ERROR("Unknown error removing IPv6 route: {}", destination); return false; } } } // namespace using fptn::routing::RouteManager; RouteManager::RouteManager(std::string out_interface_name, std::string tun_interface_name, fptn::common::network::IPv4Address vpn_server_ip, fptn::common::network::IPv4Address dns_server_ipv4, fptn::common::network::IPv6Address dns_server_ipv6, fptn::common::network::IPv4Address gateway_ipv4, fptn::common::network::IPv6Address gateway_ipv6, fptn::common::network::IPv4Address tun_interface_address_ipv4, fptn::common::network::IPv6Address tun_interface_address_ipv6 #if _WIN32 , bool enable_advanced_dns_management #endif ) : running_(false), out_interface_name_(std::move(out_interface_name)), tun_interface_name_(std::move(tun_interface_name)), vpn_server_ip_(std::move(vpn_server_ip)), dns_server_ipv4_(std::move(dns_server_ipv4)), dns_server_ipv6_(std::move(dns_server_ipv6)), gateway_ipv4_(std::move(gateway_ipv4)), gateway_ipv6_(std::move(gateway_ipv6)), tun_interface_address_ipv4_(std::move(tun_interface_address_ipv4)), tun_interface_address_ipv6_(std::move(tun_interface_address_ipv6)) #if _WIN32 , enable_advanced_dns_management_(enable_advanced_dns_management) #endif { } RouteManager::~RouteManager() { // NOLINT(bugprone-exception-escape) if (running_) { Clean(); } } bool RouteManager::Apply() { const std::unique_lock lock(mutex_); // mutex running_ = true; #if defined(__APPLE__) || defined(__linux__) detected_out_interface_name_ = (out_interface_name_.empty() ? GetDefaultNetworkInterfaceName() : out_interface_name_); #endif detected_out_interface_name_ = (out_interface_name_.empty() ? GetDefaultNetworkInterfaceName() : out_interface_name_); detected_gateway_ipv4_ = gateway_ipv4_.IsEmpty() ? GetDefaultGatewayIPAddress() : gateway_ipv4_; SPDLOG_INFO("=== Setting up routing ==="); SPDLOG_INFO("IPTABLES VPN SERVER IP: {}", vpn_server_ip_.ToString()); SPDLOG_INFO( "IPTABLES OUT NETWORK INTERFACE: {}", detected_out_interface_name_); SPDLOG_INFO( "IPTABLES GATEWAY IP: {}", detected_gateway_ipv4_.ToString()); SPDLOG_INFO( "IPTABLES DNS SERVER: {}", dns_server_ipv4_.ToString()); #ifdef __linux__ original_dns_servers_ = GetLinuxDnsServers(detected_out_interface_name_); for (const auto& dns : original_dns_servers_) { SPDLOG_INFO("Saved dns: {}", dns); } std::vector commands = {fmt::format("systemctl start sysctl"), fmt::format("sysctl -w net.ipv4.ip_forward=1"), fmt::format("sysctl -w net.ipv6.conf.default.disable_ipv6=0"), fmt::format("sysctl -w net.ipv6.conf.all.disable_ipv6=0"), fmt::format("sysctl -w net.ipv6.conf.lo.disable_ipv6=0"), fmt::format("sysctl -w net.ipv6.conf.all.forward=1"), fmt::format("sysctl -p"), // iptables fmt::format("iptables -t nat -A POSTROUTING -o {} -j MASQUERADE", detected_out_interface_name_), fmt::format("iptables -A FORWARD -i {} -o {} -m state --state " "RELATED,ESTABLISHED -j ACCEPT", detected_out_interface_name_, tun_interface_name_), fmt::format("iptables -A FORWARD -i {} -o {} -j ACCEPT", tun_interface_name_, detected_out_interface_name_), fmt::format("iptables -A OUTPUT -o {} -d {} -j ACCEPT", detected_out_interface_name_, vpn_server_ip_.ToString()), fmt::format("iptables -A INPUT -i {} -s {} -j ACCEPT", detected_out_interface_name_, vpn_server_ip_.ToString()), // IPv4 default & DNS route fmt::format("ip route add default dev {}", tun_interface_name_), fmt::format("ip route add {} dev {}", dns_server_ipv4_.ToString(), tun_interface_name_), // via TUN // IPv6 default fmt::format("ip -6 route add {} dev {}", dns_server_ipv6_.ToString(), tun_interface_name_), fmt::format("ip -6 route add default via {} dev {}", dns_server_ipv6_.ToString(), tun_interface_name_), // exclude vpn server fmt::format("ip route add {} via {} dev {}", vpn_server_ip_.ToString(), detected_gateway_ipv4_.ToString(), detected_out_interface_name_), // Allow DNS responses from TUN (sport 53, not dport 53) fmt::format("iptables -A OUTPUT -o {} -p udp --sport 53 -j ACCEPT", tun_interface_name_), fmt::format("iptables -A OUTPUT -o {} -p tcp --sport 53 -j ACCEPT", tun_interface_name_), fmt::format("ip6tables -A OUTPUT -o {} -p udp --sport 53 -j ACCEPT", tun_interface_name_), fmt::format("ip6tables -A OUTPUT -o {} -p tcp --sport 53 -j ACCEPT", tun_interface_name_), // Block DNS requests on physical interface fmt::format("iptables -A OUTPUT -o {} -p udp --dport 53 -j DROP", detected_out_interface_name_), fmt::format("iptables -A OUTPUT -o {} -p tcp --dport 53 -j DROP", detected_out_interface_name_), // Block DNS IPv6 fmt::format("ip6tables -A OUTPUT -o {} -p udp --dport 53 -j DROP", detected_out_interface_name_), fmt::format("ip6tables -A OUTPUT -o {} -p tcp --dport 53 -j DROP", detected_out_interface_name_), // Block DoT IPv4 fmt::format("iptables -A OUTPUT -o {} -p udp --dport 853 -j DROP", detected_out_interface_name_), fmt::format("iptables -A OUTPUT -o {} -p tcp --dport 853 -j DROP", detected_out_interface_name_), // Block DoT IPv6 fmt::format("ip6tables -A OUTPUT -o {} -p udp --dport 853 -j DROP", detected_out_interface_name_), fmt::format("ip6tables -A OUTPUT -o {} -p tcp --dport 853 -j DROP", detected_out_interface_name_), // Also allow DNS to specific DNS server IP fmt::format("iptables -A OUTPUT -d {} -p udp --dport 53 -j ACCEPT", dns_server_ipv4_.ToString()), fmt::format("iptables -A OUTPUT -d {} -p tcp --dport 53 -j ACCEPT", dns_server_ipv4_.ToString()), // DNS via resolvectl fmt::format("resolvectl resolv-conf false"), fmt::format("resolvectl dns {} {}", detected_out_interface_name_, dns_server_ipv4_.ToString()), fmt::format( "resolvectl default-route {} false", detected_out_interface_name_), fmt::format("resolvectl dns {} {}", tun_interface_name_, dns_server_ipv4_.ToString()), fmt::format("resolvectl default-route {} true", tun_interface_name_), fmt::format("resolvectl domain {} ~.", tun_interface_name_), fmt::format(R"(bash -c "chattr -i /etc/resolv.conf")"), fmt::format( R"(bash -c "grep -q '^nameserver {}$' /etc/resolv.conf || sed -i '1i nameserver {}' /etc/resolv.conf")", dns_server_ipv6_.ToString(), dns_server_ipv6_.ToString()), fmt::format( R"(bash -c "grep -q '^nameserver {}$' /etc/resolv.conf || sed -i '1i nameserver {}' /etc/resolv.conf")", dns_server_ipv4_.ToString(), dns_server_ipv4_.ToString()), fmt::format(R"(bash -c "chattr +i /etc/resolv.conf")"), fmt::format("resolvectl flush-caches")}; #elif __APPLE__ const std::vector commands = { fmt::format( R"(bash -c "networksetup -listallnetworkservices | grep -v '^An asterisk' | grep -v '^\* ' | xargs -I {{}} networksetup -setdnsservers '{{}}' empty")", dns_server_ipv4_.ToString()), // clean DNS fmt::format("sysctl -w net.inet.ip.forwarding=1"), fmt::format("sysctl -w net.inet6.ip6.forwarding=1"), fmt::format( R"(bash -c "printf 'nat on {findOutInterfaceName} from {tunInterfaceName}:network to any -> ({findOutInterfaceName}) nat on {findOutInterfaceName} inet6 from {tunInterfaceName}:network to any -> ({findOutInterfaceName}) pass out on {findOutInterfaceName} proto tcp from any to {vpnServerIP} pass in on {findOutInterfaceName} proto tcp from {vpnServerIP} to any pass in on {tunInterfaceName} proto tcp from any to any pass out on {tunInterfaceName} proto tcp from any to any pass in on {tunInterfaceName} proto udp from any to any pass out on {tunInterfaceName} proto udp from any to any pass in on {tunInterfaceName} proto udp from any to any port 53 pass out on {tunInterfaceName} proto udp from any to any port 53 pass in on {tunInterfaceName} proto tcp from any to any port 53 pass out on {tunInterfaceName} proto tcp from any to any port 53 ' > /tmp/pf.conf")", fmt::arg("findOutInterfaceName", detected_out_interface_name_), fmt::arg("tunInterfaceName", tun_interface_name_), fmt::arg("vpnServerIP", vpn_server_ip_.ToString())), fmt::format("pfctl -ef /tmp/pf.conf"), // IPv4 default & DNS route fmt::format( "route add -net 0.0.0.0/1 -interface {}", tun_interface_name_), fmt::format( "route add -net 128.0.0.0/1 -interface {}", tun_interface_name_), fmt::format("route add -host {} -interface {}", dns_server_ipv4_.ToString(), tun_interface_name_), // via TUN // IPv6 routes fmt::format( "route add -inet6 -net ::0/1 -interface {}", tun_interface_name_), fmt::format( "route add -inet6 -net 8000::/1 -interface {}", tun_interface_name_), fmt::format("route add -inet6 default -interface {} 2>/dev/null || true", tun_interface_name_), fmt::format("route add -inet6 -host {} -interface {}", dns_server_ipv6_.ToString(), tun_interface_name_), // DNS IPv6 route fmt::format("route add -inet6 -host {} -interface {}", dns_server_ipv6_.ToString(), tun_interface_name_), // exclude vpn server & networks fmt::format("route add -host {} {}", vpn_server_ip_.ToString(), detected_gateway_ipv4_.ToString()), // DNS fmt::format("dscacheutil -flushcache"), fmt::format( R"(bash -c "networksetup -listallnetworkservices | grep -v '^An asterisk' | grep -v '^\* ' | xargs -I {{}} networksetup -setdnsservers '{{}}' {} {}")", dns_server_ipv6_.ToString(), dns_server_ipv4_.ToString())}; #elif _WIN32 const std::string win_interface_number = GetWindowsInterfaceNumber(tun_interface_name_); const std::string interface_info = win_interface_number.empty() ? "" : " if " + win_interface_number; const std::string backup_dns_cmd = R"PSHELL(powershell -Command " if (-not (Test-Path \"$env:TEMP\\fptn_orig_dns.txt\")) { $interface = ')PSHELL" + detected_out_interface_name_ + R"PSHELL('; if (-not $interface) { $interface = ''; } if ($interface) { # IPv4 $netshIPv4 = netsh interface ipv4 show dnsservers \"$interface\" 2>`$null; if ($netshIPv4 -match 'DHCP') { $output = @{IPv4='DHCP'}; } else { $dns4 = Get-DnsClientServerAddress -InterfaceAlias $interface -AddressFamily IPv4 2>`$null | Select-Object -ExpandProperty ServerAddresses; if ($dns4) { $output = @{IPv4=($dns4 -join ',')}; } } # IPv6 $netshIPv6 = netsh interface ipv6 show dnsservers \"$interface\" 2>`$null; if ($netshIPv6 -match 'DHCP') { $output.IPv6 = 'DHCP'; } else { $dns6 = Get-DnsClientServerAddress -InterfaceAlias $interface -AddressFamily IPv6 2>`$null | Select-Object -ExpandProperty ServerAddresses; if ($dns6) { $output.IPv6 = $dns6 -join ','; } } if ($output) { $output | ConvertTo-Json | Out-File \"$env:TEMP\\fptn_orig_dns.txt\" -Encoding UTF8; } } }")PSHELL"; const std::string configure_dns_cmd = R"PSHELL(powershell -Command " $dns4 = ')PSHELL" + dns_server_ipv4_.ToString() + R"PSHELL('; $dns6 = ')PSHELL" + dns_server_ipv6_.ToString() + R"PSHELL('; $interface = ')PSHELL" + detected_out_interface_name_ + R"PSHELL('; if (-not $interface) { $interface = ''; } if ($interface) { # IPv4 Set-DnsClientServerAddress -InterfaceAlias $interface -ServerAddresses $dns4 -ErrorAction SilentlyContinue; netsh interface ipv4 set dnsservers name=\"$interface\" source=static address=$dns4 validate=no register=no 2>`$null; # IPv6 Set-DnsClientServerAddress -InterfaceAlias $interface -ServerAddresses $dns6 -ErrorAction SilentlyContinue; netsh interface ipv6 set dnsservers name=\"$interface\" source=static address=$dns6 validate=no register=no 2>`$null; }")PSHELL"; const std::vector commands = { fmt::format("route add {} mask 255.255.255.255 {} METRIC 2", vpn_server_ip_.ToString(), detected_gateway_ipv4_.ToString()), // Default gateway & dns fmt::format("route add 0.0.0.0 mask 0.0.0.0 {} METRIC 1 {}", tun_interface_address_ipv4_.ToString(), interface_info), fmt::format("route add {} mask 255.255.255.255 {} METRIC 2 {}", dns_server_ipv4_.ToString(), tun_interface_address_ipv4_.ToString(), interface_info), // via TUN // DNS enable_advanced_dns_management_ ? backup_dns_cmd : "echo \"No advanced DNS management\" ", enable_advanced_dns_management_ ? configure_dns_cmd : "echo \"No advanced DNS management\" ", fmt::format("netsh interface ip set dns name=\"{}\" static {}", tun_interface_name_, dns_server_ipv4_.ToString()), // IPv6 fmt::format("netsh interface ipv6 add route ::/0 \"{}\" \"{}\" ", tun_interface_name_, tun_interface_address_ipv6_.ToString()), fmt::format("netsh interface ipv6 add dnsservers=\"{}\" \"{}\" index=1", tun_interface_name_, dns_server_ipv6_.ToString()), // Flush DNS cache "ipconfig /flushdns"}; #else #error "Unsupported system!" #endif try { for (const auto& cmd : commands) { fptn::common::system::command::run(cmd); } } catch (const std::exception& e) { SPDLOG_ERROR("IPTables error: {}", e.what()); } catch (...) { SPDLOG_ERROR("Undefined error"); } SPDLOG_INFO("=== Routing setup completed successfully ==="); return true; } bool RouteManager::Clean() { // NOLINT(bugprone-exception-escape) if (!running_) { SPDLOG_INFO("No need to clean rules!"); return true; } const std::unique_lock lock(mutex_); // mutex running_ = false; // clean dns ipv4 for (const auto& ip : dns_routes_ipv4_) { std::string interface_name; if (ip.policy == RoutingPolicy::kExcludeFromVpn) { if (!detected_out_interface_name_.empty()) { interface_name = detected_out_interface_name_; } else if (!out_interface_name_.empty()) { interface_name = out_interface_name_; } else { interface_name = GetDefaultNetworkInterfaceName(); } } else { interface_name = tun_interface_name_; } RemoveIPv4RouteFromSystem( ip.destination, gateway_ipv4_.ToString(), interface_name); } dns_routes_ipv4_.clear(); // clean dns ipv6 for (const auto& ip : dns_routes_ipv6_) { std::string interface_name; if (ip.policy == RoutingPolicy::kExcludeFromVpn) { if (!detected_out_interface_name_.empty()) { interface_name = detected_out_interface_name_; } else if (!out_interface_name_.empty()) { interface_name = out_interface_name_; } else { interface_name = GetDefaultNetworkInterfaceName(); } } else { interface_name = tun_interface_name_; } RemoveIPv6RouteFromSystem( ip.destination, gateway_ipv6_.ToString(), interface_name); } dns_routes_ipv6_.clear(); // clean route ipv4 for (const auto& route : additional_routes_ipv4_) { if (route.policy == RoutingPolicy::kExcludeFromVpn) { RemoveIPv4RouteFromSystem( route.destination, gateway_ipv4_.ToString(), out_interface_name_); } else { // Include route - remove through VPN interface RemoveIPv4RouteFromSystem(route.destination, tun_interface_address_ipv4_.ToString(), tun_interface_name_); } } additional_routes_ipv4_.clear(); // Remove additional IPv6 routes for (const auto& route : additional_routes_ipv6_) { if (route.policy == RoutingPolicy::kExcludeFromVpn) { RemoveIPv6RouteFromSystem( route.destination, gateway_ipv6_.ToString(), out_interface_name_); } else { // Include route - remove through VPN interface RemoveIPv6RouteFromSystem(route.destination, tun_interface_address_ipv6_.ToString(), tun_interface_name_); } } additional_routes_ipv6_.clear(); #ifdef __linux__ std::vector commands = { fmt::format("iptables -t nat -D POSTROUTING -o {} -j MASQUERADE", detected_out_interface_name_), fmt::format("iptables -D FORWARD -i {} -o {} -m state --state " "RELATED,ESTABLISHED -j ACCEPT", detected_out_interface_name_, tun_interface_name_), fmt::format("iptables -D FORWARD -i {} -o {} -j ACCEPT", tun_interface_name_, detected_out_interface_name_), fmt::format("iptables -D OUTPUT -o {} -d {} -j ACCEPT", detected_out_interface_name_, vpn_server_ip_.ToString()), fmt::format("iptables -D INPUT -i {} -s {} -j ACCEPT", detected_out_interface_name_, vpn_server_ip_.ToString()), // del routes fmt::format("ip route del default dev {}", tun_interface_name_), fmt::format("ip route del {} via {} dev {}", vpn_server_ip_.ToString(), detected_gateway_ipv4_.ToString(), detected_out_interface_name_), // Delete DNS server route fmt::format("ip route del {} dev {}", dns_server_ipv4_.ToString(), tun_interface_name_), fmt::format( R"(bash -c "chattr -i /etc/resolv.conf; sed -i '/^nameserver {}$/d' /etc/resolv.conf; sed -i '/^nameserver {}$/d' /etc/resolv.conf")", dns_server_ipv4_.ToString(), dns_server_ipv6_.ToString()), // Delete DNS to specific DNS server IP rules fmt::format("iptables -D OUTPUT -d {} -p udp --dport 53 -j ACCEPT", dns_server_ipv4_.ToString()), fmt::format("iptables -D OUTPUT -d {} -p tcp --dport 53 -j ACCEPT", dns_server_ipv4_.ToString()), // Delete DNS block rules IPv4 fmt::format("iptables -D OUTPUT -o {} -p udp --dport 53 -j DROP", detected_out_interface_name_), fmt::format("iptables -D OUTPUT -o {} -p tcp --dport 53 -j DROP", detected_out_interface_name_), fmt::format("iptables -D OUTPUT -o {} -p udp --dport 853 -j DROP", detected_out_interface_name_), fmt::format("iptables -D OUTPUT -o {} -p tcp --dport 853 -j DROP", detected_out_interface_name_), // Delete DNS block rules IPv6 fmt::format("ip6tables -D OUTPUT -o {} -p udp --dport 53 -j DROP", detected_out_interface_name_), fmt::format("ip6tables -D OUTPUT -o {} -p tcp --dport 53 -j DROP", detected_out_interface_name_), fmt::format("ip6tables -D OUTPUT -o {} -p udp --dport 853 -j DROP", detected_out_interface_name_), fmt::format("ip6tables -D OUTPUT -o {} -p tcp --dport 853 -j DROP", detected_out_interface_name_), // Delete TUN allow rules IPv4 fmt::format("iptables -D OUTPUT -o {} -p udp --sport 53 -j ACCEPT", tun_interface_name_), fmt::format("iptables -D OUTPUT -o {} -p tcp --sport 53 -j ACCEPT", tun_interface_name_), // Delete TUN allow rules IPv6 fmt::format("ip6tables -D OUTPUT -o {} -p udp --sport 53 -j ACCEPT", tun_interface_name_), fmt::format("ip6tables -D OUTPUT -o {} -p tcp --sport 53 -j ACCEPT", tun_interface_name_)}; // Restore DNS if (!original_dns_servers_.empty()) { std::string all_dns; for (const auto& dns : original_dns_servers_) { if (!all_dns.empty()) { all_dns += " "; } all_dns += dns; } commands.push_back(fmt::format( "resolvectl dns {} {}", detected_out_interface_name_, all_dns)); commands.push_back( fmt::format("resolvectl domain {} .", detected_out_interface_name_)); commands.push_back(fmt::format( "resolvectl default-route {} true", detected_out_interface_name_)); SPDLOG_INFO("Restoring {} DNS servers for {}", original_dns_servers_.size(), detected_out_interface_name_); } else { commands.push_back( fmt::format("resolvectl revert {}", detected_out_interface_name_)); SPDLOG_INFO("Reverting DNS to DHCP for {}", detected_out_interface_name_); } commands.emplace_back("resolvectl flush-caches"); #elif __APPLE__ const std::vector commands = { fmt::format( R"(bash -c "networksetup -listallnetworkservices | grep -v '^An asterisk' | grep -v '^\* ' | xargs -I {{}} networksetup -setdnsservers '{{}}' empty")"), // clean DNS fmt::format("pfctl -F all -f /etc/pf.conf"), // del routes fmt::format("route delete -host {} -interface {}", dns_server_ipv4_.ToString(), tun_interface_name_), // via TUN fmt::format( "route delete -net 0.0.0.0/1 -interface {}", tun_interface_name_), fmt::format( "route delete -net 128.0.0.0/1 -interface {}", tun_interface_name_), // del IPv6 routes fmt::format( "route delete -inet6 -net ::0/1 -interface {}", tun_interface_name_), fmt::format("route delete -inet6 -net 8000::/1 -interface {}", tun_interface_name_), fmt::format("route delete -host {} {}", vpn_server_ip_.ToString(), detected_gateway_ipv4_.ToString()), // DNS fmt::format( R"(bash -c "networksetup -listallnetworkservices | grep -v '^An asterisk' | grep -v '^\* ' | xargs -I {{}} networksetup -setdnsservers '{{}}' empty")") // clean DNS }; #elif _WIN32 std::string current_interface_name = detected_out_interface_name_; if (current_interface_name.empty()) { current_interface_name = out_interface_name_; } if (current_interface_name.empty()) { current_interface_name = GetDefaultNetworkInterfaceName(); } const std::string restore_dns_cmd = R"PSHELL(powershell -Command " $interface = ')PSHELL" + current_interface_name + R"PSHELL('; if ($interface) { if (Test-Path \"$env:TEMP\\fptn_orig_dns.txt\") { $config = Get-Content \"$env:TEMP\\fptn_orig_dns.txt\" -Raw | ConvertFrom-Json; # IPv4 if ($config.IPv4 -eq 'DHCP') { netsh interface ip set dns \"$interface\" dhcp } elseif ($config.IPv4) { $dns4Servers = $config.IPv4 -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }; if ($dns4Servers.Count -gt 0) { netsh interface ip set dns \"$interface\" static $($dns4Servers[0]) if ($dns4Servers.Count -gt 1) { netsh interface ip add dns \"$interface\" $($dns4Servers[1]) index=2 } } } # IPv6 if ($config.IPv6 -eq 'DHCP') { netsh interface ipv6 set dnsservers \"$interface\" dhcp } elseif ($config.IPv6) { $dns6Servers = $config.IPv6 -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }; if ($dns6Servers.Count -gt 0) { netsh interface ipv6 set dnsservers \"$interface\" static $($dns6Servers[0]) primary if ($dns6Servers.Count -gt 1) { netsh interface ipv6 add dnsservers \"$interface\" $($dns6Servers[1]) index=2 } } } Remove-Item \"$env:TEMP\\fptn_orig_dns.txt\" -Force } else { netsh interface ip set dns \"$interface\" dhcp netsh interface ipv6 set dnsservers \"$interface\" dhcp } }")PSHELL"; const std::vector commands = { enable_advanced_dns_management_ ? restore_dns_cmd : "echo \"No advanced DNS management\" ", // Remove routes fmt::format( "route delete {} mask 255.255.255.255", vpn_server_ip_.ToString()), fmt::format("route delete 0.0.0.0 mask 0.0.0.0"), fmt::format( "route delete {} mask 255.255.255.255", dns_server_ipv4_.ToString()), fmt::format( "netsh interface ipv6 delete route ::/0 \"{}\"", tun_interface_name_), // Final cleanup "ipconfig /flushdns", // restore routing fmt::format("route add 0.0.0.0 mask 0.0.0.0 {} METRIC 1", detected_gateway_ipv4_.ToString())}; #else #error "Unsupported system!" #endif try { for (const auto& cmd : commands) { fptn::common::system::command::run(cmd); } } catch (const std::exception& e) { SPDLOG_ERROR("IPTables error: {}", e.what()); } catch (...) { SPDLOG_ERROR("Undefined error"); } running_ = false; return true; } bool RouteManager::AddDnsRoutesIPv4( const std::vector& ips, const RoutingPolicy policy) { std::string interface_name; std::string gateway_ip; if (policy == RoutingPolicy::kExcludeFromVpn) { if (!detected_out_interface_name_.empty()) { interface_name = detected_out_interface_name_; } else if (!out_interface_name_.empty()) { interface_name = out_interface_name_; } else { interface_name = GetDefaultNetworkInterfaceName(); } gateway_ip = gateway_ipv4_.ToString(); } else { interface_name = tun_interface_name_; gateway_ip = tun_interface_address_ipv4_.ToString(); } if (interface_name.empty()) { interface_name = fptn::routing::GetDefaultNetworkInterfaceName(); } if (interface_name.empty()) { SPDLOG_WARN( "Cannot add DNS IPv4 routes: interface name is empty for policy {}", policy == RoutingPolicy::kExcludeFromVpn ? "EXCLUDE" : "INCLUDE"); return false; } std::vector entries_to_add; { const std::unique_lock lock(mutex_); for (const auto& ip : ips) { std::string ip_str = ip.ToString(); RouteEntry entry{.destination = ip_str, .policy = policy}; if (!dns_routes_ipv4_.contains(entry)) { dns_routes_ipv4_.insert(entry); entries_to_add.push_back(std::move(entry)); } } } if (entries_to_add.empty()) { return true; } bool status = true; for (const auto& entry : entries_to_add) { try { const bool rv = AddIPv4RouteToSystem(entry.destination, gateway_ip, interface_name); if (rv) { const std::string policy_str = (policy == RoutingPolicy::kExcludeFromVpn ? "EXCLUDE (bypass VPN)" : "INCLUDE (through VPN)"); SPDLOG_INFO("DNS route added: {} [{}]", entry.destination, policy_str); } else { SPDLOG_WARN("Failed to add DNS route: {}", entry.destination); const std::unique_lock lock(mutex_); dns_routes_ipv4_.erase(entry); status = false; } } catch (const std::exception& e) { SPDLOG_WARN("Exception adding DNS IPv4 route {}: {}", entry.destination, e.what()); const std::unique_lock lock(mutex_); dns_routes_ipv4_.erase(entry); status = false; } } return status; } bool RouteManager::AddDnsRoutesIPv6( const std::vector& ips, const RoutingPolicy policy) { std::string interface_name; std::string gateway_ip; if (policy == RoutingPolicy::kExcludeFromVpn) { if (!detected_out_interface_name_.empty()) { interface_name = detected_out_interface_name_; } else if (!out_interface_name_.empty()) { interface_name = out_interface_name_; } else { interface_name = GetDefaultNetworkInterfaceName(); } gateway_ip = gateway_ipv6_.ToString(); } else { interface_name = tun_interface_name_; gateway_ip = tun_interface_address_ipv6_.ToString(); } if (interface_name.empty()) { SPDLOG_WARN( "Cannot add DNS IPv6 routes: interface name is empty for policy {}", policy == RoutingPolicy::kExcludeFromVpn ? "EXCLUDE" : "INCLUDE"); return false; } if (gateway_ip.empty()) { SPDLOG_WARN("Cannot add DNS IPv6 routes: gateway IP is empty for policy {}", policy == RoutingPolicy::kExcludeFromVpn ? "EXCLUDE" : "INCLUDE"); return false; } std::vector entries_to_add; { const std::unique_lock lock(mutex_); for (const auto& ip : ips) { std::string ip_str = ip.ToString(); RouteEntry entry{.destination = ip_str, .policy = policy}; if (!dns_routes_ipv6_.contains(entry)) { dns_routes_ipv6_.insert(entry); entries_to_add.push_back(std::move(entry)); } } } if (entries_to_add.empty()) { return true; } bool status = true; for (const auto& entry : entries_to_add) { try { const bool rv = AddIPv6RouteToSystem(entry.destination, gateway_ip, interface_name); if (rv) { const std::string policy_str = (policy == RoutingPolicy::kExcludeFromVpn ? "EXCLUDE (bypass VPN)" : "INCLUDE (through VPN)"); SPDLOG_INFO( "DNS IPv6 route added: {} [{}]", entry.destination, policy_str); } else { SPDLOG_WARN("Failed to add DNS IPv6 route: {}", entry.destination); status = false; } } catch (const std::exception& e) { SPDLOG_WARN("Exception adding DNS IPv6 route {}: {}", entry.destination, e.what()); status = false; } } return status; } bool RouteManager::AddExcludeNetworks( const std::vector& networks) { std::vector> networks_to_add; { const std::unique_lock lock(mutex_); for (const auto& network : networks) { if (network.empty()) { continue; } const bool is_ipv6 = network.find(':') != std::string::npos; // NOLINT RouteEntry entry{ .destination = network, .policy = RoutingPolicy::kExcludeFromVpn}; if (is_ipv6) { if (!additional_routes_ipv6_.contains(entry)) { networks_to_add.emplace_back(network, true); additional_routes_ipv6_.insert(entry); } } else { if (!additional_routes_ipv4_.contains(entry)) { networks_to_add.emplace_back(network, false); additional_routes_ipv4_.insert(entry); } } } if (networks_to_add.empty()) { return true; } } const std::string interface_name = !detected_out_interface_name_.empty() ? detected_out_interface_name_ : out_interface_name_; bool all_success = true; for (const auto& [network, is_ipv6] : networks_to_add) { try { bool success = false; if (is_ipv6) { success = AddIPv6RouteToSystem( network, gateway_ipv6_.ToString(), interface_name); } else { success = AddIPv4RouteToSystem( network, gateway_ipv4_.ToString(), interface_name); } if (success) { SPDLOG_INFO( "Added {} exclude network: {}", is_ipv6 ? "IPv6" : "IPv4", network); } else { SPDLOG_WARN("Failed to add route: {}", network); all_success = false; } } catch (const std::exception& e) { SPDLOG_WARN("Failed to add exclude network '{}': {}", network, e.what()); const std::unique_lock lock(mutex_); RouteEntry entry{ .destination = network, .policy = RoutingPolicy::kExcludeFromVpn}; if (is_ipv6) { additional_routes_ipv6_.erase(entry); } else { additional_routes_ipv4_.erase(entry); } all_success = false; } } return all_success; } bool RouteManager::AddIncludeNetworks( const std::vector& networks) { std::vector> networks_to_add; { const std::unique_lock lock(mutex_); for (const auto& network : networks) { if (network.empty()) { continue; } const bool is_ipv6 = network.find(':') != std::string::npos; // NOLINT RouteEntry entry{ .destination = network, .policy = RoutingPolicy::kIncludeInVpn}; if (is_ipv6) { if (!additional_routes_ipv6_.contains(entry)) { networks_to_add.emplace_back(network, true); additional_routes_ipv6_.insert(entry); } } else { if (!additional_routes_ipv4_.contains(entry)) { networks_to_add.emplace_back(network, false); additional_routes_ipv4_.insert(entry); } } } if (networks_to_add.empty()) { return true; } } bool all_success = true; for (const auto& [network, is_ipv6] : networks_to_add) { try { bool success = false; if (is_ipv6) { success = AddIPv6RouteToSystem(network, tun_interface_address_ipv6_.ToString(), tun_interface_name_); } else { success = AddIPv4RouteToSystem(network, tun_interface_address_ipv4_.ToString(), tun_interface_name_); } if (success) { SPDLOG_INFO( "Added {} include network: {}", is_ipv6 ? "IPv6" : "IPv4", network); } else { SPDLOG_ERROR("Failed to add route: {}", network); all_success = false; } } catch (const std::exception& e) { SPDLOG_WARN("Failed to add include network '{}': {}", network, e.what()); all_success = false; } } return all_success; } // NOLINT(bugprone-exception-escape) fptn::common::network::IPv4Address fptn::routing::ResolveDomain( const std::string& domain) { try { try { // error test boost::asio::ip::make_address(domain); return fptn::common::network::IPv4Address::Create(domain); } catch (const std::exception&) { // NOLINT(bugprone-empty-catch) // Not a valid IP address, proceed with domain name resolution } boost::asio::io_context io_context; boost::asio::ip::tcp::resolver resolver(io_context); boost::asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(domain, ""); for (const auto& endpoint : endpoints) { return fptn::common::network::IPv4Address( endpoint.endpoint().address().to_string()); } } catch (const std::exception& e) { SPDLOG_ERROR("Error resolving domain: {}", e.what()); } return fptn::common::network::IPv4Address(domain); } fptn::common::network::IPv4Address fptn::routing::GetDefaultGatewayIPAddress() { try { #ifdef __linux__ const std::string command = "ip route get 8.8.8.8 | awk '{print $3; exit}'"; #elif __APPLE__ const std::string command = "route get 8.8.8.8 | grep gateway | awk '{print $2}' "; #elif _WIN32 const std::string command = R"(cmd.exe /c FOR /f "tokens=3" %i in ('route print ^| find "0.0.0.0"') do @echo %i)"; #else #error "Unsupported system!" #endif std::vector cmd_stdout; fptn::common::system::command::run(command, cmd_stdout); for (const auto& line : cmd_stdout) { std::string result = line; result.erase( // NOLINTNEXTLINE(modernize-use-ranges) std::remove_if(result.begin(), result.end(), [](char c) { /* Allow: a-z, A-Z, 0-9, dot, dash */ return !std::isalnum(c) && c != '.' && c != '-' && c != '_'; }), result.end()); if (!result.empty()) { return ResolveDomain(result); } } } catch (const std::exception& ex) { SPDLOG_ERROR("Error: Failed to retrieve the default gateway IP address. {}", ex.what()); } return {}; } fptn::common::network::IPv6Address fptn::routing::GetDefaultGatewayIPv6Address() { try { #ifdef __linux__ const std::string command = "ip -6 route | grep default | head -1 | awk '{print $3}'"; #elif __APPLE__ const std::string command = "route get -inet6 default | grep gateway | awk '{print $2}'"; #elif _WIN32 const std::string command = R"(netsh interface ipv6 show routes | find "::/0" | head -1 | awk "{print $3}")"; #else return {}; #endif std::vector cmd_stdout; fptn::common::system::command::run(command, cmd_stdout); for (const auto& line : cmd_stdout) { std::string result = line; std::erase_if(result, [](const char c) { return !std::isalnum(c) && c != ':' && c != '.' && c != '-'; }); if (!result.empty()) { return fptn::common::network::IPv6Address::Create(result); } } } catch (const std::exception& ex) { SPDLOG_ERROR("Error getting IPv6 gateway: {}", ex.what()); } return {}; } std::string fptn::routing::GetDefaultNetworkInterfaceName() { std::string result; try { #ifdef __linux__ const std::string command = "ip route get 8.8.8.8 | awk '{print $5; exit}' "; #elif __APPLE__ const std::string command = "route get 8.8.8.8 | grep interface | awk '{print $2}' "; #elif _WIN32 const std::string command = R"(powershell -Command "(Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Where-Object {$_.NextHop -ne '0.0.0.0'} | Select-Object -First 1).InterfaceAlias")"; #endif std::vector cmd_stdout; fptn::common::system::command::run(command, cmd_stdout); if (cmd_stdout.empty()) { SPDLOG_WARN("Warning: Default gateway IP address not found."); return {}; } for (const auto& line : cmd_stdout) { result = line; result.erase(result.find_last_not_of(" \n\r\t") + 1); result.erase(0, result.find_first_not_of(" \n\r\t")); } } catch (const std::exception& ex) { SPDLOG_ERROR("Error: Failed to retrieve the default gateway IP address. {}", ex.what()); } return result; } ================================================ FILE: src/fptn-client/routing/route_manager.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include "common/network/ip_address.h" namespace fptn::routing { std::string GetDefaultNetworkInterfaceName(); fptn::common::network::IPv4Address GetDefaultGatewayIPAddress(); fptn::common::network::IPv4Address ResolveDomain(const std::string& domain); fptn::common::network::IPv6Address GetDefaultGatewayIPv6Address(); enum class RoutingPolicy { kExcludeFromVpn, // Traffic bypasses VPN kIncludeInVpn // Traffic goes through VPN }; class RouteManager final { protected: struct RouteEntry { std::string destination; RoutingPolicy policy; bool operator==(const RouteEntry& other) const { return destination == other.destination && policy == other.policy; } struct Hash { std::size_t operator()(const RouteEntry& entry) const { return std::hash{}(entry.destination) ^ (std::hash{}(static_cast(entry.policy)) << 1); } }; }; public: RouteManager(std::string out_interface_name, std::string tun_interface_name, fptn::common::network::IPv4Address vpn_server_ip, fptn::common::network::IPv4Address dns_server_ipv4, fptn::common::network::IPv6Address dns_server_ipv6, fptn::common::network::IPv4Address gateway_ipv4, fptn::common::network::IPv6Address gateway_ipv6, fptn::common::network::IPv4Address tun_interface_address_ipv4, fptn::common::network::IPv6Address tun_interface_address_ipv6 #if _WIN32 , bool enable_advanced_dns_management_ #endif ); // NOLINT ~RouteManager(); void UpdateTunInterfaceName(const std::string& name) { tun_interface_name_ = name; } bool Apply(); bool Clean(); bool AddDnsRoutesIPv4( const std::vector& ips, RoutingPolicy policy); bool AddDnsRoutesIPv6( const std::vector& ips, RoutingPolicy policy); bool AddExcludeNetworks(const std::vector& networks); bool AddIncludeNetworks(const std::vector& networks); private: mutable std::mutex mutex_; std::atomic running_; const std::string out_interface_name_; std::string tun_interface_name_; const fptn::common::network::IPv4Address vpn_server_ip_; const fptn::common::network::IPv4Address dns_server_ipv4_; const fptn::common::network::IPv6Address dns_server_ipv6_; const fptn::common::network::IPv4Address gateway_ipv4_; const fptn::common::network::IPv6Address gateway_ipv6_; const fptn::common::network::IPv4Address tun_interface_address_ipv4_; const fptn::common::network::IPv6Address tun_interface_address_ipv6_; #if _WIN32 const bool enable_advanced_dns_management_; #endif std::unordered_set dns_routes_ipv4_; std::unordered_set dns_routes_ipv6_; std::unordered_set additional_routes_ipv4_; std::unordered_set additional_routes_ipv6_; private: std::string detected_out_interface_name_; fptn::common::network::IPv4Address detected_gateway_ipv4_; #ifdef __linux__ std::vector original_dns_servers_; #endif }; using RouteManagerSPtr = std::shared_ptr; } // namespace fptn::routing ================================================ FILE: src/fptn-client/utils/brotli/brotli.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include // NOLINT(build/include_order) namespace fptn::utils::brotli { inline std::string Decompress(const std::string& compressed_data) { const std::size_t encoded_size = compressed_data.size(); std::size_t decoded_size = encoded_size * 30; while (true) { std::vector decoded_buffer(decoded_size); std::size_t available_out = decoded_size; const BROTLI_BOOL result = BrotliDecoderDecompress(encoded_size, reinterpret_cast(compressed_data.data()), &available_out, decoded_buffer.data()); if (result == BROTLI_TRUE) { return std::string( reinterpret_cast(decoded_buffer.data()), available_out); } decoded_size *= 2; } } } // namespace fptn::utils::brotli ================================================ FILE: src/fptn-client/utils/macos/admin.h ================================================ #pragma once /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include // NOLINT(build/include_order) #ifdef __APPLE__ #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) namespace fptn::utils::macos { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" bool RestartApplicationWithAdminRights() { // Already running as root? No need to restart. if (geteuid() == 0) { return true; } // Initialize AuthorizationRef AuthorizationRef auth_ref = nullptr; OSStatus status = AuthorizationCreate(nullptr, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth_ref); if (status != errAuthorizationSuccess) { std::cerr << "Failed to create authorization reference.\n"; return false; } // Request admin rights const char* kAdminRights[] = {kAuthorizationRightExecute, "system.preferences", "system.preferences.network", "system.services.systemconfiguration.network"}; AuthorizationItem rights[4] = {}; for (size_t i = 0; i < 4; ++i) { rights[i] = {kAdminRights[i], 0, nullptr, 0}; } AuthorizationRights rights_set = {4, rights}; AuthorizationFlags flags = kAuthorizationFlagDefaults | kAuthorizationFlagInteractionAllowed | kAuthorizationFlagExtendRights; status = AuthorizationCopyRights(auth_ref, &rights_set, nullptr, flags, nullptr); if (status != errAuthorizationSuccess) { AuthorizationFree(auth_ref, kAuthorizationFlagDefaults); std::cerr << "Failed to obtain admin rights.\n"; return false; } // Get current executable path char executablePath[PATH_MAX] = {}; uint32_t pathSize = sizeof(executablePath); if (_NSGetExecutablePath(executablePath, &pathSize) != 0) { AuthorizationFree(auth_ref, kAuthorizationFlagDefaults); std::cerr << "Failed to get executable path.\n"; return false; } // Restart with privileges char* args[] = {const_cast(executablePath), nullptr}; status = AuthorizationExecuteWithPrivileges( auth_ref, executablePath, kAuthorizationFlagDefaults, args, nullptr); AuthorizationFree(auth_ref, kAuthorizationFlagDefaults); if (status != errAuthorizationSuccess) { std::cerr << "Failed to restart with admin rights.\n"; return false; } // Exit the current instance (only the elevated one will continue) exit(EXIT_SUCCESS); return true; } #pragma clang diagnostic pop } // namespace fptn::utils::macos #endif ================================================ FILE: src/fptn-client/utils/signal/main_loop.h ================================================ #pragma once /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include "vpn/vpn_client.h" namespace fptn::utils { void WaitForSignal(fptn::vpn::VpnClient& vpn_client) { boost::asio::io_context io_context; boost::asio::signal_set signals(io_context, SIGINT, SIGTERM); boost::asio::steady_timer check_timer(io_context); std::function check_connection = [&]() { if (!vpn_client.IsStarted()) { SPDLOG_ERROR("VPN connection lost! Exiting..."); io_context.stop(); return; } check_timer.expires_after(std::chrono::seconds(1)); check_timer.async_wait([&](const boost::system::error_code& ec) { if (!ec) { check_connection(); } }); }; signals.async_wait([&](auto, auto) { io_context.stop(); }); check_timer.expires_after(std::chrono::seconds(1)); check_timer.async_wait([&](const boost::system::error_code& ec) { if (!ec) { check_connection(); } }); io_context.run(); } } // namespace fptn::utils ================================================ FILE: src/fptn-client/utils/speed_estimator/server_info.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include namespace fptn::utils::speed_estimator { struct ServerInfo { std::string name; std::string host; int port; bool is_using; std::string md5_fingerprint; std::string username; std::string password; std::string service_name; ServerInfo() : port(0), is_using(false) {} ServerInfo(std::string _name, std::string _host, int _port, std::string _md5_fingerprint) : name(std::move(_name)), host(std::move(_host)), port(_port), is_using(false), md5_fingerprint(std::move(_md5_fingerprint)) {} }; } // namespace fptn::utils::speed_estimator ================================================ FILE: src/fptn-client/utils/speed_estimator/speed_estimator.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-client/utils/speed_estimator/speed_estimator.h" #include #include #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "fptn-protocol-lib/https/api_client/api_client.h" using fptn::protocol::https::ApiClient; using fptn::utils::speed_estimator::ServerInfo; constexpr std::uint64_t kMaxTimeout = UINT64_MAX; namespace fptn::utils::speed_estimator { std::uint64_t GetDownloadTimeMs(const ServerInfo& server, const std::string& sni, int timeout, const std::string& md5_fingerprint, fptn::protocol::https::CensorshipStrategy censorship_strategy) { try { auto const start = std::chrono::high_resolution_clock::now(); ApiClient cli( server.host, server.port, sni, md5_fingerprint, censorship_strategy); auto const resp = cli.Get("/api/v1/test/file.bin", timeout); if (resp.code == 200) { auto const end = std::chrono::high_resolution_clock::now(); const std::uint64_t ms = std::chrono::duration_cast(end - start) .count(); return ms; } } catch (const std::exception& ex) { SPDLOG_WARN("Exception in GetDownloadTimeMs: {}", ex.what()); } catch (...) { SPDLOG_WARN("Unknown exception in GetDownloadTimeMs"); } return kMaxTimeout; } ServerInfo FindFastestServer(const std::string& sni, const std::vector& servers, fptn::protocol::https::CensorshipStrategy censorship_strategy, int timeout_sec) { // randomly select half of the servers std::vector shuffled_servers = servers; std::random_device rd; std::mt19937 generator(rd()); std::ranges::shuffle(shuffled_servers, generator); const std::size_t half_size = std::max(1, shuffled_servers.size() / 2); std::vector selected_servers( shuffled_servers.begin(), shuffled_servers.begin() + half_size); // Create promises and futures for all requests std::vector> promises(selected_servers.size()); std::vector> futures; futures.reserve(selected_servers.size()); for (auto& promise : promises) { // cppcheck-suppress useStlAlgorithm futures.push_back(promise.get_future()); } // Launch all requests for (std::size_t i = 0; i < selected_servers.size(); ++i) { // NOLINTNEXTLINE(bugprone-exception-escape) std::thread([&promise = promises[i], server = selected_servers[i], sni, timeout_sec, censorship_strategy]() { try { const auto time_ms = GetDownloadTimeMs(server, sni, timeout_sec, server.md5_fingerprint, censorship_strategy); promise.set_value(time_ms); } catch (...) { // NOLINT // Set max timeout in case of exception promise.set_value(kMaxTimeout); } }).detach(); } // Wait for all futures with timeout std::vector times; times.reserve(futures.size()); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout_sec + 2); for (auto& future : futures) { if (future.wait_until(deadline) == std::future_status::ready) { times.push_back(future.get()); } else { // Timeout for this future times.push_back(kMaxTimeout); } } // Find fastest server const auto min_it = std::ranges::min_element(times); if (min_it == times.end() || *min_it == kMaxTimeout) { throw std::runtime_error("All servers unavailable!"); } return selected_servers[std::distance(times.begin(), min_it)]; } } // namespace fptn::utils::speed_estimator ================================================ FILE: src/fptn-client/utils/speed_estimator/speed_estimator.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include "fptn-client/utils/speed_estimator/server_info.h" #include "fptn-protocol-lib/https/censorship_strategy.h" #include "fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h" namespace fptn::utils::speed_estimator { std::uint64_t GetDownloadTimeMs(const ServerInfo& server, const std::string& sni, int timeout, const std::string& md5_fingerprint, fptn::protocol::https::CensorshipStrategy censorship_strategy); ServerInfo FindFastestServer(const std::string& sni, const std::vector& servers, fptn::protocol::https::CensorshipStrategy censorship_strategy, int timeout_sec = 15); }; // namespace fptn::utils::speed_estimator ================================================ FILE: src/fptn-client/utils/utils.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include // NOLINT(build/include_order) #include "common/utils/utils.h" namespace fptn::utils { inline std::string DomainToRegex(const std::string& pattern) { const std::string domain_prefix = "domain:"; const std::string trimmed = fptn::common::utils::Trim(pattern); if (!trimmed.starts_with(domain_prefix)) { return {}; } const std::string domain = trimmed.substr(domain_prefix.length()); if (domain.empty()) { return {}; } std::string escaped; escaped.reserve(domain.length() * 2); for (const char c : fptn::common::utils::ToLowerCase(domain)) { if (c == '.') { escaped += "\\."; } else { escaped += c; } } // return R"(\.)" + escaped + R"($)"; // return R"((?:^|\.))" + escaped + R"((?:\.|$)?)"; return R"((?:^|\.))" + escaped + R"($)"; } } // namespace fptn::utils ================================================ FILE: src/fptn-client/utils/windows/vpn_conflict.h ================================================ #pragma once /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include "common/system/command.h" namespace fptn::utils::windows { inline bool HasVpnConflicts(std::string& found_adapters) { found_adapters = ""; // Command to list network interfaces that might indicate VPN conflicts constexpr char command[] = "powershell.exe -Command \"Get-NetAdapter | Where-Object { " "$_.InterfaceDescription -match 'VPN|TAP|WireGuard|OpenVPN|Tunnel' -and " "$_.Status -eq 'Up' } | Select-Object -ExpandProperty Name\""; std::vector adapter_names; fptn::common::system::command::run(command, adapter_names); // Check if any VPN-related interfaces were found for (const auto& name : adapter_names) { if (!name.empty()) { if (found_adapters.empty()) { found_adapters = name; } else { found_adapters += ", " + name; } } } return !found_adapters.empty(); } } // namespace fptn::utils::windows ================================================ FILE: src/fptn-client/vpn/http/client.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "vpn/http/client.h" #include #include #include #include #include // NOLINT(build/include_order) #include #include // NOLINT(build/include_order) #include "common/network/ip_address.h" #include "fptn-protocol-lib/https/api_client/api_client.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h" #include "routing/route_manager.h" using fptn::common::network::IPv4Address; using fptn::common::network::IPv6Address; using fptn::protocol::https::ApiClient; using fptn::vpn::http::Client; Client::Client(IPv4Address server_ip, int server_port, IPv4Address tun_interface_address_ipv4, IPv6Address tun_interface_address_ipv6, std::string sni, std::string md5_fingerprint, fptn::protocol::https::CensorshipStrategy censorship_strategy, NewIPPacketCallback new_ip_pkt_callback) : running_(false), server_ip_(std::move(server_ip)), server_port_(server_port), tun_interface_address_ipv4_(std::move(tun_interface_address_ipv4)), tun_interface_address_ipv6_(std::move(tun_interface_address_ipv6)), sni_(std::move(sni)), md5_fingerprint_(std::move(md5_fingerprint)), censorship_strategy_(censorship_strategy), new_ip_pkt_callback_(std::move(new_ip_pkt_callback)), reconnection_attempts_(kMaxReconnectionAttempts_) {} Client::~Client() { Stop(); } bool Client::Login( const std::string& username, const std::string& password, int timeout_sec) { const std::string request = fmt::format( R"({{ "username": "{}", "password": "{}" }})", username, password); const std::string ip = server_ip_.ToString(); ApiClient cli(ip, server_port_, sni_, md5_fingerprint_, censorship_strategy_); const auto resp = cli.Post("/api/v1/login", request, "application/json", timeout_sec); if (resp.code == 200) { try { const auto msg = resp.Json(); if (!msg.contains("access_token")) { SPDLOG_ERROR( "Error: Access token not found in the response. Check your " "conection"); } else { access_token_ = msg["access_token"]; SPDLOG_INFO("Login successful"); return true; } } catch (const nlohmann::json::parse_error& e) { latest_error_ = e.what(); SPDLOG_ERROR("Error parsing JSON response: {} ", e.what()); } catch (const std::exception& ex) { latest_error_ = ex.what(); SPDLOG_ERROR("Exception: {}", ex.what()); } } else { latest_error_ = resp.errmsg; SPDLOG_ERROR( "Error: Request failed code: {} msg: {}", resp.code, resp.errmsg); } return false; } std::pair Client::GetDns() { SPDLOG_INFO("Obtained DNS server address. Connecting to {}:{}", server_ip_.ToString(), server_port_); const std::string ip = server_ip_.ToString(); ApiClient cli(ip, server_port_, sni_, md5_fingerprint_, censorship_strategy_); const auto resp = cli.Get("/api/v1/dns"); if (resp.code == 200) { try { const auto msg = resp.Json(); if (!msg.contains("dns")) { SPDLOG_ERROR( "Error: dns not found in the response. Check your connection"); } else { const std::string dns_ipv4 = msg["dns"]; const std::string dns_ipv6 = (msg.contains("dns_ipv6") ? msg["dns_ipv6"] : FPTN_SERVER_DEFAULT_ADDRESS_IP6); return {IPv4Address(dns_ipv4), IPv6Address(dns_ipv6)}; } } catch (const nlohmann::json::parse_error& e) { latest_error_ = e.what(); SPDLOG_ERROR("Error parsing JSON response: {}", e.what()); } catch (const std::exception& ex) { latest_error_ = ex.what(); SPDLOG_ERROR("Exception: {}", ex.what()); } } else { latest_error_ = resp.errmsg; SPDLOG_ERROR( "Error: Request failed code: {} msg: {}", resp.code, resp.errmsg); } return {IPv4Address(), IPv6Address()}; } void Client::SetRecvIPPacketCallback( const NewIPPacketCallback& callback) noexcept { new_ip_pkt_callback_ = callback; } bool Client::Send(fptn::common::network::IPPacketPtr packet) const { try { const std::unique_lock lock(mutex_); // mutex if (ws_ && running_) { ws_->Send(std::move(packet)); return true; } } catch (const std::runtime_error& err) { SPDLOG_ERROR("Send error: {}", err.what()); } catch (const std::exception& e) { SPDLOG_ERROR("Exception occurred: {}", e.what()); } return false; } void Client::Run() { // Time window for counting attempts (1 minute) constexpr auto kReconnectionWindow = std::chrono::seconds(120); // Delay between reconnection attempts constexpr auto kReconnectionDelay = std::chrono::milliseconds(300); // Current count of reconnection attempts reconnection_attempts_ = kMaxReconnectionAttempts_; auto window_start_time = std::chrono::steady_clock::now(); while (running_ && reconnection_attempts_ > 0) { { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress identicalInnerCondition if (running_) { // Double-check after acquiring lock ws_ = std::make_shared( server_ip_, server_port_, tun_interface_address_ipv4_, tun_interface_address_ipv6_, new_ip_pkt_callback_, sni_, access_token_, md5_fingerprint_, censorship_strategy_, nullptr, 32); } } if (running_ && ws_) { ws_->Run(); // Start the WebSocket client } if (!running_) { break; } // clean if (ws_) { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress knownConditionTrueFalse if (ws_ && running_) { ws_->Stop(); ws_.reset(); } } // Calculate time since last window start const auto current_time = std::chrono::steady_clock::now(); const auto elapsed = current_time - window_start_time; // Reconnection attempt counting logic if (elapsed >= kReconnectionWindow) { // Reset counter if we're past the time window SPDLOG_INFO("Reconnection window reset. New attempt window started"); reconnection_attempts_ = kMaxReconnectionAttempts_; window_start_time = current_time; } if (reconnection_attempts_ > 0) { --reconnection_attempts_; } // Log connection failure and wait before retrying SPDLOG_ERROR( "Connection closed (attempt {}/{} in current window). Reconnecting in " "{}ms...", kMaxReconnectionAttempts_ - reconnection_attempts_, kMaxReconnectionAttempts_, kReconnectionDelay.count()); std::this_thread::sleep_for(kReconnectionDelay); } if (running_ && !reconnection_attempts_) { SPDLOG_ERROR("Connection failure: Could not establish connection"); } } bool Client::Start() { running_ = true; th_ = std::thread(&Client::Run, this); return th_.joinable(); } bool Client::Stop() { if (!running_) { return false; } SPDLOG_INFO("Stopping client"); { const std::unique_lock lock(mutex_); // mutex if (!running_) { // Double-check after acquiring lock return false; } running_ = false; } if (ws_) { ws_->Stop(); } if (th_.joinable()) { try { th_.join(); } catch (...) { SPDLOG_WARN("Unexpected exception during thread join"); } } ws_.reset(); return true; } bool Client::IsStarted() const { return running_ && reconnection_attempts_ > 0; } const std::string& Client::LatestError() const { return latest_error_; } ================================================ FILE: src/fptn-client/vpn/http/client.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include "common/network/ip_address.h" #include "common/network/ip_packet.h" #include "fptn-protocol-lib/https/censorship_strategy.h" #include "fptn-protocol-lib/https/websocket_client/websocket_client.h" namespace fptn::vpn::http { using IPv4Address = fptn::common::network::IPv4Address; using IPv6Address = fptn::common::network::IPv6Address; class Client final { public: using NewIPPacketCallback = std::function; public: Client(IPv4Address server_ip, int server_port, IPv4Address tun_interface_address_ipv4, IPv6Address tun_interface_address_ipv6, std::string sni, std::string md5_fingerprint, fptn::protocol::https::CensorshipStrategy censorship_strategy, NewIPPacketCallback new_ip_pkt_callback = nullptr); ~Client(); bool Login(const std::string& username, const std::string& password, int timeout_sec = 15); std::pair GetDns(); bool Start(); bool Stop(); bool Send(fptn::common::network::IPPacketPtr packet) const; void SetRecvIPPacketCallback(const NewIPPacketCallback& callback) noexcept; bool IsStarted() const; const std::string& LatestError() const; protected: void Run(); private: const int kMaxReconnectionAttempts_ = 15; std::thread th_; mutable std::mutex mutex_; std::atomic running_; const IPv4Address server_ip_; const int server_port_; const IPv4Address tun_interface_address_ipv4_; const IPv6Address tun_interface_address_ipv6_; const std::string sni_; const std::string md5_fingerprint_; fptn::protocol::https::CensorshipStrategy censorship_strategy_; NewIPPacketCallback new_ip_pkt_callback_; std::string access_token_; fptn::protocol::https::WebsocketClientSPtr ws_; std::string latest_error_; std::atomic reconnection_attempts_; }; using ClientPtr = std::unique_ptr; } // namespace fptn::vpn::http ================================================ FILE: src/fptn-client/vpn/vpn_client.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "vpn/vpn_client.h" #include #include #include #include #include // NOLINT(build/include_order) namespace fptn::vpn { VpnClient::VpnClient(fptn::vpn::http::ClientPtr http_client, fptn::common::network::TunInterfacePtr virtual_net_interface, fptn::common::network::IPv4Address dns_server_ipv4, fptn::common::network::IPv6Address dns_server_ipv6, fptn::plugin::PluginList plugins, std::size_t thread_pool_size) : running_(false), http_client_(std::move(http_client)), virtual_net_interface_(std::move(virtual_net_interface)), dns_server_ipv4_(std::move(dns_server_ipv4)), dns_server_ipv6_(std::move(dns_server_ipv6)), plugins_(std::move(plugins)), thread_pool_size_(thread_pool_size) {} // NOLINT VpnClient::~VpnClient() { Stop(); } bool VpnClient::IsStarted() { if (!running_) { return false; } const std::unique_lock lock(mutex_); // mutex return running_ && http_client_ && http_client_->IsStarted(); } bool VpnClient::Start() { if (running_) { return false; } { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress identicalConditionAfterEarlyExit if (running_) { return false; } } // NOLINTNEXTLINE(modernize-avoid-bind) http_client_->SetRecvIPPacketCallback(std::bind( &VpnClient::HandlePacketFromWebSocket, this, std::placeholders::_1)); virtual_net_interface_->SetRecvIPPacketCallback( // NOLINTNEXTLINE(modernize-avoid-bind) std::bind(&VpnClient::HandlePacketFromVirtualNetworkInterface, this, std::placeholders::_1)); http_client_->Start(); virtual_net_interface_->Start(); running_ = true; // Start workers worker_threads_.reserve(thread_pool_size_); for (std::size_t i = 0; i < thread_pool_size_; ++i) { worker_threads_.emplace_back(&VpnClient::ProcessWebSocketPackets, this); } return true; } bool VpnClient::Stop() { if (!running_) { return false; } { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress identicalConditionAfterEarlyExit if (!running_) { return false; } running_ = false; } ws_queue_cv_.notify_all(); SPDLOG_INFO("Stopping VPN Websocket-workers..."); for (auto& thread : worker_threads_) { if (thread.joinable()) { thread.join(); } } worker_threads_.clear(); SPDLOG_INFO("Stopping VPN client..."); if (virtual_net_interface_) { SPDLOG_INFO("Stopping virtual network interface"); virtual_net_interface_->Stop(); virtual_net_interface_.reset(); SPDLOG_DEBUG("Virtual network interface stopped successfully"); } if (http_client_) { SPDLOG_INFO("Stopping HTTP client"); http_client_->Stop(); http_client_.reset(); SPDLOG_DEBUG("HTTP client stopped successfully"); } return true; } std::size_t VpnClient::GetSendRate() { if (!running_) { return 0; } const std::unique_lock lock(mutex_); // mutex if (running_ && virtual_net_interface_) { return virtual_net_interface_->GetSendRate(); } return 0; } std::size_t VpnClient::GetReceiveRate() { if (!running_) { return 0; } const std::unique_lock lock(mutex_); // mutex if (running_ && virtual_net_interface_) { return virtual_net_interface_->GetReceiveRate(); } return 0; } std::string VpnClient::GetInterfaceName() const { if (virtual_net_interface_) { return virtual_net_interface_->Name(); } return {}; } void VpnClient::HandlePacketFromVirtualNetworkInterface( fptn::common::network::IPPacketPtr packet) { if (!running_) { return; } const std::unique_lock lock(mutex_); // mutex if (running_ && http_client_) { http_client_->Send(std::move(packet)); } } void VpnClient::HandlePacketFromWebSocket( fptn::common::network::IPPacketPtr packet) { if (!running_ || !packet) { return; } constexpr std::size_t kMaxQueueSize = 128; std::unique_lock lock(mutex_); // mutex if (ws_packet_queue_.size() >= kMaxQueueSize) { SPDLOG_WARN("WebSocket packet queue is full, dropping packet"); return; } ws_packet_queue_.push(std::move(packet)); lock.unlock(); ws_queue_cv_.notify_one(); } void VpnClient::ProcessWebSocketPackets() { fptn::common::network::IPPacketPtr packet; while (running_) { { std::unique_lock lock(mutex_); // mutex ws_queue_cv_.wait( lock, [this]() { return !ws_packet_queue_.empty() || !running_; }); if (!running_ && ws_packet_queue_.empty()) { break; } if (!ws_packet_queue_.empty()) { packet = std::move(ws_packet_queue_.front()); ws_packet_queue_.pop(); } } if (!packet) { continue; } // Обрабатываем пакет через плагины if (running_ && !plugins_.empty()) { for (const auto& plugin : plugins_) { if (packet) { auto [processed_packet, triggered] = plugin->HandlePacket(std::move(packet)); packet = std::move(processed_packet); if (triggered) { break; } } } } if (running_ && packet) { const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress knownConditionTrueFalse if (running_ && virtual_net_interface_) { virtual_net_interface_->Send(std::move(packet)); } } } } } // namespace fptn::vpn ================================================ FILE: src/fptn-client/vpn/vpn_client.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "common/network/ip_address.h" #include "common/network/ip_packet.h" #include "common/network/net_interface.h" #include "http/client.h" #include "plugins/split/tunneling.h" namespace fptn::vpn { class VpnClient final { public: explicit VpnClient(fptn::vpn::http::ClientPtr http_client, fptn::common::network::TunInterfacePtr virtual_net_interface, fptn::common::network::IPv4Address dns_server_ipv4, fptn::common::network::IPv6Address dns_server_ipv6, fptn::plugin::PluginList plugins, std::size_t thread_pool_size = 4); ~VpnClient(); bool Start(); bool Stop(); std::size_t GetSendRate(); std::size_t GetReceiveRate(); bool IsStarted(); [[nodiscard]] std::string GetInterfaceName() const; protected: void HandlePacketFromVirtualNetworkInterface( fptn::common::network::IPPacketPtr packet); void HandlePacketFromWebSocket(fptn::common::network::IPPacketPtr packet); void ProcessWebSocketPackets(); private: mutable std::mutex mutex_; std::atomic running_; fptn::vpn::http::ClientPtr http_client_; fptn::common::network::TunInterfacePtr virtual_net_interface_; const fptn::common::network::IPv4Address dns_server_ipv4_; const fptn::common::network::IPv6Address dns_server_ipv6_; const fptn::plugin::PluginList plugins_; const std::size_t thread_pool_size_; std::vector worker_threads_; std::condition_variable ws_queue_cv_; std::queue ws_packet_queue_; }; using VpnClientPtr = std::unique_ptr; } // namespace fptn::vpn ================================================ FILE: src/fptn-passwd/CMakeLists.txt ================================================ project(fptn-passwd) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) find_package(Boost REQUIRED) find_package(OpenSSL REQUIRED) find_package(argparse REQUIRED) add_executable("${PROJECT_NAME}" fptn-passwd.cpp) target_link_libraries("${PROJECT_NAME}" PRIVATE OpenSSL::SSL OpenSSL::Crypto argparse::argparse) ================================================ FILE: src/fptn-passwd/fptn-passwd.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include #include "common/user/common_user_manager.h" namespace { std::string GetPassword(const std::string& prompt) { std::string password; struct termios oldt = {}; std::cout << prompt; tcgetattr(STDIN_FILENO, &oldt); struct termios newt = oldt; newt.c_lflag &= ~(ECHO); // Turn off echo tcsetattr(STDIN_FILENO, TCSANOW, &newt); std::getline(std::cin, password); tcsetattr(STDIN_FILENO, TCSANOW, &oldt); std::cout << std::endl; return password; } } // namespace int main(int argc, char* argv[]) { if (geteuid() != 0) { std::cerr << "You must be root to run this program." << std::endl; return EXIT_FAILURE; } try { argparse::ArgumentParser parser("fptn-passwd", FPTN_VERSION); parser.add_argument("--add-user").help("Username to add"); parser.add_argument("--del-user").help("Username to delete"); parser.add_argument("--bandwidth") .help("Bandwidth limit for the user in Megabit (default: 100)") .default_value(100) .scan<'i', int>(); parser.add_argument("--userfile") .help("Path to users file (default: /etc/fptn/users.list)") .default_value("/etc/fptn/users.list"); parser.add_argument("--list") .help("List all users") .default_value(false) .implicit_value(true); parser.add_argument("--get-bandwidth") .help("Get bandwidth limit for a user"); parser.parse_args(argc, argv); const auto add_user = parser.present("--add-user").value_or(""); const auto del_user = parser.present("--del-user").value_or(""); const auto bandwidth = parser.get("--bandwidth"); const auto file_path = parser.get("--userfile"); const bool list = parser.get("--list"); const auto get_bandwidth_user = parser.present("--get-bandwidth").value_or(""); fptn::common::user::CommonUserManager user_manager(file_path); if (!add_user.empty()) { const std::string password = GetPassword("Type password: "); const std::string retype_password = GetPassword("Retype password: "); if (password != retype_password) { std::cout << "Passwords do not match." << std::endl; return 1; } user_manager.AddUser(add_user, password, bandwidth); } else if (!del_user.empty()) { std::string confirm; std::cout << "Are you sure you want to delete user " << del_user << "? (Y/N): "; std::cin >> confirm; if (confirm == "Y" || confirm == "y") { user_manager.DeleteUser(del_user); } else { std::cout << "Deletion cancelled." << std::endl; } } else if (list) { user_manager.ListUsers(); } else if (!get_bandwidth_user.empty()) { const int bw = user_manager.GetUserBandwidth(get_bandwidth_user); if (bw != -1) { std::cout << "Bandwidth for user " << get_bandwidth_user << ": " << bw << " MB" << std::endl; } } else { std::cerr << "No command specified. Use --help for usage information." << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS; } catch (const std::runtime_error& err) { std::cerr << "Argument parsing error: " << err.what() << std::endl; } catch (const std::exception& ex) { std::cerr << "An error occurred: " << ex.what() << " Exiting..." << std::endl; } catch (...) { std::cerr << "An unknown error occurred. Exiting..." << std::endl; } return EXIT_FAILURE; } ================================================ FILE: src/fptn-protocol-lib/CMakeLists.txt ================================================ project(fptn-protocol-lib LANGUAGES CXX) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) include(../../depends/cmake/NtpClient.cmake) include(../../depends/cmake/CamouflageTLS.cmake) if (IOS) find_library(SECURITY Security) find_library(CFNETWORK CFNetwork) find_library(SYSTEMCONFIGURATION SystemConfiguration) add_compile_definitions(FPTN_USER_OS=\"iOS\") elseif(__ANDROID__) add_compile_definitions(FPTN_USER_OS=\"Android\") elseif(APPLE) add_compile_definitions(FPTN_USER_OS=\"MacOS\") elseif(WIN32) add_compile_definitions(FPTN_USER_OS=\"Windows\") elseif(UNIX) add_compile_definitions(FPTN_USER_OS=\"Linux\") else() message(FATAL_ERROR "Unsupported platform") endif() find_package(absl REQUIRED) find_package(protobuf REQUIRED) find_package(OpenSSL REQUIRED) find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(re2 REQUIRED) find_package(Boost REQUIRED COMPONENTS random filesystem nowide locale) find_package(ZLIB REQUIRED) find_package(nlohmann_json REQUIRED) if (FPTN_BUILD_ONLY_FPTN_LIB) # build fptnlib without pcappp set(FPTN_IP_ADDRESS_WITHOUT_PCAP ON) endif() if (FPTN_WITH_LIBIDN2) find_package(libidn2 REQUIRED) endif () if(NOT FPTN_IP_ADDRESS_WITHOUT_PCAP) find_package(PcapPlusPlus REQUIRED) endif() include_directories(${nlohmann_json_INCLUDE_DIRS}) # include if(ABSL_INCLUDE_DIRS) include_directories(${ABSL_INCLUDE_DIRS}) endif() if(absl_INCLUDE_DIRS) include_directories(${absl_INCLUDE_DIRS}) endif() if(PROTOBUF_INCLUDE_DIR) include_directories(${PROTOBUF_INCLUDE_DIR}) endif() if(protobuf_INCLUDE_DIRS) include_directories(${protobuf_INCLUDE_DIRS}) endif() if(Protobuf_INCLUDE_DIRS) include_directories(${Protobuf_INCLUDE_DIRS}) endif() if(CONAN_INCLUDE_DIRS_PROTOBUF) include_directories(${CONAN_INCLUDE_DIRS_PROTOBUF}) endif() include_directories(${SOURCE_DIR} ${INCLUDE_DIR} ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}/fptn_protocol) include_directories(${CMAKE_BINARY_DIR}/src/fptn-protocol-lib/protobuf) include_directories("${CMAKE_CURRENT_BINARY_DIR}/protobuf") # Generate C++ source and header files from the .proto files set(protobuf_files protobuf/protocol.proto) protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${protobuf_files}) # --- disable clang-tidy for protobuf generated code --- set_source_files_properties($ PROPERTIES SKIP_CLANG_TIDY ON) set_source_files_properties($ PROPERTIES SKIP_CLANG_TIDY ON) set(DISABLE_TIDY_DIR "${CMAKE_BINARY_DIR}/src/fptn-protocol-lib/protobuf/") file(MAKE_DIRECTORY "${DISABLE_TIDY_DIR}") file( WRITE "${DISABLE_TIDY_DIR}/.clang-tidy" "Checks: '-*,readability-inconsistent-declaration-parameter-name' WarningsAsErrors: '' ") set(FPTN_CLIENT_PROTOCOL_SOURCES ${PROTO_SRCS} ${PROTO_HDRS} https/api_client/api_client.h https/api_client/api_client.cpp https/obfuscator/methods/tls/tls_obfuscator.cpp https/obfuscator/methods/tls/tls_obfuscator.h https/obfuscator/methods/tls2/tls_obfuscator2.cpp https/obfuscator/methods/tls2/tls_obfuscator2.h https/obfuscator/methods/obfuscator_interface.h https/obfuscator/tcp_stream/tcp_stream.h https/utils/tls/tls.h https/utils/tls/tls.cpp https/websocket_client/websocket_client.h https/websocket_client/websocket_client.cpp protobuf/protocol.h protobuf/protocol.cpp time/time_provider.h time/time_provider.cpp https/obfuscator/methods/detector.h) add_library("${PROJECT_NAME}_static" STATIC ${FPTN_CLIENT_PROTOCOL_SOURCES}) foreach(target "${PROJECT_NAME}_static") if(FPTN_IP_ADDRESS_WITHOUT_PCAP) set(PCAP_LIB "") else() set(PCAP_LIB PcapPlusPlus::PcapPlusPlus) endif() if(FPTN_WITH_LIBIDN2) set(LIBIDN2_LIB libidn2::libidn2) else() set(LIBIDN2_LIB "") endif() if(MSVC) target_compile_options(${target} PRIVATE /wd4100 /wd4702) # disable warning C4100 endif() if(IOS) set(IOS_LIBS "${SECURITY} ${CFNETWORK} ${SYSTEMCONFIGURATION} -framework Foundation z resolv") else() set(IOS_LIBS "") endif() if (IOS) set_target_properties(${target} PROPERTIES CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF XCODE_ATTRIBUTE_ENABLE_BITCODE "NO" XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH "NO" XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "" ) endif() target_include_directories(${target} INTERFACE ${CMAKE_CURRENT_BINARY_DIR} protobuf::protobuf) target_link_libraries( ${target} ${Protobuf_LIBRARIES} ${protobuf_LIBRARIES} protobuf::protobuf ZLIB::ZLIB Boost::boost Boost::nowide Boost::random Boost::locale OpenSSL::SSL OpenSSL::Crypto nlohmann_json::nlohmann_json spdlog::spdlog fmt::fmt re2::re2 ntp_client camouflage-tls ${PCAP_LIB} ${LIBIDN2_LIB} ${IOS_LIBS} ) endforeach() ================================================ FILE: src/fptn-protocol-lib/https/api_client/api_client.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-protocol-lib/https/api_client/api_client.h" #include #include #include #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/utils.h" #ifdef _WIN32 #pragma warning(push) #pragma warning(disable : 4996) #pragma warning(disable : 4267) #pragma warning(disable : 4244) #pragma warning(disable : 4702) #endif #include #include #include #include #include #include #include #include #include #include #include #include #include "common/network/resolv.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls2/tls_obfuscator2.h" #include "fptn-protocol-lib/https/obfuscator/tcp_stream/tcp_stream.h" #include "fptn-protocol-lib/https/utils/change_cipher_spec.h" #include "fptn-protocol-lib/https/utils/tls/tls.h" #ifdef _WIN32 #pragma warning(pop) #endif namespace { std::string DecompressGzip(const std::string& compressed) { constexpr std::size_t kChunkSize = 4096; std::vector buffer(kChunkSize); ::z_stream strm{}; strm.next_in = reinterpret_cast(const_cast(compressed.data())); strm.avail_in = static_cast(compressed.size()); if (::inflateInit2(&strm, 16 + MAX_WBITS) != Z_OK) { return {}; } std::string decompressed; int ret = 0; do { strm.next_out = reinterpret_cast(buffer.data()); strm.avail_out = static_cast(buffer.size()); ret = inflate(&strm, Z_NO_FLUSH); if (ret == Z_STREAM_ERROR || ret == Z_DATA_ERROR || ret == Z_MEM_ERROR) { inflateEnd(&strm); return {}; } decompressed.append(buffer.data(), buffer.size() - strm.avail_out); } while (ret != Z_STREAM_END); inflateEnd(&strm); return decompressed; } std::string GetHttpBody( const boost::beast::http::response& res) { auto body = boost::beast::buffers_to_string(res.body().data()); if (res[boost::beast::http::field::content_encoding] == "gzip") { return DecompressGzip(body); } return body; } void SetSocketTimeouts( boost::asio::ip::tcp::socket& socket, int timeout_seconds) { auto native_socket = socket.native_handle(); #ifdef _WIN32 DWORD timeout_ms = timeout_seconds * 1000; ::setsockopt(native_socket, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout_ms), sizeof(timeout_ms)); ::setsockopt(native_socket, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&timeout_ms), sizeof(timeout_ms)); #else timeval tv = {}; tv.tv_sec = timeout_seconds; tv.tv_usec = 0; ::setsockopt(native_socket, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&tv), sizeof(tv)); ::setsockopt(native_socket, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&tv), sizeof(tv)); #endif } using Headers = std::unordered_map; Headers RealBrowserHeaders() { /* Just to ensure that FPTN is as similar to a web browser as possible. */ #ifdef __linux__ // chromium ubuntu arm return {{"User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like " "Gecko) Chrome/134.0.0.0 Safari/537.36"}, {"Accept-Language", "en-US,en;q=0.9"}, {"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/" "avif,image/webp,image/apng,*/*;q=0.8,application/" "signed-exchange;v=b3;q=0.7"}, {"Referer", "https://www.google.com/"}, {"Accept-Encoding", "gzip, deflate, br, zstd"}, {"Sec-Ch-Ua", R"("Not:A-Brand";v="24", "Chromium";v="134")"}, {"Sec-Ch-Ua-Mobile", "?0"}, {"Sec-Ch-Ua-Platform", R"("Linux")"}, {"Upgrade-Insecure-Requests", "1"}, {"Sec-Fetch-Site", "cross-site"}, {"Sec-Fetch-Mode", "navigate"}, {"Sec-Fetch-User", "?1"}, {"Sec-Fetch-Dest", "document"}, {"Priority", "u=0, i"}}; #elif __APPLE__ // apple silicon chrome return { {"sec-ch-ua", R"("Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128")"}, {"sec-ch-ua-platform", "\"macOS\""}, {"sec-ch-ua-mobile", "?0"}, {"upgrade-insecure-requests", "1"}, {"User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"}, {"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/" "avif,image/webp,image/apng,*/*;q=0.8,application/" "signed-exchange;v=b3;q=0.7"}, {"sec-fetch-site", "none"}, {"sec-fetch-mode", "no-cors"}, {"sec-fetch-dest", "empty"}, {"Referer", "https://www.google.com/"}, {"Accept-Encoding", "gzip, deflate, br"}, {"Accept-Language", "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7"}, {"priority", "u=4, i"}}; #elif _WIN32 // chrome windows amd64 return { {"sec-ch-ua", R"("Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128")"}, {"sec-ch-ua-mobile", "?0"}, {"sec-ch-ua-platform", "\"Windows\""}, {"upgrade-insecure-requests", "1"}, {"User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36"}, {"Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/" "avif,image/webp,image/apng,*/*;q=0.8,application/" "signed-exchange;v=b3;q=0.7"}, {"sec-fetch-site", "cross-site"}, {"sec-fetch-mode", "navigate"}, {"sec-fetch-user", "?1"}, {"sec-fetch-dest", "document"}, {"Referer", "https://www.google.com/"}, {"Accept-Encoding", "gzip, deflate, br, zstd"}, {"Accept-Language", "en-US,en;q=0.9,ru;q=0.8"}, {"priority", "u=0, i"}}; #else #error Undefined platform #endif } template TResult ExecuteWithTimeout(const std::function& operation, int timeout, const std::string& operation_name, const std::string& handle, const std::string& host, const TResult& timeout_result) { try { // Shared state struct SharedState { std::mutex mutex; std::condition_variable cv; bool ready = false; TResult result; std::atomic cancelled{false}; }; const auto start_time = std::chrono::steady_clock::now(); auto state = std::make_shared(); // NOLINTNEXTLINE(bugprone-exception-escape) std::weak_ptr weak_state = state; std::thread([weak_state, operation]() { TResult impl_result = operation(); // check state if (auto state = weak_state.lock()) { const std::scoped_lock lock(state->mutex); if (!state->cancelled) { state->result = impl_result; state->ready = true; state->cv.notify_one(); } } }).detach(); std::unique_lock lock(state->mutex); // mutex if (!state->cv.wait_for(lock, std::chrono::seconds(timeout), [state]() { return state->ready; })) { const auto end_time = std::chrono::steady_clock::now(); const auto duration = std::chrono::duration_cast( end_time - start_time); state->cancelled = true; SPDLOG_WARN("{} [{}] - Timeout after {} ms for server {}", operation_name, handle, duration.count(), host); return timeout_result; } return state->result; } catch (...) { SPDLOG_ERROR("Undefined error: {} {}", operation_name, handle); } return timeout_result; } }; // namespace namespace fptn::protocol::https { using tcp_stream_type = boost::beast::tcp_stream; using obfuscator_socket_type = obfuscator::TcpStream; using ssl_stream_type = boost::beast::ssl_stream; ApiClient::ApiClient( const std::string& host, int port, CensorshipStrategy censorship_strategy) : host_(host), port_(port), sni_(host), censorship_strategy_(censorship_strategy) {} // NOLINT ApiClient::ApiClient(std::string host, int port, std::string sni, CensorshipStrategy censorship_strategy) : host_(std::move(host)), port_(port), sni_(std::move(sni)), censorship_strategy_(censorship_strategy) {} // NOLINT ApiClient::ApiClient(std::string host, int port, std::string sni, std::string md5_fingerprint, CensorshipStrategy censorship_strategy) : host_(std::move(host)), port_(port), sni_(std::move(sni)), expected_md5_fingerprint_(std::move(md5_fingerprint)), censorship_strategy_(censorship_strategy) {} // NOLINT Response ApiClient::Get(const std::string& handle, int timeout) const { // NOLINTNEXTLINE(bugprone-exception-escape) return ExecuteWithTimeout( // NOLINTNEXTLINE(bugprone-exception-escape) [this, handle, timeout]() { const ApiClient cloned_client = Clone(); return cloned_client.GetImpl(handle, timeout); }, timeout, "GET", handle, host_, Response{"", 608, "Operation timeout"}); } Response ApiClient::Post(const std::string& handle, const std::string& request, const std::string& content_type, int timeout) const { // NOLINTNEXTLINE(bugprone-exception-escape) return ExecuteWithTimeout( // NOLINTNEXTLINE(bugprone-exception-escape) [this, handle, request, content_type, timeout]() { const ApiClient cloned_client = Clone(); return cloned_client.PostImpl(handle, request, content_type, timeout); }, timeout, "POST", handle, host_, Response{"", 608, "Operation timeout"}); } bool ApiClient::TestHandshake(int timeout) const { // NOLINTNEXTLINE(bugprone-exception-escape) return ExecuteWithTimeout( // NOLINTNEXTLINE(bugprone-exception-escape) [this, timeout]() { const ApiClient cloned_client = Clone(); return cloned_client.TestHandshakeImpl(timeout); }, timeout, "TestHandshake", "", host_, false); } ApiClient ApiClient::Clone() const { ApiClient temp_client( host_, port_, sni_, expected_md5_fingerprint_, censorship_strategy_); return temp_client; } bool ApiClient::PerformFakeHandshake2( boost::asio::ip::tcp::socket& socket) const { try { SPDLOG_INFO("Fake TLS handshake started for SNI: {}", sni_); /* Send client hello */ const auto client_hello = GenerateHandshakePacket(); if (client_hello.empty()) { SPDLOG_WARN("Failed to generate ClientHello for SNI: {}", sni_); return false; } const std::size_t client_hello_bytes_size = boost::asio::write(socket, boost::asio::buffer(client_hello)); if (client_hello_bytes_size != client_hello.size()) { SPDLOG_ERROR("Error ClientHello sent: {} of {} bytes", client_hello_bytes_size, client_hello.size()); return false; } /* Wait for server answer */ const auto server_hello = common::network::WaitForServerTlsHello(socket); if (!server_hello.has_value()) { SPDLOG_ERROR("Failed to receive ServerHello from {}", sni_); return false; } // clean common::network::CleanSocket(socket); /* Send change cipher spec */ const auto change_cipher_spec = fptn::protocol::https::utils::MakeClientChangeCipherSpec(); const std::size_t change_cipher_spec_size = boost::asio::write(socket, boost::asio::buffer(change_cipher_spec)); if (change_cipher_spec_size != change_cipher_spec.size()) { SPDLOG_ERROR("Failed to send ClientHello to {}: {}", change_cipher_spec_size, change_cipher_spec.size()); return false; } // timeout std::this_thread::sleep_for(std::chrono::milliseconds(150)); SPDLOG_INFO( "Fake TLS handshake completed for {}, received {} bytes from server", sni_, server_hello.value().size()); return true; } catch (const std::exception& e) { SPDLOG_ERROR("Fake TLS handshake exception for {}: {}", sni_, e.what()); } return false; } Response ApiClient::GetImpl(const std::string& handle, int timeout) const { std::string body; std::string error; int respcode = 400; const auto start_time = std::chrono::steady_clock::now(); SSL* ssl = nullptr; std::string server_ip; try { boost::asio::io_context ioc; auto* ssl_ctx = fptn::protocol::https::utils::CreateNewSslCtx(); boost::asio::ssl::context ctx(ssl_ctx); fptn::protocol::https::obfuscator::IObfuscatorSPtr obfuscator = nullptr; if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { obfuscator = std::make_shared(); } tcp_stream_type tcp_stream(ioc); obfuscator_socket_type obfuscator_stream(std::move(tcp_stream), obfuscator); ssl_stream_type stream(std::move(obfuscator_stream), ctx); const std::string port_str = std::to_string(port_); auto resolve_result = fptn::common::network::ResolveWithTimeout( ioc, host_, port_str, timeout); if (!resolve_result) { error = resolve_result.error.message(); respcode = 603; SPDLOG_ERROR("GET [{}] - DNS resolution failed for {}:{}: {}", handle, host_, port_, error); } else { SPDLOG_INFO( "GET [{}] - Connecting to server: {}:{}", handle, host_, port_); boost::beast::get_lowest_layer(stream).expires_after( std::chrono::seconds(timeout)); stream.next_layer().next_layer().expires_after( std::chrono::seconds(timeout)); auto connected_endpoint = boost::beast::get_lowest_layer(stream).connect( resolve_result.results); server_ip = connected_endpoint.address().to_string(); SPDLOG_INFO("GET [{}] - Successfully connected to {}", handle, host_); auto& socket = boost::beast::get_lowest_layer(stream).socket(); SetSocketTimeouts(socket, timeout); // Perform fake handshake if enabled if (IsRealityModeWithFakeHandshake(censorship_strategy_)) { const bool perform_status = PerformFakeHandshake2(socket); if (!perform_status) { SPDLOG_WARN( "GET [{}] - Fake handshake failed, continuing with real " "handshake", handle); } // For Reality Mode we use TLS obfuscator after fake handshake // This provides additional encryption layer for the real connection stream.next_layer().set_obfuscator( std::make_shared()); } utils::SetHandshakeSessionID(stream.native_handle()); utils::SetHandshakeSni(stream.native_handle(), sni_); if (!expected_md5_fingerprint_.empty()) { ssl = stream.native_handle(); utils::AttachCertificateVerificationCallback( ssl, [this, &error](const std::string& md5_fingerprint) { return onVerifyCertificate(md5_fingerprint, error); }); } else { ctx.set_verify_mode(boost::asio::ssl::verify_none); } stream.handshake(boost::asio::ssl::stream_base::client); // Reset obfuscator after TLS-handshake stream.next_layer().set_obfuscator(nullptr); // Clean common::network::CleanSocket(socket); common::network::CleanSsl(ssl); // timeout std::this_thread::sleep_for(std::chrono::milliseconds(150)); boost::beast::http::request req{ boost::beast::http::verb::get, handle, 11}; // set http headers const auto headers = RealBrowserHeaders(); for (const auto& [key, value] : headers) { req.set(key, value); } boost::beast::http::write(stream, req); boost::beast::flat_buffer buffer; boost::beast::http::response res; boost::beast::http::read(stream, buffer, res); respcode = static_cast(res.result_int()); body = GetHttpBody(res); boost::system::error_code ec; stream.shutdown(ec); try { boost::beast::get_lowest_layer(stream).close(); } catch (boost::system::system_error const& e) { SPDLOG_ERROR( "GET [{}] - Exception during connection close for server {}: {}", handle, host_, e.what()); } } } catch (const boost::system::system_error& err) { #ifdef _WIN32 error = boost::nowide::narrow(boost::nowide::widen(err.what())); #else error = err.what(); #endif respcode = 600; SPDLOG_ERROR("GET [{}] - System error for server {} (IP: {}): {}", handle, host_, server_ip, error); } catch (const std::exception& e) { #ifdef _WIN32 error = boost::nowide::narrow(boost::nowide::widen(e.what())); #else error = e.what(); #endif respcode = 601; SPDLOG_ERROR("GET [{}] - Exception for server {} (IP: {}): {}", handle, host_, server_ip, error); } catch (...) { error = "Unknown exception"; respcode = 602; SPDLOG_ERROR("GET [{}] - Unknown exception for server {} (IP: {})", handle, host_, server_ip); } if (ssl) { utils::AttachCertificateVerificationCallbackDelete(ssl); } const auto end_time = std::chrono::steady_clock::now(); const auto duration = std::chrono::duration_cast( end_time - start_time); if (respcode >= 200 && respcode < 300) { SPDLOG_INFO( "GET [{}] - Success from server {} (IP: {}) in {} ms - Status: {}, " "Body size: {} bytes", handle, host_, server_ip, duration.count(), respcode, body.size()); } else { SPDLOG_WARN( "GET [{}] - Failed from server {} (IP: {}) in {} ms - Status: {}, " "Error: {}, Body size: {} bytes", handle, host_, server_ip, duration.count(), respcode, error, body.size()); } return {body, respcode, error}; } Response ApiClient::PostImpl(const std::string& handle, const std::string& request, const std::string& content_type, int timeout) const { std::string body; std::string error; int respcode = 400; const auto start_time = std::chrono::steady_clock::now(); SSL* ssl = nullptr; std::string server_ip; try { boost::asio::io_context ioc; auto* ssl_ctx = utils::CreateNewSslCtx(); boost::asio::ssl::context ctx(ssl_ctx); fptn::protocol::https::obfuscator::IObfuscatorSPtr obfuscator = nullptr; if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { obfuscator = std::make_shared(); } tcp_stream_type tcp_stream(ioc); obfuscator_socket_type obfuscator_stream(std::move(tcp_stream), obfuscator); ssl_stream_type stream(std::move(obfuscator_stream), ctx); const std::string port_str = std::to_string(port_); auto resolve_result = fptn::common::network::ResolveWithTimeout( ioc, host_, port_str, timeout); if (!resolve_result) { error = resolve_result.error.message(); respcode = 603; SPDLOG_ERROR("POST [{}] - DNS resolution failed for {}:{}: {}", handle, host_, port_, error); } else { SPDLOG_INFO( "POST [{}] - Connecting to server: {}:{}", handle, host_, port_); boost::beast::get_lowest_layer(stream).expires_after( std::chrono::seconds(timeout)); stream.next_layer().next_layer().expires_after( std::chrono::seconds(timeout)); auto connected_endpoint = boost::beast::get_lowest_layer(stream).connect( resolve_result.results); server_ip = connected_endpoint.address().to_string(); SPDLOG_INFO("POST [{}] - Successfully connected to {}", handle, host_); auto& socket = boost::beast::get_lowest_layer(stream).socket(); SetSocketTimeouts(socket, timeout); // Perform fake handshake if enabled if (IsRealityModeWithFakeHandshake(censorship_strategy_)) { const bool perform_status = PerformFakeHandshake2(socket); if (!perform_status) { SPDLOG_WARN( "GET [{}] - Fake handshake failed, continuing with real " "handshake", handle); } // For Reality Mode we use TLS obfuscator after fake handshake // This provides additional encryption layer for the real connection stream.next_layer().set_obfuscator( std::make_shared()); } utils::SetHandshakeSessionID(stream.native_handle()); utils::SetHandshakeSni(stream.native_handle(), sni_); if (!expected_md5_fingerprint_.empty()) { ssl = stream.native_handle(); utils::AttachCertificateVerificationCallback( ssl, [this, &error](const std::string& md5_fingerprint) { return onVerifyCertificate(md5_fingerprint, error); }); } else { ctx.set_verify_mode(boost::asio::ssl::verify_none); } stream.handshake(boost::asio::ssl::stream_base::client); // Reset obfuscator after TLS-handshake stream.next_layer().set_obfuscator(nullptr); // Clean common::network::CleanSocket(socket); common::network::CleanSsl(ssl); // timeout std::this_thread::sleep_for(std::chrono::milliseconds(150)); boost::beast::http::request req{ boost::beast::http::verb::post, handle, 11}; req.set(boost::beast::http::field::host, host_); req.set(boost::beast::http::field::accept, "*/*"); req.set(boost::beast::http::field::content_type, content_type); req.set(boost::beast::http::field::content_length, std::to_string(request.size())); // set http headers const auto headers = RealBrowserHeaders(); for (const auto& [key, value] : headers) { req.set(key, value); } req.body() = request; req.prepare_payload(); boost::beast::http::write(stream, req); boost::beast::flat_buffer buffer; boost::beast::http::response res; boost::beast::http::read(stream, buffer, res); respcode = static_cast(res.result_int()); body = GetHttpBody(res); boost::system::error_code ec; stream.shutdown(ec); try { boost::beast::get_lowest_layer(stream).close(); } catch (boost::system::system_error const& e) { SPDLOG_ERROR( "POST [{}] - Exception during connection close for server {}: {}", handle, host_, e.what()); } } } catch (const boost::system::system_error& err) { #ifdef _WIN32 error = boost::nowide::narrow(boost::nowide::widen(err.what())); #else error = err.what(); #endif respcode = 600; SPDLOG_ERROR("POST [{}] - System error for server {} (IP: {}): {}", handle, host_, server_ip, error); } catch (const std::exception& e) { #ifdef _WIN32 error = boost::nowide::narrow(boost::nowide::widen(e.what())); #else error = e.what(); #endif respcode = 601; SPDLOG_ERROR("POST [{}] - Exception for server {} (IP: {}): {}", handle, host_, server_ip, error); } catch (...) { error = "Unknown exception"; respcode = 602; SPDLOG_ERROR("POST [{}] - Unknown exception for server {} (IP: {})", handle, host_, server_ip); } if (ssl) { utils::AttachCertificateVerificationCallbackDelete(ssl); } const auto end_time = std::chrono::steady_clock::now(); const auto duration = std::chrono::duration_cast( end_time - start_time); if (respcode >= 200 && respcode < 300) { SPDLOG_INFO( "POST [{}] - Success from server {} (IP: {}) in {} ms - Status: {}, " "Request: {} bytes, Response: {} bytes", handle, host_, server_ip, duration.count(), respcode, request.size(), body.size()); } else { SPDLOG_WARN( "POST [{}] - Failed from server {} (IP: {}) in {} ms - Status: {}, " "Error: {}, Request: {} bytes, Response: {} bytes", handle, host_, server_ip, duration.count(), respcode, error, request.size(), body.size()); } return {body, respcode, error}; } bool ApiClient::TestHandshakeImpl(int timeout) const { const auto start_time = std::chrono::steady_clock::now(); std::string server_ip; SSL* ssl = nullptr; try { boost::asio::io_context ioc; auto* ssl_ctx = utils::CreateNewSslCtx(); boost::asio::ssl::context ctx(ssl_ctx); fptn::protocol::https::obfuscator::IObfuscatorSPtr obfuscator = nullptr; if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { obfuscator = std::make_shared(); } tcp_stream_type tcp_stream(ioc); obfuscator_socket_type obfuscator_stream(std::move(tcp_stream), obfuscator); ssl_stream_type stream(std::move(obfuscator_stream), ctx); const std::string port_str = std::to_string(port_); auto resolve_result = fptn::common::network::ResolveWithTimeout( ioc, host_, port_str, timeout); if (!resolve_result) { SPDLOG_WARN("TestHandshake - DNS resolution failed for {}:{}: {}", host_, port_, resolve_result.error.message()); const auto end_time = std::chrono::steady_clock::now(); const auto duration = std::chrono::duration_cast( end_time - start_time); SPDLOG_WARN( "Handshake failed for server {} in {} ms - DNS resolution error", host_, duration.count()); return false; } SPDLOG_INFO("TestHandshake - Connecting to server: {}:{}", host_, port_); boost::beast::get_lowest_layer(stream).expires_after( std::chrono::seconds(timeout)); stream.next_layer().next_layer().expires_after( std::chrono::seconds(timeout)); auto connected_endpoint = boost::beast::get_lowest_layer(stream).connect(resolve_result.results); server_ip = connected_endpoint.address().to_string(); SPDLOG_INFO("TestHandshake - Successfully connected to {} (IP: {})", host_, server_ip); auto& socket = boost::beast::get_lowest_layer(stream).socket(); SetSocketTimeouts(socket, timeout); // Perform fake handshake if enabled if (IsRealityModeWithFakeHandshake(censorship_strategy_)) { SPDLOG_INFO("TestHandshake - Performing fake handshake"); if (!PerformFakeHandshake2(socket)) { SPDLOG_WARN( "TestHandshake - Fake handshake failed, continuing with real " "handshake"); } } utils::SetHandshakeSessionID(stream.native_handle()); utils::SetHandshakeSni(stream.native_handle(), sni_); if (!expected_md5_fingerprint_.empty()) { ssl = stream.native_handle(); std::string error; utils::AttachCertificateVerificationCallback( ssl, [this, &error](const std::string& md5_fingerprint) { return onVerifyCertificate(md5_fingerprint, error); }); } else { ctx.set_verify_mode(boost::asio::ssl::verify_none); } // Perform TLS handshake stream.handshake(boost::asio::ssl::stream_base::client); // Clean shutdown boost::system::error_code ec; stream.shutdown(ec); // Close connection boost::beast::get_lowest_layer(stream).close(); const auto end_time = std::chrono::steady_clock::now(); const auto duration = std::chrono::duration_cast( end_time - start_time); SPDLOG_INFO("Handshake successful for server {} (IP: {}) in {} ms", host_, server_ip, duration.count()); if (ssl) { utils::AttachCertificateVerificationCallbackDelete(ssl); } return true; } catch (const boost::system::system_error& err) { std::string host_copy = host_; std::string server_ip_copy = server_ip; std::string error_msg; #ifdef _WIN32 error_msg = boost::nowide::narrow(boost::nowide::widen(err.what())); #else error_msg = err.what(); #endif SPDLOG_WARN("Handshake failed for server {} (IP: {}): {}", host_copy, server_ip_copy, error_msg); } catch (const std::exception& e) { // Создаем копии строк перед использованием в логгере std::string host_copy = host_; std::string server_ip_copy = server_ip; std::string error_msg; #ifdef _WIN32 error_msg = boost::nowide::narrow(boost::nowide::widen(e.what())); #else error_msg = e.what(); #endif SPDLOG_WARN("Handshake failed for server {} (IP: {}): {}", host_copy, server_ip_copy, error_msg); } catch (...) { // Создаем копии строк перед использованием в логгере std::string host_copy = host_; std::string server_ip_copy = server_ip; SPDLOG_WARN("Handshake failed for server {} (IP: {}): Unknown exception", host_copy, server_ip_copy); } if (ssl) { utils::AttachCertificateVerificationCallbackDelete(ssl); } const auto end_time = std::chrono::steady_clock::now(); const auto duration = std::chrono::duration_cast( end_time - start_time); SPDLOG_WARN("Handshake failed for server {} (IP: {}) in {} ms", host_, server_ip, duration.count()); return false; } bool ApiClient::onVerifyCertificate( const std::string& md5_fingerprint, std::string& error) const { if (expected_md5_fingerprint_.empty()) { return true; } if (md5_fingerprint == expected_md5_fingerprint_) { return true; } error = fmt::format( "Certificate MD5 mismatch. Expected: {}, got: {}. " "Please update your token.", expected_md5_fingerprint_, md5_fingerprint); SPDLOG_ERROR( "Certificate verification failed for server {}: {}", host_, error); return false; } std::vector ApiClient::GenerateHandshakePacket() const { auto builder = camouflage::tls::Builder::Create(); switch (censorship_strategy_) { case CensorshipStrategy::kSniRealityModeChrome147: builder.GoogleChrome( camouflage::tls::google_chrome::Version::kV_147_0_7727_56); break; case CensorshipStrategy::kSniRealityModeChrome146: builder.GoogleChrome( camouflage::tls::google_chrome::Version::kV_146_0_7680_178); break; case CensorshipStrategy::kSniRealityModeChrome145: builder.GoogleChrome( camouflage::tls::google_chrome::Version::kV_145_0_7632_46); break; case CensorshipStrategy::kSniRealityModeFirefox149: builder.Firefox(camouflage::tls::firefox::Version::kV_149_0); break; case CensorshipStrategy::kSniRealityModeSafari26: builder.Safari(camouflage::tls::safari::Version::kV_26_4); break; case CensorshipStrategy::kSniRealityModeYandex26: builder.YandexBrowser( camouflage::tls::yandex_browser::Version::kV_26_3_3_881); break; case CensorshipStrategy::kSniRealityModeYandex25: builder.YandexBrowser( camouflage::tls::yandex_browser::Version::kV_25_8_3_828); break; case CensorshipStrategy::kSniRealityModeYandex24: builder.YandexBrowser( camouflage::tls::yandex_browser::Version::kV_24_12_0_1772); break; default: SPDLOG_DEBUG("Using fallback handshake generator for SNI: {}", sni_); return utils::GenerateDecoyTlsHandshake(sni_); } SPDLOG_INFO("Generating handshake for SNI: {}", sni_); const auto session_id = utils::GenerateDecoyTlsSessionId2(); if (!session_id.has_value()) { SPDLOG_WARN("Session ID generation failed for handshake, using fallback"); return utils::GenerateDecoyTlsHandshake(sni_); } const auto handshake = builder.SetSNI(sni_).SetSessionId(session_id.value()).Generate(); if (!handshake.has_value()) { SPDLOG_WARN( "Handshake generation failed for SNI: {}, using fallback", sni_); return utils::GenerateDecoyTlsHandshake(sni_); } SPDLOG_INFO("Handshake generated: SNI={}, size={} bytes", sni_, handshake->handshake_packet_size); return std::vector(handshake->handshake_packet, handshake->handshake_packet + handshake->handshake_packet_size); } } // namespace fptn::protocol::https ================================================ FILE: src/fptn-protocol-lib/https/api_client/api_client.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include "fptn-protocol-lib/https/censorship_strategy.h" namespace fptn::protocol::https { struct Response final { std::string body; int code; std::string errmsg; Response() : code(600) {} Response(std::string b, int c, std::string e) : body(std::move(b)), code(c), errmsg(std::move(e)) {} Response(const Response& other) : body(other.body), code(other.code), errmsg(other.errmsg) {} Response& operator=(const Response& other) { if (this != &other) { this->~Response(); new (this) Response(other); } return *this; } Response(Response&& other) = delete; Response& operator=(Response&& other) = delete; nlohmann::json Json() const { return nlohmann::json::parse(body); } }; class ApiClient { public: ApiClient(const std::string& host, int port, CensorshipStrategy censorship_strategy); ApiClient(std::string host, int port, std::string sni, CensorshipStrategy censorship_strategy); ApiClient(std::string host, int port, std::string sni, std::string md5_fingerprint, CensorshipStrategy censorship_strategy); Response Get(const std::string& handle, int timeout = 15) const; Response Post(const std::string& handle, const std::string& request, const std::string& content_type = "application/json", int timeout = 15) const; bool TestHandshake(int timeout = 10) const; protected: ApiClient Clone() const; Response GetImpl(const std::string& handle, int timeout) const; Response PostImpl(const std::string& handle, const std::string& request, const std::string& content_type, int timeout) const; bool TestHandshakeImpl(int timeout) const; bool PerformFakeHandshake2(boost::asio::ip::tcp::socket& socket) const; bool onVerifyCertificate( const std::string& md5_fingerprint, std::string& error) const; std::vector GenerateHandshakePacket() const; private: const std::string host_; const int port_; const std::string sni_; const std::string expected_md5_fingerprint_; const CensorshipStrategy censorship_strategy_; }; using HttpsClientPtr = std::unique_ptr; } // namespace fptn::protocol::https ================================================ FILE: src/fptn-protocol-lib/https/censorship_strategy.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once namespace fptn::protocol::https { enum class CensorshipStrategy : int { kSni = 0, kTlsObfuscator = 1, kSniRealityMode = 2, /* Chrome */ kSniRealityModeChrome147 = 20, kSniRealityModeChrome146 = 21, kSniRealityModeChrome145 = 22, /* Firefox */ kSniRealityModeFirefox149 = 60, /* Yandex Browser */ kSniRealityModeYandex26 = 80, kSniRealityModeYandex25 = 81, kSniRealityModeYandex24 = 82, /* Safari */ kSniRealityModeSafari26 = 100, }; inline bool IsRealityModeWithFakeHandshake(const CensorshipStrategy& strategy) { return strategy == CensorshipStrategy::kSniRealityMode || strategy == CensorshipStrategy::kSniRealityModeChrome147 || strategy == CensorshipStrategy::kSniRealityModeChrome146 || strategy == CensorshipStrategy::kSniRealityModeChrome145 || strategy == CensorshipStrategy::kSniRealityModeFirefox149 || strategy == CensorshipStrategy::kSniRealityModeYandex26 || strategy == CensorshipStrategy::kSniRealityModeYandex25 || strategy == CensorshipStrategy::kSniRealityModeYandex24 || strategy == CensorshipStrategy::kSniRealityModeSafari26; } } // namespace fptn::protocol::https ================================================ FILE: src/fptn-protocol-lib/https/obfuscator/methods/detector.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include "fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls2/tls_obfuscator2.h" namespace fptn::protocol::https::obfuscator { inline IObfuscatorSPtr DetectObfuscator( const std::uint8_t* data, std::size_t size) { auto tls_obfuscator2 = std::make_shared(); if (tls_obfuscator2->CheckProtocol(data, size)) { return tls_obfuscator2; } // deprecated auto tls_obfuscator = std::make_shared(); if (tls_obfuscator->CheckProtocol(data, size)) { return tls_obfuscator; } return nullptr; } inline std::vector GetObfuscatorNames() { return {"tls", "none"}; } inline std::optional GetObfuscatorByName( const std::string& name) { if (name == "tls") { return std::make_shared(); } if (name == "none") { return nullptr; } return std::nullopt; } }; // namespace fptn::protocol::https::obfuscator ================================================ FILE: src/fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include namespace fptn::protocol::https::obfuscator { using PreparedData = std::optional>; class IObfuscator { public: virtual ~IObfuscator() = default; virtual bool AddData(const std::uint8_t* data, std::size_t size) = 0; virtual PreparedData Deobfuscate() = 0; virtual PreparedData Obfuscate( const std::uint8_t* data, std::size_t size) = 0; virtual void Reset() = 0; virtual bool HasPendingData() const = 0; virtual bool CheckProtocol(const std::uint8_t* data, std::size_t size) = 0; virtual std::shared_ptr Clone() const = 0; }; using IObfuscatorSPtr = std::shared_ptr; }; // namespace fptn::protocol::https::obfuscator ================================================ FILE: src/fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h" #include #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif namespace { constexpr std::size_t kMmaxBufferSize = 65536; enum { kFptnTlsApplicationHeaderType = 0x17, kFptnTlsApplicationHeaderMajor = 0x03, kFptnTlsApplicationHeaderMinor = 0x03, kFptnTlsApplicationProtocolVersion = 0x01, kFptnTlsApplicationMagicFlag = 0x9763 }; #pragma pack(push, 1) struct TLSAppDataRecordHeader { /* Standard TLS header */ std::uint8_t headertype; std::uint8_t headermajor; std::uint8_t headerminor; std::uint16_t content_length; // Must be in network byte order! /* FPTN TLS obfuscator protocol */ std::uint64_t random_data; std::uint16_t magic_flag; // Must be in network byte order! std::uint8_t protocol_version; std::uint8_t xor_key; std::uint16_t payload_length; // Must be in network byte order! std::uint8_t padding_length; // std::uint8_t xor_payload[payload_length]; // std::uint8_t padding[padding_length] }; #pragma pack(pop) std::uint16_t HostToNetwork16(const std::uint16_t value) { return htons(value); } std::uint16_t NetworkToHost16(const std::uint16_t value) { return ntohs(value); } std::uint64_t GetRandomData() { static std::mt19937 gen{std::random_device {} ()}; std::uniform_int_distribution dist(1024, UINT64_MAX); return dist(gen); } std::uint8_t GetRandomByte( const std::uint8_t min = 0, const std::uint8_t max = UINT8_MAX) { static std::mt19937 gen{std::random_device {}()}; std::uniform_int_distribution dist(min, max); return static_cast(dist(gen)); } std::vector GenerateRandomPadding(const std::size_t length) { std::vector padding(length); for (std::size_t i = 0; i < length; ++i) { padding[i] = GetRandomByte(); } return padding; } void ApplyXorTransform( std::uint8_t* data, const std::size_t size, const std::uint8_t key) { for (std::size_t i = 0; i < size; ++i) { data[i] ^= key; } } } // namespace namespace fptn::protocol::https::obfuscator { bool TlsObfuscator::AddData(const std::uint8_t* data, std::size_t size) { const std::scoped_lock lock(mutex_); // mutex if (data && size > 0) { // Limit total buffer size to 64KB to prevent memory exhaustion if (input_buffer_.size() + size > kMmaxBufferSize) { // If buffer would exceed 64KB, only add what fits std::size_t available_space = kMmaxBufferSize - input_buffer_.size(); if (available_space > 0) { input_buffer_.insert(input_buffer_.end(), data, data + available_space); return true; } return false; } // Normal case - add all data input_buffer_.insert(input_buffer_.end(), data, data + size); return true; } return false; } PreparedData TlsObfuscator::Deobfuscate() { const std::scoped_lock lock(mutex_); // mutex if (input_buffer_.size() < sizeof(TLSAppDataRecordHeader)) { return std::nullopt; } std::size_t total_processed = 0; std::size_t search_offset = 0; std::vector output; // Search for valid TLS records in the buffer while ( input_buffer_.size() - search_offset >= sizeof(TLSAppDataRecordHeader)) { // Read potential header at current search offset TLSAppDataRecordHeader header = {}; std::memcpy(&header, input_buffer_.data() + search_offset, sizeof(TLSAppDataRecordHeader)); const std::uint16_t total_content_length = NetworkToHost16(header.content_length); const std::uint16_t magic_flag = NetworkToHost16(header.magic_flag); const std::uint16_t payload_length = NetworkToHost16(header.payload_length); const std::uint8_t padding_length = header.padding_length; const bool is_fptn_protocol = (magic_flag == kFptnTlsApplicationMagicFlag) && (header.protocol_version == kFptnTlsApplicationProtocolVersion) && (header.headermajor == kFptnTlsApplicationHeaderMajor) && (header.headerminor == kFptnTlsApplicationHeaderMinor); // Если данные не нашего протокола - возвращаем как есть if (!is_fptn_protocol) { std::vector result = std::move(input_buffer_); input_buffer_.clear(); return result; } // Validate header fields const bool is_valid_header = is_fptn_protocol && (total_content_length >= 11 + sizeof(header.xor_key) + sizeof(header.payload_length) + sizeof(header.padding_length)); if (!is_valid_header) { // Invalid header - shift search position by 1 byte and continue searching search_offset++; continue; } // Calculate full record size including padding const size_t full_record_size = sizeof(TLSAppDataRecordHeader) + payload_length + padding_length; // Check if we have a complete record at this position if (input_buffer_.size() - search_offset < full_record_size) { // Incomplete record - wait for more data break; } // Extract and process payload data const std::uint8_t* encrypted_payload = input_buffer_.data() + search_offset + sizeof(TLSAppDataRecordHeader); // Copy encrypted payload to temporary buffer for XOR processing std::vector decrypted_payload( encrypted_payload, encrypted_payload + payload_length); // Apply XOR decryption ApplyXorTransform( decrypted_payload.data(), decrypted_payload.size(), header.xor_key); // Add decrypted payload to output output.insert( output.end(), decrypted_payload.begin(), decrypted_payload.end()); // Remove the processed record from buffer starting from search_offset input_buffer_.erase(input_buffer_.begin() + search_offset, input_buffer_.begin() + search_offset + full_record_size); total_processed += full_record_size; break; } // If we searched through the entire buffer without finding valid headers, // clear the processed portion to prevent infinite growth if (search_offset > 0 && total_processed == 0) { // We found only invalid data - remove the searched portion input_buffer_.erase( input_buffer_.begin(), input_buffer_.begin() + search_offset); } if (!output.empty()) { return output; } return std::nullopt; } PreparedData TlsObfuscator::Obfuscate( const std::uint8_t* data, std::size_t size) { const std::scoped_lock lock(mutex_); // mutex // Generate random padding (0-255 bytes) const std::uint8_t padding_length = GetRandomByte(64, 255); std::vector random_padding = GenerateRandomPadding(padding_length); // Generate XOR key const std::uint8_t xor_key = GetRandomByte(); // Prepare payload for XOR encryption std::vector encrypted_payload(data, data + size); ApplyXorTransform( encrypted_payload.data(), encrypted_payload.size(), xor_key); const std::uint16_t total_content_length = sizeof(TLSAppDataRecordHeader::random_data) + sizeof(TLSAppDataRecordHeader::magic_flag) + sizeof(TLSAppDataRecordHeader::protocol_version) + sizeof(TLSAppDataRecordHeader::xor_key) + sizeof(TLSAppDataRecordHeader::payload_length) + sizeof(TLSAppDataRecordHeader::padding_length) + static_cast(size) + padding_length; TLSAppDataRecordHeader header = {}; header.headertype = kFptnTlsApplicationHeaderType; header.headermajor = kFptnTlsApplicationHeaderMajor; header.headerminor = kFptnTlsApplicationHeaderMinor; // Convert to network byte order header.content_length = HostToNetwork16(total_content_length); header.random_data = GetRandomData(); header.magic_flag = HostToNetwork16(kFptnTlsApplicationMagicFlag); header.protocol_version = kFptnTlsApplicationProtocolVersion; header.xor_key = xor_key; header.payload_length = HostToNetwork16(static_cast(size)); header.padding_length = padding_length; std::vector result; result.resize(sizeof(TLSAppDataRecordHeader) + size + padding_length); // Copy header std::memcpy(result.data(), &header, sizeof(TLSAppDataRecordHeader)); // Copy encrypted payload if (size > 0) { std::memcpy(result.data() + sizeof(TLSAppDataRecordHeader), encrypted_payload.data(), size); } // Copy random padding if (padding_length > 0) { std::memcpy(result.data() + sizeof(TLSAppDataRecordHeader) + size, random_padding.data(), padding_length); } if (!result.empty()) { return result; } return std::nullopt; } void TlsObfuscator::Reset() { input_buffer_.clear(); } bool TlsObfuscator::CheckProtocol(const std::uint8_t* data, std::size_t size) { const std::scoped_lock lock(mutex_); // mutex if (data == nullptr || size < sizeof(TLSAppDataRecordHeader)) { return false; } TLSAppDataRecordHeader header = {}; std::memcpy(&header, data, sizeof(TLSAppDataRecordHeader)); const std::uint16_t magic_flag = NetworkToHost16(header.magic_flag); const std::uint16_t content_length = NetworkToHost16(header.content_length); const std::uint16_t payload_length = NetworkToHost16(header.payload_length); const bool is_valid_protocol = (header.headertype == kFptnTlsApplicationHeaderType) && (header.headermajor == kFptnTlsApplicationHeaderMajor) && (header.headerminor == kFptnTlsApplicationHeaderMinor) && (header.protocol_version == kFptnTlsApplicationProtocolVersion) && (magic_flag == kFptnTlsApplicationMagicFlag) && (content_length >= 11 + sizeof(header.xor_key) + sizeof(header.payload_length) + sizeof(header.padding_length)) && (content_length <= 16384) && (payload_length <= content_length - 11 - sizeof(header.xor_key) - sizeof(header.payload_length) - sizeof(header.padding_length)); return is_valid_protocol; } bool TlsObfuscator::HasPendingData() const { const std::scoped_lock lock(mutex_); // mutex bool result = !input_buffer_.empty(); return result; } std::shared_ptr TlsObfuscator::Clone() const { return std::make_shared(); } }; // namespace fptn::protocol::https::obfuscator ================================================ FILE: src/fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include "fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h" namespace fptn::protocol::https::obfuscator { // DEPRECATED class TlsObfuscator : public IObfuscator { public: TlsObfuscator() = default; ~TlsObfuscator() override = default; bool AddData(const std::uint8_t* data, std::size_t size) override; PreparedData Deobfuscate() override; PreparedData Obfuscate(const std::uint8_t* data, std::size_t size) override; void Reset() override; bool HasPendingData() const override; bool CheckProtocol(const std::uint8_t* data, std::size_t size) override; std::shared_ptr Clone() const override; private: mutable std::mutex mutex_; std::vector input_buffer_; }; }; // namespace fptn::protocol::https::obfuscator ================================================ FILE: src/fptn-protocol-lib/https/obfuscator/methods/tls2/tls_obfuscator2.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-protocol-lib/https/obfuscator/methods/tls2/tls_obfuscator2.h" #include #include #include #include #include #ifdef _WIN32 #include #else #include #endif #include "fptn-protocol-lib/time/time_provider.h" namespace { constexpr std::size_t kMmaxBufferSize = 65536; enum { kFptnTlsApplicationHeaderType = 0x17, kFptnTlsApplicationHeaderMajor = 0x03, kFptnTlsApplicationHeaderMinor = 0x03 }; #pragma pack(push, 1) struct TLSAppDataRecordHeader { /* Standard TLS header */ std::uint8_t headertype; std::uint8_t headermajor; std::uint8_t headerminor; std::uint16_t content_length; // Must be in network byte order! /* FPTN TLS obfuscator protocol */ std::uint64_t random_data; std::uint32_t timestamp; // Must be in network byte order! std::uint8_t xor_key; std::uint16_t payload_length; // Must be in network byte order! std::uint16_t padding_length; // Must be in network byte order! // std::uint8_t xor_payload[payload_length]; // std::uint8_t padding[padding_length] }; #pragma pack(pop) std::uint16_t HostToNetwork16(const std::uint16_t value) { return htons(value); } std::uint16_t NetworkToHost16(const std::uint16_t value) { return ntohs(value); } std::uint32_t HostToNetwork32(const std::uint32_t value) { return htonl(value); } std::uint32_t NetworkToHost32(const std::uint32_t value) { return ntohl(value); } bool IsValidTimestamp(const std::uint32_t timestamp) { const auto now = fptn::time::TimeProvider::Instance()->NowTimestamp(); constexpr std::uint32_t kTimeShiftSeconds = 10; return (timestamp <= now + kTimeShiftSeconds) && (timestamp + kTimeShiftSeconds >= now); } std::uint64_t GetRandomData() { static std::mt19937 gen{std::random_device {}()}; std::uniform_int_distribution dist(1024, UINT64_MAX); return dist(gen); } std::uint8_t GetRandomByte( const std::uint8_t min = 0, const std::uint8_t max = UINT8_MAX) { static std::mt19937 gen{std::random_device {}()}; std::uniform_int_distribution dist(min, max); return static_cast(dist(gen)); } std::uint16_t GetRandomPaddingLength() { static std::mt19937 gen{std::random_device {}()}; std::uniform_int_distribution dist(4095, 8192); return dist(gen); } std::vector GenerateRandomPadding(const std::size_t length) { std::vector padding(length); for (std::size_t i = 0; i < length; ++i) { padding[i] = GetRandomByte(); } return padding; } void ApplyXorTransform( std::uint8_t* data, const std::size_t size, const std::uint8_t key) { for (std::size_t i = 0; i < size; ++i) { data[i] ^= key; } } } // namespace namespace fptn::protocol::https::obfuscator { bool TlsObfuscator2::AddData(const std::uint8_t* data, std::size_t size) { const std::scoped_lock lock(mutex_); if (data && size > 0) { if (input_buffer_.size() + size > kMmaxBufferSize) { std::size_t available_space = kMmaxBufferSize - input_buffer_.size(); if (available_space > 0) { input_buffer_.insert(input_buffer_.end(), data, data + available_space); return true; } return false; } input_buffer_.insert(input_buffer_.end(), data, data + size); return true; } return false; } PreparedData TlsObfuscator2::Deobfuscate() { const std::scoped_lock lock(mutex_); if (input_buffer_.size() < sizeof(TLSAppDataRecordHeader)) { return std::nullopt; } std::size_t total_processed = 0; std::size_t search_offset = 0; std::vector output; while ( input_buffer_.size() - search_offset >= sizeof(TLSAppDataRecordHeader)) { TLSAppDataRecordHeader header = {}; std::memcpy(&header, input_buffer_.data() + search_offset, sizeof(TLSAppDataRecordHeader)); const std::uint16_t total_content_length = NetworkToHost16(header.content_length); const std::uint32_t timestamp = NetworkToHost32(header.timestamp); const std::uint16_t payload_length = NetworkToHost16(header.payload_length); const std::uint16_t padding_length = NetworkToHost16(header.padding_length); const bool is_fptn_protocol = IsValidTimestamp(timestamp) && (header.headermajor == kFptnTlsApplicationHeaderMajor) && (header.headerminor == kFptnTlsApplicationHeaderMinor); if (!is_fptn_protocol) { search_offset++; continue; } const bool is_valid_header = (total_content_length >= sizeof(TLSAppDataRecordHeader::random_data) + sizeof(TLSAppDataRecordHeader::timestamp) + sizeof(TLSAppDataRecordHeader::xor_key) + sizeof(TLSAppDataRecordHeader::payload_length) + sizeof(TLSAppDataRecordHeader::padding_length) + payload_length + padding_length); if (!is_valid_header) { search_offset++; continue; } const size_t full_record_size = sizeof(TLSAppDataRecordHeader) + payload_length + padding_length; if (input_buffer_.size() - search_offset < full_record_size) { break; } const std::uint8_t* encrypted_payload = input_buffer_.data() + search_offset + sizeof(TLSAppDataRecordHeader); std::vector decrypted_payload( encrypted_payload, encrypted_payload + payload_length); ApplyXorTransform( decrypted_payload.data(), decrypted_payload.size(), header.xor_key); output.insert( output.end(), decrypted_payload.begin(), decrypted_payload.end()); input_buffer_.erase(input_buffer_.begin() + search_offset, input_buffer_.begin() + search_offset + full_record_size); total_processed += full_record_size; break; } if (search_offset > 0 && total_processed == 0) { input_buffer_.erase( input_buffer_.begin(), input_buffer_.begin() + search_offset); } if (!output.empty()) { return output; } return std::nullopt; } PreparedData TlsObfuscator2::Obfuscate( const std::uint8_t* data, std::size_t size) { const std::scoped_lock lock(mutex_); const std::uint16_t padding_length = GetRandomPaddingLength(); const std::vector random_padding = GenerateRandomPadding(padding_length); const std::uint8_t xor_key = GetRandomByte(); std::vector encrypted_payload(data, data + size); ApplyXorTransform( encrypted_payload.data(), encrypted_payload.size(), xor_key); const std::uint16_t total_content_length = sizeof(TLSAppDataRecordHeader::random_data) + sizeof(TLSAppDataRecordHeader::timestamp) + sizeof(TLSAppDataRecordHeader::xor_key) + sizeof(TLSAppDataRecordHeader::payload_length) + sizeof(TLSAppDataRecordHeader::padding_length) + static_cast(size) + padding_length; TLSAppDataRecordHeader header = {}; header.headertype = kFptnTlsApplicationHeaderType; header.headermajor = kFptnTlsApplicationHeaderMajor; header.headerminor = kFptnTlsApplicationHeaderMinor; header.content_length = HostToNetwork16(total_content_length); header.random_data = GetRandomData(); const std::uint32_t current_timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); header.timestamp = HostToNetwork32(current_timestamp); header.xor_key = xor_key; header.payload_length = HostToNetwork16(static_cast(size)); header.padding_length = HostToNetwork16(padding_length); std::vector result; result.resize(sizeof(TLSAppDataRecordHeader) + size + padding_length); std::memcpy(result.data(), &header, sizeof(TLSAppDataRecordHeader)); if (size > 0) { std::memcpy(result.data() + sizeof(TLSAppDataRecordHeader), encrypted_payload.data(), size); } if (padding_length > 0) { std::memcpy(result.data() + sizeof(TLSAppDataRecordHeader) + size, random_padding.data(), padding_length); } if (!result.empty()) { return result; } return std::nullopt; } void TlsObfuscator2::Reset() { input_buffer_.clear(); } bool TlsObfuscator2::CheckProtocol(const std::uint8_t* data, std::size_t size) { const std::scoped_lock lock(mutex_); if (data == nullptr || size < sizeof(TLSAppDataRecordHeader)) { return false; } TLSAppDataRecordHeader header = {}; std::memcpy(&header, data, sizeof(TLSAppDataRecordHeader)); const std::uint32_t timestamp = NetworkToHost32(header.timestamp); const std::uint16_t content_length = NetworkToHost16(header.content_length); const std::uint16_t payload_length = NetworkToHost16(header.payload_length); const std::uint16_t padding_length = NetworkToHost16(header.padding_length); const bool is_valid_protocol = (header.headertype == kFptnTlsApplicationHeaderType) && (header.headermajor == kFptnTlsApplicationHeaderMajor) && (header.headerminor == kFptnTlsApplicationHeaderMinor) && IsValidTimestamp(timestamp) && (content_length >= sizeof(TLSAppDataRecordHeader::random_data) + sizeof(TLSAppDataRecordHeader::timestamp) + sizeof(TLSAppDataRecordHeader::xor_key) + sizeof(TLSAppDataRecordHeader::payload_length) + sizeof(TLSAppDataRecordHeader::padding_length) + payload_length + padding_length) && (content_length <= 16384); return is_valid_protocol; } bool TlsObfuscator2::HasPendingData() const { const std::scoped_lock lock(mutex_); return !input_buffer_.empty(); } std::shared_ptr TlsObfuscator2::Clone() const { return std::make_shared(); } }; // namespace fptn::protocol::https::obfuscator ================================================ FILE: src/fptn-protocol-lib/https/obfuscator/methods/tls2/tls_obfuscator2.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include "fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h" namespace fptn::protocol::https::obfuscator { class TlsObfuscator2 : public IObfuscator { public: TlsObfuscator2() = default; ~TlsObfuscator2() override = default; bool AddData(const std::uint8_t* data, std::size_t size) override; PreparedData Deobfuscate() override; PreparedData Obfuscate(const std::uint8_t* data, std::size_t size) override; void Reset() override; bool HasPendingData() const override; bool CheckProtocol(const std::uint8_t* data, std::size_t size) override; std::shared_ptr Clone() const override; private: mutable std::mutex mutex_; std::vector input_buffer_; }; }; // namespace fptn::protocol::https::obfuscator ================================================ FILE: src/fptn-protocol-lib/https/obfuscator/tcp_stream/tcp_stream.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include "fptn-protocol-lib/https/obfuscator/methods/obfuscator_interface.h" namespace fptn::protocol::https::obfuscator { template class TcpStream { public: using executor_type = typename Stream::executor_type; using next_layer_type = Stream; using lowest_layer_type = Stream; explicit TcpStream(executor_type ex) : stream_(ex), strand_(ex), obfuscator_(nullptr) {} explicit TcpStream(executor_type ex, IObfuscatorSPtr obfuscator = nullptr) : stream_(ex), strand_(ex), obfuscator_(std::move(obfuscator)) {} explicit TcpStream(Stream&& stream, IObfuscatorSPtr obfuscator = nullptr) : stream_(std::move(stream)), strand_(stream_.get_executor()), obfuscator_(std::move(obfuscator)) {} TcpStream(const TcpStream&) = delete; TcpStream& operator=(const TcpStream&) = delete; TcpStream(TcpStream&& other) noexcept : stream_(std::move(other.stream_)), strand_(std::move(other.strand_)), obfuscator_(std::move(other.obfuscator_)) {} TcpStream& operator=(TcpStream&& other) noexcept { if (this != &other) { stream_ = std::move(other.stream_); strand_ = std::move(other.strand_); obfuscator_ = std::move(other.obfuscator_); } return *this; } executor_type get_executor() { return stream_.get_executor(); } next_layer_type& next_layer() { return stream_; } const next_layer_type& next_layer() const { return stream_; } lowest_layer_type& lowest_layer() { return stream_; } const lowest_layer_type& lowest_layer() const { return stream_; } template std::size_t read_some( const MutableBufferSequence& buffers, boost::system::error_code& ec) { if (!obfuscator_) { return stream_.read_some(buffers, ec); } constexpr std::size_t kTempBufferSize = 16 * 1024; std::array temp_buffer; while (true) { if (obfuscator_->HasPendingData()) { auto deobfuscated = obfuscator_->Deobfuscate(); if (deobfuscated.has_value()) { return boost::asio::buffer_copy(buffers, boost::asio::buffer(deobfuscated->data(), deobfuscated->size())); } } const std::size_t bytes_read = stream_.read_some(boost::asio::buffer(temp_buffer), ec); if (ec) { return bytes_read; } if (bytes_read == 0) { return 0; } obfuscator_->AddData(temp_buffer.data(), bytes_read); auto deobfuscated = obfuscator_->Deobfuscate(); if (deobfuscated.has_value()) { return boost::asio::buffer_copy(buffers, boost::asio::buffer(deobfuscated->data(), deobfuscated->size())); } } } template void async_read_some( const MutableBufferSequence& buffers, ReadHandler&& handler) { if (!obfuscator_) { boost::asio::dispatch( strand_, [this, buffers, handler = std::forward(handler)]() mutable { stream_.async_read_some(buffers, std::move(handler)); }); return; } boost::asio::dispatch(strand_, [this, buffers, handler = std::forward( handler)]() mutable { if (obfuscator_->HasPendingData()) { auto deobfuscated = obfuscator_->Deobfuscate(); if (deobfuscated.has_value()) { const std::size_t bytes_copied = boost::asio::buffer_copy( buffers, boost::asio::buffer(deobfuscated.value())); handler(boost::system::error_code{}, bytes_copied); return; } } stream_.async_read_some(buffers, [this, buffers, handler = std::move(handler)]( boost::system::error_code ec, std::size_t bytes_read) mutable { if (ec || bytes_read == 0) { handler(ec, bytes_read); return; } if (has_single_buffer(buffers)) { const auto& it = boost::asio::buffer_sequence_begin(buffers); const boost::asio::mutable_buffer& first_buffer = *it; const std::uint8_t* data_ptr = static_cast(first_buffer.data()); obfuscator_->AddData(data_ptr, bytes_read); auto deobfuscated = obfuscator_->Deobfuscate(); if (deobfuscated.has_value()) { const std::size_t bytes_copied = boost::asio::buffer_copy( buffers, boost::asio::buffer(deobfuscated.value())); handler(ec, bytes_copied); } else { this->async_read_some(buffers, std::move(handler)); } } else { std::vector temp_data(bytes_read); boost::asio::buffer_copy(boost::asio::buffer(temp_data), buffers); obfuscator_->AddData(temp_data.data(), bytes_read); auto deobfuscated = obfuscator_->Deobfuscate(); if (deobfuscated.has_value()) { const std::size_t bytes_copied = boost::asio::buffer_copy( buffers, boost::asio::buffer(deobfuscated.value())); handler(ec, bytes_copied); } else { boost::asio::dispatch(strand_, [this, buffers, handler = std::forward(handler)]() mutable { this->async_read_some(buffers, std::move(handler)); }); } } }); }); } template std::size_t write_some( const ConstBufferSequence& buffers, boost::system::error_code& ec) { if (!obfuscator_) { return stream_.write_some(buffers, ec); } std::vector plain_data(boost::asio::buffer_size(buffers)); boost::asio::buffer_copy(boost::asio::buffer(plain_data), buffers); auto obfuscated = obfuscator_->Obfuscate(plain_data.data(), plain_data.size()); if (!obfuscated.has_value()) { ec = boost::asio::error::eof; return 0; } return stream_.write_some(boost::asio::buffer(obfuscated.value()), ec); } template void async_write_some( const ConstBufferSequence& buffers, WriteHandler&& handler) { if (!obfuscator_) { boost::asio::dispatch(strand_, [this, buffers, handler = std::forward(handler)]() mutable { stream_.async_write_some(buffers, std::move(handler)); }); return; } const std::size_t total_size = boost::asio::buffer_size(buffers); auto plain_data = std::make_shared>(total_size); boost::asio::buffer_copy(boost::asio::buffer(*plain_data), buffers); boost::asio::dispatch( strand_, [this, plain_data, handler = std::forward(handler)]() mutable { auto obfuscated_data = obfuscator_->Obfuscate(plain_data->data(), plain_data->size()); if (!obfuscated_data.has_value()) { handler(boost::system::error_code(boost::asio::error::eof), 0); return; } stream_.async_write_some( boost::asio::buffer(obfuscated_data.value()), std::move(handler)); }); } template auto async_connect(Args&&... args) { return stream_.async_connect(std::forward(args)...); } void close() { stream_.close(); } void close(boost::system::error_code& ec) { stream_.close(ec); } template void set_option(const Option& option) { stream_.set_option(option); } void expires_after(std::chrono::steady_clock::duration expiry_time) { stream_.expires_after(expiry_time); } void expires_never() { stream_.expires_never(); } bool is_open() const { return stream_.is_open(); } auto remote_endpoint() { return stream_.socket().remote_endpoint(); } auto remote_endpoint(boost::system::error_code& ec) { return stream_.socket().remote_endpoint(ec); } auto local_endpoint() { return stream_.socket().local_endpoint(); } auto local_endpoint(boost::system::error_code& ec) { return stream_.socket().local_endpoint(ec); } void set_obfuscator(IObfuscatorSPtr obfuscator) { obfuscator_ = std::move(obfuscator); } IObfuscatorSPtr get_obfuscator() const { return obfuscator_; } protected: template static bool has_single_buffer(const Sequence& buffers) { std::size_t count = 0; auto end = boost::asio::buffer_sequence_end(buffers); for (auto it = boost::asio::buffer_sequence_begin(buffers); it != end; ++it) { ++count; } return count == 1; } private: Stream stream_; boost::asio::strand strand_; IObfuscatorSPtr obfuscator_; }; } // namespace fptn::protocol::https::obfuscator namespace boost::beast { template inline void teardown(boost::beast::role_type role, fptn::protocol::https::obfuscator::TcpStream& stream, boost::system::error_code& ec) { teardown(role, stream.next_layer(), ec); } template inline void async_teardown(boost::beast::role_type role, fptn::protocol::https::obfuscator::TcpStream& stream, TeardownHandler&& handler) { async_teardown( role, stream.next_layer(), std::forward(handler)); } } // namespace boost::beast ================================================ FILE: src/fptn-protocol-lib/https/utils/change_cipher_spec.h ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include namespace fptn::protocol::https::utils { inline std::vector MakeClientChangeCipherSpec() { return {0x14, 0x03, 0x03, 0x00, 0x01, 0x01}; } } // namespace fptn::protocol::https::utils ================================================ FILE: src/fptn-protocol-lib/https/utils/tls/tls.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-protocol-lib/https/utils/tls/tls.h" #include #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "fptn-protocol-lib/time/time_provider.h" namespace fptn::protocol::https::utils { constexpr int kSessionLen = 32; constexpr std::size_t kFptnKeyLength = 4; constexpr int kDecoyHandshakeSessionIDShift = 10; constexpr int kDecoyHandshakeSessionIDShift2 = 14; std::string GetSHA1Hash(std::uint32_t number) { EVP_MD_CTX* mdctx = EVP_MD_CTX_new(); if (!mdctx) { return {}; } const EVP_MD* md = EVP_get_digestbyname("SHA1"); if (!md) { EVP_MD_CTX_free(mdctx); return {}; } if (!EVP_DigestInit_ex(mdctx, md, nullptr)) { EVP_MD_CTX_free(mdctx); return {}; } if (!EVP_DigestUpdate(mdctx, &number, sizeof(number))) { EVP_MD_CTX_free(mdctx); return {}; } unsigned int outlen = 0; unsigned char buffer[EVP_MAX_MD_SIZE] = {0}; if (!EVP_DigestFinal_ex(mdctx, buffer, &outlen)) { EVP_MD_CTX_free(mdctx); return {}; } EVP_MD_CTX_free(mdctx); return std::string(reinterpret_cast(buffer), outlen); } std::string GenerateFptnKey(std::uint32_t timestamp) { std::string result = GetSHA1Hash(htonl(timestamp)); if (result.size() > kFptnKeyLength) { // key len return result.substr(0, kFptnKeyLength); } throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), "Error generate Session ID"); } bool SetDecoyHandshakeSessionID(SSL* ssl) { // random std::uint8_t session_id[kSessionLen] = {0}; if (::RAND_bytes(session_id, sizeof(session_id)) != 1) { return false; } // copy timestamp const auto timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); const std::string key = GenerateFptnKey(timestamp); std::memcpy( &session_id[kDecoyHandshakeSessionIDShift], key.c_str(), key.size()); return 0 != ::SSL_set_tls_hello_custom_session_id( ssl, session_id, sizeof(session_id)); } bool IsDecoyHandshakeSessionID( const std::uint8_t* session, std::size_t session_len) { (void)session_len; char data[kFptnKeyLength] = {0}; std::memcpy(&data, &session[kDecoyHandshakeSessionIDShift], sizeof(data)); const std::string recv_key(data, sizeof(data)); const auto now_timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); constexpr std::uint32_t kTimeShiftSeconds = 10; // ten seconds const std::uint32_t timestamp = now_timestamp + (kTimeShiftSeconds / 2); for (std::uint32_t shift = 0; shift <= kTimeShiftSeconds; shift++) { const std::string key = GenerateFptnKey(timestamp - shift); if (recv_key == key) { return true; } } return false; } bool IsDecoyHandshakeSessionID2( const std::uint8_t* session, std::size_t session_len) { (void)session_len; char data[kFptnKeyLength] = {0}; std::memcpy(&data, &session[kDecoyHandshakeSessionIDShift2], sizeof(data)); const std::string recv_key(data, sizeof(data)); const auto now_timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); constexpr std::uint32_t kTimeShiftSeconds = 10; // ten seconds const std::uint32_t timestamp = now_timestamp + (kTimeShiftSeconds / 2); for (std::uint32_t shift = 0; shift <= kTimeShiftSeconds; shift++) { const std::string key = GenerateFptnKey(timestamp - shift); if (recv_key == key) { return true; } } return false; } bool SetHandshakeSessionID(SSL* ssl) { // random std::uint8_t session_id[kSessionLen] = {0}; if (::RAND_bytes(session_id, sizeof(session_id)) != 1) { return false; } // copy timestamp const auto timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); const std::string key = GenerateFptnKey(timestamp); std::memcpy(&session_id[kSessionLen - key.size()], key.c_str(), key.size()); return 0 != ::SSL_set_tls_hello_custom_session_id( ssl, session_id, sizeof(session_id)); } bool IsFptnClientSessionID( const std::uint8_t* session, std::size_t session_len) { char data[kFptnKeyLength] = {0}; std::memcpy(&data, &session[session_len - sizeof(data)], sizeof(data)); const std::string recv_key(data, sizeof(data)); const auto now_timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); constexpr std::uint32_t kTimeShiftSeconds = 10; // ten seconds const std::uint32_t timestamp = now_timestamp + (kTimeShiftSeconds / 2); for (std::uint32_t shift = 0; shift <= kTimeShiftSeconds; shift++) { const std::string key = GenerateFptnKey(timestamp - shift); if (recv_key == key) { return true; } } return false; } bool SetHandshakeSni(SSL* ssl, const std::string& sni) { // Set SNI (Server Name) if (1 != ::SSL_set_tlsext_host_name(ssl, sni.c_str())) { throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), fmt::format(R"(Failed to set SNI "{}")", sni)); } // Add Chrome-like padding (to match packet size) SSL_set_options(ssl, SSL_OP_LEGACY_SERVER_CONNECT); return true; } SSL_CTX* CreateNewSslCtx() { SSL_CTX* handle = ::SSL_CTX_new(::TLS_client_method()); if (!handle) { throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), "Failed to create SSL context"); } if (0 == ::SSL_CTX_set_min_proto_version(handle, TLS1_2_VERSION)) { ::SSL_CTX_free(handle); throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), "Failed to set min TLS version"); } if (0 == ::SSL_CTX_set_max_proto_version(handle, TLS1_3_VERSION)) { ::SSL_CTX_free(handle); throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), "Failed to set max TLS version"); } SSL_CTX_set_grease_enabled(handle, 1); SSL_CTX_enable_ocsp_stapling(handle); SSL_CTX_enable_signed_cert_timestamps(handle); // Set ciphers ТОЧНО КАК В RUST КОДЕ const std::string ciphers_list = ChromeCiphers(); if (0 == ::SSL_CTX_set_cipher_list(handle, ciphers_list.c_str())) { ::SSL_CTX_free(handle); throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), "Failed to set ciphers"); } const char* groups = "X25519MLKEM768:X25519:secp256r1:secp384r1"; SSL_CTX_set1_groups_list(handle, groups); static unsigned char alpn[] = { 0x02, 'h', '2', 0x08, 'h', 't', 't', 'p', '/', '1', '.', '1'}; if (0 != ::SSL_CTX_set_alpn_protos(handle, alpn, sizeof(alpn))) { ::SSL_CTX_free(handle); throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), "Failed to set ALPN"); } const std::string sigalgs_list = "ecdsa_secp256r1_sha256:" "rsa_pss_rsae_sha256:" "rsa_pkcs1_sha256:" "ecdsa_secp384r1_sha384:" "rsa_pss_rsae_sha384:" "rsa_pkcs1_sha384:" "rsa_pss_rsae_sha512:" "rsa_pkcs1_sha512"; if (1 != SSL_CTX_set1_sigalgs_list(handle, sigalgs_list.c_str())) { ::SSL_CTX_free(handle); throw boost::beast::system_error( boost::beast::error_code(static_cast(::ERR_get_error()), boost::asio::error::get_ssl_category()), "Failed to set signature algorithms"); } SSL_CTX_set_mode(handle, SSL_MODE_RELEASE_BUFFERS); return handle; } std::string ChromeCiphers() { return "TLS_AES_128_GCM_SHA256:" "TLS_AES_256_GCM_SHA384:" "TLS_CHACHA20_POLY1305_SHA256:" "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:" "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:" "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:" "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:" "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:" "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:" "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:" "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:" "TLS_RSA_WITH_AES_128_GCM_SHA256:" "TLS_RSA_WITH_AES_256_GCM_SHA384:" "TLS_RSA_WITH_AES_128_CBC_SHA:" "TLS_RSA_WITH_AES_256_CBC_SHA"; } std::string GetCertificateMD5Fingerprint(const X509* cert) { unsigned char md[MD5_DIGEST_LENGTH] = {}; if (X509_digest(cert, EVP_md5(), md, nullptr) != 1) { SPDLOG_ERROR("Failed to compute MD5 digest"); return {}; } std::stringstream ss; // NOLINTNEXTLINE(modernize-loop-convert) for (int i = 0; i < MD5_DIGEST_LENGTH; i++) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(md[i]); } return ss.str(); } std::vector GenerateDecoyTlsHandshake(const std::string& sni) { std::vector handshake_data; try { SSL_CTX* ssl_ctx = CreateNewSslCtx(); SSL* ssl = ::SSL_new(ssl_ctx); BIO* bio_out = ::BIO_new(BIO_s_mem()); BIO* bio_in = ::BIO_new(BIO_s_mem()); ::SSL_set_bio(ssl, bio_in, bio_out); SetHandshakeSni(ssl, sni); SetDecoyHandshakeSessionID(ssl); ::SSL_set_connect_state(ssl); int handshake_result; int retry_count = 0; constexpr int kMaxRetries = 10; do { handshake_result = ::SSL_do_handshake(ssl); char* bio_data = nullptr; auto bio_length = ::BIO_get_mem_data(bio_out, &bio_data); if (bio_data && bio_length > 0) { handshake_data.insert( handshake_data.end(), bio_data, bio_data + bio_length); BIO_reset(bio_out); } retry_count++; } while (handshake_result <= 0 && SSL_get_error(ssl, handshake_result) == SSL_ERROR_WANT_WRITE && retry_count < kMaxRetries); if (handshake_data.empty()) { SPDLOG_WARN("No handshake data was generated for SNI: {}", sni); } else { SPDLOG_INFO( "Successfully generated {} bytes of TLS handshake for SNI: {}", handshake_data.size(), sni); } ::SSL_free(ssl); ::SSL_CTX_free(ssl_ctx); } catch (const std::exception& e) { SPDLOG_ERROR( "GenerateTlsHandshake exception for SNI {}: {}", sni, e.what()); } return handshake_data; } std::optional> GenerateDecoyTlsSessionId() { std::array session_id{}; if (::RAND_bytes(session_id.data(), session_id.size()) != 1) { return std::nullopt; } // Get timestamp and generate key const auto timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); const std::string key = GenerateFptnKey(timestamp); const std::size_t copy_size = std::min(key.size(), session_id.size() - kDecoyHandshakeSessionIDShift); std::memcpy(session_id.data() + kDecoyHandshakeSessionIDShift, key.c_str(), copy_size); return session_id; } std::optional> GenerateDecoyTlsSessionId2() { std::array session_id{}; if (::RAND_bytes(session_id.data(), session_id.size()) != 1) { return std::nullopt; } // Get timestamp and generate key const auto timestamp = fptn::time::TimeProvider::Instance()->NowTimestamp(); const std::string key = GenerateFptnKey(timestamp); const std::size_t copy_size = std::min(key.size(), session_id.size() - kDecoyHandshakeSessionIDShift2); std::memcpy(session_id.data() + kDecoyHandshakeSessionIDShift2, key.c_str(), copy_size); return session_id; } // MAYBE IT WILL REFACTOR namespace { std::unordered_map attach_callbacks; std::mutex attach_callback_mutex; } // namespace void AttachCertificateVerificationCallback( SSL* ssl, const CertificateVerificationCallback& callback) { auto* func_ptr = new CertificateVerificationCallback(callback); { const std::scoped_lock lock(attach_callback_mutex); // mutex attach_callbacks[ssl] = func_ptr; } ::SSL_set_verify( ssl, SSL_VERIFY_PEER, [](int preverified, X509_STORE_CTX* ctx) -> int { (void)preverified; const X509* cert = ::X509_STORE_CTX_get_current_cert(ctx); if (!cert) { return 0; } SSL* ssl = static_cast(::X509_STORE_CTX_get_ex_data( ctx, ::SSL_get_ex_data_X509_STORE_CTX_idx())); if (!ssl) { return 0; } const std::string md5_fingerprint = GetCertificateMD5Fingerprint(cert); if (md5_fingerprint.empty()) { return 0; } const std::scoped_lock lock(attach_callback_mutex); // mutex { const auto it = attach_callbacks.find(ssl); if (it == attach_callbacks.end()) { return 0; } return (it->second && (*it->second)(md5_fingerprint) ? 1 : 0); } }); } void AttachCertificateVerificationCallbackDelete(SSL* ssl) { const std::scoped_lock lock(attach_callback_mutex); // mutex auto it = attach_callbacks.find(ssl); if (it != attach_callbacks.end()) { delete it->second; // Clean up the allocated callback attach_callbacks.erase(it); } } } // namespace fptn::protocol::https::utils ================================================ FILE: src/fptn-protocol-lib/https/utils/tls/tls.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include // NOLINT(build/include_order) namespace fptn::protocol::https::utils { std::string GetSHA1Hash(std::uint32_t number); std::string GenerateFptnKey(std::uint32_t timestamp); bool SetDecoyHandshakeSessionID(SSL* ssl); // DEPRECATED bool IsDecoyHandshakeSessionID( const std::uint8_t* session, std::size_t session_len); bool IsDecoyHandshakeSessionID2( const std::uint8_t* session, std::size_t session_len); bool SetHandshakeSessionID(SSL* ssl); bool IsFptnClientSessionID( const std::uint8_t* session, std::size_t session_len); bool SetHandshakeSni(SSL* ssl, const std::string& sni); SSL_CTX* CreateNewSslCtx(); std::string ChromeCiphers(); std::string GetCertificateMD5Fingerprint(const X509* cert); std::vector GenerateDecoyTlsHandshake(const std::string& sni); // DEPRECATED std::optional> GenerateDecoyTlsSessionId(); std::optional> GenerateDecoyTlsSessionId2(); // Callbacks using CertificateVerificationCallback = std::function; void AttachCertificateVerificationCallback( SSL* ssl, const CertificateVerificationCallback& callback); void AttachCertificateVerificationCallbackDelete(SSL* ssl); } // namespace fptn::protocol::https::utils ================================================ FILE: src/fptn-protocol-lib/https/websocket_client/websocket_client.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-protocol-lib/https/websocket_client/websocket_client.h" #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "common/network/utils.h" // NOLINT(build/include_order) #include "fptn-protocol-lib/https/api_client/api_client.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls2/tls_obfuscator2.h" namespace fptn::protocol::https { WebsocketClient::WebsocketClient(fptn::common::network::IPv4Address server_ip, int server_port, fptn::common::network::IPv4Address tun_interface_address_ipv4, fptn::common::network::IPv6Address tun_interface_address_ipv6, NewIPPacketCallback new_ip_pkt_callback, std::string sni, std::string access_token, std::string expected_md5_fingerprint, CensorshipStrategy censorship_strategy, OnConnectedCallback on_connected_callback, int thread_number) : ioc_(thread_number), ctx_(https::utils::CreateNewSslCtx()), resolver_(boost::asio::make_strand(ioc_)), censorship_strategy_(censorship_strategy), ws_(ssl_stream_type( obfuscator_socket_type(boost::asio::make_strand(ioc_), nullptr), ctx_)), strand_(boost::asio::make_strand(ioc_)), watchdog_timer_(strand_), write_channel_(strand_, kMaxSizeOutQueue_), server_ip_(std::move(server_ip)), server_port_str_(std::to_string(server_port)), tun_interface_address_ipv4_(std::move(tun_interface_address_ipv4)), tun_interface_address_ipv6_(std::move(tun_interface_address_ipv6)), new_ip_pkt_callback_(std::move(new_ip_pkt_callback)), sni_(std::move(sni)), access_token_(std::move(access_token)), expected_md5_fingerprint_(std::move(expected_md5_fingerprint)), on_connected_callback_(std::move(on_connected_callback)) { auto* ssl = ws_.next_layer().native_handle(); https::utils::SetHandshakeSni(ssl, sni_); https::utils::SetHandshakeSessionID(ssl); // Set SSL buffer sizes SSL_set_mode(ssl, SSL_MODE_RELEASE_BUFFERS); if (censorship_strategy_ == CensorshipStrategy::kSni) { obfuscator_ = nullptr; } if (censorship_strategy_ == CensorshipStrategy::kTlsObfuscator) { obfuscator_ = std::make_shared(); ws_.next_layer().next_layer().set_obfuscator(obfuscator_); } if (censorship_strategy_ == CensorshipStrategy::kSniRealityMode) { obfuscator_ = nullptr; } https::utils::AttachCertificateVerificationCallback( ssl, [this](const std::string& md5_fingerprint) mutable { if (expected_md5_fingerprint_.empty()) { return true; } if (md5_fingerprint == expected_md5_fingerprint_) { return true; } SPDLOG_ERROR("Certificate MD5 mismatch. Expected: {}, got: {}.", expected_md5_fingerprint_, md5_fingerprint); return false; }); ws_.text(false); ws_.binary(true); ws_.auto_fragment(true); ws_.read_message_max(256 * 1024); ws_.set_option(boost::beast::websocket::stream_base::timeout::suggested( boost::beast::role_type::client)); } WebsocketClient::~WebsocketClient() { try { Stop(); } catch (...) { SPDLOG_WARN("Unknown error in ~WebsocketClient"); } // Stop io_context try { if (!ioc_.stopped()) { SPDLOG_INFO("Stopping io_context..."); ioc_.stop(); } } catch (const boost::system::system_error& err) { SPDLOG_ERROR("Exception while stopping io_context: {}", err.what()); } catch (...) { SPDLOG_ERROR("Unknown exception while stopping io_context"); } SPDLOG_INFO("WebsocketClient removed"); } void WebsocketClient::Run() { if (running_.exchange(true)) { SPDLOG_WARN("WebsocketClient is already running"); return; } SPDLOG_INFO("Connecting to {}:{}", server_ip_.ToString(), server_port_str_); auto self = weak_from_this(); boost::asio::co_spawn( ioc_, [self]() -> boost::asio::awaitable { if (auto shared_self = self.lock()) { const bool status = co_await shared_self->RunInternal(); if (!status) { shared_self->Stop(); } } }, boost::asio::detached); try { ioc_.restart(); while (running_) { ioc_.run_one(); } } catch (...) { SPDLOG_WARN("Exception while running"); } } bool WebsocketClient::Stop() { if (!running_) { return false; } const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress identicalConditionAfterEarlyExit if (!running_) { // Double-check after acquiring lock return false; } SPDLOG_INFO("Marked client as stopped and disconnected"); running_ = false; was_connected_ = false; new_ip_pkt_callback_ = nullptr; on_connected_callback_ = nullptr; boost::system::error_code ec; try { watchdog_timer_.cancel(); } catch (const boost::system::system_error&) { SPDLOG_WARN("Cancellation timer error"); } catch (...) { SPDLOG_ERROR("Unknown exception while stopping timer"); } try { SPDLOG_INFO("Emit cancel signal"); if (was_inited_) { cancel_signal_.emit(boost::asio::cancellation_type::all); } } catch (const std::exception&) { SPDLOG_DEBUG("Exception during cancellation"); } catch (...) { SPDLOG_ERROR("Unknown exception during cancellation"); } try { SPDLOG_INFO("Closing write_channel"); if (was_inited_) { write_channel_.close(); } } catch (const std::exception&) { SPDLOG_DEBUG("Exception closing write channel"); } catch (...) { SPDLOG_ERROR("Unknown exception during closing write channel"); } try { SPDLOG_INFO("Closing resolver"); if (was_inited_) { resolver_.cancel(); } } catch (const std::exception&) { SPDLOG_DEBUG("Exception cancelling resolver"); } catch (...) { SPDLOG_ERROR("Unknown exception during closing resolver"); } // Close TCP connection try { if (was_inited_) { SPDLOG_INFO("Shutting down TCP socket..."); auto& tcp = boost::beast::get_lowest_layer(ws_); boost::asio::socket_base::linger linger(true, 0); tcp.socket().set_option(linger); if (tcp.socket().is_open()) { tcp.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec); if (ec && ec != boost::asio::error::not_connected) { SPDLOG_WARN("TCP socket shutdown error: {}", ec.message()); } else { SPDLOG_INFO("TCP socket shutdown successfully"); } tcp.socket().close(ec); if (ec) { SPDLOG_WARN("TCP socket close error: {}", ec.message()); } else { SPDLOG_INFO("TCP socket closed successfully"); } } } } catch (const boost::system::system_error& err) { SPDLOG_ERROR("Exception during TCP shutdown: {}", err.what()); } catch (...) { SPDLOG_ERROR("Unknown exception during TCP shutdown"); } // Close SSL try { if (was_inited_) { SPDLOG_INFO("Shutting down SSL layer..."); auto& ssl = ws_.next_layer(); if (ssl.native_handle()) { // More robust SSL shutdown ::SSL_set_quiet_shutdown(ssl.native_handle(), 1); ::SSL_shutdown(ssl.native_handle()); } ssl.shutdown(ec); } } catch (const boost::system::system_error& err) { SPDLOG_ERROR("Exception during SSL shutdown: {}", err.what()); } catch (const std::exception& e) { SPDLOG_ERROR("Unexpected exception during SSL shutdown: {}", e.what()); } catch (...) { SPDLOG_ERROR("Unknown exception occurred during SSL shutdown"); } if (auto* ssl = ws_.next_layer().native_handle()) { https::utils::AttachCertificateVerificationCallbackDelete(ssl); } was_stopped_ = true; SPDLOG_INFO("WebSocket client stopped successfully"); return true; } bool WebsocketClient::Send(fptn::common::network::IPPacketPtr packet) { if (!running_ || !was_connected_) { return false; } try { return write_channel_.try_send( boost::system::error_code(), std::move(packet)); } catch (...) { return false; } } bool WebsocketClient::IsStarted() const { return running_ && was_connected_; } boost::asio::awaitable WebsocketClient::RunInternal() { try { const bool connected = co_await Connect(); if (!connected) { co_return false; } // Optimize socket buffer sizes try { boost::beast::get_lowest_layer(ws_).socket().set_option( boost::asio::socket_base::receive_buffer_size(1 * 1024 * 1024)); boost::beast::get_lowest_layer(ws_).socket().set_option( boost::asio::socket_base::send_buffer_size(1 * 1024 * 1024)); } catch (const boost::system::system_error& e) { SPDLOG_WARN("Failed to set socket options: {}", e.what()); } boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::hours(24)); // Start timer StartWatchdog(); // Start reader and sender was_inited_ = true; auto self = shared_from_this(); boost::asio::co_spawn( strand_, [self]() { return self->RunReader(); }, boost::asio::detached); boost::asio::co_spawn( strand_, [self]() { return self->RunSender(); }, boost::asio::detached); co_return true; } catch (const std::exception& e) { SPDLOG_ERROR("RunInternal exception: {}", e.what()); } catch (...) { SPDLOG_ERROR("Unknown exception while running"); } co_return false; } boost::asio::awaitable WebsocketClient::Connect() { try { boost::system::error_code ec; // DNS resolution boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); const auto results = co_await resolver_.async_resolve(server_ip_.ToString(), server_port_str_, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("Resolve error: {}", ec.message()); co_return false; } // TCP connect co_await boost::beast::get_lowest_layer(ws_).async_connect( results, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("Connect error: {}", ec.message()); co_return false; } auto& socket = boost::beast::get_lowest_layer(ws_).socket(); if (!socket.is_open()) { SPDLOG_ERROR("Socket not open after connect"); co_return false; } const auto remote_ep = socket.remote_endpoint(ec); if (ec) { SPDLOG_ERROR("Socket reported connected but remote_endpoint() failed: {}", ec.message()); co_return false; } SPDLOG_INFO("Successfully connected to {}:{}", remote_ep.address().to_string(), remote_ep.port()); // TCP options socket.set_option(boost::asio::ip::tcp::no_delay(true)); socket.set_option(boost::asio::socket_base::reuse_address(true)); // Optimize socket buffers try { constexpr int kBufferSize = 4 * 1024 * 1024; socket.set_option( boost::asio::socket_base::receive_buffer_size(kBufferSize)); socket.set_option( boost::asio::socket_base::send_buffer_size(kBufferSize)); } catch (...) { SPDLOG_WARN("Failed to set socket buffer sizes in Connect()"); } // Reality Mode: Enhanced stealth connection protocol // First, establishes a genuine TLS handshake as a decoy to bypass deep // packet inspection Then resets the connection state and activates // obfuscation for the real encrypted tunnel This dual-handshake approach // makes traffic analysis significantly more difficult if (IsRealityModeWithFakeHandshake(censorship_strategy_)) { const bool status = co_await PerformFakeHandshake2(); if (!status) { co_return false; } // For Reality Mode we use TLS obfuscator after fake handshake // This provides additional encryption layer for the real connection ws_.next_layer().next_layer().set_obfuscator( std::make_shared()); } else if (obfuscator_ != nullptr) { // Set obfuscator ws_.next_layer().next_layer().set_obfuscator(obfuscator_); } // SSL handshake boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(10)); // timeout co_await boost::asio::steady_timer{ co_await boost::asio::this_coro::executor, std::chrono::milliseconds(150)} .async_wait(boost::asio::use_awaitable); co_await ws_.next_layer().async_handshake( boost::asio::ssl::stream_base::client, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("SSL handshake error: {}", ec.message()); co_return false; } // CLEAN WEBSOCKET common::network::CleanSocket(socket); common::network::CleanSsl(ws_.next_layer().native_handle()); // Reset obfuscator after TLS-handshake ws_.next_layer().next_layer().set_obfuscator(nullptr); // timeout co_await boost::asio::steady_timer{ co_await boost::asio::this_coro::executor, std::chrono::milliseconds(150)} .async_wait(boost::asio::use_awaitable); SPDLOG_INFO("SSL handshake completed"); // WebSocket connection options try { boost::beast::websocket::stream_base::timeout timeout_option; timeout_option.handshake_timeout = std::chrono::seconds(10); timeout_option.idle_timeout = std::chrono::seconds(10); timeout_option.keep_alive_pings = true; ws_.set_option(timeout_option); } catch (const std::exception& e) { SPDLOG_ERROR("Failed to set timeout: {}", e.what()); } // WebSocket handshake ws_.set_option(boost::beast::websocket::stream_base::decorator( [this](boost::beast::websocket::request_type& req) { req.set("Authorization", "Bearer " + access_token_); req.set("ClientIP", tun_interface_address_ipv4_.ToString()); req.set("ClientIPv6", tun_interface_address_ipv6_.ToString()); req.set("Client-Agent", fmt::format("FptnClient({}/{})", FPTN_USER_OS, FPTN_VERSION)); })); // Websocket handshake co_await ws_.async_handshake(server_ip_.ToString(), kUrlWebSocket_, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("WebSocket handshake error: {}", ec.message()); co_return false; } was_connected_ = true; SPDLOG_INFO("WebSocket connection established successfully"); if (on_connected_callback_) { on_connected_callback_(); } // WebSocket options try { boost::beast::websocket::stream_base::timeout timeout_option; timeout_option.handshake_timeout = std::chrono::seconds(10); timeout_option.idle_timeout = std::chrono::seconds(4); timeout_option.keep_alive_pings = true; ws_.set_option(timeout_option); } catch (const std::exception& e) { SPDLOG_ERROR("Failed to set timeout: {}", e.what()); } // timeout co_await boost::asio::steady_timer{ co_await boost::asio::this_coro::executor, std::chrono::milliseconds(10)} .async_wait(boost::asio::use_awaitable); co_return true; } catch (const std::exception& e) { SPDLOG_ERROR("Connect exception: {}", e.what()); } catch (...) { SPDLOG_ERROR("Unknown exception"); } co_return false; } boost::asio::awaitable WebsocketClient::RunReader() { boost::beast::flat_buffer buffer; buffer.reserve(4 * 1024 * 1024); try { boost::system::error_code ec; while (running_ && was_connected_ && ws_.is_open()) { co_await ws_.async_read( buffer, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { if (ec != boost::beast::websocket::error::closed) { SPDLOG_DEBUG("WebSocket read error: {}", ec.message()); } break; } if (!buffer.size()) { continue; } try { auto raw_ip = protobuf::GetProtoPayload(buffer); if (raw_ip.has_value()) { auto packet = fptn::common::network::IPPacket::Parse(std::move(raw_ip.value())); if (running_ && packet && new_ip_pkt_callback_) { new_ip_pkt_callback_(std::move(packet)); } } } catch (const std::exception& e) { SPDLOG_WARN("IP packet error: {}", e.what()); } buffer.consume(buffer.size()); } } catch (const std::exception& e) { SPDLOG_ERROR("RunReader exception: {}", e.what()); } catch (...) { SPDLOG_ERROR("RunReader unknown exception"); } was_connected_ = false; co_return; } boost::asio::awaitable WebsocketClient::RunSender() { try { while (running_ && was_connected_ && ws_.is_open()) { auto [ec, packet] = co_await write_channel_.async_receive( boost::asio::bind_cancellation_slot(cancel_signal_.slot(), boost::asio::as_tuple(boost::asio::use_awaitable))); if (packet != nullptr && running_ && ws_.is_open() && !ec) { auto msg = fptn::protocol::protobuf::CreateProtoPayload(std::move(packet)); if (msg.has_value()) { co_await ws_.async_write(boost::asio::buffer(std::move(msg.value())), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); } } if (ec) { SPDLOG_ERROR("WebSocket error: {}", ec.message()); break; } } } catch (const boost::system::system_error& err) { if (err.code() != boost::asio::error::operation_aborted) { SPDLOG_ERROR("RunSender error: {}", err.what()); } } catch (const std::exception& e) { SPDLOG_ERROR("RunSender exception: {}", e.what()); } catch (...) { SPDLOG_ERROR("RunSender unknown exception"); } was_connected_ = false; co_return; } boost::asio::awaitable WebsocketClient::PerformFakeHandshake2() { try { boost::system::error_code ec; auto& tcp_layer = boost::beast::get_lowest_layer(ws_); auto& tcp_socket = tcp_layer.socket(); SPDLOG_INFO("Fake TLS handshake started for SNI: {}", sni_); /* Send client hello */ const auto client_hello = GenerateHandshakePacket(); if (client_hello.empty()) { SPDLOG_WARN("Failed to generate ClientHello for SNI: {}", sni_); co_return false; } const std::size_t client_hello_bytes_size = co_await boost::asio::async_write(tcp_socket, boost::asio::buffer(client_hello), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("Failed to send ClientHello to {}: {}", sni_, ec.message()); co_return false; } if (client_hello_bytes_size != client_hello.size()) { SPDLOG_ERROR("Error ClientHello sent: {} of {} bytes", client_hello_bytes_size, client_hello.size()); co_return false; } /* Wait for server answer */ const auto server_hello = co_await common::network::WaitForServerTlsHelloAsync(tcp_socket); if (!server_hello.has_value()) { SPDLOG_ERROR("Failed to receive ServerHello from {}", sni_); co_return false; } /* Send change cipher spec */ const auto change_cipher_spec = fptn::protocol::https::utils::MakeClientChangeCipherSpec(); const std::size_t change_cipher_spec_size = co_await boost::asio::async_write(tcp_socket, boost::asio::buffer(change_cipher_spec), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_ERROR("Failed to send ClientHello to {}: {}", sni_, ec.message()); co_return false; } if (change_cipher_spec_size != change_cipher_spec.size()) { SPDLOG_ERROR("Failed to send ClientHello to {}: {}", change_cipher_spec_size, change_cipher_spec.size()); co_return false; } // timeout boost::asio::steady_timer timer(co_await boost::asio::this_coro::executor, std::chrono::milliseconds(150)); co_await timer.async_wait(boost::asio::use_awaitable); SPDLOG_INFO( "Fake TLS handshake completed for {}, received {} bytes from server", sni_, server_hello.value().size()); co_return true; } catch (const std::exception& e) { SPDLOG_ERROR("Fake TLS handshake exception for {}: {}", sni_, e.what()); } co_return false; } void WebsocketClient::StartWatchdog() { if (!running_) { return; } constexpr std::chrono::milliseconds kTimeout(300); watchdog_timer_.expires_after(kTimeout); watchdog_timer_.async_wait([self = weak_from_this()]( const boost::system::error_code& ec) { if (auto shared_self = self.lock()) { if (!ec && shared_self->running_) { // cppcheck-suppress knownConditionTrueFalse if (!shared_self->was_connected_.load() && shared_self->running_) { SPDLOG_INFO("Watchdog detected disconnected state, calling Stop()"); shared_self->Stop(); } else { shared_self->StartWatchdog(); } } } }); } std::vector WebsocketClient::GenerateHandshakePacket() const { auto builder = camouflage::tls::Builder::Create(); switch (censorship_strategy_) { /* Chrome */ case CensorshipStrategy::kSniRealityModeChrome147: builder.GoogleChrome( camouflage::tls::google_chrome::Version::kV_147_0_7727_56); break; case CensorshipStrategy::kSniRealityModeChrome146: builder.GoogleChrome( camouflage::tls::google_chrome::Version::kV_146_0_7680_178); break; case CensorshipStrategy::kSniRealityModeChrome145: builder.GoogleChrome( camouflage::tls::google_chrome::Version::kV_145_0_7632_46); break; /* Firefox */ case CensorshipStrategy::kSniRealityModeFirefox149: builder.Firefox(camouflage::tls::firefox::Version::kV_149_0); break; /* Yandex */ case CensorshipStrategy::kSniRealityModeYandex26: builder.YandexBrowser( camouflage::tls::yandex_browser::Version::kV_26_3_3_881); break; case CensorshipStrategy::kSniRealityModeYandex25: builder.YandexBrowser( camouflage::tls::yandex_browser::Version::kV_25_8_3_828); break; case CensorshipStrategy::kSniRealityModeYandex24: builder.YandexBrowser( camouflage::tls::yandex_browser::Version::kV_24_12_0_1772); break; /* Safari */ case CensorshipStrategy::kSniRealityModeSafari26: builder.Safari(camouflage::tls::safari::Version::kV_26_4); break; /* Default */ default: SPDLOG_DEBUG("Using fallback handshake generator for SNI: {}", sni_); return utils::GenerateDecoyTlsHandshake(sni_); } const auto session_id = utils::GenerateDecoyTlsSessionId2(); if (!session_id.has_value()) { SPDLOG_WARN("Session ID generation failed"); return utils::GenerateDecoyTlsHandshake(sni_); } const auto handshake = builder.SetSNI(sni_).SetSessionId(session_id.value()).Generate(); if (!handshake.has_value()) { SPDLOG_WARN( "Handshake generation failed for SNI: {}, using fallback", sni_); return utils::GenerateDecoyTlsHandshake(sni_); } SPDLOG_INFO("Handshake generated: SNI={}, size={} bytes", sni_, handshake->handshake_packet_size); return std::vector(handshake->handshake_packet, handshake->handshake_packet + handshake->handshake_packet_size); } } // namespace fptn::protocol::https ================================================ FILE: src/fptn-protocol-lib/https/websocket_client/websocket_client.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/network/ip_address.h" #include "common/network/ip_packet.h" #include "fptn-protocol-lib/https/censorship_strategy.h" #include "fptn-protocol-lib/https/obfuscator/tcp_stream/tcp_stream.h" #include "fptn-protocol-lib/https/utils/tls/tls.h" #include "fptn-protocol-lib/protobuf/protocol.h" namespace fptn::protocol::https { class WebsocketClient : public std::enable_shared_from_this { public: using NewIPPacketCallback = std::function; using OnConnectedCallback = std::function; explicit WebsocketClient(fptn::common::network::IPv4Address server_ip, int server_port, fptn::common::network::IPv4Address tun_interface_address_ipv4, fptn::common::network::IPv6Address tun_interface_address_ipv6, NewIPPacketCallback new_ip_pkt_callback, std::string sni, std::string access_token, std::string expected_md5_fingerprint, CensorshipStrategy censorship_strategy, OnConnectedCallback on_connected_callback = nullptr, int thread_number = 4); virtual ~WebsocketClient(); WebsocketClient(const WebsocketClient&) = delete; WebsocketClient& operator=(const WebsocketClient&) = delete; WebsocketClient(WebsocketClient&&) = delete; WebsocketClient& operator=(WebsocketClient&&) = delete; void Run(); bool Stop(); bool Send(fptn::common::network::IPPacketPtr packet); bool IsStarted() const; protected: boost::asio::awaitable RunInternal(); boost::asio::awaitable RunReader(); boost::asio::awaitable RunSender(); boost::asio::awaitable Connect(); boost::asio::awaitable PerformFakeHandshake2(); void StartWatchdog(); std::vector GenerateHandshakePacket() const; private: const std::string kUrlWebSocket_ = "/fptn"; const std::size_t kMaxSizeOutQueue_ = 128; mutable std::mutex mutex_; std::atomic running_{false}; std::atomic was_stopped_{false}; std::atomic was_inited_{false}; std::atomic was_connected_{false}; boost::asio::io_context ioc_; boost::asio::ssl::context ctx_; boost::asio::ip::tcp::resolver resolver_; const CensorshipStrategy censorship_strategy_; // TCP -> obfuscator -> SSL -> WebSocket using tcp_stream_type = boost::beast::tcp_stream; using obfuscator_socket_type = obfuscator::TcpStream; using ssl_stream_type = boost::beast::ssl_stream; using websocket_type = boost::beast::websocket::stream; websocket_type ws_; boost::asio::strand strand_; boost::asio::steady_timer watchdog_timer_; boost::asio::experimental::concurrent_channel write_channel_; const fptn::common::network::IPv4Address server_ip_; const std::string server_port_str_; const fptn::common::network::IPv4Address tun_interface_address_ipv4_; const fptn::common::network::IPv6Address tun_interface_address_ipv6_; NewIPPacketCallback new_ip_pkt_callback_; const std::string sni_; const std::string access_token_; const std::string expected_md5_fingerprint_; OnConnectedCallback on_connected_callback_; boost::asio::cancellation_signal cancel_signal_; obfuscator::IObfuscatorSPtr obfuscator_; }; using WebsocketClientSPtr = std::shared_ptr; } // namespace fptn::protocol::https ================================================ FILE: src/fptn-protocol-lib/protobuf/protocol.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-protocol-lib/protobuf/protocol.h" #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ip_packet.h" #include "common/utils/utils.h" namespace fptn::protocol::protobuf { ProtoPayloadOpt GetProtoPayload(const boost::beast::flat_buffer& buffer) { const std::size_t total_size = buffer.size(); if (total_size == 0) { SPDLOG_ERROR("Failed to parse Protobuf message: empty buffer"); return std::nullopt; } const void* data_ptr = static_cast(buffer.cdata().data()); fptn::protocol::Message message; if (!message.ParseFromArray(data_ptr, static_cast(total_size))) { SPDLOG_ERROR("Failed to parse Protobuf message: parse error"); return std::nullopt; } if (message.protocol_version() != FPTN_PROTOBUF_PROTOCOL_VERSION) { SPDLOG_ERROR( "Unsupported protocol version: {}", message.protocol_version()); return std::nullopt; } switch (message.msg_type()) { case fptn::protocol::MSG_IP_PACKET: if (message.has_packet()) { const auto& payload = message.packet().payload(); std::vector result; result.reserve(payload.size()); result.assign(payload.begin(), payload.end()); return result; } SPDLOG_ERROR("Malformed IP packet: no packet field"); break; case fptn::protocol::MSG_ERROR: if (message.has_error()) { SPDLOG_ERROR("Message error: {}", message.error().error_msg()); } else { SPDLOG_ERROR("Malformed error message: no error field"); } break; default: SPDLOG_ERROR("Unknown message type"); } return std::nullopt; } ProtoPayloadOpt CreateProtoPayload(fptn::common::network::IPPacketPtr packet) { if (!packet) { SPDLOG_ERROR("Cannot create proto payload: packet is null"); return std::nullopt; } fptn::protocol::Message message; message.set_protocol_version(FPTN_PROTOBUF_PROTOCOL_VERSION); message.set_msg_type(fptn::protocol::MSG_IP_PACKET); const auto* raw_packet = packet->GetRawPacket(); if (!raw_packet) { SPDLOG_ERROR("Cannot create proto payload: raw packet is null"); return std::nullopt; } const void* data = raw_packet->getRawData(); const auto current_size = static_cast(raw_packet->getRawDataLen()); if (!data || current_size == 0) { SPDLOG_ERROR("Cannot create proto payload: invalid packet data"); return std::nullopt; } message.mutable_packet()->set_payload(data, current_size); #ifdef FPTN_ENABLE_PACKET_PADDING /** * Fill with random data to prevent issues related to TLS-inside-TLS. */ if (current_size < FPTN_IP_PACKET_MAX_SIZE) { constexpr std::size_t kMaxPaddingBytes = FPTN_IP_PACKET_MAX_SIZE; const std::size_t available_space = FPTN_IP_PACKET_MAX_SIZE - current_size; const std::size_t max_padding = std::min(kMaxPaddingBytes, available_space); if (max_padding > 0) { static thread_local std::mt19937 gen{std::random_device {}()}; std::uniform_int_distribution dist(0, max_padding); const std::size_t padding_size = dist(gen); if (padding_size > 0) { std::string padding_buffer; padding_buffer.resize(padding_size); fptn::common::utils::GenerateRandomBytes( reinterpret_cast(padding_buffer.data()), padding_size); message.mutable_packet()->set_padding_data( padding_buffer.data(), padding_size); } } } #endif const std::size_t estimated_size = message.ByteSizeLong(); if (estimated_size == 0) { SPDLOG_ERROR("Failed to serialize Message: estimated size is 0"); return std::nullopt; } std::vector serialized_data(estimated_size); if (!message.SerializeToArray( serialized_data.data(), static_cast(estimated_size))) { SPDLOG_ERROR("Failed to serialize Message."); return std::nullopt; } return serialized_data; } } // namespace fptn::protocol::protobuf ================================================ FILE: src/fptn-protocol-lib/protobuf/protocol.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include "common/network/ip_packet.h" #include "common/utils/utils.h" namespace fptn::protocol::protobuf { using ProtoPayloadOpt = std::optional>; ProtoPayloadOpt GetProtoPayload(const boost::beast::flat_buffer& buffer); ProtoPayloadOpt CreateProtoPayload(fptn::common::network::IPPacketPtr packet); } // namespace fptn::protocol::protobuf ================================================ FILE: src/fptn-protocol-lib/protobuf/protocol.proto ================================================ syntax = "proto3"; package fptn.protocol; option java_package = "org.fptn.protocol"; enum MessageType { MSG_ERROR = 0; MSG_IP_PACKET = 1; } enum ErrorType { ERROR_DEFAULT = 0; ERROR_WRONG_VERSION = 1; ERROR_SESSION_EXPIRED = 2; } message ErrorMessage { ErrorType error_type = 1; string error_msg = 2; } message IPPacket { bytes payload = 1; bytes padding_data = 2; } message Message { int32 protocol_version = 1; MessageType msg_type = 2; oneof message_content { ErrorMessage error = 3; IPPacket packet = 4; } } ================================================ FILE: src/fptn-protocol-lib/time/time_provider.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "fptn-protocol-lib/time/time_provider.h" #include #include #include #include // NOLINT(build/include_order) namespace fptn::time { TimeProvider::TimeProvider(NtpServers servers) : servers_(std::move(servers)), offset_seconds_(0) { SyncWithNtp(); } std::string TimeProvider::Rfc7231Date() { const std::uint32_t timestamp = NowTimestamp(); const auto now = static_cast(timestamp); char buf[128] = {0}; #ifdef _WIN32 std::tm tm{}; if (gmtime_s(&tm, &now) == 0) { if (std::strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm)) { return std::string(buf); } } #else std::tm tm{}; if (gmtime_r(&now, &tm)) { if (std::strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", &tm)) { return std::string(buf); } } #endif return {}; } std::int32_t TimeProvider::OffsetSeconds() const { return offset_seconds_.load(); } std::uint32_t TimeProvider::NowTimestamp() { const std::scoped_lock lock(mutex_); // mutex const auto now = std::chrono::steady_clock::now(); if (now - last_sync_time_.load() > kSyncInterval_) { Refresh(); } return static_cast( std::time(nullptr) + offset_seconds_.load()); } bool TimeProvider::SyncWithNtp() { const std::scoped_lock lock(mutex_); // mutex return Refresh(); } bool TimeProvider::Refresh() { for (const auto& [server, port] : servers_) { try { ntp::NTPClient ntp_client(server, port); if (const auto epoch_server_ms = ntp_client.request_time()) { const auto server_timestamp = static_cast(epoch_server_ms / 1000); const auto client_timestamp = static_cast(std::time(nullptr)); offset_seconds_ = static_cast(server_timestamp - client_timestamp); last_sync_time_ = std::chrono::steady_clock::now(); SPDLOG_INFO( "Successfully synchronized with NTP server '{}'. " "Server timestamp: {}, " "local timestamp: {}, " "calculated offset: {} seconds", server, server_timestamp, client_timestamp, offset_seconds_.load()); return true; } } catch (...) { SPDLOG_WARN("Unknown error during NTP request to {}:{}", server, port); } } SPDLOG_ERROR( "Failed to get time from NTP server. " "Using local system time without synchronization"); return false; } } // namespace fptn::time ================================================ FILE: src/fptn-protocol-lib/time/time_provider.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include namespace fptn::time { using NtpServers = std::vector>; class TimeProvider final { public: static TimeProvider* Instance() { static TimeProvider provider; return &provider; } std::string Rfc7231Date(); std::int32_t OffsetSeconds() const; std::uint32_t NowTimestamp(); bool SyncWithNtp(); protected: explicit TimeProvider( NtpServers servers = { {"pool.ntp.org", 123}, {"ru.pool.ntp.org", 123}, {"ntp.ix.ru", 123} }); bool Refresh(); private: const std::chrono::hours kSyncInterval_{1}; mutable std::mutex mutex_; const NtpServers servers_; std::atomic offset_seconds_; std::atomic last_sync_time_; }; } // namespace fptn::time ================================================ FILE: src/fptn-server/.gitignore ================================================ keys/ tmp/ ================================================ FILE: src/fptn-server/CMakeLists.txt ================================================ project(fptn-server) include_directories(${CMAKE_CURRENT_SOURCE_DIR}) include_directories("${CMAKE_CURRENT_BINARY_DIR}/depends/protobuf/fptn_protocol/") if(APPLE) set(CMAKE_CXX_STANDARD 20) else() add_definitions(-DBOOST_ASIO_HAS_CO_AWAIT) add_definitions(-DBOOST_ASIO_HAS_CO_SPAWN) add_definitions(-DBOOST_ASIO_HAS_COROUTINES) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argumen") endif() endif() find_package(Boost REQUIRED COMPONENTS asio coroutines future process locale) find_package(ZLIB REQUIRED) find_package(OpenSSL REQUIRED) find_package(argparse REQUIRED) find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(PcapPlusPlus REQUIRED) find_package(nlohmann_json REQUIRED) find_package(jwt-cpp REQUIRED) find_package(prometheus-cpp REQUIRED) # Include Boost directory include_directories(${Boost_INCLUDE_DIRS}) add_executable( "${PROJECT_NAME}" fptn-server.cpp nat/table.h nat/table.cpp client/session.h client/session.cpp network/virtual_interface.h network/virtual_interface.cpp traffic_shaper/leaky_bucket.h traffic_shaper/leaky_bucket.cpp routing/iptables.h routing/iptables.cpp web/listener/listener.h web/listener/listener.cpp web/session/session.h web/session/session.cpp web/server.h web/server.cpp web/handshake/handshake_cache_manager.h web/handshake/handshake_cache_manager.cpp filter/manager.h filter/manager.cpp filter/filters/base_filter.h filter/filters/antiscan/antiscan.h filter/filters/antiscan/antiscan.cpp filter/filters/bittorrent/bittorrent.h filter/filters/bittorrent/bittorrent.cpp vpn/manager.h vpn/manager.cpp statistic/metrics.h statistic/metrics.cpp config/command_line_config.cpp config/command_line_config.h user/user_manager.cpp user/user_manager.h) target_link_libraries( "${PROJECT_NAME}" PRIVATE ZLIB::ZLIB Boost::boost Boost::random Boost::filesystem Boost::process Boost::locale OpenSSL::SSL OpenSSL::Crypto argparse::argparse nlohmann_json::nlohmann_json jwt-cpp::jwt-cpp spdlog::spdlog fmt::fmt PcapPlusPlus::PcapPlusPlus tuntap++ fptn-protocol-lib_static prometheus-cpp::prometheus-cpp) ================================================ FILE: src/fptn-server/client/session.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "client/session.h" #include #include using fptn::client::Session; using fptn::common::network::IPv4Address; using fptn::common::network::IPv6Address; Session::Session(ClientID client_id, std::string user_name, IPv4Address client_ipv4, IPv4Address fake_client_ipv4, IPv6Address client_ipv6, IPv6Address fake_client_ipv6, fptn::traffic_shaper::LeakyBucketSPtr to_client, fptn::traffic_shaper::LeakyBucketSPtr from_client) : client_id_(client_id), user_name_(std::move(user_name)), client_ipv4_(std::move(client_ipv4)), fake_client_ipv4_(std::move(fake_client_ipv4)), client_ipv6_(std::move(client_ipv6)), fake_client_ipv6_(std::move(fake_client_ipv6)), to_client_(std::move(to_client)), from_client_(std::move(from_client)) {} const fptn::ClientID& Session::ClientId() const noexcept { return client_id_; } const std::string& Session::UserName() const noexcept { return user_name_; } const IPv4Address& Session::ClientIPv4() const noexcept { return client_ipv4_; } const IPv4Address& Session::FakeClientIPv4() const noexcept { return fake_client_ipv4_; } const IPv6Address& Session::ClientIPv6() const noexcept { return client_ipv6_; } const IPv6Address& Session::FakeClientIPv6() const noexcept { return fake_client_ipv6_; } fptn::traffic_shaper::LeakyBucketSPtr& Session::TrafficShaperToClient() noexcept { return to_client_; } fptn::traffic_shaper::LeakyBucketSPtr& Session::TrafficShaperFromClient() noexcept { return from_client_; } fptn::common::network::IPPacketPtr Session::ChangeIPAddressToClientIP( fptn::common::network::IPPacketPtr packet) noexcept { packet->SetClientId(client_id_); #ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP if (packet->IsIPv4()) { packet->SetDstIPv4Address(client_ipv4_.ToString()); } else if (packet->IsIPv6()) { packet->SetDstIPv6Address(client_ipv6_.ToString()); } #else if (packet->IsIPv4()) { packet->SetDstIPv4Address(client_ipv4_.Get()); } else if (packet->IsIPv6()) { packet->SetDstIPv6Address(client_ipv6_.Get()); } #endif packet->ComputeCalculateFields(); return packet; } fptn::common::network::IPPacketPtr Session::ChangeIPAddressToFakeIP( fptn::common::network::IPPacketPtr packet) noexcept { packet->SetClientId(client_id_); #ifdef FPTN_IP_ADDRESS_WITHOUT_PCAP if (packet->IsIPv4()) { packet->SetSrcIPv4Address(fake_client_ipv4_.ToString()); } else if (packet->IsIPv6()) { packet->SetSrcIPv6Address(fake_client_ipv6_.ToString()); } #else if (packet->IsIPv4()) { packet->SetSrcIPv4Address(fake_client_ipv4_.Get()); } else if (packet->IsIPv6()) { packet->SetSrcIPv6Address(fake_client_ipv6_.Get()); } #endif packet->ComputeCalculateFields(); return packet; } ================================================ FILE: src/fptn-server/client/session.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/client_id.h" #include "common/network/ip_address.h" #include "traffic_shaper/leaky_bucket.h" namespace fptn::client { using fptn::common::network::IPv4Address; using fptn::common::network::IPv6Address; class Session final { public: Session(ClientID client_id, std::string user_name, IPv4Address client_ipv4, IPv4Address fake_client_ipv4, IPv6Address client_ipv6, IPv6Address fake_client_ipv6, fptn::traffic_shaper::LeakyBucketSPtr to_client, fptn::traffic_shaper::LeakyBucketSPtr from_client); [[nodiscard]] const ClientID& ClientId() const noexcept; [[nodiscard]] const std::string& UserName() const noexcept; [[nodiscard]] const IPv4Address& ClientIPv4() const noexcept; [[nodiscard]] const IPv4Address& FakeClientIPv4() const noexcept; [[nodiscard]] const IPv6Address& ClientIPv6() const noexcept; [[nodiscard]] const IPv6Address& FakeClientIPv6() const noexcept; fptn::traffic_shaper::LeakyBucketSPtr& TrafficShaperToClient() noexcept; fptn::traffic_shaper::LeakyBucketSPtr& TrafficShaperFromClient() noexcept; fptn::common::network::IPPacketPtr ChangeIPAddressToClientIP( fptn::common::network::IPPacketPtr packet) noexcept; fptn::common::network::IPPacketPtr ChangeIPAddressToFakeIP( fptn::common::network::IPPacketPtr packet) noexcept; private: const ClientID client_id_; const std::string user_name_; const IPv4Address client_ipv4_; const IPv4Address fake_client_ipv4_; const IPv6Address client_ipv6_; const IPv6Address fake_client_ipv6_; fptn::traffic_shaper::LeakyBucketSPtr to_client_; fptn::traffic_shaper::LeakyBucketSPtr from_client_; }; using SessionSPtr = std::shared_ptr; } // namespace fptn::client ================================================ FILE: src/fptn-server/config/command_line_config.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "config/command_line_config.h" #include #include #include #include #include // NOLINT(build/include_order) #include "common/utils/utils.h" namespace { bool ParseBoolean(std::string value) noexcept { try { // С++17 // NOLINTNEXTLINE(modernize-use-ranges) std::transform(value.begin(), value.end(), value.begin(), [](unsigned char c) { return std::tolower(c); }); return value == "true"; } catch (...) { return false; } } } // namespace namespace fptn::config { using fptn::common::network::IPv4Address; using fptn::common::network::IPv6Address; CommandLineConfig::CommandLineConfig(int argc, char* argv[]) : argc_(argc), argv_(argv), args_("fptn-server", FPTN_VERSION) { // Required arguments args_.add_argument("--server-crt").required().help("Path to server.crt file"); args_.add_argument("--server-key").required().help("Path to server.key file"); args_.add_argument("--out-network-interface") .required() .help("Network out interface"); // Optional arguments args_.add_argument("--server-port") .default_value(443) .help("Port number") .scan<'i', int>(); args_.add_argument("--tun-interface-name") .default_value("tun0") .help("Network interface name"); /* IPv4 */ args_.add_argument("--tun-interface-ip") .default_value(FPTN_SERVER_DEFAULT_ADDRESS_IP4) .help("IP address of the virtual interface"); args_.add_argument("--tun-interface-network-address") .default_value(FPTN_SERVER_DEFAULT_NET_ADDRESS_IP4) .help("IP network of the virtual interface"); args_.add_argument("--tun-interface-network-mask") .default_value(16) .help("Network mask") .scan<'i', int>(); /* IPv6 */ args_.add_argument("--tun-interface-ipv6") .default_value(FPTN_SERVER_DEFAULT_ADDRESS_IP6) .help("IPv6 address of the virtual interface"); args_.add_argument("--tun-interface-network-ipv6-address") .default_value(FPTN_SERVER_DEFAULT_NET_ADDRESS_IP6) .help("IPv6 network address of the virtual interface"); args_.add_argument("--tun-interface-network-ipv6-mask") .default_value(64) .help("IPv6 network mask") .scan<'i', int>(); args_.add_argument("--userfile") .help("Path to users file (default: /etc/fptn/users.list)") .default_value("/etc/fptn/users.list"); // Packet filters args_.add_argument("--disable-bittorrent") .help( "Disable BitTorrent traffic filtering. Use this flag to disable " "filtering.") .default_value("false"); // Allow prometheus metric args_.add_argument("--prometheus-access-key") .help( "Secret key required for accessing Prometheus metrics. Set this to a " "secret value if metrics is needed.") .default_value(""); // Remote server auth args_.add_argument("--use-remote-server-auth") .help( "Enable remote server authentication. Set to 'true' to use a remote " "server for authentication.") .default_value("false"); args_.add_argument("--remote-server-auth-host") .help( "Specify the remote server's IP address or hostname for " "authentication.") .default_value("1.1.1.1"); args_.add_argument("--remote-server-auth-port") .help( "Specify the port number for the remote server authentication. Set " "to 0 to use the default port.") .default_value(443) .scan<'i', int>(); args_.add_argument("--max-active-sessions-per-user") .help("Maximum number of active sessions allowed per VPN user") .default_value(3) .scan<'i', int>(); // Probing args_.add_argument("--enable-detect-probing") .help( "Enable detection of non-FPTN clients or probing attempts during SSL " "handshake. ") .default_value("false"); args_.add_argument("--default-proxy-domain") .help("Default domain for proxying non-VPN clients.") .default_value(FPTN_DEFAULT_SNI); args_.add_argument("--allowed-sni-list") .help( "Comma-separated list of allowed SNI hostnames for non-VPN clients.\n" "Behavior logic:\n" " - List is empty (default): proxy all non-VPN traffic to " "--default-proxy-domain\n" " - List is NOT empty: use as whitelist:\n" " - Client SNI in list -> proxy to client's SNI\n" " - Client SNI not in list -> proxy to --default-proxy-domain") .default_value(""); // Prevent self-proxy args_.add_argument("--server-external-ips") .help( "Public IPv4 address of this VPN server. " "Prevents proxy loops when clients connect via IP. " "Example: --server-external-ip 1.2.3.4,5.6.7.8") .default_value(""); } bool CommandLineConfig::Parse() noexcept { // NOLINT(bugprone-exception-escape) try { args_.parse_args(argc_, argv_); return true; } catch (const std::runtime_error& err) { const std::string help = args_.help().str(); SPDLOG_ERROR("Argument parsing error: {}\n{}", err.what(), help); } catch (...) { SPDLOG_ERROR("Undefined parser error"); } return false; } std::string CommandLineConfig::ServerCrt() const { return args_.get("--server-crt"); } std::string CommandLineConfig::ServerKey() const { return args_.get("--server-key"); } std::string CommandLineConfig::OutNetworkInterface() const { return args_.get("--out-network-interface"); } int CommandLineConfig::ServerPort() const { return args_.get("--server-port"); } std::string CommandLineConfig::TunInterfaceName() const { return args_.get("--tun-interface-name"); } IPv4Address CommandLineConfig::TunInterfaceIPv4() const { return IPv4Address(args_.get("--tun-interface-ip")); } IPv4Address CommandLineConfig::TunInterfaceNetworkIPv4Address() const { return IPv4Address(args_.get("--tun-interface-network-address")); } int CommandLineConfig::TunInterfaceNetworkIPv4Mask() const { return args_.get("--tun-interface-network-mask"); } IPv6Address CommandLineConfig::TunInterfaceIPv6() const { return IPv6Address(args_.get("--tun-interface-ipv6")); } IPv6Address CommandLineConfig::TunInterfaceNetworkIPv6Address() const { return IPv6Address( args_.get("--tun-interface-network-ipv6-address")); } int CommandLineConfig::TunInterfaceNetworkIPv6Mask() const { return args_.get("--tun-interface-network-ipv6-mask"); } std::string CommandLineConfig::UserFile() const { return args_.get("--userfile"); } bool CommandLineConfig::DisableBittorrent() const { return ParseBoolean(args_.get("--disable-bittorrent")); } std::string CommandLineConfig::PrometheusAccessKey() const { return args_.get("--prometheus-access-key"); } bool CommandLineConfig::UseRemoteServerAuth() const { return ParseBoolean(args_.get("--use-remote-server-auth")); } std::string CommandLineConfig::RemoteServerAuthHost() const { return args_.get("--remote-server-auth-host"); } int CommandLineConfig::RemoteServerAuthPort() const { return args_.get("--remote-server-auth-port"); } bool CommandLineConfig::EnableDetectProbing() const { return ParseBoolean(args_.get("--enable-detect-probing")); } [[nodiscard]] std::string CommandLineConfig::DefaultProxyDomain() const { auto default_domain = args_.get("--default-proxy-domain"); if (default_domain.empty()) { return FPTN_DEFAULT_SNI; } return default_domain; } [[nodiscard]] std::vector CommandLineConfig::AllowedSniList() const { const auto allowed_sni = args_.get("--allowed-sni-list"); if (!allowed_sni.empty()) { return common::utils::SplitCommaSeparated( allowed_sni + "," + DefaultProxyDomain()); } return {}; } std::size_t CommandLineConfig::MaxActiveSessionsPerUser() const { return static_cast( args_.get("--max-active-sessions-per-user")); } [[nodiscard]] std::string CommandLineConfig::ServerExternalIPs() const { return args_.get("--server-external-ips"); } } // namespace fptn::config ================================================ FILE: src/fptn-server/config/command_line_config.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include // NOLINT(build/include_order) #include "common/network/ip_address.h" namespace fptn::config { using fptn::common::network::IPv4Address; using fptn::common::network::IPv6Address; class CommandLineConfig { public: explicit CommandLineConfig(int argc, char* argv[]); bool Parse() noexcept; public: /* options */ [[nodiscard]] std::string ServerCrt() const; [[nodiscard]] std::string ServerKey() const; [[nodiscard]] std::string OutNetworkInterface() const; [[nodiscard]] int ServerPort() const; [[nodiscard]] std::string TunInterfaceName() const; /* IPv4 */ [[nodiscard]] IPv4Address TunInterfaceIPv4() const; [[nodiscard]] IPv4Address TunInterfaceNetworkIPv4Address() const; [[nodiscard]] int TunInterfaceNetworkIPv4Mask() const; /* IPv6 */ [[nodiscard]] IPv6Address TunInterfaceIPv6() const; [[nodiscard]] IPv6Address TunInterfaceNetworkIPv6Address() const; [[nodiscard]] int TunInterfaceNetworkIPv6Mask() const; [[nodiscard]] std::string UserFile() const; [[nodiscard]] bool DisableBittorrent() const; [[nodiscard]] std::string PrometheusAccessKey() const; [[nodiscard]] bool UseRemoteServerAuth() const; [[nodiscard]] std::string RemoteServerAuthHost() const; [[nodiscard]] int RemoteServerAuthPort() const; [[nodiscard]] bool EnableDetectProbing() const; [[nodiscard]] std::string DefaultProxyDomain() const; [[nodiscard]] std::vector AllowedSniList() const; [[nodiscard]] std::size_t MaxActiveSessionsPerUser() const; [[nodiscard]] std::string ServerExternalIPs() const; private: int argc_; char** argv_; argparse::ArgumentParser args_; }; } // namespace fptn::config ================================================ FILE: src/fptn-server/filter/filters/antiscan/antiscan.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "filter/filters/antiscan/antiscan.h" #if defined(__APPLE__) || defined(__linux__) #include #elif _WIN32 #pragma warning(disable : 4996) #include #pragma warning(default : 4996) #endif #include using fptn::common::network::IPPacketPtr; using fptn::filter::AntiScan; AntiScan::AntiScan( /* IPv4 */ const fptn::common::network::IPv4Address& server_ipv4, const fptn::common::network::IPv4Address& server_ipv4_net, const int serverIPv4Mask, /* IPv6 */ const fptn::common::network::IPv6Address& server_ipv6, const fptn::common::network::IPv6Address& server_ipv6_net, const int serverIPv6Mask) : server_ipv4_(ntohl(server_ipv4.ToInt())), server_ipv4_net_(ntohl(server_ipv4_net.ToInt())), server_ipv4_mask_((0xFFFFFFFF << (32 - serverIPv4Mask))), server_ipv6_( fptn::common::network::ipv6::toUInt128(server_ipv6.ToString())), server_ipv6_net_( fptn::common::network::ipv6::toUInt128(server_ipv6_net.ToString())), server_ipv6_mask_( (boost::multiprecision::uint128_t(1) << (128 - serverIPv6Mask)) - 1) { } IPPacketPtr AntiScan::apply(IPPacketPtr packet) const { // Prevent sending requests to the VPN virtual network from the client static fptn::common::network::IPv4Address ipv4_broadcast("255.255.255.255"); static std::uint32_t ipv4_broadcast_int = ntohl(ipv4_broadcast.ToInt()); if (packet->IsIPv4()) { const std::uint32_t dst = ntohl(packet->IPv4Layer()->getDstIPv4Address().toInt()); const bool is_in_network = (dst & server_ipv4_mask_) == (server_ipv4_net_ & server_ipv4_mask_); if (server_ipv4_ == dst || (!is_in_network && ipv4_broadcast_int != dst)) { return packet; } } else if (packet->IsIPv6()) { const auto dst = fptn::common::network::ipv6::toUInt128( packet->IPv6Layer()->getDstIPv6Address()); const auto max_addr = server_ipv6_net_ | server_ipv6_mask_; const bool is_in_network = (server_ipv6_net_ <= dst && dst <= max_addr); if (server_ipv6_ == dst || !is_in_network) { return packet; } } return nullptr; } ================================================ FILE: src/fptn-server/filter/filters/antiscan/antiscan.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include "common/network/ip_address.h" namespace fptn::filter { /** * @class AntiScanFilter * @brief A filter class that blocks packets from IP addresses belonging to a * specific network. * * This class is used to block IP packets that match a given network address and * subnet mask. It compares the destination IP address of the packet against the * specified network and mask. If the packet belongs to the network, it is * blocked. * * @note This filter does not modify the packets that are not blocked. It simply * returns `nullptr` for blocked packets. */ class AntiScan : public BaseFilter { public: AntiScan( /* IPv4 */ const fptn::common::network::IPv4Address& server_ipv4, const fptn::common::network::IPv4Address& server_ipv4_net, const int serverIPv4Mask, /* IPv6 */ const fptn::common::network::IPv6Address& server_ipv6, const fptn::common::network::IPv6Address& server_ipv6_net, const int serverIPv6Mask); fptn::common::network::IPPacketPtr apply( fptn::common::network::IPPacketPtr packet) const override; ~AntiScan() override = default; private: /* IPv4 */ const std::uint32_t server_ipv4_; const std::uint32_t server_ipv4_net_; const int server_ipv4_mask_; /* IPv6 */ const boost::multiprecision::uint128_t server_ipv6_; const boost::multiprecision::uint128_t server_ipv6_net_; const boost::multiprecision::uint128_t server_ipv6_mask_; }; } // namespace fptn::filter ================================================ FILE: src/fptn-server/filter/filters/base_filter.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include "common/network/ip_packet.h" namespace fptn::filter { class BaseFilter { public: virtual fptn::common::network::IPPacketPtr apply( fptn::common::network::IPPacketPtr packet) const = 0; virtual ~BaseFilter() = default; }; using BaseFilterSPtr = std::shared_ptr; } // namespace fptn::filter ================================================ FILE: src/fptn-server/filter/filters/bittorrent/bittorrent.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "filter/filters/bittorrent/bittorrent.h" #include #include using fptn::common::network::IPPacketPtr; using fptn::filter::BitTorrent; static constexpr std::uint8_t kClassic[] = {0x13, 'B', 'i', 't', 'T', 'o', 'r', 'r', 'e', 'n', 't', ' ', 'p', 'r', 'o', 't', 'o', 'c', 'o', 'l'}; static constexpr std::uint8_t kExtensionProtocol[] = { 0x14, 'e', 'x', 't', 'e', 'n', 's', 'i', 'o', 'n'}; static constexpr std::uint8_t kDht[] = { 'd', '1', ':', 'a', 'd', '2', ':', 'i', 'd', '2'}; namespace { bool DetectBitTorrent(const std::uint8_t* payload, std::size_t payload_size) { if (!payload_size) { return false; } const std::uint8_t first_byte = payload[0]; // Classic Protocol if (first_byte == kClassic[0]) { constexpr std::size_t kClassicSignatureSize = sizeof(kClassic); return payload_size >= kClassicSignatureSize && std::memcmp(payload, kClassic, kClassicSignatureSize) == 0; } // Extension Protocol if (first_byte == kExtensionProtocol[0]) { constexpr std::size_t kExtProtocolSignSize = sizeof(kExtensionProtocol); return payload_size >= kExtProtocolSignSize && std::memcmp(payload, kExtensionProtocol, kExtProtocolSignSize) == 0; } // BT-DHT if (first_byte == kDht[0]) { constexpr std::size_t kDhtSignatureSize = sizeof(kDht); return payload_size >= kDhtSignatureSize && std::memcmp(payload, kDht, kDhtSignatureSize) == 0; } return false; } } // namespace IPPacketPtr BitTorrent::apply(IPPacketPtr packet) const { if (const auto* tcp = packet->Pkt().getLayerOfType()) { if (DetectBitTorrent(tcp->getLayerPayload(), tcp->getLayerPayloadSize())) { return nullptr; } } else if (const auto* udp = packet->Pkt().getLayerOfType()) { if (DetectBitTorrent(udp->getLayerPayload(), udp->getLayerPayloadSize())) { return nullptr; } } return packet; } ================================================ FILE: src/fptn-server/filter/filters/bittorrent/bittorrent.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include "filter/filters/base_filter.h" namespace fptn::filter { class BitTorrent : public BaseFilter { public: BitTorrent() = default; ~BitTorrent() override = default; fptn::common::network::IPPacketPtr apply( fptn::common::network::IPPacketPtr packet) const override; }; } // namespace fptn::filter ================================================ FILE: src/fptn-server/filter/manager.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "filter/manager.h" #include using fptn::common::network::IPPacketPtr; using fptn::filter::Manager; void Manager::Add(BaseFilterSPtr filter) noexcept { try { filters_.push_back(std::move(filter)); } catch (const std::bad_alloc& err) { SPDLOG_ERROR( "Memory allocation failed while adding filter: {}", err.what()); } catch (...) { SPDLOG_ERROR("An unknown exception occurred while adding a filter."); } } IPPacketPtr Manager::Apply(IPPacketPtr packet) const noexcept { for (const auto& filter : filters_) { packet = filter->apply(std::move(packet)); if (!packet) { return nullptr; // packet was filtered } } return packet; } ================================================ FILE: src/fptn-server/filter/manager.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include "common/network/ip_packet.h" #include "filters/base_filter.h" namespace fptn::filter { class Manager { public: Manager() = default; void Add(BaseFilterSPtr filter) noexcept; [[nodiscard]] fptn::common::network::IPPacketPtr Apply( fptn::common::network::IPPacketPtr packet) const noexcept; private: std::vector filters_; }; using ManagerSPtr = std::shared_ptr; } // namespace fptn::filter ================================================ FILE: src/fptn-server/fptn-server.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include #include #include #include // NOLINT(build/include_order) #include "common/jwt_token/token_manager.h" #include "common/logger/logger.h" #include "common/network/ip_address.h" #include "config/command_line_config.h" #include "filter/filters/antiscan/antiscan.h" #include "filter/filters/bittorrent/bittorrent.h" #include "filter/manager.h" #include "nat/table.h" #include "network/virtual_interface.h" #include "routing/iptables.h" #include "statistic/metrics.h" #include "user/user_manager.h" #include "vpn/manager.h" #include "web/server.h" namespace { void WaitForSignal() { boost::asio::io_context io_context; boost::asio::signal_set signals(io_context, SIGINT, SIGTERM /*,SIGQUIT*/); signals.async_wait([&](auto, auto) { io_context.stop(); }); io_context.run(); } } // namespace int main(int argc, char* argv[]) { #if defined(__linux__) || defined(__APPLE__) if (geteuid() != 0) { std::cerr << "You must be root to run this program." << std::endl; return EXIT_FAILURE; } #endif try { /* Check options */ fptn::config::CommandLineConfig config(argc, argv); if (!config.Parse()) { return EXIT_FAILURE; } if (!std::filesystem::exists(config.ServerCrt()) || !std::filesystem::exists(config.ServerKey())) { SPDLOG_ERROR("SSL certificate or key file does not exist!"); return EXIT_FAILURE; } /* Init logger */ if (fptn::logger::init("fptn-server")) { SPDLOG_INFO("Application started successfully."); } else { std::cerr << "Logger initialization failed. Exiting application." << std::endl; return EXIT_FAILURE; } /* Init iptables */ auto iptables = std::make_unique( config.OutNetworkInterface(), config.TunInterfaceName()); /* Init virtual network interface */ auto virtual_network_interface = std::make_unique( fptn::common::network::TunInterface::Config{ .name = config.TunInterfaceName(), .ipv4_addr = config.TunInterfaceIPv4(), .ipv4_netmask = config.TunInterfaceNetworkIPv4Mask(), .ipv6_addr = config.TunInterfaceIPv6(), .ipv6_netmask = config.TunInterfaceNetworkIPv6Mask(), }, std::move(iptables)); /* Init web server */ auto token_manager = std::make_shared( config.ServerCrt(), config.ServerKey()); /* Init user manager */ auto user_manager = std::make_shared( config.UserFile(), config.UseRemoteServerAuth(), config.RemoteServerAuthHost(), config.RemoteServerAuthPort()); /* Init NAT */ auto nat_table = std::make_shared( /* IPv4 */ config.TunInterfaceIPv4(), config.TunInterfaceNetworkIPv4Address(), config.TunInterfaceNetworkIPv4Mask(), /* IPv6 */ config.TunInterfaceIPv6(), config.TunInterfaceNetworkIPv6Address(), config.TunInterfaceNetworkIPv6Mask()); /* Init prometheus */ auto prometheus = std::make_shared(); /* Init webserver */ auto web_server = std::make_unique(config.ServerPort(), nat_table, user_manager, token_manager, prometheus, config.PrometheusAccessKey(), config.TunInterfaceIPv4(), config.TunInterfaceIPv6(), /* probing */ config.EnableDetectProbing(), config.DefaultProxyDomain(), config.AllowedSniList(), /* sessions */ config.MaxActiveSessionsPerUser(), /* External IPs */ config.ServerExternalIPs()); /* init packet filter */ auto filter_manager = std::make_shared(); if (config.DisableBittorrent()) { // block bittorrent traffic filter_manager->Add(std::make_shared()); } // Prevent sending requests to the VPN virtual network from the client filter_manager->Add(std::make_shared( /* IPv4 */ config.TunInterfaceIPv4(), config.TunInterfaceNetworkIPv4Address(), config.TunInterfaceNetworkIPv4Mask(), /* IPv6 */ config.TunInterfaceIPv6(), config.TunInterfaceNetworkIPv6Address(), config.TunInterfaceNetworkIPv6Mask())); SPDLOG_INFO( "\n--- Starting server---\n" "VERSION: {}\n" "NETWORK INTERFACE: {}\n" "VPN NETWORK IPv4: {}\n" "VPN NETWORK IPv6: {}\n" "VPN SERVER PORT: {}\n" "DETECT_PROBING: {}\n" "DEFAULT_PROXY_DOMAIN: {}\n" "ALLOWED_SNI_LIST: {}\n" "MAX_ACTIVE_SESSIONS_PER_USER: {}\n", FPTN_VERSION, // Network settings config.OutNetworkInterface(), config.TunInterfaceNetworkIPv4Address().ToString(), config.TunInterfaceNetworkIPv6Address().ToString(), config.ServerPort(), // Probing settings config.EnableDetectProbing() ? "YES" : "NO", config.DefaultProxyDomain(), fmt::format("[{}]", fmt::join(config.AllowedSniList(), ", ")), // max session config.MaxActiveSessionsPerUser()); // Init vpn manager fptn::vpn::Manager manager(std::move(web_server), std::move(virtual_network_interface), nat_table, filter_manager, prometheus); /* start/wait/stop */ manager.Start(); WaitForSignal(); manager.Stop(); return EXIT_SUCCESS; } catch (const std::exception& ex) { SPDLOG_ERROR("An error occurred: {}. Exiting...", ex.what()); } catch (...) { SPDLOG_ERROR("An unknown error occurred. Exiting..."); } return EXIT_FAILURE; } ================================================ FILE: src/fptn-server/nat/table.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "nat/table.h" #include #include #include #include // NOLINT(build/include_order) using fptn::nat::Table; Table::Table(fptn::common::network::IPv4Address tun_ipv4, fptn::common::network::IPv4Address tun_ipv4_network_address, std::uint32_t tun_network_ipv4_mask, fptn::common::network::IPv6Address tun_ipv6, fptn::common::network::IPv6Address tun_ipv6_network_address, std::uint32_t tun_network_ipv6_mask) : client_number_(0), tun_ipv4_(std::move(tun_ipv4)), tun_ipv4_network_address_(std::move(tun_ipv4_network_address)), tun_network_ipv4_mask_(tun_network_ipv4_mask), tun_ipv6_(std::move(tun_ipv6)), tun_ipv6_network_address_(std::move(tun_ipv6_network_address)), tun_network_ipv6_mask_(tun_network_ipv6_mask), ipv4_generator_(tun_ipv4_network_address_, tun_network_ipv4_mask_), ipv6_generator_(tun_ipv6_network_address_, tun_network_ipv6_mask_) {} fptn::client::SessionSPtr Table::CreateClientSession(ClientID client_id, const std::string& user_name, const fptn::common::network::IPv4Address& client_ipv4, const fptn::common::network::IPv6Address& client_ipv6, const fptn::traffic_shaper::LeakyBucketSPtr& to_client, const fptn::traffic_shaper::LeakyBucketSPtr& from_client) { const std::unique_lock lock(mutex_); // mutex if (!client_id_to_sessions_.contains(client_id)) { if (client_number_ >= ipv4_generator_.NumAvailableAddresses()) { /* || client_number_ >= client_ipv6.NumAvailableAddresses() */ SPDLOG_INFO("Client limit was exceeded"); return nullptr; } client_number_ += 1; try { const auto fake_ipv4 = GetUniqueIPv4Address(); const auto fake_ipv6 = GetUniqueIPv6Address(); auto session = std::make_shared(client_id, user_name, client_ipv4, fake_ipv4, client_ipv6, fake_ipv6, to_client, from_client); client_id_to_sessions_.insert({client_id, session}); ipv4_to_sessions_.insert( {fake_ipv4.ToInt(), session}); // ipv4 -> session ipv6_to_sessions_.insert( {fake_ipv6.ToString(), session}); // ipv6 -> session return session; } catch (const std::runtime_error& err) { SPDLOG_INFO("Client error: {}", err.what()); } catch (const std::exception& e) { SPDLOG_ERROR( "Standard exception while creating client session: {}", e.what()); } catch (...) { SPDLOG_ERROR("An unknown error occurred while creating client session."); } } return nullptr; } bool Table::DelClientSession(ClientID client_id) { fptn::client::SessionSPtr ipv4_session; fptn::client::SessionSPtr ipv6_session; { const std::unique_lock lock(mutex_); // mutex auto it = client_id_to_sessions_.find(client_id); if (it != client_id_to_sessions_.end()) { const IPv4INT ipv4_int = it->second->FakeClientIPv4().ToInt(); const std::string ipv6_str = it->second->FakeClientIPv6().ToString(); client_id_to_sessions_.erase(it); // delete ipv4 -> session { auto it_ipv4 = ipv4_to_sessions_.find(ipv4_int); if (it_ipv4 != ipv4_to_sessions_.end()) { ipv4_session = std::move(it_ipv4->second); ipv4_to_sessions_.erase(it_ipv4); } } // delete ipv6 -> session { auto it_ipv6 = ipv6_to_sessions_.find(ipv6_str); if (it_ipv6 != ipv6_to_sessions_.end()) { ipv6_session = std::move(it_ipv6->second); ipv6_to_sessions_.erase(it_ipv6); } } } } return ipv4_session != nullptr && ipv6_session != nullptr; } fptn::client::SessionSPtr Table::GetSessionByFakeIPv4( const fptn::common::network::IPv4Address& ip) noexcept { const std::unique_lock lock(mutex_); // mutex const auto it = ipv4_to_sessions_.find(ip.ToInt()); if (it != ipv4_to_sessions_.end()) { return it->second; } return nullptr; } fptn::client::SessionSPtr Table::GetSessionByFakeIPv6( const fptn::common::network::IPv6Address& ip) noexcept { const std::unique_lock lock(mutex_); // mutex auto it = ipv6_to_sessions_.find(ip.ToString()); if (it != ipv6_to_sessions_.end()) { return it->second; } return nullptr; } fptn::client::SessionSPtr Table::GetSessionByClientId( ClientID clientId) noexcept { const std::unique_lock lock(mutex_); // mutex auto it = client_id_to_sessions_.find(clientId); if (it != client_id_to_sessions_.end()) { return it->second; } return nullptr; } std::size_t Table::GetNumberActiveSessionByUsername( const std::string& username) { const std::unique_lock lock(mutex_); // mutex return std::count_if(ipv4_to_sessions_.begin(), ipv4_to_sessions_.end(), [&username]( const auto& pair) { return pair.second->UserName() == username; }); } fptn::common::network::IPv4Address Table::GetUniqueIPv4Address() { for (std::uint32_t i = 0; i < ipv4_generator_.NumAvailableAddresses(); i++) { const auto ip = ipv4_generator_.GetNextAddress(); if (ip != tun_ipv4_ && !ipv4_to_sessions_.contains(ip.ToInt())) { return ip; } } throw std::runtime_error("No available address"); } fptn::common::network::IPv6Address Table::GetUniqueIPv6Address() { for (int i = 0; i < ipv6_generator_.NumAvailableAddresses(); i++) { const auto ip = ipv6_generator_.GetNextAddress(); if (ip != tun_ipv6_ && !ipv6_to_sessions_.contains(ip.ToString())) { return ip; } } throw std::runtime_error("No available address"); } void Table::UpdateStatistic(const fptn::statistic::MetricsSPtr& prometheus) { const std::unique_lock lock(mutex_); // mutex prometheus->UpdateActiveSessions(client_id_to_sessions_.size()); for (const auto& client : client_id_to_sessions_) { auto client_id = client.first; const auto& session = client.second; prometheus->UpdateStatistics(client_id, session->UserName(), session->TrafficShaperToClient()->FullDataAmount(), session->TrafficShaperFromClient()->FullDataAmount()); } } ================================================ FILE: src/fptn-server/nat/table.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ip_address.h" #include "common/network/ipv4_generator.h" #include "common/network/ipv6_generator.h" #include "client/session.h" #include "statistic/metrics.h" #include "traffic_shaper/leaky_bucket.h" namespace fptn::nat { class Table final { using IPv4INT = std::uint32_t; public: Table(fptn::common::network::IPv4Address tun_ipv4, fptn::common::network::IPv4Address tun_ipv4_network_address, std::uint32_t tun_network_ipv4_mask, fptn::common::network::IPv6Address tun_ipv6, fptn::common::network::IPv6Address tun_ipv6_network_address, std::uint32_t tun_network_ipv6_mask); fptn::client::SessionSPtr CreateClientSession(ClientID client_id, const std::string& user_name, const fptn::common::network::IPv4Address& client_ipv4, const fptn::common::network::IPv6Address& client_ipv6, const fptn::traffic_shaper::LeakyBucketSPtr& to_client, const fptn::traffic_shaper::LeakyBucketSPtr& from_client); bool DelClientSession(ClientID client_id); void UpdateStatistic(const fptn::statistic::MetricsSPtr& prometheus); public: fptn::client::SessionSPtr GetSessionByFakeIPv4( const fptn::common::network::IPv4Address& ip) noexcept; fptn::client::SessionSPtr GetSessionByFakeIPv6( const fptn::common::network::IPv6Address& ip) noexcept; fptn::client::SessionSPtr GetSessionByClientId(ClientID clientId) noexcept; std::size_t GetNumberActiveSessionByUsername(const std::string& username); protected: fptn::common::network::IPv4Address GetUniqueIPv4Address(); fptn::common::network::IPv6Address GetUniqueIPv6Address(); private: mutable std::mutex mutex_; std::uint32_t client_number_; const fptn::common::network::IPv4Address tun_ipv4_; const fptn::common::network::IPv4Address tun_ipv4_network_address_; const std::uint32_t tun_network_ipv4_mask_; const fptn::common::network::IPv6Address tun_ipv6_; const fptn::common::network::IPv6Address tun_ipv6_network_address_; const std::uint32_t tun_network_ipv6_mask_; fptn::common::network::IPv4AddressGenerator ipv4_generator_; fptn::common::network::IPv6AddressGenerator ipv6_generator_; std::unordered_map ipv4_to_sessions_; // ipv4 std::unordered_map ipv6_to_sessions_; // ipv6 std::unordered_map client_id_to_sessions_; }; typedef std::shared_ptr TableSPtr; } // namespace fptn::nat ================================================ FILE: src/fptn-server/network/virtual_interface.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "network/virtual_interface.h" #include #include #include using fptn::common::network::TunInterface; using fptn::network::VirtualInterface; VirtualInterface::VirtualInterface( fptn::common::network::TunInterface::Config config, fptn::routing::RouteManagerPtr iptables) : running_(false), iptables_(std::move(iptables)) { // NOLINTNEXTLINE(modernize-avoid-bind) auto callback = std::bind( &VirtualInterface::IPPacketFromNetwork, this, std::placeholders::_1); virtual_network_interface_ = std::make_unique(std::move(config)); virtual_network_interface_->SetRecvIPPacketCallback(callback); } VirtualInterface::~VirtualInterface() { Stop(); } bool VirtualInterface::Check() noexcept { return thread_.joinable(); } bool VirtualInterface::Start() noexcept { running_ = true; virtual_network_interface_->Start(); thread_ = std::thread(&VirtualInterface::Run, this); return thread_.joinable(); } bool VirtualInterface::Stop() noexcept { running_ = false; virtual_network_interface_->Stop(); if (thread_.joinable()) { iptables_->Clean(); thread_.join(); return true; } return false; } void VirtualInterface::Send( fptn::common::network::IPPacketPtr packet) noexcept { try { to_network_.Push(std::move(packet)); } catch (const std::bad_alloc& err) { SPDLOG_ERROR( "Memory allocation failed while sending packet: {}", err.what()); } catch (...) { SPDLOG_ERROR("Unknown exception occurred while sending packet."); } } fptn::common::network::IPPacketPtr VirtualInterface::WaitForPacket( const std::chrono::milliseconds& duration) noexcept { return from_network_.WaitForPacket(duration); } void VirtualInterface::Run() noexcept { const auto timeout = std::chrono::milliseconds(300); iptables_->Apply(); // activate route while (running_) { auto packet = to_network_.WaitForPacket(timeout); if (packet != nullptr) { virtual_network_interface_->Send(std::move(packet)); } } } void VirtualInterface::IPPacketFromNetwork( fptn::common::network::IPPacketPtr packet) noexcept { from_network_.Push(std::move(packet)); } ================================================ FILE: src/fptn-server/network/virtual_interface.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include "common/data/channel.h" #include "common/network/ip_packet.h" #include "common/network/net_interface.h" #include "routing/iptables.h" namespace fptn::network { class VirtualInterface final { public: VirtualInterface(fptn::common::network::TunInterface::Config config, fptn::routing::RouteManagerPtr iptables); ~VirtualInterface(); bool Check() noexcept; bool Start() noexcept; bool Stop() noexcept; void Send(fptn::common::network::IPPacketPtr packet) noexcept; fptn::common::network::IPPacketPtr WaitForPacket( const std::chrono::milliseconds& duration) noexcept; protected: void Run() noexcept; void IPPacketFromNetwork(fptn::common::network::IPPacketPtr packet) noexcept; private: std::thread thread_; std::atomic running_; const fptn::routing::RouteManagerPtr iptables_; fptn::common::data::Channel to_network_; fptn::common::data::Channel from_network_; fptn::common::network::TunInterfacePtr virtual_network_interface_; }; using VirtualInterfacePtr = std::unique_ptr; } // namespace fptn::network ================================================ FILE: src/fptn-server/routing/iptables.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "routing/iptables.h" #ifdef __linux__ #include #include #include // NOLINT(build/include_order) #endif #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) using fptn::routing::RouteManager; namespace { bool RunCommand(const std::string& command) noexcept { try { boost::process::v1::child child(command, boost::process::v1::std_out > stdout, boost::process::v1::std_err > stderr); child.wait(); if (child.exit_code() == 0) { return true; } } catch (const std::exception& e) { SPDLOG_WARN("IPTables warning: {}", e.what()); } catch (...) { SPDLOG_WARN("Undefined warning"); } return false; } #ifdef __linux__ bool SetMaxFileDescriptors() { struct rlimit current_limits = {}; if (getrlimit(RLIMIT_NOFILE, ¤t_limits) != 0) { SPDLOG_WARN( "Failed to get current file descriptor limits: {}", strerror(errno)); return false; } SPDLOG_INFO("Current file descriptor limits: soft={}, hard={}", current_limits.rlim_cur, current_limits.rlim_max); struct rlimit new_limits = {}; new_limits.rlim_cur = current_limits.rlim_max; new_limits.rlim_max = current_limits.rlim_max; // Try to set the new limits if (setrlimit(RLIMIT_NOFILE, &new_limits) != 0) { SPDLOG_WARN("Failed to set file descriptor limits: {}", strerror(errno)); return false; } // Verify the changes were applied struct rlimit verified_limits; if (getrlimit(RLIMIT_NOFILE, &verified_limits) != 0) { SPDLOG_WARN( "Failed to verify new file descriptor limits: {}", strerror(errno)); return false; } SPDLOG_INFO("New file descriptor limits: soft={}, hard={}", verified_limits.rlim_cur, verified_limits.rlim_max); return true; } #endif } // namespace RouteManager::RouteManager( std::string out_net_interface_name, std::string tun_net_interface_name) : out_net_interface_name_(std::move(out_net_interface_name)), tun_net_interface_name_(std::move(tun_net_interface_name)), running_(false) {} RouteManager::~RouteManager() { try { Clean(); } catch (const std::exception& ex) { SPDLOG_ERROR("Exception in IPTables destructor: {}", ex.what()); } catch (...) { SPDLOG_ERROR("Unknown error in IPTables destructor"); } } bool RouteManager::Apply() { // NOLINT(bugprone-exception-escape) const std::unique_lock lock(mutex_); // mutex #ifdef __linux__ SetMaxFileDescriptors(); #endif running_ = true; #ifdef __linux__ const std::vector commands = {"systemctl start sysctl", "sysctl -w net.ipv4.ip_forward=1", "sysctl -w net.ipv4.conf.all.rp_filter=0", "sysctl -w net.ipv4.conf.default.rp_filter=0", "sysctl -w net.ipv6.conf.default.disable_ipv6=0", "sysctl -w net.ipv6.conf.all.disable_ipv6=0", "sysctl -w net.ipv6.conf.lo.disable_ipv6=0", "sysctl -w net.ipv6.conf.all.forwarding=1", "sysctl -p", /* IPv4 */ "iptables -P INPUT ACCEPT", "iptables -P FORWARD ACCEPT", "iptables -P OUTPUT ACCEPT", fmt::format("iptables -A FORWARD -i {} -o {} -j ACCEPT", tun_net_interface_name_, out_net_interface_name_), fmt::format("iptables -A FORWARD -i {} -o {} -j ACCEPT", out_net_interface_name_, tun_net_interface_name_), fmt::format("iptables -t nat -A POSTROUTING -o {} -j MASQUERADE", out_net_interface_name_), /* IPv6 */ "ip6tables -P INPUT ACCEPT", "ip6tables -P FORWARD ACCEPT", "ip6tables -P OUTPUT ACCEPT", fmt::format("ip6tables -A FORWARD -i {} -o {} -j ACCEPT", tun_net_interface_name_, out_net_interface_name_), fmt::format("ip6tables -A FORWARD -i {} -o {} -j ACCEPT", out_net_interface_name_, tun_net_interface_name_), fmt::format("ip6tables -t nat -A POSTROUTING -o {} -j MASQUERADE", out_net_interface_name_)}; #elif __APPLE__ // NEED CHECK const std::vector commands = { fmt::format("echo 'set skip on lo0' > /tmp/pf.conf"), fmt::format("echo 'block in all' >> /tmp/pf.conf"), fmt::format("echo 'pass out all' >> /tmp/pf.conf"), fmt::format("echo 'block in on {} from any to any' >> /tmp/pf.conf", out_net_interface_name_), "pfctl -ef /tmp/pf.conf", }; #else #error "Unsupported system!" #endif SPDLOG_INFO("=== Setting up routing ==="); try { for (const auto& cmd : commands) { if (!RunCommand(cmd)) { SPDLOG_ERROR("COMMAND ERROR: {}", cmd); } } } catch (const std::exception& e) { SPDLOG_ERROR("Exception occurred while applying routing: {}", e.what()); return false; } catch (...) { SPDLOG_ERROR("Unknown exception occurred while applying routing."); return false; } SPDLOG_INFO("=== Routing setup completed successfully ==="); return true; } // NOLINT(bugprone-exception-escape) bool RouteManager::Clean() { const std::unique_lock lock(mutex_); // mutex if (!running_) { return true; } #ifdef __linux__ const std::vector commands = { fmt::format("iptables -D FORWARD -i {} -o {} -j ACCEPT", tun_net_interface_name_, out_net_interface_name_), fmt::format("iptables -D FORWARD -i {} -o {} -j ACCEPT", out_net_interface_name_, tun_net_interface_name_), fmt::format("iptables -t nat -D POSTROUTING -o {} -j MASQUERADE", out_net_interface_name_)}; #elif __APPLE__ const std::vector commands = { "echo 'set skip on lo0' > /tmp/pf.conf", "echo 'block in all' >> /tmp/pf.conf", "echo 'pass out all' >> /tmp/pf.conf", fmt::format("echo 'pass in on {} from any to any' >> /tmp/pf.conf", out_net_interface_name_), fmt::format("echo 'pass in on {} from {} to any' >> /tmp/pf.conf", tun_net_interface_name_, tun_net_interface_name_), "pfctl -f /tmp/pf.conf", "pfctl -e"}; #else #error "Unsupported system!" #endif try { for (const auto& cmd : commands) { RunCommand(cmd); } } catch (const std::exception& e) { SPDLOG_ERROR("IPTables error: {}", e.what()); } catch (...) { SPDLOG_ERROR("Undefined error"); } running_ = false; return true; } ================================================ FILE: src/fptn-server/routing/iptables.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include namespace fptn::routing { class RouteManager final { public: RouteManager( std::string out_net_interface_name, std::string tun_net_interface_name); ~RouteManager(); bool Apply(); bool Clean(); private: mutable std::mutex mutex_; const std::string out_net_interface_name_; const std::string tun_net_interface_name_; bool running_; }; using RouteManagerPtr = std::unique_ptr; } // namespace fptn::routing ================================================ FILE: src/fptn-server/statistic/metrics.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "statistic/metrics.h" #include #include #include #include // NOLINT(build/include_order) using fptn::statistic::Metrics; Metrics::Metrics() : registry_(std::make_shared()) { active_sessions_ = &prometheus::BuildGauge() .Name("fptn_active_sessions") .Help("Number of active VPN sessions") .Register(*registry_) .Add({}); incoming_bytes_counter_ = &prometheus::BuildCounter() .Name("fptn_user_incoming_traffic_bytes") .Help("Incoming traffic for each user session in bytes") .Register(*registry_); outgoing_bytes_counter_ = &prometheus::BuildCounter() .Name("fptn_user_outgoing_traffic_bytes") .Help("Outgoing traffic for each user session in bytes") .Register(*registry_); } void Metrics::UpdateStatistics(fptn::ClientID session_id, const std::string& username, std::size_t incoming_bytes, std::size_t outgoing_bytes) noexcept { const std::scoped_lock lock(mutex_); // mutex auto& incoming_metric = incoming_bytes_counter_->Add( {{"username", username}, {"session_id", std::to_string(session_id)}}); auto& outgoing_metric = outgoing_bytes_counter_->Add( {{"username", username}, {"session_id", std::to_string(session_id)}}); incoming_metric.Increment( static_cast(incoming_bytes) - incoming_metric.Value()); outgoing_metric.Increment( static_cast(outgoing_bytes) - outgoing_metric.Value()); } void Metrics::UpdateActiveSessions(std::size_t count) noexcept { const std::scoped_lock lock(mutex_); // mutex active_sessions_->Set(static_cast(count)); } std::string Metrics::Collect() noexcept { const std::scoped_lock lock(mutex_); // mutex try { std::ostringstream result; prometheus::TextSerializer serializer; serializer.Serialize(result, registry_->Collect()); return result.str(); } catch (const std::exception& e) { return "Error collecting metrics: " + std::string(e.what()); } } ================================================ FILE: src/fptn-server/statistic/metrics.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/client_id.h" namespace fptn::statistic { class Metrics { public: Metrics(); void UpdateStatistics(fptn::ClientID session_id, const std::string& username, std::size_t incoming_bytes, std::size_t outgoing_bytes) noexcept; void UpdateActiveSessions(std::size_t count) noexcept; std::string Collect() noexcept; public: Metrics(const Metrics&) = delete; Metrics& operator=(const Metrics&) = delete; private: mutable std::mutex mutex_; std::unique_ptr exposer_; std::shared_ptr registry_; prometheus::Gauge* active_sessions_; prometheus::Family* incoming_bytes_counter_; prometheus::Family* outgoing_bytes_counter_; }; using MetricsSPtr = std::shared_ptr; } // namespace fptn::statistic ================================================ FILE: src/fptn-server/traffic_shaper/leaky_bucket.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "traffic_shaper/leaky_bucket.h" using fptn::traffic_shaper::LeakyBucket; LeakyBucket::LeakyBucket(std::size_t max_bites_per_second) : current_amount_(0), max_bytes_per_second_(max_bites_per_second / 8), last_leak_time_(std::chrono::steady_clock::now()), full_data_amount_(0) {} std::size_t LeakyBucket::FullDataAmount() const noexcept { return full_data_amount_; } bool LeakyBucket::CheckSpeedLimit(std::size_t packet_size) noexcept { const std::unique_lock lock(mutex_); // mutex const auto now = std::chrono::steady_clock::now(); const auto elapsed = std::chrono::duration_cast( now - last_leak_time_).count(); if (elapsed < 1000) { if (current_amount_ + packet_size < max_bytes_per_second_) { current_amount_ += packet_size; full_data_amount_ += packet_size; return true; } return false; } last_leak_time_ = now; current_amount_ = packet_size; return true; } ================================================ FILE: src/fptn-server/traffic_shaper/leaky_bucket.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include "common/network/ip_packet.h" namespace fptn::traffic_shaper { class LeakyBucket final { public: explicit LeakyBucket(std::size_t max_bites_per_second); bool CheckSpeedLimit(std::size_t packet_size) noexcept; std::size_t FullDataAmount() const noexcept; private: mutable std::mutex mutex_; std::size_t current_amount_; std::size_t max_bytes_per_second_; std::chrono::steady_clock::time_point last_leak_time_; std::size_t full_data_amount_; }; using LeakyBucketSPtr = std::shared_ptr; } // namespace fptn::traffic_shaper ================================================ FILE: src/fptn-server/user/user_manager.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "user/user_manager.h" #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) using fptn::user::UserManager; UserManager::UserManager(const std::string& userfile, bool use_remote_server, std::string remote_server_ip, int remote_server_port) : use_remote_server_(use_remote_server), remote_server_ip_(std::move(remote_server_ip)), remote_server_port_(remote_server_port) { if (use_remote_server_) { // remote user list http_api_client_ = std::make_unique(remote_server_ip_, remote_server_port, protocol::https::CensorshipStrategy::kSni); } else { // local user list common_manager_ = std::make_unique(userfile); } } bool UserManager::Login(const std::string& username, const std::string& password, int& bandwidth_bit) const { bandwidth_bit = 0; // reset if (use_remote_server_) { SPDLOG_INFO( "Login request to {}:{}", remote_server_ip_, remote_server_port_); const std::string request = fmt::format( R"({{ "username": "{}", "password": "{}" }})", username, password); const auto resp = http_api_client_->Post("/api/v1/login", request, "application/json"); if (resp.code == 200) { try { const auto msg = resp.Json(); if (msg.contains("access_token") && msg.contains("bandwidth_bit")) { bandwidth_bit = msg["bandwidth_bit"].get(); return true; } SPDLOG_INFO( "User manager error: Access token not found in the response. " "Check your connection"); } catch (const nlohmann::json::parse_error& e) { SPDLOG_INFO("User manager: Error parsing JSON response: {}\n{}", e.what(), resp.body); } } else { SPDLOG_INFO( "User manager: request failed or response is null. Code: {} Msg: {}", resp.code, resp.errmsg); } } else if (common_manager_->Authenticate(username, password)) { bandwidth_bit = common_manager_->GetUserBandwidthBit(username); return true; } return false; } ================================================ FILE: src/fptn-server/user/user_manager.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include "common/user/common_user_manager.h" #include "fptn-protocol-lib/https/api_client/api_client.h" namespace fptn::user { /** * @class Manager * @brief Handles user authentication and bandwidth retrieval for both local and * remote servers. */ class UserManager { public: /** * @brief Constructs a Manager object. * * @param userfile Path to the local user file for authentication. * @param use_remote_server Flag indicating whether to use remote server * authentication. * @param remote_server_ip IP address of the remote authentication server. * @param remote_server_port Port number of the remote authentication server. */ explicit UserManager(const std::string& userfile, bool use_remote_server, std::string remote_server_ip, int remote_server_port); /** * @brief Authenticates the user and retrieves the bandwidth limit. * * If the remote server is enabled, the credentials will be checked against * the remote server. Otherwise, local authentication will be used. * * @param username The username of the user. * @param password The password of the user. * @param bandwidth_bit The bandwidth limit in bits for the user. Set upon * successful login. * * @return `true` if the login is successful, `false` otherwise. */ bool Login(const std::string& username, const std::string& password, int& bandwidth_bit) const; private: /// Indicates whether to use remote server authentication. const bool use_remote_server_; /// IP address of the remote authentication server. const std::string remote_server_ip_; /// Port number of the remote authentication server. const int remote_server_port_; /// HTTP client for sending requests to the remote authentication server. fptn::protocol::https::HttpsClientPtr http_api_client_; /// Local user manager for handling authentication using a local user file. fptn::common::user::CommonUserManagerPtr common_manager_; }; using UserManagerSPtr = std::shared_ptr; } // namespace fptn::user ================================================ FILE: src/fptn-server/vpn/manager.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "vpn/manager.h" #include using fptn::vpn::Manager; Manager::Manager(fptn::web::ServerPtr web_server, fptn::network::VirtualInterfacePtr network_interface, fptn::nat::TableSPtr nat, fptn::filter::ManagerSPtr filter, fptn::statistic::MetricsSPtr prometheus, std::size_t thread_pool_size) : web_server_(std::move(web_server)), network_interface_(std::move(network_interface)), nat_(std::move(nat)), filter_(std::move(filter)), prometheus_(std::move(prometheus)), thread_pool_size_(thread_pool_size > 0 ? thread_pool_size : 1) { read_to_client_threads_.reserve(thread_pool_size_); read_from_client_threads_.reserve(thread_pool_size_); } Manager::~Manager() { Stop(); } bool Manager::Stop() { running_ = false; network_interface_->Stop(); web_server_->Stop(); for (auto& thread : read_to_client_threads_) { if (thread.joinable()) { thread.join(); } } read_to_client_threads_.clear(); for (auto& thread : read_from_client_threads_) { if (thread.joinable()) { thread.join(); } } read_from_client_threads_.clear(); if (collect_statistics_.joinable()) { collect_statistics_.join(); } return true; } bool Manager::Start() { running_ = true; web_server_->Start(); network_interface_->Start(); for (size_t i = 0; i < thread_pool_size_; ++i) { read_to_client_threads_.emplace_back(&Manager::RunToClient, this); } for (size_t i = 0; i < thread_pool_size_; ++i) { read_from_client_threads_.emplace_back(&Manager::RunFromClient, this); } collect_statistics_ = std::thread(&Manager::RunCollectStatistics, this); const bool collect_statistic_status = collect_statistics_.joinable(); return collect_statistic_status; } void Manager::RunToClient() const noexcept { constexpr std::chrono::milliseconds kTimeout{100}; while (running_) { auto packet = network_interface_->WaitForPacket(kTimeout); if (!packet || !running_) { continue; } if (!packet->IsIPv4() && !packet->IsIPv6()) { continue; } // get session using "fake" client address fptn::client::SessionSPtr session = nullptr; if (packet->IsIPv4()) { session = nat_->GetSessionByFakeIPv4(fptn::common::network::IPv4Address( packet->IPv4Layer()->getDstIPv4Address())); } else if (packet->IsIPv6()) { session = nat_->GetSessionByFakeIPv6(fptn::common::network::IPv6Address( packet->IPv6Layer()->getDstIPv6Address())); } if (!session || !running_) { continue; } // check shaper auto& shaper = session->TrafficShaperToClient(); if (shaper && !shaper->CheckSpeedLimit(packet->Size())) { continue; } // send if (running_) { web_server_->Send(session->ChangeIPAddressToClientIP(std::move(packet))); } } } void Manager::RunFromClient() const noexcept { constexpr std::chrono::milliseconds kTimeout{100}; while (running_) { auto packet = web_server_->WaitForPacket(kTimeout); if (!packet || !running_) { continue; } if (!packet->IsIPv4() && !packet->IsIPv6()) { continue; } // get session const auto session = nat_->GetSessionByClientId(packet->ClientId()); if (!session || !running_) { continue; } // check shaper auto shaper = session->TrafficShaperFromClient(); if (shaper && !shaper->CheckSpeedLimit(packet->Size())) { continue; } // filter packet = filter_->Apply(std::move(packet)); if (!packet || !running_) { continue; } // send if (running_) { network_interface_->Send( session->ChangeIPAddressToFakeIP(std::move(packet))); } } } void Manager::RunCollectStatistics() noexcept { const std::chrono::milliseconds timeout{300}; const std::chrono::seconds collect_interval{2}; std::chrono::steady_clock::time_point last_collection_time; while (running_) { auto now = std::chrono::steady_clock::now(); if (now - last_collection_time > collect_interval) { nat_->UpdateStatistic(prometheus_); last_collection_time = now; } std::this_thread::sleep_for(timeout); } } ================================================ FILE: src/fptn-server/vpn/manager.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include "filter/manager.h" #include "nat/table.h" #include "network/virtual_interface.h" #include "web/server.h" namespace fptn::vpn { class Manager final { public: Manager(fptn::web::ServerPtr web_server, fptn::network::VirtualInterfacePtr network_interface, fptn::nat::TableSPtr nat, fptn::filter::ManagerSPtr filter, fptn::statistic::MetricsSPtr prometheus, std::size_t thread_pool_size = 4); ~Manager(); bool Stop(); bool Start(); protected: void RunToClient() const noexcept; void RunFromClient() const noexcept; void RunCollectStatistics() noexcept; private: std::atomic running_ = false; const fptn::web::ServerPtr web_server_; const fptn::network::VirtualInterfacePtr network_interface_; const fptn::nat::TableSPtr nat_; const fptn::filter::ManagerSPtr filter_; const fptn::statistic::MetricsSPtr prometheus_; const std::size_t thread_pool_size_; std::vector read_to_client_threads_; std::vector read_from_client_threads_; std::thread collect_statistics_; }; using UserManagerSPtr = std::shared_ptr; } // namespace fptn::vpn ================================================ FILE: src/fptn-server/web/api/handle.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include "common/client_id.h" #include "common/network/ip_address.h" #include "common/network/ip_packet.h" namespace fptn::web { namespace http { using request = boost::beast::http::request; using response = boost::beast::http::response; } // namespace http using ApiHandle = std::function; using ApiHandleMap = std::unordered_map; inline std::string GetApiKey( const std::string& url, const std::string& method) { return method + " " + url; } inline void AddApiHandle(ApiHandleMap& m, const std::string& url, const std::string& method, const ApiHandle& handle) noexcept { const std::string key = GetApiKey(url, method); m[key] = handle; } inline ApiHandle GetApiHandle(const ApiHandleMap& m, const std::string& url, const std::string& method) noexcept { const std::string key = GetApiKey(url, method); const auto& it = m.find(key); if (it != m.end()) { return it->second; } return nullptr; } class Session; using WebSocketOpenConnectionCallback = std::function& session, const std::string& url, const std::string& access_token)>; using WebSocketNewIPPacketCallback = std::function; using WebSocketCloseConnectionCallback = std::function; } // namespace fptn::web ================================================ FILE: src/fptn-server/web/handshake/handshake_cache_manager.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "web/handshake/handshake_cache_manager.h" #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/resolv.h" #include "common/network/utils.h" namespace fptn::web { HandshakeCacheManager::HandshakeCacheManager(boost::asio::io_context& ioc, std::string default_domain, std::chrono::seconds cache_ttl) : ioc_(ioc), cache_ttl_(cache_ttl), default_domain_(std::move(default_domain)) {} // NOLINT HandshakeResponse HandshakeCacheManager::CheckCache( const std::string& cache_key) { const std::unique_lock lock(mutex_); // mutex const auto it = cache_.find(cache_key); if (it != cache_.end()) { const auto& entry = it->second; const auto now = std::chrono::steady_clock::now(); if (now - entry.timestamp < cache_ttl_) { return it->second.data; } cache_.erase(it); } return nullptr; } boost::asio::awaitable HandshakeCacheManager::GetHandshake( const std::string& sni, const std::uint8_t* buffer_ptr, std::size_t size, const std::chrono::seconds& timeout) { std::vector client_handshake_data( buffer_ptr, buffer_ptr + size); const auto cached_response = CheckCache(sni); if (cached_response) { SPDLOG_INFO("Cache hit for SNI: {} (TLS fingerprint size: {})", sni, cached_response->size()); co_return cached_response; } HandshakeResponse response = co_await FetchRealHandshake(sni, client_handshake_data, timeout); if (!response) { SPDLOG_WARN( "Failed to fetch handshake from original SNI: {}, trying default " "domain: {}", sni, default_domain_); // RET const auto default_cached_response = CheckCache(default_domain_); if (default_cached_response) { SPDLOG_INFO("Returning cached handshake for SNI: {} (using cache)", default_domain_); co_return default_cached_response; } // Get new response = co_await FetchRealHandshake( default_domain_, client_handshake_data, timeout); if (response) { const std::unique_lock lock(mutex_); // mutex cache_[default_domain_] = CacheEntry{ .data = response, .timestamp = std::chrono::steady_clock::now()}; SPDLOG_INFO("Successfully fetched handshake from default domain: {}", default_domain_); } } if (response) { const std::unique_lock lock(mutex_); // mutex cache_[sni] = CacheEntry{ .data = response, .timestamp = std::chrono::steady_clock::now()}; SPDLOG_INFO( "Cached handshake response for SNI: {}, " "size: {} bytes", sni, response->size()); co_return response; } SPDLOG_WARN("Failed to fetch handshake from real server for SNI: {}", sni); co_return HandshakeResponse(); } boost::asio::awaitable HandshakeCacheManager::FetchRealHandshake(const std::string& sni, const std::vector& client_handshake_data, const std::chrono::seconds& timeout) const { boost::asio::ip::tcp::socket target_socket(ioc_); constexpr std::size_t kMaxTotalSize = 65536; auto full_response = std::make_shared>(); full_response->reserve(kMaxTotalSize); try { // DNS resolution boost::asio::io_context resolve_ioc; const auto resolve_result = fptn::common::network::ResolveWithTimeout( resolve_ioc, sni, "443", timeout.count()); if (!resolve_result.success()) { SPDLOG_WARN("DNS failed for {}: {}", sni, resolve_result.error.message()); co_return nullptr; } // Connect to real server co_await boost::asio::async_connect( target_socket, resolve_result.results, boost::asio::use_awaitable); // Send client handshake co_await boost::asio::async_write(target_socket, boost::asio::buffer(client_handshake_data), boost::asio::use_awaitable); const auto server_response = co_await common::network::WaitForServerTlsHelloAsync( target_socket, timeout); if (server_response.has_value()) { *full_response = server_response.value(); } SPDLOG_INFO("Received {} bytes from {}", full_response->size(), sni); } catch (const std::exception& e) { SPDLOG_ERROR("Error fetching handshake from {}: {}", sni, e.what()); } boost::system::error_code close_ec; target_socket.close(close_ec); co_return full_response; } } // namespace fptn::web ================================================ FILE: src/fptn-server/web/handshake/handshake_cache_manager.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include namespace fptn::web { using HandshakeResponse = std::shared_ptr>; class HandshakeCacheManager final { public: explicit HandshakeCacheManager(boost::asio::io_context& ioc, std::string default_domain, std::chrono::seconds cache_ttl = std::chrono::seconds(1200)); boost::asio::awaitable GetHandshake(const std::string& sni, const std::uint8_t* buffer_ptr, std::size_t size, const std::chrono::seconds& timeout); protected: boost::asio::awaitable FetchRealHandshake( const std::string& sni, const std::vector& client_handshake_data, const std::chrono::seconds& timeout) const; HandshakeResponse CheckCache(const std::string& cache_key); private: struct CacheEntry { HandshakeResponse data; std::chrono::steady_clock::time_point timestamp; }; mutable std::mutex mutex_; boost::asio::io_context& ioc_; std::chrono::seconds cache_ttl_; const std::string default_domain_; std::unordered_map cache_; }; using HandshakeCacheManagerSPtr = std::shared_ptr; } // namespace fptn::web ================================================ FILE: src/fptn-server/web/listener/listener.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "web/listener/listener.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) using fptn::web::Listener; Listener::Listener(std::uint16_t port, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, boost::asio::io_context& ioc, fptn::common::jwt_token::TokenManagerSPtr token_manager, HandshakeCacheManagerSPtr handshake_cache_manager, std::string server_external_ips, WebSocketOpenConnectionCallback ws_open_callback, WebSocketNewIPPacketCallback ws_new_ippacket_callback, WebSocketCloseConnectionCallback ws_close_callback) : port_(port), enable_detect_probing_(enable_detect_probing), default_proxy_domain_(std::move(default_proxy_domain)), allowed_sni_list_(std::move(allowed_sni_list)), ioc_(ioc), ctx_(boost::asio::ssl::context::tlsv13_server), acceptor_(ioc_), token_manager_(std::move(token_manager)), handshake_cache_manager_(std::move(handshake_cache_manager)), server_external_ips_(std::move(server_external_ips)), ws_open_callback_(std::move(ws_open_callback)), ws_new_ippacket_callback_(std::move(ws_new_ippacket_callback)), ws_close_callback_(std::move(ws_close_callback)), endpoint_( boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port)), running_(false) { ctx_.set_options(boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::no_sslv3 | boost::asio::ssl::context::single_dh_use); ctx_.use_certificate_chain_file(token_manager_->ServerCrtPath()); ctx_.use_private_key_file( token_manager_->ServerKeyPath(), boost::asio::ssl::context::pem); ctx_.set_verify_mode(boost::asio::ssl::verify_none); } void Listener::AddApiHandle(const std::string& url, const std::string& method, const ApiHandle& handle) { fptn::web::AddApiHandle(api_handles_, url, method, handle); } boost::asio::awaitable Listener::Run() { constexpr int kBufferSize = 4 * 1024 * 1024; try { acceptor_.open(endpoint_.protocol()); acceptor_.set_option(boost::asio::ip::tcp::no_delay(true)); acceptor_.set_option(boost::asio::socket_base::reuse_address(true)); // Optimize socket buffers for high throughput (1 Gbit/s+) // Set send/recv buffers to 4MB (typical for high-speed WAN) acceptor_.set_option( boost::asio::socket_base::receive_buffer_size(kBufferSize)); acceptor_.set_option( boost::asio::socket_base::send_buffer_size(kBufferSize)); acceptor_.bind(endpoint_); acceptor_.listen(boost::asio::socket_base::max_listen_connections); } catch (boost::system::system_error& err) { SPDLOG_ERROR("Listener::prepare error: {}", err.what()); co_return; } running_ = true; boost::system::error_code ec; while (running_) { try { boost::asio::ip::tcp::socket socket(ioc_); co_await acceptor_.async_accept( socket, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (!ec) { // Propagate buffer settings to the accepted socket socket.set_option(boost::asio::ip::tcp::no_delay(true)); socket.set_option( boost::asio::socket_base::receive_buffer_size(kBufferSize)); socket.set_option( boost::asio::socket_base::send_buffer_size(kBufferSize)); auto session = std::make_shared(port_, // probing settings enable_detect_probing_, default_proxy_domain_, allowed_sni_list_, server_external_ips_, std::move(socket), ctx_, // handlers api_handles_, handshake_cache_manager_, ws_open_callback_, ws_new_ippacket_callback_, ws_close_callback_); // run coroutine boost::asio::co_spawn( ioc_, [session]() mutable -> boost::asio::awaitable { co_await session->Run(); }, boost::asio::detached); } else if (running_) { SPDLOG_ERROR("Error onAccept: {}", ec.message()); // Add delay after exception boost::asio::steady_timer timer(ioc_); timer.expires_after(std::chrono::milliseconds(300)); co_await timer.async_wait(boost::asio::use_awaitable); } } catch (boost::system::system_error& err) { if (running_) { SPDLOG_ERROR("Listener::run error: {}", err.what()); } co_return; } } co_return; } bool Listener::Stop() { try { running_ = false; acceptor_.close(); } catch (boost::system::system_error& err) { SPDLOG_ERROR("Listener::stop error: {}", err.what()); return false; } return true; } ================================================ FILE: src/fptn-server/web/listener/listener.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include "common/jwt_token/token_manager.h" #include "web/api/handle.h" #include "web/handshake/handshake_cache_manager.h" #include "web/session/session.h" namespace fptn::web { class Listener final { public: explicit Listener(std::uint16_t port, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, boost::asio::io_context& ioc, fptn::common::jwt_token::TokenManagerSPtr token_manager, HandshakeCacheManagerSPtr handshake_cache_manager, std::string server_external_ips, WebSocketOpenConnectionCallback ws_open_callback, WebSocketNewIPPacketCallback ws_new_ippacket_callback, WebSocketCloseConnectionCallback ws_close_callback); boost::asio::awaitable Run(); bool Stop(); void AddApiHandle(const std::string& url, const std::string& method, const ApiHandle& handle); protected: const std::uint16_t port_; const bool enable_detect_probing_; const std::string default_proxy_domain_; const std::vector allowed_sni_list_; boost::asio::io_context& ioc_; boost::asio::ssl::context ctx_; boost::asio::ip::tcp::acceptor acceptor_; const fptn::common::jwt_token::TokenManagerSPtr token_manager_; HandshakeCacheManagerSPtr handshake_cache_manager_; const std::string server_external_ips_; const WebSocketOpenConnectionCallback ws_open_callback_; const WebSocketNewIPPacketCallback ws_new_ippacket_callback_; const WebSocketCloseConnectionCallback ws_close_callback_; boost::asio::ip::tcp::endpoint endpoint_; std::atomic running_; ApiHandleMap api_handles_; }; using ListenerSPtr = std::shared_ptr; } // namespace fptn::web ================================================ FILE: src/fptn-server/web/server.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "web/server.h" #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "common/utils/utils.h" using fptn::web::Server; Server::Server(std::uint16_t port, const fptn::nat::TableSPtr& nat_table, const fptn::user::UserManagerSPtr& user_manager, const fptn::common::jwt_token::TokenManagerSPtr& token_manager, const fptn::statistic::MetricsSPtr& prometheus, const std::string& prometheus_access_key, fptn::common::network::IPv4Address dns_server_ipv4, fptn::common::network::IPv6Address dns_server_ipv6, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, std::size_t max_active_sessions_per_user, std::string server_external_ips, int thread_number) : running_(false), port_(port), nat_table_(nat_table), user_manager_(user_manager), token_manager_(token_manager), prometheus_(prometheus), prometheus_access_key_(prometheus_access_key), dns_server_ipv4_(std::move(dns_server_ipv4)), dns_server_ipv6_(std::move(dns_server_ipv6)), enable_detect_probing_(enable_detect_probing), default_proxy_domain_(std::move(default_proxy_domain)), allowed_sni_list_(std::move(allowed_sni_list)), max_active_sessions_per_user_(max_active_sessions_per_user), server_external_ips_(std::move(server_external_ips)), thread_number_(std::max(1, thread_number)), ioc_(thread_number), from_client_(std::make_unique()), to_client_(std::make_unique(ioc_)) { using std::placeholders::_1; using std::placeholders::_2; using std::placeholders::_3; using std::placeholders::_4; using std::placeholders::_5; using std::placeholders::_6; using std::placeholders::_7; handshake_cache_manager_ = std::make_shared(ioc_, default_proxy_domain_); listener_ = std::make_shared(port_, // proxy settings enable_detect_probing_, default_proxy_domain_, allowed_sni_list_, // ioc ioc_, token_manager, handshake_cache_manager_, server_external_ips_, // NOLINTNEXTLINE(modernize-avoid-bind) std::bind( &Server::HandleWsOpenConnection, this, _1, _2, _3, _4, _5, _6, _7), // NOLINTNEXTLINE(modernize-avoid-bind) std::bind(&Server::HandleWsNewIPPacket, this, _1), // NOLINTNEXTLINE(modernize-avoid-bind) std::bind(&Server::HandleWsCloseConnection, this, _1)); listener_->AddApiHandle( // NOLINTNEXTLINE(modernize-avoid-bind) kUrlDns_, "GET", std::bind(&Server::HandleApiDns, this, _1, _2)); listener_->AddApiHandle( // NOLINTNEXTLINE(modernize-avoid-bind) kUrlLogin_, "POST", std::bind(&Server::HandleApiLogin, this, _1, _2)); listener_->AddApiHandle(kUrlTestFileBin_, "GET", // NOLINTNEXTLINE(modernize-avoid-bind) std::bind(&Server::HandleApiTestFile, this, _1, _2)); if (!prometheus_access_key.empty()) { // Construct the URL for accessing Prometheus statistics by appending the // access key const std::string metrics = kUrlMetrics_ + '/' + prometheus_access_key; listener_->AddApiHandle( // NOLINTNEXTLINE(modernize-avoid-bind) metrics, "GET", std::bind(&Server::HandleApiMetrics, this, _1, _2)); } } Server::~Server() { Stop(); } bool Server::Start() { running_ = true; try { // run listener boost::asio::co_spawn( ioc_, [this]() -> boost::asio::awaitable { co_await listener_->Run(); }, boost::asio::detached); // run senders boost::asio::co_spawn( ioc_, [this]() -> boost::asio::awaitable { co_await RunSender(); }, boost::asio::detached); // run threads ioc_threads_.reserve(thread_number_); for (std::size_t i = 0; i < thread_number_; ++i) { ioc_threads_.emplace_back([this]() { ioc_.run(); }); } } catch (boost::system::system_error& err) { SPDLOG_ERROR("Server::start error: {}", err.what()); running_ = false; return false; } return true; } boost::asio::awaitable Server::RunSender() { constexpr std::chrono::milliseconds kTimeout{10}; while (running_) { auto optpacket = co_await to_client_->WaitForPacketAsync(kTimeout); if (optpacket && running_) { SessionSPtr session; { const std::unique_lock lock(mutex_); // mutex auto it = sessions_.find(optpacket->get()->ClientId()); if (it != sessions_.end()) { session = it->second; } } if (session) { const bool status = co_await session->Send(std::move(*optpacket)); if (!status) { session->Close(); } } } } co_return; } bool Server::Stop() { if (running_) { running_ = false; SPDLOG_INFO("Server stop"); listener_->Stop(); for (auto& session : sessions_) { if (session.second) { session.second->Close(); } } sessions_.clear(); if (!ioc_.stopped()) { ioc_.stop(); } for (auto& th : ioc_threads_) { if (th.joinable()) { th.join(); } } return true; } return false; } void Server::Send(fptn::common::network::IPPacketPtr packet) { to_client_->Push(std::move(packet)); } fptn::common::network::IPPacketPtr Server::WaitForPacket( const std::chrono::milliseconds& duration) { return from_client_->WaitForPacket(duration); } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) int Server::HandleApiDns(const http::request& req, http::response& resp) { (void)req; resp.body() = fmt::format(R"({{"dns": "{}", "dns_ipv6": "{}" }})", dns_server_ipv4_.ToString(), dns_server_ipv6_.ToString()); resp.set(boost::beast::http::field::content_type, "application/json; charset=utf-8"); return 200; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) int Server::HandleApiLogin(const http::request& req, http::response& resp) { try { const auto request = nlohmann::json::parse(req.body()); const auto username = request.at("username").get(); const auto password = request.at("password").get(); int bandwidth_bit = 0; if (user_manager_->Login(username, password, bandwidth_bit)) { SPDLOG_INFO("Successful login for user {}", username); const auto tokens = token_manager_->Generate(username, bandwidth_bit); resp.body() = fmt::format( R"({{ "access_token": "{}", "refresh_token": "{}", "bandwidth_bit": {} }})", tokens.first, tokens.second, std::to_string(bandwidth_bit)); return 200; } SPDLOG_WARN("Wrong password for user: \"{}\" ", username); resp.body() = R"({"status": "error", "message": "Invalid login or password."})"; } catch (const nlohmann::json::exception& e) { SPDLOG_ERROR("HTTP JSON AUTH ERROR: {}", e.what()); resp.body() = R"({"status": "error", "message": "Invalid JSON format."})"; return 400; } catch (const std::exception& e) { SPDLOG_ERROR("HTTP AUTH ERROR: {}", e.what()); resp.body() = R"({"status": "error", "message": "An unexpected error occurred."})"; return 500; } catch (...) { SPDLOG_ERROR("UNDEFINED SERVER ERROR"); resp.body() = R"({"status": "error", "message": "Undefined server error"})"; return 501; } return 401; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) int Server::HandleApiMetrics(const http::request& req, http::response& resp) { (void)req; resp.set(boost::beast::http::field::content_type, "text/html; charset=utf-8"); resp.body() = prometheus_->Collect(); return 200; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) int Server::HandleApiTestFile(const http::request& req, http::response& resp) { (void)req; static const std::string kData = fptn::common::utils::GenerateRandomString(100 * 1024); // 100KB resp.set(boost::beast::http::field::content_type, "application/octet-stream"); resp.body() = kData; return 200; } bool Server::HandleWsOpenConnection(fptn::ClientID client_id, const fptn::common::network::IPv4Address& client_ip, const fptn::common::network::IPv4Address& client_vpn_ipv4, const fptn::common::network::IPv6Address& client_vpn_ipv6, const SessionSPtr& session, const std::string& url, const std::string& access_token) { if (!running_) { SPDLOG_ERROR("Server is not running"); return false; } if (url != kUrlWebSocket_) { SPDLOG_ERROR("Wrong URL \"{}\"", url); return false; } if (!client_vpn_ipv4.IsEmpty() && !client_vpn_ipv6.IsEmpty()) { std::string username; std::size_t bandwidth_bites_seconds = 0; if (token_manager_->Validate( access_token, username, bandwidth_bites_seconds)) { const std::unique_lock lock(mutex_); // mutex const auto active_sessions = nat_table_->GetNumberActiveSessionByUsername(username); if (active_sessions > max_active_sessions_per_user_) { SPDLOG_WARN( "Session limit exceeded for user '{}': {} active (limit: {})", username, active_sessions, max_active_sessions_per_user_); return false; } if (sessions_.contains(client_id)) { SPDLOG_WARN("Client with same ID already exists!"); } else { const auto shaper_to_client = std::make_shared( bandwidth_bites_seconds); const auto shaper_from_client = std::make_shared( bandwidth_bites_seconds); const auto nat_session = nat_table_->CreateClientSession(client_id, username, client_vpn_ipv4, client_vpn_ipv6, shaper_to_client, shaper_from_client); SPDLOG_INFO( "NEW SESSION! Username={} client_id={} Bandwidth={} ClientIP={} " "VirtualIPv4={} VirtualIPv6={}", username, client_id, bandwidth_bites_seconds, client_ip.ToString(), nat_session->FakeClientIPv4().ToString(), nat_session->FakeClientIPv6().ToString()); if (running_) { sessions_.insert({client_id, session}); return true; } return false; } } else { SPDLOG_WARN("WRONG TOKEN: {}", username); } } else { SPDLOG_WARN("Wrong ClientIP or ClientIPv6"); } return false; } void Server::HandleWsNewIPPacket( fptn::common::network::IPPacketPtr packet) noexcept { from_client_->Push(std::move(packet)); } void Server::HandleWsCloseConnection(fptn::ClientID client_id) noexcept { SessionSPtr session; if (running_) { const std::unique_lock lock(mutex_); // mutex auto it = sessions_.find(client_id); if (it != sessions_.end()) { session = std::move(it->second); sessions_.erase(it); } } if (session != nullptr) { session->Close(); SPDLOG_INFO("Session closed and removed (client_id={})", client_id); } else { SPDLOG_WARN( "Attempted to close non-existent session (client_id={})", client_id); } nat_table_->DelClientSession(client_id); } ================================================ FILE: src/fptn-server/web/server.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include "common/data/channel.h" #include "common/data/channel_async.h" #include "common/jwt_token/token_manager.h" #include "common/network/ip_packet.h" #include "handshake/handshake_cache_manager.h" #include "listener/listener.h" #include "nat/table.h" #include "user/user_manager.h" namespace fptn::web { class Server final { public: Server(std::uint16_t port, const fptn::nat::TableSPtr& nat_table, const fptn::user::UserManagerSPtr& user_manager, const fptn::common::jwt_token::TokenManagerSPtr& token_manager, const fptn::statistic::MetricsSPtr& prometheus, const std::string& prometheus_access_key, fptn::common::network::IPv4Address dns_server_ipv4, fptn::common::network::IPv6Address dns_server_ipv6, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, std::size_t max_active_sessions_per_user, std::string server_external_ips, int thread_number = 8); ~Server(); bool Start(); bool Stop(); void Send(fptn::common::network::IPPacketPtr packet); fptn::common::network::IPPacketPtr WaitForPacket( const std::chrono::milliseconds& duration); protected: boost::asio::awaitable RunSender(); protected: // http int HandleApiDns(const http::request& req, http::response& resp); int HandleApiLogin(const http::request& req, http::response& resp); int HandleApiMetrics(const http::request& req, http::response& resp); int HandleApiTestFile(const http::request& req, http::response& resp); protected: // websocket bool HandleWsOpenConnection(fptn::ClientID client_id, const fptn::common::network::IPv4Address& client_ip, const fptn::common::network::IPv4Address& client_vpn_ipv4, const fptn::common::network::IPv6Address& client_vpn_ipv6, const SessionSPtr& session, const std::string& url, const std::string& access_token); void HandleWsNewIPPacket(fptn::common::network::IPPacketPtr packet) noexcept; void HandleWsCloseConnection(fptn::ClientID client_id) noexcept; private: const std::string kUrlDns_ = "/api/v1/dns"; const std::string kUrlLogin_ = "/api/v1/login"; const std::string kUrlMetrics_ = "/api/v1/metrics"; const std::string kUrlTestFileBin_ = "/api/v1/test/file.bin"; const std::string kUrlWebSocket_ = "/fptn"; mutable std::mutex mutex_; std::atomic running_; const std::uint16_t port_; const fptn::nat::TableSPtr& nat_table_; const fptn::user::UserManagerSPtr& user_manager_; const fptn::common::jwt_token::TokenManagerSPtr token_manager_; const fptn::statistic::MetricsSPtr& prometheus_; const std::string prometheus_access_key_; const fptn::common::network::IPv4Address dns_server_ipv4_; const fptn::common::network::IPv6Address dns_server_ipv6_; const bool enable_detect_probing_; const std::string default_proxy_domain_; const std::vector allowed_sni_list_; const std::size_t max_active_sessions_per_user_; const std::string server_external_ips_; const std::size_t thread_number_; boost::asio::io_context ioc_; fptn::common::data::ChannelPtr from_client_; fptn::common::data::ChannelAsyncPtr to_client_; ListenerSPtr listener_; HandshakeCacheManagerSPtr handshake_cache_manager_; std::vector ioc_threads_; std::unordered_map sessions_; }; using ServerPtr = std::unique_ptr; } // namespace fptn::web ================================================ FILE: src/fptn-server/web/session/session.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include "web/session/session.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/resolv.h" #include "common/network/utils.h" #include "fptn-protocol-lib/https/obfuscator/methods/detector.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls/tls_obfuscator.h" #include "fptn-protocol-lib/https/obfuscator/methods/tls2/tls_obfuscator2.h" #include "fptn-protocol-lib/https/utils/tls/tls.h" #include "fptn-protocol-lib/protobuf/protocol.h" #include "fptn-protocol-lib/time/time_provider.h" namespace { std::atomic client_id_counter = 0; std::vector GetServerIpAddresses( const std::string& server_external_ips) { static std::mutex ip_mutex; static std::vector server_ips; const std::scoped_lock lock(ip_mutex); // mutex if (server_ips.empty()) { server_ips = fptn::common::network::GetServerIpAddresses(); if (!server_external_ips.empty()) { const auto external_ips = fptn::common::utils::SplitCommaSeparated(server_external_ips); std::ranges::copy_if(external_ips, std::back_inserter(server_ips), [](const std::string& ip) { return fptn::common::network::IsIpAddress(ip); }); } } return server_ips; } void SetSocketTimeouts(boost::asio::ip::tcp::socket& socket, int timeout_sec) { timeval tv = {}; tv.tv_sec = timeout_sec; tv.tv_usec = 0; const int socket_fd = socket.native_handle(); ::setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&tv), sizeof(tv)); ::setsockopt(socket_fd, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&tv), sizeof(tv)); } } // namespace namespace fptn::web { Session::Session(std::uint16_t port, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, std::string server_external_ips, boost::asio::ip::tcp::socket&& socket, boost::asio::ssl::context& ctx, const ApiHandleMap& api_handles, HandshakeCacheManagerSPtr handshake_cache_manager, WebSocketOpenConnectionCallback ws_open_callback, WebSocketNewIPPacketCallback ws_new_ippacket_callback, WebSocketCloseConnectionCallback ws_close_callback) : port_(port), enable_detect_probing_(enable_detect_probing), default_proxy_domain_(std::move(default_proxy_domain)), allowed_sni_list_(std::move(allowed_sni_list)), server_external_ips_(std::move(server_external_ips)), ws_(ssl_stream_type( obfuscator_socket_type(tcp_stream_type(std::move(socket))), ctx)), strand_(boost::asio::make_strand(ws_.get_executor())), write_channel_(strand_, 8192), api_handles_(api_handles), handshake_cache_manager_(std::move(handshake_cache_manager)), ws_open_callback_(std::move(ws_open_callback)), ws_new_ippacket_callback_(std::move(ws_new_ippacket_callback)), ws_close_callback_(std::move(ws_close_callback)), running_(false), init_completed_(false), ws_session_was_opened_(false), full_queue_(false) { try { client_id_ = ++client_id_counter; boost::beast::get_lowest_layer(ws_).socket().set_option( boost::asio::ip::tcp::no_delay(true)); ws_.text(false); ws_.binary(true); ws_.auto_fragment(false); ws_.read_message_max(4 * 1024 * 1024); // 4MB ws_.set_option(boost::beast::websocket::stream_base::timeout::suggested( boost::beast::role_type::server)); ws_.set_option(boost::beast::websocket::stream_base::timeout{ .handshake_timeout = std::chrono::seconds(60), .idle_timeout = std::chrono::seconds(60), .keep_alive_pings = true}); boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(15)); init_completed_ = true; } catch (const boost::system::system_error& err) { SPDLOG_ERROR("Session::init failed (client_id={}): {} [{}]", client_id_, err.what(), err.code().message()); } catch (const std::exception& e) { SPDLOG_ERROR("Session::init unexpected exception (client_id={}): {}", client_id_, e.what()); } catch (...) { SPDLOG_ERROR( "Session::init unknown fatal error (client_id={})", client_id_); } } Session::~Session() { Close(); } boost::asio::awaitable Session::Run() { boost::system::error_code ec; running_ = true; if (!init_completed_) { SPDLOG_ERROR("Session not initialized. Closing connection (client_id={})", client_id_); Close(); co_return; } // Setup traffic obfuscator auto obfuscator_opt = co_await DetectObfuscator(); if (!obfuscator_opt.has_value()) { SPDLOG_ERROR("Failed to initialize traffic obfuscator"); Close(); co_return; } ws_.next_layer().next_layer().set_obfuscator(obfuscator_opt.value()); // Detect probing (only for null obfuscator) if (enable_detect_probing_ && obfuscator_opt.value() == nullptr) { const auto probing_result = co_await DetectProbing(); if (probing_result.should_close) { SPDLOG_WARN( "Connection rejected during probing (client_id={})", client_id_); Close(); co_return; } if (probing_result.is_probing) { SPDLOG_WARN( "Probing detected. Redirecting to proxy (client_id={}, SNI={}, " "port={})", client_id_, probing_result.sni, port_); co_await HandleProxy(probing_result.sni, port_); Close(); co_return; } SPDLOG_INFO("SESSION ID correct. Continue setup connection (client_id={})", client_id_); } // Check for Reality Mode handshake (only when no obfuscator is detected) if (obfuscator_opt.value() == nullptr) { const auto result = co_await IsRealityHandshake(); if (result.should_close) { SPDLOG_WARN( "Reality Mode handshake check failed (client_id={})", client_id_); Close(); co_return; } // Process Reality Mode connection if detected if (result.is_reality_mode || result.is_reality_mode2) { if (result.is_reality_mode) { SPDLOG_INFO("Processing Reality Mode connection sni={} (client_id={}) ", result.sni, client_id_); } if (result.is_reality_mode2) { SPDLOG_INFO( "Processing Reality Mode2 connection sni={} (client_id={}) ", result.sni, client_id_); } // Prevent recursive proxy attempts for Reality Mode const auto self_proxy = co_await IsSniSelfProxyAttempt(result.sni); if (self_proxy) { co_await HandleProxy(default_proxy_domain_, port_); Close(); co_return; } bool reality_success = false; if (result.is_reality_mode) { // DEPRECATED reality_success = co_await PerformFakeHandshake(result.sni); // For Reality Mode we use TLS obfuscator after fake handshake // This provides additional encryption layer for the real connection ws_.next_layer().next_layer().set_obfuscator( std::make_shared()); } else { reality_success = co_await PerformFakeHandshake2(result.sni); // For Reality Mode we use TLS obfuscator after fake handshake // This provides additional encryption layer for the real connection ws_.next_layer().next_layer().set_obfuscator( std::make_shared()); } if (!reality_success) { SPDLOG_WARN("Reality mode handshake failed (client_id={})", client_id_); Close(); co_return; } } } // SSL handshake boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(10)); co_await ws_.next_layer().async_handshake( boost::asio::ssl::stream_base::server, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { SPDLOG_WARN("TLS-Handshake error (client_id={})", client_id_); Close(); co_return; } // Reset obfuscator after TLS handshake ws_.next_layer().next_layer().set_obfuscator(nullptr); // Process request (HTTP or WebSocket) const bool status = co_await ProcessRequest(); if (status) { auto self = shared_from_this(); boost::asio::co_spawn( strand_, [self]() mutable -> boost::asio::awaitable { return self->RunReader(); }, boost::asio::detached); boost::asio::co_spawn( strand_, [self]() mutable -> boost::asio::awaitable { return self->RunSender(); }, boost::asio::detached); } else { Close(); } co_return; } boost::asio::awaitable Session::DetectProbing() { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); // Peek data without consuming it from the socket buffer!!! // This allows inspection without affecting subsequent reads!!! std::array buffer; // Set socket timeout for peek boost::system::error_code ec; const std::size_t bytes_read = co_await tcp_socket.async_receive( boost::asio::buffer(buffer), boost::asio::socket_base::message_peek, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec || !bytes_read) { SPDLOG_ERROR("Peeked zero bytes from socket (client_id={})", client_id_); co_return ProbingResult{.is_probing = true, .sni = default_proxy_domain_, .should_close = true}; } // Check ssl if (!pcpp::SSLLayer::IsSSLMessage( 0, 0, buffer.data(), buffer.size(), true)) { SPDLOG_ERROR( "Not an SSL message, closing connection (client_id={})", client_id_); co_return ProbingResult{.is_probing = true, .sni = default_proxy_domain_, .should_close = true}; } // Create SslLayer pcpp::SSLLayer* ssl_layer = pcpp::SSLLayer::createSSLMessage( buffer.data(), buffer.size(), nullptr, nullptr); if (!ssl_layer) { SPDLOG_ERROR( "Failed to create SSL layer from handshake data (client_id={})", client_id_); co_return ProbingResult{.is_probing = true, .sni = default_proxy_domain_, .should_close = true}; } // Check handshake // https://github.com/wiresock/ndisapi/blob/master/examples/cpp/pcapplusplus/pcapplusplus.cpp#L40 const auto* handshake = dynamic_cast(ssl_layer); // Cleanup memory! // std::unique_ptr ssl_layer_ptr(ssl_layer); if (!handshake) { SPDLOG_ERROR("Failed to cast to SSLHandshakeLayer"); co_return ProbingResult{.is_probing = true, .sni = default_proxy_domain_, .should_close = true}; } // Get TTL-HELLO auto* hello = // cppcheck-suppress nullPointerRedundantCheck handshake->getHandshakeMessageOfType(); if (!hello) { SPDLOG_ERROR( "Failed to extract SSLClientHelloMessage from handshake " "(client_id={})", client_id_); co_return ProbingResult{.is_probing = true, .sni = default_proxy_domain_, .should_close = true}; } // Set SNI std::string sni = default_proxy_domain_; auto* sni_ext = // cppcheck-suppress nullPointerRedundantCheck hello->getExtensionOfType(); if (sni_ext) { std::string tls_sni = sni_ext->getHostName(); if (!tls_sni.empty()) { sni = std::move(tls_sni); } } // Validate allowed sni if (!allowed_sni_list_.empty()) { const bool sni_allowed = std::ranges::any_of( allowed_sni_list_, [&sni](const std::string& allowed_sni) { if (sni == allowed_sni) { return true; } // check subdomains if (sni.size() > allowed_sni.size() + 1) { return sni.ends_with("." + allowed_sni); } return false; }); if (!sni_allowed) { sni = default_proxy_domain_; SPDLOG_WARN( "SNI '{}' not in allowed list, using default domain: {} " "(client_id={})", sni, default_proxy_domain_, client_id_); } } // Detect and prevent recursive proxying to the local server if (sni != default_proxy_domain_) { const bool is_recursive_attempt = co_await IsSniSelfProxyAttempt(sni); if (is_recursive_attempt) { SPDLOG_WARN( "Detected recursive proxy attempt! " "Client: {}, SNI: {}, Redirecting to default SNI: {}", client_id_, sni, default_proxy_domain_); sni = default_proxy_domain_; } } // Get Session ID constexpr std::size_t kSessionLen = 32; std::size_t session_len = std::min( static_cast(kSessionLen), hello->getSessionIDLength()); if (session_len != kSessionLen) { SPDLOG_ERROR( "Invalid session ID length: expected {}, got {} (client_id={})", kSessionLen, session_len, client_id_); co_return ProbingResult{ .is_probing = true, .sni = sni, .should_close = false}; } std::uint8_t session_id[kSessionLen] = {0}; std::memcpy(session_id, hello->getSessionID(), session_len); // Check Session ID const bool is_fptn_session_id = protocol::https::utils::IsFptnClientSessionID(session_id, session_len); const bool is_decoy_session_id = protocol::https::utils::IsDecoyHandshakeSessionID( session_id, session_len); const bool is_decoy_session_id2 = protocol::https::utils::IsDecoyHandshakeSessionID2( session_id, session_len); if (!is_fptn_session_id && !is_decoy_session_id && !is_decoy_session_id2) { SPDLOG_ERROR( "Session ID does not match FPTN client format (client_id={})", client_id_); co_return ProbingResult{ .is_probing = true, .sni = sni, .should_close = false}; } // Valid FPTN client co_return ProbingResult{ .is_probing = false, .sni = sni, .should_close = false}; } catch (const boost::system::system_error& e) { SPDLOG_ERROR( "System error during probing: {} (client_id={})", e.what(), client_id_); } catch (const std::exception& e) { SPDLOG_ERROR( "Exception during probing: {} (client_id={})", e.what(), client_id_); } catch (...) { SPDLOG_ERROR("Unknown exception during probing (client_id={})", client_id_); } co_return ProbingResult{ .is_probing = true, .sni = default_proxy_domain_, .should_close = true}; } // NOLINTNEXTLINE(readability-convert-member-functions-to-static) boost::asio::awaitable Session::IsSniSelfProxyAttempt( const std::string& sni) const { // First check if SNI is already an IP address if (fptn::common::network::IsIpAddress(sni)) { // FIXME SPDLOG_WARN("SNI is IP address, treating as potential self-proxy: {}", sni); co_return true; } // Not an IP address - proceed with DNS resolution using our new function try { const auto server_ips = GetServerIpAddresses(server_external_ips_); boost::asio::io_context ioc; const auto resolve_result = fptn::common::network::ResolveWithTimeout(ioc, sni, "", 5); if (!resolve_result.success()) { SPDLOG_WARN("DNS resolution failed for {}: {}", sni, resolve_result.error.message()); } // Iterate through resolved endpoints for (const auto& endpoint : resolve_result.results) { const auto ip = endpoint.endpoint().address().to_string(); if (ip.empty()) { continue; } // check server interfaces if (std::ranges::find(server_ips, ip) != server_ips.end()) { SPDLOG_WARN( "SNI {} resolves to server interface IP {}, blocking self-proxy", sni, ip); co_return true; } } } catch (const std::exception& e) { SPDLOG_WARN("Exception during DNS resolution for {}: {}", sni, e.what()); co_return true; } co_return false; } boost::asio::awaitable Session::IsRealityHandshake() { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); // Peek data without consuming it std::array buffer{}; const std::size_t bytes_read = co_await tcp_socket.async_receive(boost::asio::buffer(buffer), boost::asio::socket_base::message_peek, boost::asio::use_awaitable); if (!bytes_read) { co_return RealityResult{.is_reality_mode = false, .is_reality_mode2 = false, .sni = "", .should_close = true}; } // Check if it's SSL/TLS handshake if (!pcpp::SSLLayer::IsSSLMessage( 0, 0, buffer.data(), buffer.size(), true)) { co_return RealityResult{.is_reality_mode = false, .is_reality_mode2 = false, .sni = "", .should_close = true}; } // Parse SSL handshake pcpp::SSLLayer* ssl_layer = pcpp::SSLLayer::createSSLMessage( buffer.data(), buffer.size(), nullptr, nullptr); if (!ssl_layer) { co_return RealityResult{.is_reality_mode = false, .is_reality_mode2 = false, .sni = "", .should_close = true}; } // Check handshake // https://github.com/wiresock/ndisapi/blob/master/examples/cpp/pcapplusplus/pcapplusplus.cpp#L40 auto* handshake = dynamic_cast(ssl_layer); if (!handshake) { co_return RealityResult{.is_reality_mode = false, .is_reality_mode2 = false, .sni = "", .should_close = true}; } auto* hello = // cppcheck-suppress nullPointerRedundantCheck handshake->getHandshakeMessageOfType(); if (!hello) { co_return RealityResult{.is_reality_mode = false, .is_reality_mode2 = false, .sni = "", .should_close = true}; } // Get SNI std::string sni = default_proxy_domain_; auto* sni_ext = // cppcheck-suppress nullPointerRedundantCheck hello->getExtensionOfType(); if (sni_ext) { std::string tls_sni = sni_ext->getHostName(); if (!tls_sni.empty()) { sni = std::move(tls_sni); } } // Check if this is a reality mode handshake by examining session ID constexpr std::size_t kSessionLen = 32; std::size_t session_len = std::min( static_cast(kSessionLen), hello->getSessionIDLength()); if (session_len == kSessionLen) { std::uint8_t session_id[kSessionLen] = {0}; std::memcpy(session_id, hello->getSessionID(), session_len); // Check if it's a decoy handshake (reality mode) const bool is_reality = protocol::https::utils::IsDecoyHandshakeSessionID( session_id, session_len); const bool is_reality2 = protocol::https::utils::IsDecoyHandshakeSessionID2( session_id, session_len); if (is_reality || is_reality2) { co_return RealityResult{.is_reality_mode = is_reality, .is_reality_mode2 = is_reality2, .sni = sni, .should_close = false}; } co_return RealityResult{.is_reality_mode = false, .is_reality_mode2 = false, .sni = sni, .should_close = false}; } } catch (const std::exception& e) { SPDLOG_ERROR("IsRealityHandshake exception (client_id={}): {}", client_id_, e.what()); } co_return RealityResult{.is_reality_mode = true, .is_reality_mode2 = false, .sni = "", .should_close = true}; } // DEPRECATED boost::asio::awaitable Session::PerformFakeHandshake( const std::string& sni) { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); std::vector buffer(16384, '\0'); // std::string buffer(16384, '\0'); const std::size_t bytes_read = co_await tcp_socket.async_receive( boost::asio::buffer(buffer), boost::asio::use_awaitable); if (!bytes_read || !handshake_cache_manager_) { co_return false; } buffer.resize(bytes_read); const auto handshake_answer = co_await handshake_cache_manager_->GetHandshake( sni, buffer.data(), bytes_read, std::chrono::seconds(3)); if (!handshake_answer) { co_return false; } const std::size_t bytes_wrote = co_await boost::asio::async_write(tcp_socket, boost::asio::buffer(*handshake_answer), boost::asio::use_awaitable); SPDLOG_INFO( "Reality mode completed, ready for real handshake (client_id={}) " "request_size = {} response_size: {}", client_id_, bytes_read, bytes_wrote); co_return true; } catch (const std::exception& e) { SPDLOG_ERROR( "HandleRealityMode exception (client_id={}): {}", client_id_, e.what()); } co_return false; } boost::asio::awaitable Session::PerformFakeHandshake2( const std::string& sni) { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); /* Wait for client hello */ const auto client_hello = co_await common::network::WaitForClientTlsHelloAsync(tcp_socket); if (!client_hello.has_value()) { SPDLOG_ERROR("Empty client hello"); co_return false; } const auto client_hello_size = client_hello.value().size(); common::network::CleanSocket(tcp_socket); /* Send server hello */ const auto handshake_answer = co_await handshake_cache_manager_->GetHandshake(sni, client_hello.value().data(), client_hello_size, std::chrono::seconds(5)); if (!handshake_answer) { co_return false; } const std::size_t handshake_answer_size = co_await boost::asio::async_write(tcp_socket, boost::asio::buffer(*handshake_answer), boost::asio::use_awaitable); /* Wait for ChangeCipherSpec */ const bool change_cipher_spec_size = co_await common::network::WaitForClientChangeCipherSpec(tcp_socket); if (!change_cipher_spec_size) { SPDLOG_ERROR("Failed to receive Client ChangeCipherSpec"); co_return false; } SPDLOG_INFO( "Reality mode2 completed, ready for real handshake (client_id={}) " "request_size = {} response_size: {}", client_id_, client_hello_size, handshake_answer_size); co_return true; } catch (const std::exception& e) { SPDLOG_ERROR("HandleRealityMode2 exception (client_id={}): {}", client_id_, e.what()); } co_return false; } boost::asio::awaitable Session::HandleProxy( const std::string& sni, int port) { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); boost::asio::ip::tcp::socket target_socket( co_await boost::asio::this_coro::executor); constexpr int kTimeout = 10; boost::beast::get_lowest_layer(ws_).expires_after( std::chrono::seconds(kTimeout)); bool status = false; try { const std::string port_str = std::to_string(port); boost::asio::io_context ioc; auto resolve_result = fptn::common::network::ResolveWithTimeout(ioc, sni, port_str, kTimeout); if (!resolve_result.success()) { SPDLOG_ERROR("Proxy DNS resolution failed for {}:{}: {}", sni, port_str, resolve_result.error.message()); co_return false; } co_await boost::asio::async_connect( target_socket, resolve_result.results, boost::asio::use_awaitable); const auto ep = target_socket.remote_endpoint(); SPDLOG_INFO("Proxying {}:{} <-> {}:{} (client_id={})", ep.address().to_string(), ep.port(), sni, port_str, client_id_); auto self = shared_from_this(); auto forward = [self]( auto& from, auto& to) -> boost::asio::awaitable { try { boost::system::error_code ec; std::array buf{}; while (self->running_) { const auto n = co_await from.async_read_some(boost::asio::buffer(buf), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec || n == 0) { break; } co_await boost::asio::async_write(to, boost::asio::buffer(buf.data(), n), boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (ec) { break; } } from.close(); } catch (const boost::system::system_error& e) { SPDLOG_ERROR("Coroutine system error: {} [{}] (client_id={})", e.what(), e.code().message(), self->client_id_); } co_return; }; // Set socket timeout SetSocketTimeouts(tcp_socket, kTimeout); SetSocketTimeouts(target_socket, kTimeout); auto [client_to_server_result, server_to_client_result, completion_status] = co_await boost::asio::experimental::make_parallel_group( boost::asio::co_spawn(co_await boost::asio::this_coro::executor, forward(tcp_socket, target_socket), boost::asio::deferred), boost::asio::co_spawn(co_await boost::asio::this_coro::executor, forward(target_socket, tcp_socket), boost::asio::deferred)) .async_wait(boost::asio::experimental::wait_for_all(), boost::asio::use_awaitable); (void)client_to_server_result; (void)server_to_client_result; (void)completion_status; status = true; } catch (const boost::system::system_error& e) { SPDLOG_ERROR("Proxy system error: {} [{}] (client_id={})", e.what(), e.code().message(), client_id_); } catch (const std::exception& e) { SPDLOG_ERROR("Proxy error (client_id={}): {} ", e.what(), client_id_); } // close socket try { tcp_socket.close(); } catch (const boost::system::system_error& e) { SPDLOG_ERROR( "Failed to close the socket after proxy completion (client_id={}): " "{} " "[{}]", client_id_, e.what(), e.code().message()); } // close target socket boost::system::error_code ec; target_socket.close(ec); SPDLOG_INFO("Close proxy (client_id={})", client_id_); co_return status; } boost::asio::awaitable Session::RunReader() { boost::system::error_code ec; boost::beast::flat_buffer buffer; buffer.reserve(4 * 1024 * 1024); auto token = boost::asio::redirect_error(boost::asio::use_awaitable, ec); try { while (running_ && ws_.is_open()) { co_await ws_.async_read(buffer, token); if (ec) { break; } if (buffer.size() > 0 && running_ && ws_.is_open()) { auto raw_ip = fptn::protocol::protobuf::GetProtoPayload(buffer); if (raw_ip.has_value() && running_) { auto packet = fptn::common::network::IPPacket::Parse( std::move(raw_ip.value()), client_id_); if (packet != nullptr) { ws_new_ippacket_callback_(std::move(packet)); } } } buffer.consume(buffer.size()); } } catch (const std::exception& e) { SPDLOG_ERROR( "RunReader exception (client_id={}): {}", client_id_, e.what()); } Close(); co_return; } boost::asio::awaitable Session::RunSender() { try { while (running_ && ws_.is_open()) { auto [ec, packet] = co_await write_channel_.async_receive( boost::asio::bind_cancellation_slot(cancel_signal_.slot(), boost::asio::as_tuple(boost::asio::use_awaitable))); if (running_ && ws_.is_open() && !ec && packet != nullptr) { auto msg = fptn::protocol::protobuf::CreateProtoPayload(std::move(packet)); if (msg.has_value()) { co_await ws_.async_write(boost::asio::buffer(std::move(msg.value())), boost::asio::use_awaitable); } } } } catch (const boost::system::system_error& err) { if (err.code() != boost::asio::error::operation_aborted && err.code() != boost::beast::websocket::error::closed) { SPDLOG_ERROR( "RunSender error (client_id={}): {}", client_id_, err.what()); } } catch (const std::exception& e) { SPDLOG_ERROR( "RunSender exception (client_id={}): {}", client_id_, e.what()); } Close(); co_return; } boost::asio::awaitable Session::ProcessRequest() { bool status = false; try { boost::system::error_code ec; boost::beast::flat_buffer buffer; boost::beast::http::request request; co_await boost::beast::http::async_read(ws_.next_layer(), buffer, request, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); if (boost::beast::websocket::is_upgrade(request)) { status = co_await HandleWebSocket(request); if (status) { co_await ws_.async_accept(request, boost::asio::redirect_error(boost::asio::use_awaitable, ec)); } } else { status = co_await HandleHttp(request); } } catch (const boost::system::system_error& err) { SPDLOG_ERROR("Session::handshake failed (client_id={}): {} [{}]", client_id_, err.what(), err.code().message()); } co_return status; } boost::asio::awaitable Session::HandleHttp( const boost::beast::http::request& request) { const std::string url = request.target(); const std::string method = request.method_string(); boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::seconds(30)); if (method.empty() && url.empty()) { SPDLOG_WARN( "HTTP request has empty method or URL (client_id={}): method='{}', " "url='{}'", client_id_, method, url); co_return false; } if (url.find("metrics") == std::string::npos) { // NOLINT SPDLOG_INFO("HTTP request (client_id={}): {} {}", client_id_, method, url); } else { SPDLOG_INFO("HTTP request (client_id={}): {} {}", client_id_, method, "/api/v1/metrics/"); } const auto server_info = fmt::format("fptn/{}", FPTN_VERSION); const auto http_date = fptn::time::TimeProvider::Instance()->Rfc7231Date(); boost::beast::http::response resp; resp.set(boost::beast::http::field::server, server_info); resp.set(boost::beast::http::field::content_type, "application/json; charset=utf-8"); resp.set(boost::beast::http::field::cache_control, "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"); resp.set(boost::beast::http::field::pragma, "no-cache"); resp.set(boost::beast::http::field::expires, "0"); resp.set(boost::beast::http::field::date, http_date); const ApiHandle handler = GetApiHandle(api_handles_, url, method); if (handler) { int status = handler(request, resp); resp.result(status); } else { resp.result(boost::beast::http::status::not_found); resp.body() = "404 Not Found"; } resp.prepare_payload(); auto res_ptr = std::make_shared< boost::beast::http::response>( std::move(resp)); try { co_await boost::beast::http::async_write( ws_.next_layer(), *res_ptr, boost::asio::use_awaitable); } catch (const boost::beast::system_error& e) { SPDLOG_ERROR("Session::HandleHttp write error (client_id={}): {}", client_id_, e.what()); } catch (...) { SPDLOG_ERROR( "Session::HandleHttp write unknown error (client_id={})", client_id_); } co_return false; } boost::asio::awaitable Session::HandleWebSocket( const boost::beast::http::request& request) { boost::beast::get_lowest_layer(ws_).expires_after(std::chrono::hours(12)); if (request.contains("Authorization") && request.contains("ClientIP")) { std::string token = request["Authorization"]; boost::replace_first(token, "Bearer ", ""); const std::string client_vpn_ipv4_str = request["ClientIP"]; boost::system::error_code ec; const std::string client_ip_str = boost::beast::get_lowest_layer(ws_) .socket() .remote_endpoint(ec) .address() .to_string(); if (ec) { SPDLOG_ERROR("Failed to get remote endpoint: {}", ec.message()); co_return false; } try { const common::network::IPv4Address client_ip(client_ip_str); const common::network::IPv4Address client_vpn_ipv4(client_vpn_ipv4_str); const std::string client_vpn_ipv6_str = (request.contains("ClientIPv6") ? request["ClientIPv6"] : FPTN_CLIENT_DEFAULT_ADDRESS_IP6); const common::network::IPv6Address client_vpn_ipv6(client_vpn_ipv6_str); const bool status = ws_open_callback_(client_id_, client_ip, client_vpn_ipv4, client_vpn_ipv6, shared_from_this(), request.target(), token); ws_session_was_opened_ = true; co_return status; } catch (const std::exception& ex) { SPDLOG_ERROR( "Session::Open (client_id={}): Exception caught while creating IP " "addresses or running callback: {}", client_id_, ex.what()); } catch (...) { SPDLOG_ERROR( "Session::Open (client_id={}): Unknown fatal error caught while " "creating IP addresses or running callback", client_id_); } } co_return false; } void Session::Close() { if (!running_) { return; } const std::unique_lock lock(mutex_); // mutex // cppcheck-suppress identicalConditionAfterEarlyExit if (!running_) { // Double-check after acquiring lock return; } SPDLOG_INFO("Close session {}", client_id_); running_ = false; try { cancel_signal_.emit(boost::asio::cancellation_type::all); write_channel_.close(); } catch (const std::exception& err) { SPDLOG_WARN( "Failed to cancel session or close write_channel: {}", err.what()); } catch (...) { SPDLOG_WARN( "Session::Close unknown fatal error (client_id={})", client_id_); } // Close TCP socket first try { auto& tcp_layer = boost::beast::get_lowest_layer(ws_); if (tcp_layer.socket().is_open()) { boost::system::error_code ec; tcp_layer.expires_after(std::chrono::milliseconds(50)); tcp_layer.socket().shutdown( boost::asio::ip::tcp::socket::shutdown_both, ec); tcp_layer.socket().close(ec); } } catch (const std::exception& err) { SPDLOG_WARN("Session::Close TCP socket error (client_id={}): {}", client_id_, err.what()); } catch (...) { SPDLOG_WARN( "Session::Close TCP socket unknown error (client_id={})", client_id_); } // Close WebSocket try { if (ws_.is_open()) { boost::system::error_code ec; ws_.close(boost::beast::websocket::close_code::normal, ec); } } catch (const std::exception& err) { SPDLOG_WARN("Session::Close WebSocket error (client_id={}): {}", client_id_, err.what()); } catch (...) { SPDLOG_WARN( "Session::Close WebSocket unknown error (client_id={})", client_id_); } // Close SSL try { auto& ssl_layer = ws_.next_layer(); if (ssl_layer.native_handle()) { ::SSL_set_quiet_shutdown(ssl_layer.native_handle(), 1); } } catch (const std::exception& err) { SPDLOG_ERROR("Session::Close SSL shutdown exception (client_id={}): {}", client_id_, err.what()); } catch (...) { SPDLOG_ERROR( "Session::Close SSL shutdown unknown error (client_id={})", client_id_); } if (ws_close_callback_ && ws_session_was_opened_) { try { ws_close_callback_(client_id_); } catch (const std::exception& e) { SPDLOG_WARN("WebSocket close callback threw exception (client_id={}): {}", client_id_, e.what()); } catch (...) { SPDLOG_WARN( "WebSocket close callback threw unknown exception (client_id={})", client_id_); } } } boost::asio::awaitable Session::Send(common::network::IPPacketPtr pkt) { auto self = shared_from_this(); boost::asio::post(strand_, [self, pkt = std::move(pkt)]() mutable { if (self->running_ && self->write_channel_.is_open()) { const bool status = self->write_channel_.try_send( boost::system::error_code(), std::move(pkt)); if (!status && !self->full_queue_) { self->full_queue_ = true; SPDLOG_WARN( "Session::send queue is full (client_id={})", self->client_id_); } } }); co_return true; } boost::asio::awaitable Session::DetectObfuscator() { try { auto& tcp_socket = boost::beast::get_lowest_layer(ws_).socket(); // Peek data without consuming it from the socket buffer // This allows inspection without affecting subsequent reads std::array buffer{}; const std::size_t bytes_read = co_await tcp_socket.async_receive(boost::asio::buffer(buffer), boost::asio::socket_base::message_peek, boost::asio::use_awaitable); if (!bytes_read) { SPDLOG_WARN("No data received for obfuscator detection [client_id: {}]", client_id_); co_return std::nullopt; } // Detect the appropriate obfuscator based on the peeked data auto obfuscator = fptn::protocol::https::obfuscator::DetectObfuscator( buffer.data(), bytes_read); co_return obfuscator; } catch (const boost::system::system_error& e) { SPDLOG_ERROR( "System error during obfuscator setup [client_id: {}, error: '{}', " "code: {}]", client_id_, e.what(), e.code().message()); } catch (const std::exception& e) { SPDLOG_ERROR( "Exception during obfuscator setup [client_id: {}, error: '{}']", client_id_, e.what()); } catch (...) { SPDLOG_ERROR( "Unknown error during obfuscator setup [client_id: {}]", client_id_); } co_return std::nullopt; } }; // namespace fptn::web ================================================ FILE: src/fptn-server/web/session/session.h ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "common/jwt_token/token_manager.h" #include "fptn-protocol-lib/https/obfuscator/tcp_stream/tcp_stream.h" #include "web/api/handle.h" #include "web/handshake/handshake_cache_manager.h" namespace fptn::web { using IObfuscator = std::optional; class Session : public std::enable_shared_from_this { public: explicit Session(std::uint16_t port, bool enable_detect_probing, std::string default_proxy_domain, std::vector allowed_sni_list, std::string server_external_ips, boost::asio::ip::tcp::socket&& socket, boost::asio::ssl::context& ctx, const ApiHandleMap& api_handles, HandshakeCacheManagerSPtr handshake_cache_manager, WebSocketOpenConnectionCallback ws_open_callback, WebSocketNewIPPacketCallback ws_new_ippacket_callback, WebSocketCloseConnectionCallback ws_close_callback); virtual ~Session(); void Close(); // async boost::asio::awaitable Run(); boost::asio::awaitable Send(fptn::common::network::IPPacketPtr pkt); protected: boost::asio::awaitable RunReader(); boost::asio::awaitable RunSender(); protected: struct ProbingResult { bool is_probing; std::string sni; bool should_close; }; boost::asio::awaitable DetectProbing(); protected: boost::asio::awaitable IsSniSelfProxyAttempt( const std::string& sni) const; struct RealityResult { bool is_reality_mode; // DEPRICATED bool is_reality_mode2; std::string sni; bool should_close; }; boost::asio::awaitable IsRealityHandshake(); // DEPRECATED boost::asio::awaitable PerformFakeHandshake(const std::string& sni); boost::asio::awaitable PerformFakeHandshake2(const std::string& sni); boost::asio::awaitable HandleProxy(const std::string& sni, int port); boost::asio::awaitable DetectObfuscator(); protected: boost::asio::awaitable ProcessRequest(); boost::asio::awaitable HandleHttp( const boost::beast::http::request& request); boost::asio::awaitable HandleWebSocket( const boost::beast::http::request& request); private: mutable std::mutex mutex_; fptn::ClientID client_id_ = MAX_CLIENT_ID; const std::uint16_t port_; const bool enable_detect_probing_; const std::string default_proxy_domain_; const std::vector allowed_sni_list_; const std::string server_external_ips_; // TCP -> obfuscator -> SSL -> WebSocket using tcp_stream_type = boost::beast::tcp_stream; using obfuscator_socket_type = fptn::protocol::https::obfuscator::TcpStream; using ssl_stream_type = boost::beast::ssl_stream; using websocket_type = boost::beast::websocket::stream; websocket_type ws_; boost::asio::strand strand_; boost::asio::experimental::concurrent_channel write_channel_; const ApiHandleMap& api_handles_; HandshakeCacheManagerSPtr handshake_cache_manager_; const WebSocketOpenConnectionCallback ws_open_callback_; const WebSocketNewIPPacketCallback ws_new_ippacket_callback_; const WebSocketCloseConnectionCallback ws_close_callback_; std::atomic running_; std::atomic init_completed_; std::atomic ws_session_was_opened_; std::atomic full_queue_; boost::asio::cancellation_signal cancel_signal_; }; using SessionSPtr = std::shared_ptr; } // namespace fptn::web ================================================ FILE: sysadmin-tools/grafana/.gitignore ================================================ .env docker-compose-data/ ================================================ FILE: sysadmin-tools/grafana/README.md ================================================ ## Grafana Grafana is used for monitoring server activity, including traffic amount and active users. Grafana Grafana #### To set it up: 1. **Clone the repository:** ```bash git clone https://github.com/batchar2/fptn.git ``` 2. **Navigate to the Grafana configuration folder:** ```bash cd sysadmin-tools/grafana ``` 3. **Copy and configure the environment file:** - Copy the `.env.demo` file to `.env`: ```bash cp .env.demo .env ``` - Open the `.env` file in a text editor and fill in all required fields. - Set your piblic ip for `FPTN_HOST` and your fptn port for `FPTN_PORT` - Pay special attention to the `PROMETHEUS_SECRET_ACCESS_KEY` parameter: - This value **must match** the access key defined in your `fptn-server` config at `/etc/fptn/server.conf`. - Use a **secure, random string** of **at least 30 characters** for this value. 4. **Run Docker Compose:** - Need install docker. To install it on ubuntu use [this docs](https://docs.docker.com/engine/install/ubuntu/) - Start Grafana and its dependencies using Docker Compose: ```bash docker compose down && docker compose up -d ``` 5. **Access Grafana:** - Open your browser and navigate to the Grafana interface using the selected port (**3000 by default**). - Log in using the default credentials: `admin` / `admin`. - After logging in, Grafana will prompt you to change the default password — **do it immediately and avoid using default credentials going forward!** #### Notes: Ensure that all parameters in the .env file are correctly configured before starting the services. The `PROMETHEUS_SECRET_ACCESS_KEY` parameter must be consistent with the key used in fptn-server to allow proper access to metrics. #### 🐳 Building and Running the Docker Image (Optional) To build the image: ```bash docker compose build -f docker-compose.build.yml ``` To run the services: ```bash docker compose build -f docker-compose.build.yml up -d ``` To stop the services: ```bash docker compose build -f docker-compose.build.yml down ``` ================================================ FILE: sysadmin-tools/grafana/configs/grafana/dashboards/dashboards.yaml ================================================ apiVersion: 1 providers: - name: 'default' orgId: 1 folder: '' type: file disableDeletion: false editable: true options: path: /etc/grafana/provisioning/dashboards ================================================ FILE: sysadmin-tools/grafana/configs/grafana/dashboards/fptn_dashboard.json ================================================ { "annotations": { "list": [ { "builtIn": 1, "datasource": { "type": "grafana", "uid": "-- Grafana --" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "graphTooltip": 1, "links": [], "panels": [ { "datasource": { "default": true, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, "id": 6, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "percentChangeColorMode": "standard", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showPercentChange": false, "textMode": "auto", "wideLayout": true }, "pluginVersion": "11.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "builder", "expr": "fptn_active_sessions", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "__auto", "range": true, "refId": "A", "useBackend": false } ], "title": "Active VPN sessions", "type": "stat" }, { "datasource": { "default": true, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, "id": 7, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "11.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "builder", "expr": "fptn_active_sessions", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "USERS", "range": true, "refId": "A", "useBackend": false } ], "title": "Sessions history", "type": "timeseries" }, { "datasource": { "default": true, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "binbps" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, "id": 5, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "editorMode": "code", "expr": "sum(rate(fptn_user_outgoing_traffic_bytes[$__rate_interval])*8) ", "hide": false, "instant": false, "legendFormat": "TOTAL", "range": true, "refId": "B" } ], "title": "User outgoing traffic", "type": "timeseries" }, { "datasource": { "default": true, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "binbps" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, "id": 4, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "code", "expr": "sum(rate(fptn_user_incoming_traffic_bytes[$__rate_interval])*8)", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "TOTAL", "range": true, "refId": "A", "useBackend": false } ], "title": "User incoming traffic", "type": "timeseries" }, { "datasource": { "default": true, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "fillOpacity": 80, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "type": "linear" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 8, "interval": "1h", "options": { "barRadius": 0, "barWidth": 0.97, "fullHighlight": false, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "orientation": "auto", "showValue": "auto", "stacking": "none", "tooltip": { "mode": "single", "sort": "none" }, "xTickLabelRotation": 0, "xTickLabelSpacing": 0 }, "pluginVersion": "11.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "code", "expr": "sum(increase(fptn_user_outgoing_traffic_bytes[$__rate_interval]))", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "TOTAL", "range": true, "refId": "A", "useBackend": false } ], "title": "Total Outgoing Traffic for 1h", "type": "barchart" }, { "datasource": { "default": true, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "fillOpacity": 80, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineWidth": 1, "scaleDistribution": { "type": "linear" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 9, "interval": "1h", "options": { "barRadius": 0, "barWidth": 0.97, "fullHighlight": false, "groupWidth": 0.7, "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "orientation": "auto", "showValue": "auto", "stacking": "none", "tooltip": { "mode": "single", "sort": "none" }, "xTickLabelRotation": 0, "xTickLabelSpacing": 0 }, "pluginVersion": "11.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "code", "expr": "sum(increase(fptn_user_incoming_traffic_bytes[1h]))", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "TOTAL", "range": true, "refId": "A", "useBackend": false } ], "title": "Total Incoming Traffic for the 1h", "type": "barchart" }, { "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 }, "id": 14, "panels": [], "title": "Additional info", "type": "row" }, { "datasource": { "default": false, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "binbps" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 25 }, "id": 12, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "code", "expr": "sum(rate(fptn_user_outgoing_traffic_bytes[$__rate_interval])*8) by (username) > 0", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "__auto", "range": true, "refId": "A", "useBackend": false } ], "title": "User outgoing Traffic Rate ", "type": "timeseries" }, { "datasource": { "default": false, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "binbps" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 25 }, "id": 13, "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "code", "expr": "sum(rate(fptn_user_incoming_traffic_bytes[$__rate_interval])*8) by (username) > 0", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "__auto", "range": true, "refId": "A", "useBackend": false } ], "title": "User Incoming Traffic Rate", "type": "timeseries" }, { "datasource": { "default": false, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 33 }, "id": 10, "interval": "1h", "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "11.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "code", "expr": "sum(increase(fptn_user_outgoing_traffic_bytes[$__rate_interval])) by (username) > 0", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "__auto", "range": true, "refId": "A", "useBackend": false } ], "title": "Outgoing traffic by user for 1h", "type": "timeseries" }, { "datasource": { "default": false, "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisBorderShow": false, "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "barWidthFactor": 0.6, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "insertNulls": false, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "auto", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 33 }, "id": 11, "interval": "1h", "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "PBFA97CFB590B2093" }, "disableTextWrap": false, "editorMode": "code", "expr": "sum(increase(fptn_user_incoming_traffic_bytes[$__rate_interval])) by (username) > 0 ", "fullMetaSearch": false, "includeNullMetadata": true, "instant": false, "legendFormat": "__auto", "range": true, "refId": "A", "useBackend": false } ], "title": "incoming traffic by user for 1h", "type": "timeseries" } ], "refresh": "5s", "schemaVersion": 39, "tags": [], "templating": { "list": [] }, "time": { "from": "now-6h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "3h", "6h", "12h", "24h" ] }, "timezone": "browser", "title": "FPTN Metrics Dashboard", "uid": "fptn-metrics-dashboard", "version": 1, "weekStart": "" } ================================================ FILE: sysadmin-tools/grafana/configs/grafana/dashboards/node-exporter-full.json ================================================ { "__inputs": [ { "name": "DS_PROMETHEUS", "label": "Prometheus", "description": "", "type": "datasource", "pluginId": "prometheus", "pluginName": "Prometheus" } ], "__elements": {}, "__requires": [ { "type": "panel", "id": "bargauge", "name": "Bar gauge", "version": "" }, { "type": "panel", "id": "gauge", "name": "Gauge", "version": "" }, { "type": "grafana", "id": "grafana", "name": "Grafana", "version": "9.4.3" }, { "type": "datasource", "id": "prometheus", "name": "Prometheus", "version": "1.0.0" }, { "type": "panel", "id": "stat", "name": "Stat", "version": "" }, { "type": "panel", "id": "timeseries", "name": "Time series", "version": "" } ], "annotations": { "list": [ { "$$hashKey": "object:1058", "builtIn": 1, "datasource": { "type": "datasource", "uid": "grafana" }, "enable": true, "hide": true, "iconColor": "rgba(0, 211, 255, 1)", "name": "Annotations & Alerts", "target": { "limit": 100, "matchAny": false, "tags": [], "type": "dashboard" }, "type": "dashboard" } ] }, "editable": true, "fiscalYearStartMonth": 0, "gnetId": 1860, "graphTooltip": 1, "id": null, "links": [ { "icon": "external link", "tags": [], "targetBlank": true, "title": "GitHub", "type": "link", "url": "https://github.com/rfmoz/grafana-dashboards" }, { "icon": "external link", "tags": [], "targetBlank": true, "title": "Grafana", "type": "link", "url": "https://grafana.com/grafana/dashboards/1860" } ], "liveNow": false, "panels": [ { "collapsed": false, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 261, "panels": [], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Quick CPU / Mem / Disk", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Resource pressure via PSI", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "links": [], "mappings": [], "max": 1, "min": 0, "thresholds": { "mode": "percentage", "steps": [ { "color": "green", "value": null }, { "color": "dark-yellow", "value": 70 }, { "color": "dark-red", "value": 90 } ] }, "unit": "percentunit" }, "overrides": [] }, "gridPos": { "h": 4, "w": 3, "x": 0, "y": 1 }, "id": 323, "links": [], "options": { "displayMode": "basic", "minVizHeight": 10, "minVizWidth": 0, "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showUnfilled": true, "text": {} }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "irate(node_pressure_cpu_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "instant": true, "intervalFactor": 1, "legendFormat": "CPU", "range": false, "refId": "CPU some", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "irate(node_pressure_memory_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "instant": true, "intervalFactor": 1, "legendFormat": "Mem", "range": false, "refId": "Memory some", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "irate(node_pressure_io_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "instant": true, "intervalFactor": 1, "legendFormat": "I/O", "range": false, "refId": "I/O some", "step": 240 } ], "title": "Pressure", "type": "bargauge" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Busy state of all CPU cores together", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 85 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 95 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 4, "w": 3, "x": 3, "y": 1 }, "id": 20, "links": [], "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "100 * (1 - avg(rate(node_cpu_seconds_total{mode=\"idle\", instance=\"$node\"}[$__rate_interval])))", "hide": false, "instant": true, "intervalFactor": 1, "legendFormat": "", "range": false, "refId": "A", "step": 240 } ], "title": "CPU Busy", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "System load over all CPU cores together", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 85 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 95 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 4, "w": 3, "x": 6, "y": 1 }, "id": 155, "links": [], "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "scalar(node_load1{instance=\"$node\",job=\"$job\"}) * 100 / count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))", "format": "time_series", "hide": false, "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 } ], "title": "Sys Load", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Non available RAM memory", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "mappings": [], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 80 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 90 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 4, "w": 3, "x": 9, "y": 1 }, "hideTimeOverride": false, "id": 16, "links": [], "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "((node_memory_MemTotal_bytes{instance=\"$node\", job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\", job=\"$job\"}) / node_memory_MemTotal_bytes{instance=\"$node\", job=\"$job\"}) * 100", "format": "time_series", "hide": true, "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "(1 - (node_memory_MemAvailable_bytes{instance=\"$node\", job=\"$job\"} / node_memory_MemTotal_bytes{instance=\"$node\", job=\"$job\"})) * 100", "format": "time_series", "hide": false, "instant": true, "intervalFactor": 1, "range": false, "refId": "B", "step": 240 } ], "title": "RAM Used", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Used Swap", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 10 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 25 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 4, "w": 3, "x": 12, "y": 1 }, "id": 21, "links": [], "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "((node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"}) / (node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"})) * 100", "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 } ], "title": "SWAP Used", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Used Root FS", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 80 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 90 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 4, "w": 3, "x": 15, "y": 1 }, "id": 154, "links": [], "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "100 - ((node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"} * 100) / node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"})", "format": "time_series", "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 } ], "title": "Root FS Used", "type": "gauge" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Total number of CPU cores", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 18, "y": 1 }, "id": 14, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu))", "instant": true, "legendFormat": "__auto", "range": false, "refId": "A" } ], "title": "CPU Cores", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "System uptime", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 1, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 2, "w": 4, "x": 20, "y": 1 }, "hideTimeOverride": true, "id": 15, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "node_time_seconds{instance=\"$node\",job=\"$job\"} - node_boot_time_seconds{instance=\"$node\",job=\"$job\"}", "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 } ], "title": "Uptime", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Total RootFS", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 0, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 70 }, { "color": "rgba(245, 54, 54, 0.9)", "value": 90 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 18, "y": 3 }, "id": 23, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",mountpoint=\"/\",fstype!=\"rootfs\"}", "format": "time_series", "hide": false, "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 } ], "title": "RootFS Total", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Total RAM", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 0, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 20, "y": 3 }, "id": 75, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}", "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 } ], "title": "RAM Total", "type": "stat" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Total SWAP", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "decimals": 0, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 22, "y": 3 }, "id": 18, "links": [], "maxDataPoints": 100, "options": { "colorMode": "none", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "textMode": "auto" }, "pluginVersion": "9.4.3", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"}", "instant": true, "intervalFactor": 1, "range": false, "refId": "A", "step": 240 } ], "title": "SWAP Total", "type": "stat" }, { "collapsed": false, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 263, "panels": [], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Basic CPU / Mem / Net / Disk", "type": "row" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Basic CPU info", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "smooth", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "percent" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byName", "options": "Busy Iowait" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Idle" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Busy Iowait" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Idle" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Busy System" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Busy User" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Busy Other" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 6 }, "id": 77, "links": [], "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true, "width": 250 }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "exemplar": false, "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"system\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "hide": false, "instant": false, "intervalFactor": 1, "legendFormat": "Busy System", "range": true, "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Busy User", "range": true, "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"iowait\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Busy Iowait", "range": true, "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=~\".*irq\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Busy IRQs", "range": true, "refId": "D", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode!='idle',mode!='user',mode!='system',mode!='iowait',mode!='irq',mode!='softirq'}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Busy Other", "range": true, "refId": "E", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"idle\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Idle", "range": true, "refId": "F", "step": 240 } ], "title": "CPU Basic", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Basic memory usage", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "SWAP Used" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap Used" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 }, { "id": "custom.stacking", "value": { "group": false, "mode": "normal" } } ] }, { "matcher": { "id": "byName", "options": "RAM Cache + Buffer" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Available" }, "properties": [ { "id": "color", "value": { "fixedColor": "#DEDAF7", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 }, { "id": "custom.stacking", "value": { "group": false, "mode": "normal" } } ] } ] }, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 6 }, "id": 78, "links": [], "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "RAM Total", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - (node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"})", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "RAM Used", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} + node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} + node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "RAM Cache + Buffer", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "RAM Free", "refId": "D", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})", "format": "time_series", "intervalFactor": 1, "legendFormat": "SWAP Used", "refId": "E", "step": 240 } ], "title": "Memory Basic", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Basic network info per interface", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "bps" }, "overrides": [ { "matcher": { "id": "byName", "options": "Recv_bytes_eth2" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Recv_bytes_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Recv_drop_eth2" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Recv_drop_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Recv_errs_eth2" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Recv_errs_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CCA300", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Trans_bytes_eth2" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Trans_bytes_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Trans_drop_eth2" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Trans_drop_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Trans_errs_eth2" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Trans_errs_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CCA300", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "recv_bytes_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "recv_drop_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "recv_drop_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#967302", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "recv_errs_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "recv_errs_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "trans_bytes_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "trans_bytes_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "trans_drop_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "trans_drop_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#967302", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "trans_errs_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "trans_errs_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 13 }, "id": 74, "links": [], "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", "format": "time_series", "intervalFactor": 1, "legendFormat": "recv {{device}}", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", "format": "time_series", "intervalFactor": 1, "legendFormat": "trans {{device}} ", "refId": "B", "step": 240 } ], "title": "Network Traffic Basic", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Disk space used of all filesystems mounted", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 13 }, "id": 152, "links": [], "options": { "legend": { "calcs": [], "displayMode": "list", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "100 - ((node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'} * 100) / node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'})", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{mountpoint}}", "refId": "A", "step": 240 } ], "title": "Disk Space Used Basic", "type": "timeseries" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 20 }, "id": 265, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "percentage", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 70, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "percent" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byName", "options": "Idle - Waiting for something to happen" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Iowait - Waiting for I/O to complete" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Irq - Servicing interrupts" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Nice - Niced processes executing in user mode" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Softirq - Servicing softirqs" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Steal - Time spent in other operating systems when running in a virtualized environment" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCE2DE", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "System - Processes executing in kernel mode" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "User - Normal processes executing in user mode" }, "properties": [ { "id": "color", "value": { "fixedColor": "#5195CE", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 12, "w": 12, "x": 0, "y": 21 }, "id": 3, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 250 }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"system\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "System - Processes executing in kernel mode", "range": true, "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "User - Normal processes executing in user mode", "range": true, "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"nice\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Nice - Niced processes executing in user mode", "range": true, "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"iowait\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Iowait - Waiting for I/O to complete", "range": true, "refId": "E", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"irq\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Irq - Servicing interrupts", "range": true, "refId": "F", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"softirq\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Softirq - Servicing softirqs", "range": true, "refId": "G", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"steal\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "intervalFactor": 1, "legendFormat": "Steal - Time spent in other operating systems when running in a virtualized environment", "range": true, "refId": "H", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\", mode=\"idle\"}[$__rate_interval])) / scalar(count(count(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}) by (cpu)))", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Idle - Waiting for something to happen", "range": true, "refId": "J", "step": 240 } ], "title": "CPU", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap - Swap memory usage" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused - Free memory unassigned" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Hardware Corrupted - *./" }, "properties": [ { "id": "custom.stacking", "value": { "group": false, "mode": "normal" } } ] } ] }, "gridPos": { "h": 12, "w": 12, "x": 12, "y": 21 }, "id": 24, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_MemTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"} - node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"} - node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Apps - Memory used by user-space applications", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_PageTables_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "PageTables - Memory used to map between virtual and physical memory addresses", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_SwapCached_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "SwapCache - Memory that keeps track of pages that have been fetched from swap but not yet been modified", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Slab_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Slab - Memory used by the kernel to cache data structures for its own use (caches like inode, dentry, etc)", "refId": "D", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Cached_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Cache - Parked file data (file content) cache", "refId": "E", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Buffers_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Buffers - Block device (e.g. harddisk) cache", "refId": "F", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_MemFree_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Unused - Free memory unassigned", "refId": "G", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\",job=\"$job\"} - node_memory_SwapFree_bytes{instance=\"$node\",job=\"$job\"})", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Swap - Swap space used", "refId": "H", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_HardwareCorrupted_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working", "refId": "I", "step": 240 } ], "title": "Memory Stack", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bits out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bps" }, "overrides": [ { "matcher": { "id": "byName", "options": "receive_packets_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "receive_packets_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "transmit_packets_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "transmit_packets_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 12, "w": 12, "x": 0, "y": 33 }, "id": 84, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Receive", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])*8", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Transmit", "refId": "B", "step": 240 } ], "title": "Network Traffic", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 12, "w": 12, "x": 12, "y": 33 }, "id": 156, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'} - node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{mountpoint}}", "refId": "A", "step": 240 } ], "title": "Disk Space Used", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "IO read (-) / write (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "iops" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Read.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 12, "w": 12, "x": 0, "y": 45 }, "id": 229, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", "intervalFactor": 4, "legendFormat": "{{device}} - Reads completed", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{device}} - Writes completed", "refId": "B", "step": 240 } ], "title": "Disk IOps", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes read (-) / write (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "Bps" }, "overrides": [ { "matcher": { "id": "byName", "options": "io time" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*read*./" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byType", "options": "time" }, "properties": [ { "id": "custom.axisPlacement", "value": "hidden" } ] } ] }, "gridPos": { "h": 12, "w": 12, "x": 12, "y": 45 }, "id": 42, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{device}} - Successfully read bytes", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{device}} - Successfully written bytes", "refId": "B", "step": 240 } ], "title": "I/O Usage Read / Write", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "%util", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 40, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byName", "options": "io time" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] }, { "matcher": { "id": "byType", "options": "time" }, "properties": [ { "id": "custom.axisPlacement", "value": "hidden" } ] } ] }, "gridPos": { "h": 12, "w": 12, "x": 0, "y": 57 }, "id": 127, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\",device=~\"$diskdevices\"} [$__rate_interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}", "refId": "A", "step": 240 } ], "title": "I/O Utilization", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "percentage", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "bars", "fillOpacity": 70, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "smooth", "lineWidth": 2, "pointSize": 3, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "mappings": [], "max": 1, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/^Guest - /" }, "properties": [ { "id": "color", "value": { "fixedColor": "#5195ce", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/^GuestNice - /" }, "properties": [ { "id": "color", "value": { "fixedColor": "#c15c17", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 12, "w": 12, "x": 12, "y": 57 }, "id": 319, "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum by(instance) (irate(node_cpu_guest_seconds_total{instance=\"$node\",job=\"$job\", mode=\"user\"}[1m])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[1m])))", "hide": false, "legendFormat": "Guest - Time spent running a virtual CPU for a guest operating system", "range": true, "refId": "A" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "sum by(instance) (irate(node_cpu_guest_seconds_total{instance=\"$node\",job=\"$job\", mode=\"nice\"}[1m])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[1m])))", "hide": false, "legendFormat": "GuestNice - Time spent running a niced guest (virtual CPU for guest operating system)", "range": true, "refId": "B" } ], "title": "CPU spent seconds in guests (VMs)", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "CPU / Memory / Net / Disk", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 21 }, "id": 266, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 54 }, "id": 136, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Inactive_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Inactive - Memory which has been less recently used. It is more eligible to be reclaimed for other purposes", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Active_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Active - Memory that has been used more recently and usually not reclaimed unless absolutely necessary", "refId": "B", "step": 240 } ], "title": "Memory Active / Inactive", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*CommitLimit - *./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 54 }, "id": 135, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Committed_AS_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Committed_AS - Amount of memory presently allocated on the system", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_CommitLimit_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "CommitLimit - Amount of memory currently available to be allocated on the system", "refId": "B", "step": 240 } ], "title": "Memory Committed", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 64 }, "id": 191, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Inactive_file_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Inactive_file - File-backed memory on inactive LRU list", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Inactive_anon_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Inactive_anon - Anonymous and swap cache on inactive LRU list, including tmpfs (shmem)", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Active_file_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Active_file - File-backed memory on active LRU list", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Active_anon_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Active_anon - Anonymous and swap cache on active least-recently-used (LRU) list, including tmpfs", "refId": "D", "step": 240 } ], "title": "Memory Active / Inactive Detail", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 64 }, "id": 130, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Writeback_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Writeback - Memory which is actively being written back to disk", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_WritebackTmp_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "WritebackTmp - Memory used by FUSE for temporary writeback buffers", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Dirty_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Dirty - Memory which is waiting to get written back to the disk", "refId": "C", "step": 240 } ], "title": "Memory Writeback and Dirty", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" }, "properties": [ { "id": "custom.fillOpacity", "value": 0 } ] }, { "matcher": { "id": "byName", "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" }, "properties": [ { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 74 }, "id": 138, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Mapped_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Mapped - Used memory in mapped pages files which have been mapped, such as libraries", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Shmem_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Shmem - Used shared memory (shared between several processes, thus including RAM disks)", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_ShmemHugePages_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_ShmemPmdMapped_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "ShmemPmdMapped - Amount of shared (shmem/tmpfs) memory backed by huge pages", "refId": "D", "step": 240 } ], "title": "Memory Shared and Mapped", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 74 }, "id": 131, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_SUnreclaim_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "SUnreclaim - Part of Slab, that cannot be reclaimed on memory pressure", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_SReclaimable_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "SReclaimable - Part of Slab, that might be reclaimed, such as caches", "refId": "B", "step": 240 } ], "title": "Memory Slab", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 84 }, "id": 70, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_VmallocChunk_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "VmallocChunk - Largest contiguous block of vmalloc area which is free", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_VmallocTotal_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "VmallocTotal - Total size of vmalloc memory area", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_VmallocUsed_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "VmallocUsed - Amount of vmalloc area which is used", "refId": "C", "step": 240 } ], "title": "Memory Vmalloc", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 84 }, "id": 159, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Bounce_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Bounce - Memory used for block device bounce buffers", "refId": "A", "step": 240 } ], "title": "Memory Bounce", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Inactive *./" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 94 }, "id": 129, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_AnonHugePages_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "AnonHugePages - Memory in anonymous huge pages", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_AnonPages_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "AnonPages - Memory in user pages not backed by files", "refId": "B", "step": 240 } ], "title": "Memory Anonymous", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 94 }, "id": 160, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_KernelStack_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "KernelStack - Kernel memory stack. This is not reclaimable", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Percpu_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "PerCPU - Per CPU memory allocated dynamically by loadable modules", "refId": "B", "step": 240 } ], "title": "Memory Kernel / CPU", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "pages", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 104 }, "id": 140, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_HugePages_Free{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "HugePages_Free - Huge pages in the pool that are not yet allocated", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_HugePages_Rsvd{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "HugePages_Rsvd - Huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_HugePages_Surp{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "HugePages_Surp - Huge pages in the pool above the value in /proc/sys/vm/nr_hugepages", "refId": "C", "step": 240 } ], "title": "Memory HugePages Counter", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 104 }, "id": 71, "links": [], "options": { "legend": { "calcs": [ "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_HugePages_Total{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "HugePages - Total size of the pool of huge pages", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Hugepagesize_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Hugepagesize - Huge Page size", "refId": "B", "step": 240 } ], "title": "Memory HugePages Size", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 114 }, "id": 128, "links": [], "options": { "legend": { "calcs": [ "mean", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_DirectMap1G_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "DirectMap1G - Amount of pages mapped as this size", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_DirectMap2M_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "DirectMap2M - Amount of pages mapped as this size", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_DirectMap4k_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "DirectMap4K - Amount of pages mapped as this size", "refId": "C", "step": 240 } ], "title": "Memory DirectMap", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 114 }, "id": 137, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Unevictable_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Unevictable - Amount of unevictable memory that can't be swapped out for a variety of reasons", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_Mlocked_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "MLocked - Size of pages locked to memory using the mlock() system call", "refId": "B", "step": 240 } ], "title": "Memory Unevictable and MLocked", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 124 }, "id": 132, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_memory_NFS_Unstable_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "NFS Unstable - Memory in NFS pages sent to the server, but not yet committed to the storage", "refId": "A", "step": 240 } ], "title": "Memory NFS", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Memory Meminfo", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 22 }, "id": 267, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "pages out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*out/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 41 }, "id": 176, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_pgpgin{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Pagesin - Page in operations", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_pgpgout{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Pagesout - Page out operations", "refId": "B", "step": 240 } ], "title": "Memory Pages In / Out", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "pages out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*out/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 41 }, "id": 22, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_pswpin{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Pswpin - Pages swapped in", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_pswpout{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Pswpout - Pages swapped out", "refId": "B", "step": 240 } ], "title": "Memory Pages Swap In / Out", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "faults", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "Apps" }, "properties": [ { "id": "color", "value": { "fixedColor": "#629E51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A437C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" }, "properties": [ { "id": "color", "value": { "fixedColor": "#CFFAFF", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "RAM_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab" }, "properties": [ { "id": "color", "value": { "fixedColor": "#806EB7", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#2F575E", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Unused" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Pgfault - Page major and minor fault operations" }, "properties": [ { "id": "custom.fillOpacity", "value": 0 }, { "id": "custom.stacking", "value": { "group": false, "mode": "normal" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 51 }, "id": 175, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 350 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Pgfault - Page major and minor fault operations", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Pgmajfault - Major page fault operations", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_pgfault{instance=\"$node\",job=\"$job\"}[$__rate_interval]) - irate(node_vmstat_pgmajfault{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Pgminfault - Minor page fault operations", "refId": "C", "step": 240 } ], "title": "Memory Page Faults", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#99440A", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Buffers" }, "properties": [ { "id": "color", "value": { "fixedColor": "#58140C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6D1F62", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Cached" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Committed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#508642", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Dirty" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Free" }, "properties": [ { "id": "color", "value": { "fixedColor": "#B7DBAB", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Mapped" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "PageTables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Page_Tables" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Slab_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Swap_Cache" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C15C17", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total" }, "properties": [ { "id": "color", "value": { "fixedColor": "#511749", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total RAM + Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#052B51", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Total Swap" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "VmallocUsed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 51 }, "id": 307, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_vmstat_oom_kill{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "oom killer invocations ", "refId": "A", "step": 240 } ], "title": "OOM Killer", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Memory Vmstat", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 23 }, "id": 293, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "seconds", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Variation*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 24 }, "id": 260, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_estimated_error_seconds{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Estimated error in seconds", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_offset_seconds{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Time offset in between local system and reference clock", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_maxerror_seconds{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Maximum error in seconds", "refId": "C", "step": 240 } ], "title": "Time Synchronized Drift", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 24 }, "id": 291, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_loop_time_constant{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Phase-locked loop time adjust", "refId": "A", "step": 240 } ], "title": "Time PLL Adjust", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Variation*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 34 }, "id": 168, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_sync_status{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Is clock synchronized to a reliable server (1 = yes, 0 = no)", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_frequency_adjustment_ratio{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Local clock frequency adjustment", "refId": "B", "step": 240 } ], "title": "Time Synchronized Status", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "seconds", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 34 }, "id": 294, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_tick_seconds{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Seconds between clock ticks", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_timex_tai_offset_seconds{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "International Atomic Time (TAI) offset", "refId": "B", "step": 240 } ], "title": "Time Misc", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "System Timesync", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 }, "id": 312, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 73 }, "id": 62, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_procs_blocked{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Processes blocked waiting for I/O to complete", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_procs_running{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Processes in runnable state", "refId": "B", "step": 240 } ], "title": "Processes Status", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Enable with --collector.processes argument on node-exporter", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 73 }, "id": 315, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_processes_state{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ state }}", "refId": "A", "step": 240 } ], "title": "Processes State", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "forks / sec", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 83 }, "id": 148, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_forks_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Processes forks second", "refId": "A", "step": 240 } ], "title": "Processes Forks", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "decbytes" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Max.*/" }, "properties": [ { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 83 }, "id": 149, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(process_virtual_memory_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Processes virtual memory size in bytes", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "process_resident_memory_max_bytes{instance=\"$node\",job=\"$job\"}", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Maximum amount of virtual memory available in bytes", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(process_virtual_memory_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Processes virtual memory size in bytes", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(process_virtual_memory_max_bytes{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Maximum amount of virtual memory available in bytes", "refId": "D", "step": 240 } ], "title": "Processes Memory", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Enable with --collector.processes argument on node-exporter", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "PIDs limit" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2495C", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 93 }, "id": 313, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_processes_pids{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Number of PIDs", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_processes_max_processes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "PIDs limit", "refId": "B", "step": 240 } ], "title": "PIDs Number and Limit", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "seconds", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*waiting.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 93 }, "id": 305, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_schedstat_running_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "CPU {{ cpu }} - seconds spent running a process", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_schedstat_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "CPU {{ cpu }} - seconds spent by processing waiting for this CPU", "refId": "B", "step": 240 } ], "title": "Process schedule stats Running / Waiting", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Enable with --collector.processes argument on node-exporter", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "Threads limit" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2495C", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 103 }, "id": 314, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_processes_threads{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Allocated threads", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_processes_max_threads{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Threads limit", "refId": "B", "step": 240 } ], "title": "Threads Number and Limit", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "System Processes", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 25 }, "id": 269, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 26 }, "id": 8, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_context_switches_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "Context switches", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_intr_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Interrupts", "refId": "B", "step": 240 } ], "title": "Context Switches / Interrupts", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 26 }, "id": 7, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_load1{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 4, "legendFormat": "Load 1m", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_load5{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 4, "legendFormat": "Load 5m", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_load15{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 4, "legendFormat": "Load 15m", "refId": "C", "step": 240 } ], "title": "System Load", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 0, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "hertz" }, "overrides": [ { "matcher": { "id": "byName", "options": "Max" }, "properties": [ { "id": "custom.lineStyle", "value": { "dash": [ 10, 10 ], "fill": "dash" } }, { "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 10 }, { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": false, "viz": false } }, { "id": "custom.fillBelowTo", "value": "Min" } ] }, { "matcher": { "id": "byName", "options": "Min" }, "properties": [ { "id": "custom.lineStyle", "value": { "dash": [ 10, 10 ], "fill": "dash" } }, { "id": "color", "value": { "fixedColor": "blue", "mode": "fixed" } }, { "id": "custom.hideFrom", "value": { "legend": true, "tooltip": false, "viz": false } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 36 }, "id": 321, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "desc" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "node_cpu_scaling_frequency_hertz{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "CPU {{ cpu }}", "range": true, "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "avg(node_cpu_scaling_frequency_max_hertz{instance=\"$node\",job=\"$job\"})", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Max", "range": true, "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "avg(node_cpu_scaling_frequency_min_hertz{instance=\"$node\",job=\"$job\"})", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Min", "range": true, "refId": "C", "step": 240 } ], "title": "CPU Frequency Scaling", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "https://docs.kernel.org/accounting/psi.html", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 10, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byName", "options": "Memory some" }, "properties": [ { "id": "color", "value": { "fixedColor": "dark-red", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Memory full" }, "properties": [ { "id": "color", "value": { "fixedColor": "light-red", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "I/O some" }, "properties": [ { "id": "color", "value": { "fixedColor": "dark-blue", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "I/O full" }, "properties": [ { "id": "color", "value": { "fixedColor": "light-blue", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 36 }, "id": 322, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "rate(node_pressure_cpu_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "CPU some", "range": true, "refId": "CPU some", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "rate(node_pressure_memory_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Memory some", "range": true, "refId": "Memory some", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "rate(node_pressure_memory_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "Memory full", "range": true, "refId": "Memory full", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "rate(node_pressure_io_waiting_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "I/O some", "range": true, "refId": "I/O some", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "rate(node_pressure_io_stalled_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "I/O full", "range": true, "refId": "I/O full", "step": 240 } ], "title": "Pressure Stall Information", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Enable with --collector.interrupts argument on node-exporter", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Critical*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Max*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 46 }, "id": 259, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_interrupts_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ type }} - {{ info }}", "refId": "A", "step": 240 } ], "title": "Interrupts Detail", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 46 }, "id": 306, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_schedstat_timeslices_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "CPU {{ cpu }}", "refId": "A", "step": 240 } ], "title": "Schedule timeslices executed by each cpu", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 56 }, "id": 151, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_entropy_available_bits{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Entropy available to random number generators", "refId": "A", "step": 240 } ], "title": "Entropy", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "seconds", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 56 }, "id": 308, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(process_cpu_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Time spent", "refId": "A", "step": 240 } ], "title": "CPU time spent in user and system contexts", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Max*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 66 }, "id": 64, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "process_max_fds{instance=\"$node\",job=\"$job\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Maximum open file descriptors", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "process_open_fds{instance=\"$node\",job=\"$job\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Open file descriptors", "refId": "B", "step": 240 } ], "title": "File Descriptors", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "System Misc", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 26 }, "id": 304, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "temperature", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "celsius" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Critical*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Max*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 59 }, "id": 158, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_hwmon_temp_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ chip_name }} {{ sensor }} temp", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_hwmon_temp_crit_alarm_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{ chip_name }} {{ sensor }} Critical Alarm", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_hwmon_temp_crit_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ chip_name }} {{ sensor }} Critical", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_hwmon_temp_crit_hyst_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{ chip_name }} {{ sensor }} Critical Historical", "refId": "D", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_hwmon_temp_max_celsius{instance=\"$node\",job=\"$job\"} * on(chip) group_left(chip_name) node_hwmon_chip_names{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{ chip_name }} {{ sensor }} Max", "refId": "E", "step": 240 } ], "title": "Hardware temperature monitor", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Max*./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 59 }, "id": 300, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_cooling_device_cur_state{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Current {{ name }} in {{ type }}", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_cooling_device_max_state{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Max {{ name }} in {{ type }}", "refId": "B", "step": 240 } ], "title": "Throttle cooling device", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 69 }, "id": 302, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_power_supply_online{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{ power_supply }} online", "refId": "A", "step": 240 } ], "title": "Power supply", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Hardware Misc", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 27 }, "id": 296, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 46 }, "id": 297, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_systemd_socket_accepted_connections_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ name }} Connections", "refId": "A", "step": 240 } ], "title": "Systemd Sockets", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "Failed" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2495C", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Inactive" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FF9830", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Active" }, "properties": [ { "id": "color", "value": { "fixedColor": "#73BF69", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Deactivating" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FFCB7D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "Activating" }, "properties": [ { "id": "color", "value": { "fixedColor": "#C8F2C2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 46 }, "id": 298, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"activating\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Activating", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"active\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Active", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"deactivating\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Deactivating", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"failed\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Failed", "refId": "D", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_systemd_units{instance=\"$node\",job=\"$job\",state=\"inactive\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Inactive", "refId": "E", "step": 240 } ], "title": "Systemd Units State", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Systemd", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 28 }, "id": 270, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "The number (after merges) of I/O requests completed per second for the device", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "IO read (-) / write (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "iops" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Read.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 47 }, "id": 9, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "intervalFactor": 4, "legendFormat": "{{device}} - Reads completed", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{device}} - Writes completed", "refId": "B", "step": 240 } ], "title": "Disk IOps Completed", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "The number of bytes read from or written to the device per second", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes read (-) / write (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "Bps" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Read.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 47 }, "id": 33, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_read_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 4, "legendFormat": "{{device}} - Read bytes", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_written_bytes_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Written bytes", "refId": "B", "step": 240 } ], "title": "Disk R/W Data", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "The average time for requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "time. read (-) / write (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 30, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Read.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 57 }, "id": 37, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_read_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_reads_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "hide": false, "interval": "", "intervalFactor": 4, "legendFormat": "{{device}} - Read wait time avg", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_write_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval]) / irate(node_disk_writes_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}} - Write wait time avg", "refId": "B", "step": 240 } ], "title": "Disk Average Wait Time", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "The average queue length of the requests that were issued to the device", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "aqu-sz", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 57 }, "id": 35, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_io_time_weighted_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "intervalFactor": 4, "legendFormat": "{{device}}", "refId": "A", "step": 240 } ], "title": "Average Queue Size", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "The number of read and write requests merged per second that were queued to the device", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "I/Os", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "iops" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Read.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 67 }, "id": 133, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_reads_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{device}} - Read merged", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_writes_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "intervalFactor": 1, "legendFormat": "{{device}} - Write merged", "refId": "B", "step": 240 } ], "title": "Disk R/W Merged", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Percentage of elapsed time during which I/O requests were issued to the device (bandwidth utilization for the device). Device saturation occurs when this value is close to 100% for devices serving requests serially. But for devices serving requests in parallel, such as RAID arrays and modern SSDs, this number does not reflect their performance limits.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "%util", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 30, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "percentunit" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 67 }, "id": 36, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_io_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "intervalFactor": 4, "legendFormat": "{{device}} - IO", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_discard_time_seconds_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "intervalFactor": 4, "legendFormat": "{{device}} - discard", "refId": "B", "step": 240 } ], "title": "Time Spent Doing I/Os", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "The number of outstanding requests at the instant the sample was taken. Incremented as requests are given to appropriate struct request_queue and decremented as they finish.", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "Outstanding req.", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 77 }, "id": 34, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_disk_io_now{instance=\"$node\",job=\"$job\"}", "interval": "", "intervalFactor": 4, "legendFormat": "{{device}} - IO now", "refId": "A", "step": 240 } ], "title": "Instantaneous Queue Size", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "IOs", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "iops" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*sda_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EAB839", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#6ED0E0", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EF843C", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#584477", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda2_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BA43A9", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sda3_.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F4D598", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#0A50A1", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#BF1B00", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdb3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0752D", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#962D82", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#614D93", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdc3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#9AC48A", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#65C5DB", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9934E", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#EA6460", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde1.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E0F9D7", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sdd2.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#FCEACA", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*sde3.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F9E2D2", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 77 }, "id": 301, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_discards_completed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "intervalFactor": 4, "legendFormat": "{{device}} - Discards completed", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_disk_discards_merged_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "intervalFactor": 1, "legendFormat": "{{device}} - Discards merged", "refId": "B", "step": 240 } ], "title": "Disk IOps Discards completed / merged", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Storage Disk", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 29 }, "id": 271, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 62 }, "id": 43, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_avail_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{mountpoint}} - Available", "metric": "", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_free_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", "format": "time_series", "hide": true, "intervalFactor": 1, "legendFormat": "{{mountpoint}} - Free", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_size_bytes{instance=\"$node\",job=\"$job\",device!~'rootfs'}", "format": "time_series", "hide": true, "intervalFactor": 1, "legendFormat": "{{mountpoint}} - Size", "refId": "C", "step": 240 } ], "title": "Filesystem space available", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "file nodes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 62 }, "id": 41, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_files_free{instance=\"$node\",job=\"$job\",device!~'rootfs'}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{mountpoint}} - Free file nodes", "refId": "A", "step": 240 } ], "title": "File Nodes Free", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "files", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 72 }, "id": 28, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "single", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filefd_maximum{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 4, "legendFormat": "Max open files", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filefd_allocated{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Open files", "refId": "B", "step": 240 } ], "title": "File Descriptor", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "file Nodes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 72 }, "id": 219, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_files{instance=\"$node\",job=\"$job\",device!~'rootfs'}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{mountpoint}} - File nodes total", "refId": "A", "step": 240 } ], "title": "File Nodes Size", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "max": 1, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "/ ReadOnly" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 82 }, "id": 44, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_readonly{instance=\"$node\",job=\"$job\",device!~'rootfs'}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{mountpoint}} - ReadOnly", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_filesystem_device_error{instance=\"$node\",job=\"$job\",device!~'rootfs',fstype!~'tmpfs'}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{mountpoint}} - Device error", "refId": "B", "step": 240 } ], "title": "Filesystem in ReadOnly / Error", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Storage Filesystem", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 30 }, "id": 272, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "pps" }, "overrides": [ { "matcher": { "id": "byName", "options": "receive_packets_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "receive_packets_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "transmit_packets_eth0" }, "properties": [ { "id": "color", "value": { "fixedColor": "#7EB26D", "mode": "fixed" } } ] }, { "matcher": { "id": "byName", "options": "transmit_packets_lo" }, "properties": [ { "id": "color", "value": { "fixedColor": "#E24D42", "mode": "fixed" } } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 47 }, "id": 60, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{device}} - Receive", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_packets_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{device}} - Transmit", "refId": "B", "step": 240 } ], "title": "Network Traffic by Packets", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "pps" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 47 }, "id": 142, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Receive errors", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_errs_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Transmit errors", "refId": "B", "step": 240 } ], "title": "Network Traffic Errors", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "pps" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 57 }, "id": 143, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Receive drop", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_drop_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Transmit drop", "refId": "B", "step": 240 } ], "title": "Network Traffic Drop", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "pps" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 57 }, "id": 141, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Receive compressed", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_compressed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Transmit compressed", "refId": "B", "step": 240 } ], "title": "Network Traffic Compressed", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "pps" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 67 }, "id": 146, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_multicast_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Receive multicast", "refId": "A", "step": 240 } ], "title": "Network Traffic Multicast", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "pps" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 67 }, "id": 144, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Receive fifo", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_fifo_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Transmit fifo", "refId": "B", "step": 240 } ], "title": "Network Traffic Fifo", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "pps" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 77 }, "id": 145, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_receive_frame_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{device}} - Receive frame", "refId": "A", "step": 240 } ], "title": "Network Traffic Frame", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 77 }, "id": 231, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_carrier_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Statistic transmit_carrier", "refId": "A", "step": 240 } ], "title": "Network Traffic Carrier", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Trans.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 87 }, "id": 232, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_network_transmit_colls_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{device}} - Transmit colls", "refId": "A", "step": 240 } ], "title": "Network Traffic Colls", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "entries", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byName", "options": "NF conntrack limit" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 87 }, "id": 61, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_nf_conntrack_entries{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "NF conntrack entries", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_nf_conntrack_entries_limit{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "NF conntrack limit", "refId": "B", "step": 240 } ], "title": "NF Conntrack", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "Entries", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 97 }, "id": 230, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_arp_entries{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{ device }} - ARP entries", "refId": "A", "step": 240 } ], "title": "ARP Entries", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 97 }, "id": 288, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_network_mtu_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{ device }} - Bytes", "refId": "A", "step": 240 } ], "title": "MTU", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 107 }, "id": 280, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_network_speed_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{ device }} - Speed", "refId": "A", "step": 240 } ], "title": "Speed", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packets", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "decimals": 0, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 107 }, "id": 289, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_network_transmit_queue_length{instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{ device }} - Interface transmit queue length", "refId": "A", "step": 240 } ], "title": "Queue Length", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "packetes drop (-) / process (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Dropped.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 117 }, "id": 290, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_softnet_processed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "CPU {{cpu}} - Processed", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_softnet_dropped_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "CPU {{cpu}} - Dropped", "refId": "B", "step": 240 } ], "title": "Softnet Packets", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 117 }, "id": 310, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_softnet_times_squeezed_total{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "CPU {{cpu}} - Squeezed", "refId": "A", "step": 240 } ], "title": "Softnet Out of Quota", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 127 }, "id": 309, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_network_up{operstate=\"up\",instance=\"$node\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{interface}} - Operational state UP", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_network_carrier{instance=\"$node\",job=\"$job\"}", "format": "time_series", "instant": false, "legendFormat": "{{device}} - Physical link state", "refId": "B" } ], "title": "Network Operational Status", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Network Traffic", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 31 }, "id": 273, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 48 }, "id": 63, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_TCP_alloc{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TCP_alloc - Allocated sockets", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_TCP_inuse{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TCP_inuse - Tcp sockets currently in use", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_TCP_mem{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "TCP_mem - Used memory for tcp", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_TCP_orphan{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TCP_orphan - Orphan sockets", "refId": "D", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_TCP_tw{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TCP_tw - Sockets waiting close", "refId": "E", "step": 240 } ], "title": "Sockstat TCP", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 48 }, "id": 124, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_UDPLITE_inuse{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "UDPLITE_inuse - Udplite sockets currently in use", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_UDP_inuse{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "UDP_inuse - Udp sockets currently in use", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_UDP_mem{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "UDP_mem - Used memory for udp", "refId": "C", "step": 240 } ], "title": "Sockstat UDP", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 58 }, "id": 125, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_FRAG_inuse{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "FRAG_inuse - Frag sockets currently in use", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_RAW_inuse{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "RAW_inuse - Raw sockets currently in use", "refId": "C", "step": 240 } ], "title": "Sockstat FRAG / RAW", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "bytes", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 58 }, "id": 220, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_TCP_mem_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "mem_bytes - TCP sockets in that state", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_UDP_mem_bytes{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "mem_bytes - UDP sockets in that state", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_FRAG_memory{instance=\"$node\",job=\"$job\"}", "interval": "", "intervalFactor": 1, "legendFormat": "FRAG_memory - Used memory for frag", "refId": "C" } ], "title": "Sockstat Memory Size", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "sockets", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 68 }, "id": 126, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_sockstat_sockets_used{instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sockets_used - Sockets currently in use", "refId": "A", "step": 240 } ], "title": "Sockstat Used", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Network Sockstat", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 32 }, "id": 274, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "octets out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Out.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 33 }, "id": 221, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_IpExt_InOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InOctets - Received octets", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_IpExt_OutOctets{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "intervalFactor": 1, "legendFormat": "OutOctets - Sent octets", "refId": "B", "step": 240 } ], "title": "Netstat IP In / Out Octets", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "datagrams", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 33 }, "id": 81, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true, "width": 300 }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Ip_Forwarding{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Forwarding - IP forwarding", "refId": "A", "step": 240 } ], "title": "Netstat IP Forwarding", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "messages out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Out.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 43 }, "id": 115, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Icmp_InMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InMsgs - Messages which the entity received. Note that this counter includes all those counted by icmpInErrors", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Icmp_OutMsgs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "OutMsgs - Messages which this entity attempted to send. Note that this counter includes all those counted by icmpOutErrors", "refId": "B", "step": 240 } ], "title": "ICMP In / Out", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "messages out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Out.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 43 }, "id": 50, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Icmp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InErrors - Messages which the entity received but determined as having ICMP-specific errors (bad ICMP checksums, bad length, etc.)", "refId": "A", "step": 240 } ], "title": "ICMP Errors", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "datagrams out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Out.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Snd.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 53 }, "id": 55, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Udp_InDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InDatagrams - Datagrams received", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Udp_OutDatagrams{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "OutDatagrams - Datagrams sent", "refId": "B", "step": 240 } ], "title": "UDP In / Out", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "datagrams", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 53 }, "id": 109, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Udp_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InErrors - UDP Datagrams that could not be delivered to an application", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Udp_NoPorts{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "NoPorts - UDP Datagrams received on a port with no listener", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_UdpLite_InErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "legendFormat": "InErrors Lite - UDPLite Datagrams that could not be delivered to an application", "refId": "C" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Udp_RcvbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "RcvbufErrors - UDP buffer errors received", "refId": "D", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Udp_SndbufErrors{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "SndbufErrors - UDP buffer errors send", "refId": "E", "step": 240 } ], "title": "UDP Errors", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "datagrams out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Out.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] }, { "matcher": { "id": "byRegexp", "options": "/.*Snd.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 63 }, "id": 299, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Tcp_InSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "InSegs - Segments received, including those received in error. This count includes segments received on currently established connections", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Tcp_OutSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "OutSegs - Segments sent, including those on current connections but excluding those containing only retransmitted octets", "refId": "B", "step": 240 } ], "title": "TCP In / Out", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 63 }, "id": 104, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_TcpExt_ListenOverflows{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "ListenOverflows - Times the listen queue of a socket overflowed", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_TcpExt_ListenDrops{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "ListenDrops - SYNs to LISTEN sockets ignored", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_TcpExt_TCPSynRetrans{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TCPSynRetrans - SYN-SYN/ACK retransmits to break down retransmissions in SYN, fast/timeout retransmits", "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Tcp_RetransSegs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "legendFormat": "RetransSegs - Segments retransmitted - that is, the number of TCP segments transmitted containing one or more previously transmitted octets", "refId": "D" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Tcp_InErrs{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "legendFormat": "InErrs - Segments received in error (e.g., bad TCP checksums)", "refId": "E" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Tcp_OutRsts{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "interval": "", "legendFormat": "OutRsts - Segments sent with RST flag", "refId": "F" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "irate(node_netstat_TcpExt_TCPRcvQDrop{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "hide": false, "interval": "", "legendFormat": "TCPRcvQDrop - Packets meant to be queued in rcv queue but dropped because socket rcvbuf limit hit", "range": true, "refId": "G" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "irate(node_netstat_TcpExt_TCPOFOQueue{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "hide": false, "interval": "", "legendFormat": "TCPOFOQueue - TCP layer receives an out of order packet and has enough memory to queue it", "range": true, "refId": "H" } ], "title": "TCP Errors", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "connections", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*MaxConn *./" }, "properties": [ { "id": "color", "value": { "fixedColor": "#890F02", "mode": "fixed" } }, { "id": "custom.fillOpacity", "value": 0 } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 73 }, "id": 85, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_netstat_Tcp_CurrEstab{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "CurrEstab - TCP connections for which the current state is either ESTABLISHED or CLOSE- WAIT", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_netstat_Tcp_MaxConn{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "MaxConn - Limit on the total number of TCP connections the entity can support (Dynamic is \"-1\")", "refId": "B", "step": 240 } ], "title": "TCP Connections", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter out (-) / in (+)", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*Sent.*/" }, "properties": [ { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 73 }, "id": 91, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_TcpExt_SyncookiesFailed{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "SyncookiesFailed - Invalid SYN cookies received", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_TcpExt_SyncookiesRecv{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "SyncookiesRecv - SYN cookies received", "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_TcpExt_SyncookiesSent{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "SyncookiesSent - SYN cookies sent", "refId": "C", "step": 240 } ], "title": "TCP SynCookie", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "connections", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 83 }, "id": 82, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Tcp_ActiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "ActiveOpens - TCP connections that have made a direct transition to the SYN-SENT state from the CLOSED state", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "irate(node_netstat_Tcp_PassiveOpens{instance=\"$node\",job=\"$job\"}[$__rate_interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "PassiveOpens - TCP connections that have made a direct transition to the SYN-RCVD state from the LISTEN state", "refId": "B", "step": 240 } ], "title": "TCP Direct Transition", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "Enable with --collector.tcpstat argument on node-exporter", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "connections", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 83 }, "id": 320, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "node_tcp_connection_states{state=\"established\",instance=\"$node\",job=\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "established - TCP sockets in established state", "range": true, "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "node_tcp_connection_states{state=\"fin_wait2\",instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "fin_wait2 - TCP sockets in fin_wait2 state", "range": true, "refId": "B", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "node_tcp_connection_states{state=\"listen\",instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "listen - TCP sockets in listen state", "range": true, "refId": "C", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "editorMode": "code", "expr": "node_tcp_connection_states{state=\"time_wait\",instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "time_wait - TCP sockets in time_wait state", "range": true, "refId": "D", "step": 240 } ], "title": "TCP Stat", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Network Netstat", "type": "row" }, { "collapsed": true, "datasource": { "type": "prometheus", "uid": "000000001" }, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 33 }, "id": 279, "panels": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "seconds", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "normal" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 10, "w": 12, "x": 0, "y": 66 }, "id": 40, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_scrape_collector_duration_seconds{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{collector}} - Scrape duration", "refId": "A", "step": 240 } ], "title": "Node Exporter Scrape Time", "type": "timeseries" }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "description": "", "fieldConfig": { "defaults": { "color": { "mode": "palette-classic" }, "custom": { "axisCenteredZero": false, "axisColorMode": "text", "axisLabel": "counter", "axisPlacement": "auto", "barAlignment": 0, "drawStyle": "line", "fillOpacity": 20, "gradientMode": "none", "hideFrom": { "legend": false, "tooltip": false, "viz": false }, "lineInterpolation": "linear", "lineStyle": { "fill": "solid" }, "lineWidth": 1, "pointSize": 5, "scaleDistribution": { "type": "linear" }, "showPoints": "never", "spanNulls": false, "stacking": { "group": "A", "mode": "none" }, "thresholdsStyle": { "mode": "off" } }, "links": [], "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green" }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [ { "matcher": { "id": "byRegexp", "options": "/.*error.*/" }, "properties": [ { "id": "color", "value": { "fixedColor": "#F2495C", "mode": "fixed" } }, { "id": "custom.transform", "value": "negative-Y" } ] } ] }, "gridPos": { "h": 10, "w": 12, "x": 12, "y": 66 }, "id": 157, "links": [], "options": { "legend": { "calcs": [ "mean", "lastNotNull", "max", "min" ], "displayMode": "table", "placement": "bottom", "showLegend": true }, "tooltip": { "mode": "multi", "sort": "none" } }, "pluginVersion": "9.2.0", "targets": [ { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_scrape_collector_success{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{collector}} - Scrape success", "refId": "A", "step": 240 }, { "datasource": { "type": "prometheus", "uid": "${datasource}" }, "expr": "node_textfile_scrape_error{instance=\"$node\",job=\"$job\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{collector}} - Scrape textfile error (1 = true)", "refId": "B", "step": 240 } ], "title": "Node Exporter Scrape", "type": "timeseries" } ], "targets": [ { "datasource": { "type": "prometheus", "uid": "000000001" }, "refId": "A" } ], "title": "Node Exporter", "type": "row" } ], "refresh": "1m", "revision": 1, "schemaVersion": 38, "style": "dark", "tags": [ "linux" ], "templating": { "list": [ { "current": { "selected": false, "text": "default", "value": "default" }, "hide": 0, "includeAll": false, "label": "Datasource", "multi": false, "name": "datasource", "options": [], "query": "prometheus", "queryValue": "", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${datasource}" }, "definition": "", "hide": 0, "includeAll": false, "label": "Job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(node_uname_info, job)", "refId": "Prometheus-job-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": {}, "datasource": { "type": "prometheus", "uid": "${datasource}" }, "definition": "label_values(node_uname_info{job=\"$job\"}, instance)", "hide": 0, "includeAll": false, "label": "Host", "multi": false, "name": "node", "options": [], "query": { "query": "label_values(node_uname_info{job=\"$job\"}, instance)", "refId": "Prometheus-node-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": false, "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" }, "hide": 2, "includeAll": false, "multi": false, "name": "diskdevices", "options": [ { "selected": true, "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" } ], "query": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", "skipUrlSync": false, "type": "custom" } ] }, "time": { "from": "now-24h", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ], "time_options": [ "5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d" ] }, "timezone": "browser", "title": "Node Exporter Full", "uid": "rYdddlPWk", "version": 92, "weekStart": "" } ================================================ FILE: sysadmin-tools/grafana/configs/grafana/datasources/datasources.yaml ================================================ apiVersion: 1 datasources: - name: Prometheus type: prometheus url: http://fptn-prometheus:9090 access: proxy ================================================ FILE: sysadmin-tools/grafana/configs/nginx/nginx.conf.template ================================================ server { listen 80; location / { proxy_pass https://FPTN_HOST_PORT; # WILL REPLACE proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_ssl_verify off; } } ================================================ FILE: sysadmin-tools/grafana/configs/prometheus/prometheus.yaml.template ================================================ global: scrape_interval: 15s scrape_configs: - job_name: 'fptn-server' scrape_interval: 30s metrics_path: '/metrics' params: host: ['FPTN_HOST'] port: ['FPTN_PORT'] key: ['PROMETHEUS_SECRET_ACCESS_KEY'] static_configs: - targets: ['fptn-proxy-server:80'] - job_name: 'node' scrape_interval: 30s static_configs: - targets: ['fptn-node-exporter:9100'] ================================================ FILE: sysadmin-tools/grafana/docker-compose.build.yml ================================================ volumes: fptn_grafana_data: {} fptn_prometheus_data: {} services: fptn-grafana: image: grafana/grafana:11.2.0 restart: unless-stopped ports: - ${GRAFANA_PORT}:3000 volumes: - fptn_grafana_data:/var/lib/grafana - ./configs/grafana/dashboards:/etc/grafana/provisioning/dashboards - ./configs/grafana/datasources:/etc/grafana/provisioning/datasources fptn-prometheus: image: prom/prometheus:v2.54.1 restart: unless-stopped environment: - PROMETHEUS_SECRET_ACCESS_KEY=${PROMETHEUS_SECRET_ACCESS_KEY} - FPTN_HOST=${FPTN_HOST} - FPTN_PORT=${FPTN_PORT} entrypoint: - sh - -c - | sed -e "s|PROMETHEUS_SECRET_ACCESS_KEY|${PROMETHEUS_SECRET_ACCESS_KEY}|g" \ -e "s|FPTN_HOST|${FPTN_HOST}|g" \ -e "s|FPTN_PORT|${FPTN_PORT}|g" \ /etc/prometheus/prometheus.yaml.template > /etc/prometheus/prometheus.yaml exec prometheus --config.file=/etc/prometheus/prometheus.yaml --storage.tsdb.retention.time=30d volumes: - fptn_prometheus_data:/prometheus - ./configs/prometheus/prometheus.yaml.template:/etc/prometheus/prometheus.yaml.template fptn-node-exporter: image: prom/node-exporter:v1.8.2 restart: unless-stopped command: '--path.rootfs=/host' pid: host volumes: - /:/host:ro,rslave fptn-proxy-server: build: proxy-server container_name: fptn-proxy-server restart: unless-stopped command: /usr/bin/fptn-proxy --listen-port 80 ================================================ FILE: sysadmin-tools/grafana/docker-compose.yml ================================================ volumes: fptn_grafana_data: {} fptn_prometheus_data: {} services: fptn-grafana: image: grafana/grafana:11.2.0 restart: unless-stopped ports: - ${GRAFANA_PORT}:3000 volumes: - fptn_grafana_data:/var/lib/grafana - ./configs/grafana/dashboards:/etc/grafana/provisioning/dashboards - ./configs/grafana/datasources:/etc/grafana/provisioning/datasources fptn-prometheus: image: prom/prometheus:v2.54.1 restart: unless-stopped environment: - PROMETHEUS_SECRET_ACCESS_KEY=${PROMETHEUS_SECRET_ACCESS_KEY} - FPTN_HOST=${FPTN_HOST} - FPTN_PORT=${FPTN_PORT} entrypoint: - sh - -c - | sed -e "s|PROMETHEUS_SECRET_ACCESS_KEY|${PROMETHEUS_SECRET_ACCESS_KEY}|g" \ -e "s|FPTN_HOST|${FPTN_HOST}|g" \ -e "s|FPTN_PORT|${FPTN_PORT}|g" \ /etc/prometheus/prometheus.yaml.template > /etc/prometheus/prometheus.yaml exec prometheus --config.file=/etc/prometheus/prometheus.yaml --storage.tsdb.retention.time=30d volumes: - fptn_prometheus_data:/prometheus - ./configs/prometheus/prometheus.yaml.template:/etc/prometheus/prometheus.yaml.template fptn-node-exporter: image: prom/node-exporter:v1.8.2 restart: unless-stopped command: '--path.rootfs=/host' pid: host volumes: - /:/host:ro,rslave fptn-proxy-server: image: fptnvpn/fptn-proxy-server:latest restart: unless-stopped command: /usr/bin/fptn-proxy --listen-port 80 ================================================ FILE: sysadmin-tools/grafana/proxy-server/.dockerignore ================================================ CMakeUserPresets.json libs/ build/ cmake-build-debug/ ================================================ FILE: sysadmin-tools/grafana/proxy-server/.gitignore ================================================ libs/ cmake-build-debug/ .env docker-compose-data/ CMakeUserPresets.json build/ ================================================ FILE: sysadmin-tools/grafana/proxy-server/CMakeLists.txt ================================================ cmake_minimum_required(VERSION 3.22.1) project("fptn-proxy") add_executable(${CMAKE_PROJECT_NAME} src/proxy-server.cpp) set_target_properties( ${CMAKE_PROJECT_NAME} PROPERTIES CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF ) # Rest of your CMake code... add_definitions(-DBOOST_PROCESS_V2_HEADER_ONLY) add_definitions(-DBOOST_ASIO_HAS_CO_AWAIT) add_definitions(-DBOOST_ASIO_HAS_CO_SPAWN) add_definitions(-DBOOST_ASIO_HAS_COROUTINES) set(CMAKE_INCLUDE_CURRENT_DIR ON) if (CMAKE_CXX_COMPILER_ID STREQUAL "Clang") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-unused-command-line-argument") endif () find_package(fptn REQUIRED) find_package(nlohmann_json REQUIRED) find_package(fmt REQUIRED) find_package(httplib REQUIRED) find_package(argparse REQUIRED) find_package(protobuf REQUIRED) find_package(absl REQUIRED) find_package(re2 REQUIRED) find_package(spdlog REQUIRED) find_package(OpenSSL REQUIRED) message(STATUS "fptn_FOUND: ${fptn_FOUND}") message(STATUS "fptn_INCLUDE_DIRS: ${fptn_INCLUDE_DIRS}") message(STATUS "fptn_LIBRARIES: ${fptn_LIBRARIES}") # disable pcap++ target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE FPTN_IP_ADDRESS_WITHOUT_PCAP ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE src/ "${CMAKE_CURRENT_SOURCE_DIR}/libs/fptn/src/" "${CMAKE_CURRENT_SOURCE_DIR}/libs/fptn/src/fptn-protocol-lib/" ) target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE ${fptn_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/src ) target_link_libraries(${CMAKE_PROJECT_NAME} PRIVATE fptn::fptn argparse::argparse httplib::httplib nlohmann_json::nlohmann_json fmt::fmt protobuf::libprotobuf re2::re2 spdlog::spdlog OpenSSL::SSL OpenSSL::Crypto pthread dl rt ) if(APPLE) target_link_options(${CMAKE_PROJECT_NAME} PRIVATE -framework CoreFoundation -framework Security -framework SystemConfiguration ) endif() ================================================ FILE: sysadmin-tools/grafana/proxy-server/Dockerfile ================================================ # --- Stage 1: Building --- FROM ubuntu:24.04 AS build ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC RUN apt-get update \ && apt-get upgrade -y \ && apt-get install -y cmake gcc g++ python3 python3-pip git \ && pip install conan==2.22.2 numpy --break-system-packages RUN conan profile detect --force RUN mkdir -p /code WORKDIR /code RUN git clone https://github.com/batchar2/fptn fptn-project \ && cp -rv fptn-project/.conan .conan \ && echo 2 COPY ./conanfile.py /code/ RUN conan install . --output-folder=build --build=missing -s compiler.cppstd=20 --settings build_type=Release COPY ./CMakeLists.txt /code/ COPY ./src/ /code/src/ RUN cd build \ && cmake .. -DCMAKE_TOOLCHAIN_FILE=build/Release/generators/conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release \ && cmake --build . --config Release # --- Stage 2: Runtime image --- FROM ubuntu:24.04 ENV DEBIAN_FRONTEND=noninteractive ENV TZ=Etc/UTC COPY --from=build /code/build//fptn-proxy /usr/bin/ RUN chmod +x /usr/bin/fptn-proxy ================================================ FILE: sysadmin-tools/grafana/proxy-server/README.md ================================================ ### proxy-server This service acts as a proxy for forwarding requests to the API of the `fptn-server`. The reason for this is that the `fptn-server` uses a custom TLS handshake, which this proxy handles correctly. It also retrieves Prometheus metrics from the server and exposes them in a format that Prometheus can consume. This service is built using Docker. Check the [manual](../README.md) for instructions on how to build it. ================================================ FILE: sysadmin-tools/grafana/proxy-server/conanfile.py ================================================ import os import subprocess import shutil from pathlib import Path from conan import ConanFile from conan.tools.cmake import CMakeToolchain, CMake, cmake_layout from conan.tools.files import copy from conan.tools.scm import Git class FptnProxy(ConanFile): name = "fptn-lib" version = "0.0.0" settings = ( "os", "arch", "compiler", "build_type", ) requires = ( "argparse/3.2", "cpp-httplib/0.30.0", "fmt/12.1.0", "nlohmann_json/3.12.0", ) generators = ("CMakeDeps",) default_options = { "*:fPIC": True, "*:shared": False, "fptn/*:build_only_fptn_lib": True, "fptn/*:with_gui_client": False, } def requirements(self): self._register_local_recipe("fptn", "fptn", "0.0.0") def layout(self): cmake_layout(self) def generate(self): tc = CMakeToolchain(self) if "fptn" in self.dependencies: fptn_dep = self.dependencies["fptn"] tc.variables["FPTN_INCLUDE_DIR"] = fptn_dep.cpp_info.includedirs[0] if fptn_dep.cpp_info.includedirs else "" tc.variables["FPTN_LIBRARIES"] = fptn_dep.cpp_info.libs[0] if fptn_dep.cpp_info.libs else "fptn" if fptn_dep.cpp_info.libdirs: tc.variables["FPTN_LIBRARY_DIR"] = fptn_dep.cpp_info.libdirs[0] tc.generate() def build(self): cmake = CMake(self) cmake.configure() cmake.build() def config_options(self): pass def _clone_fptn(self): fptn_path = Path(__file__).parent / "libs" / "fptn" if fptn_path.exists(): shutil.rmtree(fptn_path) self.output.info("Cloning fptn repository...") git = Git(self) git.clone(url="https://github.com/batchar2/fptn.git", target=fptn_path.as_posix()) def _register_local_recipe(self, recipe, name, version, override=False, force=False): self._clone_fptn() script_dir = os.path.dirname(os.path.abspath(__file__)) recipe_rel_path = os.path.join(script_dir, "libs", "fptn") if os.path.exists(recipe_rel_path): self.output.info(f"Exporting local recipe: {recipe_rel_path}") subprocess.run( [ "conan", "export", recipe_rel_path, f"--name={name}", f"--version={version}", "--user=local", "--channel=local", ], check=True, cwd=script_dir, ) self.requires(f"{name}/{version}@local/local", override=override, force=force) else: self.output.warning(f"Recipe path not found: {recipe_rel_path}") ================================================ FILE: sysadmin-tools/grafana/proxy-server/src/proxy-server.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #define CPPHTTPLIB_OPENSSL_SUPPORT #define CPPHTTPLIB_OPENSSL_SUPPORT #include // NOLINT(build/include_order) #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) void run_server(const int port) { httplib::Server server; server.Get("/metrics", [](const httplib::Request& req, httplib::Response& res) { const std::string host = req.get_param_value("host"); const std::string port_str = req.get_param_value("port"); const std::string key = req.get_param_value("key"); if (host.empty() || port_str.empty() || key.empty()) { res.status = 400; res.body = "Missing required parameters: host, port, key"; return; } int port = 443; try { port = std::stoi(port_str); } catch (...) { res.status = 400; res.body = "Invalid port number"; return; } try { const std::string target_url = "/api/v1/metrics/" + key; std::cout << "Proxying to: " << "https://" << host + ":" << port_str << target_url << std::endl; fptn::protocol::https::ApiClient client( host, port, fptn::protocol::https::CensorshipStrategy::kSni); auto response = client.Get(target_url); res.status = response.code; res.body = response.body; } catch (const std::exception& e) { res.status = 500; res.body = std::string("Proxy error: ") + e.what(); } }); server.listen("0.0.0.0", port); } int main(int argc, char* argv[]) { // Proxy server for forwarding requests to external hosts // Request format: http://localhost:8080/?host=&port=&key= // Example: http://localhost:8080/?host=192.168.1.100&port=443&key=abc123 argparse::ArgumentParser args("http-proxy", "1.0.1"); args.add_argument("--listen-port") .help("Port to listen on (default: 8080)") .default_value(8080) .scan<'i', int>(); try { args.parse_args(argc, argv); const auto listen_port = args.get("--listen-port"); run_server(listen_port); } catch (const std::exception& err) { std::cerr << "Error: " << err.what() << std::endl; std::cerr << args; return EXIT_FAILURE; } return EXIT_SUCCESS; } ================================================ FILE: sysadmin-tools/telegram-bot/.gitignore ================================================ logs/ configs/users.list configs/servers.json configs/premium_servers.json configs/servers_censored_zone.json # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/latest/usage/project/#working-with-version-control .pdm.toml .pdm-python .pdm-build/ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ ================================================ FILE: sysadmin-tools/telegram-bot/Dockerfile ================================================ FROM python:3.13-slim RUN mkdir -p /app/src WORKDIR /app COPY ./src/requirements.txt /app/src/requirements.txt RUN pip install --no-cache-dir -r /app/src/requirements.txt CMD ["python", "/app/src/bot.py"] ================================================ FILE: sysadmin-tools/telegram-bot/README.md ================================================ ## Telegram Bot This guide will help you build, configure, and run your Telegram bot using Docker. 0. **Prerequisites** Docker and Docker Compose should be installed on your system. To install it on ubuntu use [this docs](https://docs.docker.com/engine/install/ubuntu/) 1. **Clone the repository:** Clone the repository to any location on your server: ```bash git clone https://github.com/batchar2/fptn.git ``` 2. **Navigate to the Grafana configuration folder:** ```bash cd sysadmin-tools/telegram-bot ``` 3. **Build the Bot** To build the Docker image for your Telegram bot, run the following command: ``` docker compose build ``` 4. **Create the Configuration File:** Copy the example environment file and rename it: ```bash cp .env.demo .env ``` 5. **Edit the .env File**: To configure your bot, you'll need to edit the .env file. This file contains sensitive information required for the bot to function properly. Follow these steps to set it up: - Open the .env File: - Use a text editor to open the .env file. You can find this file in the root directory of your project. - Insert Your Bot's API Token: - Set `TELEGRAM_API_TOKEN` environment variable with your Telegram bot API token. This token is required for the bot to connect to Telegram's servers. You can obtain your API token from Telegram by using the [BotFather](https://t.me/BotFather) bot. - Set the Welcome Message (Optional): - Modify the `FPTN_WELCOME_MESSAGE` variable to customize the greeting message that your bot will send to users. - Set Maximum User Speed Limit: - Modify the `MAX_USER_SPEED_LIMIT` variable to define the maximum speed limit for users in megabits per second (Mbps). This setting controls the bandwidth cap applied to individual users. Adjust the value based on your requirements. - Configure Service Name: - Set `SERVICE_NAME` to your preferred service name (appears in logs and messages). - Enable Brotli Compression: - Set `ENABLE_BROTLI_COMPRESSION` to `true` for smaller tokens or `false` to disable. - Configure Configuration Folder Path: Set `FPTN_CONFIGS_FOLDER` to specify the directory where all configuration files are stored. - Default value: `./configs` (relative path to the project root) - Alternative: Use an absolute path like `/etc/fptn` to point to the folder where the FPTN server keeps user data - This folder will be mounted to `/etc/fptn` inside the Docker container - **All configuration files must be placed in this directory:** - `servers.json` - public servers list - `servers_censored_zone.json` - censored region servers - `users.list` - user database - `premium_servers.json` - premium servers (optional) Example `.env` configuration: ```bash # Telegram bot API token TELEGRAM_API_TOKEN=your_actual_api_token_here # Welcome messages for the bot FPTN_WELCOME_MESSAGE_EN = "⚡ Welcome to the FPTN bot! ⚡ \n Use this bot to get a VPN access token or reset it. \n\n 🌐_ You can download the client from the official project website _ [https://storage.googleapis.com/fptn.org/index.html](https://storage.googleapis.com/fptn.org/index.html) \n\n 👉_ To get your connection token, just type the command: _ /token \n " FPTN_WELCOME_MESSAGE_RU = "⚡ Добро пожаловать в бот FPTN! ⚡ \n Этот бот позволяет получить токен доступа к VPN или сбросить его. \n\n 🌐_ Клиент можно скачать с официального сайта проекта _ [https://storage.googleapis.com/fptn.org/index.html](https://storage.googleapis.com/fptn.org/index.html) \n\n 👉_ Чтобы получить токен для подключения, просто введите команду: _ /token \n " # Enable Brotli compression for smaller tokens ENABLE_BROTLI_COMPRESSION=true # Maximum speed limit for new users in Mbps. MAX_USER_SPEED_LIMIT=20 # name of service SERVICE_NAME=FPTN.ONLINE # Path to the FPTN configuration folder on the host machine # This folder will be mounted to /etc/fptn inside the container # Recommended: ./configs (relative path) or /etc/fptn (absolute path) # Create this folder and place all config files here: # - servers.json # - servers_censored_zone.json # - users.list # - premium_servers.json FPTN_CONFIGS_FOLDER=./configs ``` 6. **Initialize Configuration Folder** Important: Before running the bot, you must set up the configuration folder and populate it with necessary files. ```bash # Create the configuration folder (matches FPTN_CONFIGS_FOLDER in .env) cd ./configs # Copy demo server configurations to the config folder cp servers.json.demo servers.json cp premium_servers.json.demo premium_servers.json cp servers_censored_zone.json.demo servers_censored_zone.json # Create empty required files touch ./configs/users.list touch ./configs/premium_servers.json # Verify the folder structure ls -la ./configs/ ``` Expected folder structure after setup: ```bash telegram-bot/ ├── docker-compose.yml ├── .env ├── Dockerfile ├── logs/ └── configs/ # ← ALL CONFIGURATION FILES HERE ├── servers.json # Public servers list ├── servers_censored_zone.json # Censored region servers ├── premium_servers.json # Premium servers list └── users.list # User database ``` 7. **🟢 Configure Public Servers**: To set up your server list, follow these steps: Start by copying the demo server list: ```bash cp servers.json.demo server.json ``` Then open `servers.json` in any text editor and replace the example entries with your actual server information: - name: A label for your server (any value). - host: The public IP address or hostname of your server. - port: The public port your VPN server listens on. - md5_fingerprint: The MD5 fingerprint of your server's TLS certificate. To get the fingerprint, run this command on the server: ```bash openssl x509 -noout -fingerprint -md5 -in /etc/fptn/server.crt | cut -d'=' -f2 | tr -d ':' | tr 'A-F' 'a-f' ``` Copy the value and paste it into the md5_fingerprint field. 8. **🔴 Configure Servers for Censored Regions**: Copy the demo configuration: ```bash cp servers_censored_zone.json.demo servers_censored_zone.json ``` Then open `servers_censored_zone.json` and edit it the same way as `servers.json`, using server details intended for restricted or high-surveillance regions. 9. **⚡ Configure Premium Servers**: - Premium servers have the same structure as regular servers - These servers are only accessible to premium users - Can be used for higher speeds, special locations, or better performance 10. **Premium User Identification in `users.list` File** Premium users are identified by a special flag in the users.list file. Here's how it works: File Format. Each line in users.list follows this format: ```bash username password_hash speed_limit premium_flag ... user00001 213098467123094612309846 100 1 user00002 321o32908237249384233232 100 0 ``` Premium Flag Values: - 0 = Regular user (not premium) - 1 = Premium user 10. **Run the Bot** After setting up the environment file, start the bot with: ```bash docker compose build docker compose up -d ``` This command will start the bot in detached mode, allowing it to run in the background. 11. **Stop the Bot** To stop the bot, use: ```bash docker compose down ``` This will stop and remove the running containers associated with your bot. ================================================ FILE: sysadmin-tools/telegram-bot/configs/premium_servers.json.demo ================================================ [ { "name": "[Premium] Server 1", "host": "127.0.0.1", "md5_fingerprint": "", "port": 443 } ] ================================================ FILE: sysadmin-tools/telegram-bot/configs/servers.json.demo ================================================ [ { "name": "ServerName1", "host": "127.0.0.1", "md5_fingerprint": "", "port": 443 }, { "name": "ServerName2", "host": "127.0.0.1", "md5_fingerprint": "", "port": 443 } ] ================================================ FILE: sysadmin-tools/telegram-bot/configs/servers_censored_zone.json.demo ================================================ [ ] ================================================ FILE: sysadmin-tools/telegram-bot/docker-compose.yml ================================================ services: telegram-bot: restart: unless-stopped build: context: ./ dockerfile: Dockerfile environment: - TELEGRAM_API_TOKEN=${TELEGRAM_API_TOKEN} - FPTN_WELCOME_MESSAGE_EN=${FPTN_WELCOME_MESSAGE_EN} - FPTN_WELCOME_MESSAGE_RU=${FPTN_WELCOME_MESSAGE_RU} - MAX_USER_SPEED_LIMIT=${MAX_USER_SPEED_LIMIT} - SERVICE_NAME=${SERVICE_NAME} - ENABLE_BROTLI_COMPRESSION=${ENABLE_BROTLI_COMPRESSION} - FPTN_CONFIGS_FOLDER=${FPTN_CONFIGS_FOLDER} volumes: - ./src/:/app/src:ro - ./logs:/var/log/fptn_bot:rw - "${FPTN_CONFIGS_FOLDER}:/etc/fptn:rw" ================================================ FILE: sysadmin-tools/telegram-bot/src/bot.py ================================================ import json import os import sys import base64 import random import string import hashlib import tempfile import threading from pathlib import Path import brotli from loguru import logger from telegram import Update, ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove from telegram.constants import ParseMode from telegram.ext import ( Application, CommandHandler, MessageHandler, filters, CallbackContext, ) TELEGRAM_API_TOKEN = os.getenv("TELEGRAM_API_TOKEN") FPTN_WELCOME_MESSAGE_EN = os.getenv("FPTN_WELCOME_MESSAGE_EN", "") FPTN_WELCOME_MESSAGE_RU = os.getenv("FPTN_WELCOME_MESSAGE_RU", "") MAX_USER_SPEED_LIMIT = int(os.getenv("MAX_USER_SPEED_LIMIT")) SERVICE_NAME = os.getenv("SERVICE_NAME") USERS_FILE = Path(os.getenv("USERS_FILE", "/etc/fptn/users.list")) SERVERS_LIST_FILE = os.getenv("SERVERS_LIST_FILE", "/etc/fptn/servers.json") PREMIUM_SERVERS_FILE = os.getenv("PREMIUM_SERVERS_FILE", "/etc/fptn/premium_servers.json") SERVERS_CENSORED_LIST_FILE = os.getenv("SERVERS_CENSORED_LIST_FILE", "/etc/fptn/servers_censored_zone.json") ENABLE_BROTLI_COMPRESSION = os.getenv("ENABLE_BROTLI_COMPRESSION", "false").lower() == "true" if os.path.exists(PREMIUM_SERVERS_FILE): with open(PREMIUM_SERVERS_FILE, "r") as fp: PREMIUM_SERVERS = json.load(fp) else: PREMIUM_SERVERS = [] with open(SERVERS_LIST_FILE, "r") as fp: SERVERS_LIST = json.load(fp) if os.path.exists(SERVERS_CENSORED_LIST_FILE): with open(SERVERS_CENSORED_LIST_FILE, "r") as fp: SERVERS_CENSORED_LIST = json.load(fp) else: SERVERS_CENSORED_LIST = [] def init_logger(): logger.remove() # Remove default logger log_file = Path(os.getenv("LOG_FILE", "/var/log/fptn_bot.log")) log_file.parent.mkdir(parents=True, exist_ok=True) logger.add( str(log_file), level="INFO", format="{time} - {level} - {message}", rotation="1 MB", ) logger.add(sys.stdout, level="INFO", format="{time} - {level} - {message}") class UserManager: def __init__(self, users_file: Path): self.users_file = users_file self.user_data_lock = threading.Lock() def _generate_password(self, length=8) -> str: return "".join(random.choice(string.ascii_letters) for _ in range(length)) def _hash_password(self, password: str) -> str: sha256 = hashlib.sha256() sha256.update(password.encode("utf-8")) return sha256.hexdigest() def load_users(self) -> dict: users = {} if self.users_file.exists(): with self.users_file.open("r") as file: for line in file: parts = line.strip().split() if len(parts) == 4: username, hashed_password, speed, is_premium = parts[0], parts[1], parts[2], parts[3] == "1" elif len(parts) == 3: username, hashed_password, speed, is_premium = parts[0], parts[1], parts[2], False users[username] = { "password": hashed_password, "speed": speed, "is_premium": is_premium, } return users def save_users(self, users: dict): self.users_file.parent.mkdir(parents=True, exist_ok=True) with self.users_file.open("w") as file: for username, data in users.items(): password, speed = data["password"], data["speed"] is_premium = "1" if data["is_premium"] is True else "0" file.write(f"{username} {password} {speed} {is_premium}\n") def is_premium_user(self, user_id: str) -> bool: username = f"user{user_id}" with self.user_data_lock: users = self.load_users() if username in users: return users[username].get("is_premium", False) return False def register_user(self, user_id: str) -> (str, str): username = f"user{user_id}" with self.user_data_lock: users = self.load_users() if username in users: logger.info(f"User {user_id} attempted to register but is already registered.") return username, None else: password = self._generate_password() hashed_password = self._hash_password(password) users[username] = { "password": hashed_password, "speed": str(MAX_USER_SPEED_LIMIT), "is_premium": False, } self.save_users(users) logger.info(f"User {user_id} registered with username: {username}") return username, password def is_registered(self, user_id: str) -> bool: username = f"user{user_id}" with self.user_data_lock: users = self.load_users() return username in users return False def reset_password(self, user_id: str) -> (str, str): username = f"user{user_id}" with self.user_data_lock: users = self.load_users() if username in users: new_password = self._generate_password() hashed_password = self._hash_password(new_password) current_speed = users[username]["speed"] current_premium = users[username].get("is_premium", False) users[username] = {"password": hashed_password, "speed": current_speed, "is_premium": current_premium} self.save_users(users) logger.info(f"User {user_id} reset password. Premium: {current_premium}") return username, new_password else: logger.info(f"User {user_id} attempted to reset password but is not registered.") return username, None user_manager = UserManager(USERS_FILE) async def start(update: Update, context: CallbackContext) -> None: MESSAGES = { "en": { "welcome": FPTN_WELCOME_MESSAGE_EN, "token_button": "Get access token", }, "ru": { "welcome": FPTN_WELCOME_MESSAGE_RU, "token_button": "Получить токен доступа", }, } try: language_code = update.message.from_user.language_code or "en" messages = MESSAGES.get(language_code, MESSAGES["en"]) await update.message.reply_text( messages["welcome"], parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True, reply_markup=ReplyKeyboardRemove(), ) logger.info(f"User {update.message.from_user.id} started the bot.") except Exception as e: logger.error(f"Error: {e}") def generate_token(username: str, password: str, is_premium: bool) -> str: if is_premium is True: servers = PREMIUM_SERVERS + SERVERS_LIST else: servers = SERVERS_LIST data = { "version": 1, "service_name": SERVICE_NAME, "username": username, "password": password, "servers": servers, "censored_zone_servers": SERVERS_CENSORED_LIST, } return json.dumps(data, separators=(",", ":")) def generate_access_link(token: str) -> str: if ENABLE_BROTLI_COMPRESSION is True: compressed = brotli.compress(token.encode("utf-8"), quality=11, lgwin=24, lgblock=24, mode=brotli.MODE_TEXT) return "fptnb:" + base64.b64encode(compressed).decode("utf-8").replace("=", "") return "fptn:" + base64.b64encode(token.encode("utf-8")).decode().replace("=", "") async def get_access_token(update: Update, context: CallbackContext) -> None: MESSAGES = { "en": { "status_registered": "🎉✨ You have successfully registered! 🎉", "status_reset": "🔑 Your token has been reset! 🔑", "info": "🌐 You can download the client from https://storage.googleapis.com/fptn.org/index.html", "click_to_copy": "📋💾 Tap the **token below** to copy it and paste it into the app! ⬇️", "support_info": "You can support our small hobby project on [Boosty](https://boosty.to/fptn) by donating to help cover server costs. ❤️❤️❤️", "support_benefits": "_Sponsors enjoy unlimited speed, access to more servers, and can optionally have their names featured in our VPN clients' credits. More details in our Telegram chat _ https://t.me/fptn\_project ", }, "ru": { "status_registered": "🎉✨ Вы успешно зарегистрированы! 🎉", "status_reset": "🔑 Ваш токен был сброшен!🔑", "info": "🌐 Клиент можно скачать с https://storage.googleapis.com/fptn.org/index.html", "click_to_copy": "📋💾 Нажмите на **токен ниже**, чтобы скопировать и вставите его в приложение! ⬇️", "support_info": "Вы можете поддержать наш небольшой хобби-проект на [Boosty](https://boosty.to/fptn), сделав донат для оплаты серверов. ❤️❤️❤️", "support_benefits": "_Спонсорам мы убираем лимиты скорости, предоставляем доступ к большему числу серверов и, по желанию, отображаем их ники в списке благодарностей прямо в наших VPN-клиентах. Подробнее — в нашем Telegram-чате _ https://t.me/fptn\_project ", }, } user_id = update.message.from_user.id language_code = update.message.from_user.language_code or "en" messages = MESSAGES.get(language_code, MESSAGES["en"]) if user_manager.is_registered(user_id): username, password = user_manager.reset_password(user_id) status_message = messages["status_reset"] else: username, password = user_manager.register_user(user_id) status_message = messages["status_registered"] is_premium = user_manager.is_premium_user(user_id) token = generate_token(username, password, is_premium) fptn_link = generate_access_link(token) click_to_copy = messages["click_to_copy"] info = messages["info"] support_info = messages["support_info"] support_benefits = messages["support_benefits"] await update.message.reply_text( f"{status_message}\n\n" f"{info}\n\n" f"{click_to_copy}\n\n" f"`{fptn_link}` \n\n{support_info} \n{support_benefits}", parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True, ) async def send_credentials_file(update: Update, context: CallbackContext, token: str) -> None: # Create a unique temporary file with tempfile.NamedTemporaryFile(delete=False, suffix=".fptn") as temp_file: temp_file_path = temp_file.name temp_file.write(token.encode("utf-8")) try: await context.bot.send_document( chat_id=update.message.chat_id, document=open(temp_file_path, "rb"), filename=f"{SERVICE_NAME}.fptn", ) logger.info(f"Sent credentials file to user {update.message.from_user.id}.") except Exception as e: logger.error(f"Failed to send credentials file: {e}") finally: if os.path.exists(temp_file_path): os.remove(temp_file_path) def main() -> None: if not TELEGRAM_API_TOKEN: logger.error("API_TOKEN is not set. Please set the TELEGRAM_API_TOKEN environment variable.") sys.exit(1) application = Application.builder().token(TELEGRAM_API_TOKEN).build() application.add_handler(CommandHandler("start", start)) application.add_handler(CommandHandler("token", get_access_token)) # depricated old function application.add_handler(CommandHandler("token_mac", get_access_token)) # UPDATE KEYBOARD (OLD VERSION) application.add_handler(MessageHandler(filters.TEXT & filters.Regex("Get access file"), start)) logger.info("Bot started and is polling for messages.") application.run_polling() if __name__ == "__main__": init_logger() main() ================================================ FILE: sysadmin-tools/telegram-bot/src/requirements.txt ================================================ anyio==4.4.0 brotli==1.2.0 certifi==2024.7.4 h11==0.14.0 httpcore==1.0.5 httpx==0.27.2 idna==3.8 loguru==0.7.2 python-telegram-bot==21.4 sniffio==1.3.1 ================================================ FILE: tests/CMakeLists.txt ================================================ find_package(GTest REQUIRED) add_subdirectory(common) add_subdirectory(server) add_subdirectory(fptnlib) add_custom_target( run_tests COMMAND ${CMAKE_CTEST_COMMAND} DEPENDS tests_common common COMMENT "Running all tests" VERBATIM) ================================================ FILE: tests/common/CMakeLists.txt ================================================ find_package(GTest REQUIRED) find_package(Boost REQUIRED) find_package(OpenSSL REQUIRED) find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(PcapPlusPlus REQUIRED) find_package(nlohmann_json REQUIRED) find_package(jwt-cpp REQUIRED) find_package(Boost REQUIRED COMPONENTS random filesystem) if(WIN32) set(TUNTAP_LIB Wintun rpcrt4) add_definitions(-D_WIN32_WINNT=0x0601) else() set(TUNTAP_LIB tuntap++) endif() set(LIBS GTest::gtest GTest::gtest_main Boost::boost Boost::random Boost::filesystem OpenSSL::SSL OpenSSL::Crypto nlohmann_json::nlohmann_json jwt-cpp::jwt-cpp spdlog::spdlog fmt::fmt PcapPlusPlus::PcapPlusPlus fptn-protocol-lib_static ${TUNTAP_LIB}) # --- Channel test --- add_executable(ChannelTest data/ChannelTest.cpp) target_link_libraries(ChannelTest PRIVATE ${LIBS}) add_test(NAME ChannelTest COMMAND ChannelTest) # --- IPv4 Generator test --- add_executable(IPv4GeneratorTest network/IPv4GeneratorTest.cpp) target_link_libraries(IPv4GeneratorTest PRIVATE ${LIBS}) add_test(NAME IPv4GeneratorTest COMMAND IPv4GeneratorTest) # --- IPv6 Generator test --- add_executable(IPv6GeneratorTest network/IPv6GeneratorTest.cpp) target_link_libraries(IPv6GeneratorTest PRIVATE ${LIBS}) add_test(NAME IPv6GeneratorTest COMMAND IPv6GeneratorTest) # --- IPv6 Utils test --- add_executable(IPv6UtilsTest network/IPv6UtilsTest.cpp) target_link_libraries(IPv6UtilsTest PRIVATE ${LIBS}) add_test(NAME IPv6UtilsTest COMMAND IPv6UtilsTest) # --- TUN Device test --- add_executable(TunDeviceTest network/TunDeviceTest.cpp) target_link_libraries(TunDeviceTest PRIVATE ${LIBS}) add_test(NAME TunDeviceTest COMMAND TunDeviceTest) # --- Client Stop race condition test --- add_executable(ClientStopRaceTest network/ClientStopRaceTest.cpp) target_link_libraries(ClientStopRaceTest PRIVATE GTest::gtest GTest::gtest_main) add_test(NAME ClientStopRaceTest COMMAND ClientStopRaceTest) ================================================ FILE: tests/common/data/ChannelTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include // NOLINT(build/include_order) #include "common/data/channel.h" #include "common/network/ip_packet.h" TEST(ChannelTest, PushAndWaitForPacket) { fptn::common::data::Channel channel(10); fptn::common::network::IPPacketData packet_data; const char* test_data = "packet-data"; packet_data.insert( packet_data.end(), test_data, test_data + strlen(test_data)); auto packet = std::make_unique(std::move(packet_data), static_cast(1), // Cast to proper ClientID type pcpp::LINKTYPE_IPV4); channel.Push(std::move(packet)); EXPECT_NE(channel.WaitForPacket(std::chrono::milliseconds(100)), nullptr); } TEST(ChannelTest, WaitForPacketTimeout) { fptn::common::data::Channel channel(10); EXPECT_EQ(channel.WaitForPacket(std::chrono::milliseconds(100)), nullptr); } ================================================ FILE: tests/common/network/ClientStopRaceTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Pavel Shpilev Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ // Reproduces the use-after-free race condition in vpn::http::Client::Stop(). // // The bug: Stop() called ws_.reset() (destroying the WebSocket) BEFORE // th_.join() (waiting for Run() to exit). The Run() thread was still // executing ws_->Run() on the destroyed object. // // The fix: join the thread first, then reset the shared_ptr. // // This test uses a mock to exercise the exact same pattern. #include #include #include #include #include #include // NOLINT(build/include_order) namespace { // Simulates WebsocketClient: Run() blocks until Stop() is called. class MockWebSocket { public: MockWebSocket() : stopped_(false), run_entered_(false) {} void Run() { run_entered_ = true; std::unique_lock lock(mutex_); cv_.wait(lock, [this]() { return stopped_.load(); }); // Simulate cleanup work after stop signal std::this_thread::sleep_for(std::chrono::microseconds(100)); // Access member after wakeup — crashes if object is destroyed run_exited_ = true; } bool Stop() { stopped_ = true; cv_.notify_all(); return true; } bool RunEntered() const { return run_entered_; } bool RunExited() const { return run_exited_; } private: std::mutex mutex_; std::condition_variable cv_; std::atomic stopped_; std::atomic run_entered_; std::atomic run_exited_{false}; }; // Reproduces the original buggy Client::Stop() pattern. // With the bug: ws_.reset() before th_.join() → use-after-free. // With the fix: th_.join() before ws_.reset() → safe. class MockClient { public: bool Start() { running_ = true; ws_ = std::make_shared(); th_ = std::thread(&MockClient::RunLoop, this); return true; } // Fixed version: join before reset bool StopFixed() { if (!running_) { return false; } running_ = false; if (ws_) { ws_->Stop(); } if (th_.joinable()) { th_.join(); } ws_.reset(); return true; } bool IsRunning() const { return running_; } bool WsRunExited() const { return ws_ && ws_->RunExited(); } private: void RunLoop() { while (running_) { if (ws_) { ws_->Run(); } if (!running_) { break; } } } std::thread th_; std::mutex mutex_; std::atomic running_{false}; std::shared_ptr ws_; }; } // namespace // Verify Stop() completes cleanly without crash TEST(ClientStopRaceTest, StopWhileRunning) { MockClient client; ASSERT_TRUE(client.Start()); // Let Run() enter the blocking call std::this_thread::sleep_for(std::chrono::milliseconds(10)); // Stop should complete without crash or hang EXPECT_TRUE(client.StopFixed()); EXPECT_FALSE(client.IsRunning()); } // Rapid start/stop cycles stress the race window TEST(ClientStopRaceTest, RapidStartStopCycles) { constexpr int kCycles = 50; for (int i = 0; i < kCycles; ++i) { MockClient client; ASSERT_TRUE(client.Start()); // Vary the timing to hit different race windows if (i % 3 == 0) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); } EXPECT_TRUE(client.StopFixed()); } } // Stop immediately after Start (thread may not have entered Run yet) TEST(ClientStopRaceTest, ImmediateStop) { MockClient client; ASSERT_TRUE(client.Start()); EXPECT_TRUE(client.StopFixed()); EXPECT_FALSE(client.IsRunning()); } // Verify Run() thread fully exits before ws_ is destroyed TEST(ClientStopRaceTest, ThreadExitsBeforeReset) { MockClient client; ASSERT_TRUE(client.Start()); std::this_thread::sleep_for(std::chrono::milliseconds(10)); // After StopFixed, the ws_ shared_ptr is reset. // The fact that we reach here without ASan/TSan errors confirms // the thread finished before the object was destroyed. EXPECT_TRUE(client.StopFixed()); } ================================================ FILE: tests/common/network/IPv4GeneratorTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ipv4_generator.h" TEST(IPv4GeneratorTest, InitialAddress) { fptn::common::network::IPv4AddressGenerator generator( fptn::common::network::IPv4Address("192.168.1.0"), 24); EXPECT_EQ(generator.NumAvailableAddresses(), 254); const auto address1 = generator.GetNextAddress(); EXPECT_EQ(address1.ToString(), "192.168.1.1"); const auto address2 = generator.GetNextAddress(); EXPECT_EQ(address2.ToString(), "192.168.1.2"); const auto address3 = generator.GetNextAddress(); EXPECT_EQ(address3.ToString(), "192.168.1.3"); } TEST(IPv4GeneratorTest, NumAvaliableAddresses) { fptn::common::network::IPv4AddressGenerator generator( fptn::common::network::IPv4Address("192.168.0.0"), 24); EXPECT_EQ(generator.NumAvailableAddresses(), 254); for (int i = 1; i <= 254; i++) { const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), fmt::format("192.168.0.{}", i)); } { // the repeat test const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), "192.168.0.1"); } } TEST(IPv4GeneratorTest, SmallDifficultNetsMask) { fptn::common::network::IPv4AddressGenerator generator( fptn::common::network::IPv4Address("192.168.0.0"), 28); EXPECT_EQ(generator.NumAvailableAddresses(), 14); for (int i = 1; i <= 14; i++) { const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), fmt::format("192.168.0.{}", i)); } } TEST(IPGeneratorTest, BigDifficultNetsMask) { fptn::common::network::IPv4AddressGenerator generator( fptn::common::network::IPv4Address("192.168.0.0"), 16); EXPECT_EQ(generator.NumAvailableAddresses(), 65534); std::uint32_t counter = 0; for (int i = 0; i <= 255; i++) { for (int j = 0; j <= 255; j++) { if ((i == 0 && j == 0) || (i == 255 && j == 255)) { continue; // network address } const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), fmt::format("192.168.{}.{}", i, j)); counter += 1; } } EXPECT_EQ(counter, 65534); } ================================================ FILE: tests/common/network/IPv6GeneratorTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include #include "common/network/ipv6_generator.h" namespace { // Helper: produce the canonical compressed IPv6 string for a given // full-hex address, so test expectations match inet_ntop / pcpp output. std::string canonical(const std::string& full_hex) { return boost::asio::ip::make_address_v6(full_hex).to_string(); } } // namespace TEST(IPv6GeneratorTest, InitialAddress) { fptn::common::network::IPv6AddressGenerator generator( fptn::common::network::IPv6Address( "2001:0db8:0000:0000:0000:0000:0000:0000"), 120); EXPECT_EQ(generator.NumAvailableAddresses(), 254); const auto address1 = generator.GetNextAddress(); EXPECT_EQ(address1.ToString(), canonical("2001:0db8:0000:0000:0000:0000:0000:0001")); const auto address2 = generator.GetNextAddress(); EXPECT_EQ(address2.ToString(), canonical("2001:0db8:0000:0000:0000:0000:0000:0002")); const auto address3 = generator.GetNextAddress(); EXPECT_EQ(address3.ToString(), canonical("2001:0db8:0000:0000:0000:0000:0000:0003")); } TEST(IPv6GeneratorTest, NumAvaliableAddresses) { fptn::common::network::IPv6AddressGenerator generator( fptn::common::network::IPv6Address( "2001:0db8:0001:0000:0000:0000:0000:0000"), 120); EXPECT_EQ(generator.NumAvailableAddresses(), 254); for (int i = 1; i <= 254; i++) { const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), canonical( fmt::format("2001:0db8:0001:0000:0000:0000:0000:{:04x}", i))); } { // Repeat test const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), canonical("2001:0db8:0001:0000:0000:0000:0000:0001")); } } TEST(IPv6GeneratorTest, SmallDifficultNetsMask) { fptn::common::network::IPv6AddressGenerator generator( fptn::common::network::IPv6Address( "2001:0db8:0002:0000:0000:0000:0000:0000"), 124); EXPECT_EQ(generator.NumAvailableAddresses(), 14); for (int i = 1; i <= 14; i++) { const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), canonical( fmt::format("2001:0db8:0002:0000:0000:0000:0000:{:04x}", i))); } } TEST(IPv6GeneratorTest, BigDifficultNetsMask) { fptn::common::network::IPv6AddressGenerator generator( fptn::common::network::IPv6Address( "2001:0db8:0003:0000:0000:0000:0000:0000"), 112); EXPECT_EQ(generator.NumAvailableAddresses(), (1ULL << 16) - 2); std::uint32_t counter = 0; for (int i = 0; i <= 255; i++) { for (int j = 0; j <= 255; j++) { if ((i == 0 && j == 0) || (i == 255 && j == 255)) { continue; // Skip network and broadcast addresses } const auto address = generator.GetNextAddress(); EXPECT_EQ(address.ToString(), canonical(fmt::format( "2001:0db8:0003:0000:0000:0000:{:02x}{:02x}:{:02x}{:02x}", 0, 0, i, j))); counter += 1; } } EXPECT_EQ(counter, (1ULL << 16) - 2); } ================================================ FILE: tests/common/network/IPv6UtilsTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Pavel Shpilev Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ipv6_generator.h" #include "common/network/ipv6_utils.h" // This test demonstrates that the IPv6 string representation from the // generator must match the format produced by pcpp::IPv6Address::toString(), // because the server NAT table uses these strings as map keys. // If the formats differ, the NAT lookup fails and IPv6 response packets // are silently dropped. TEST(IPv6UtilsTest, ToStringMatchesPcppFormat) { // Simulate what the IPv6 generator does: convert an address to uint128, // then back to string using ipv6::toString() const std::string original = "fc00:1::2"; const auto boost_addr = boost::asio::ip::make_address_v6(original); const auto uint128_val = fptn::common::network::ipv6::toUInt128(boost_addr); const std::string generator_str = fptn::common::network::ipv6::toString(uint128_val); // Simulate what happens when a response packet arrives: pcpp extracts // the IPv6 address and we call toString() on it const pcpp::IPv6Address pcpp_addr(original); const std::string pcpp_str = pcpp_addr.toString(); // These MUST match, otherwise the NAT table lookup will fail EXPECT_EQ(generator_str, pcpp_str) << "IPv6 string format mismatch between generator ('" << generator_str << "') and pcpp ('" << pcpp_str << "'). This causes NAT table lookups to fail, " "dropping all IPv6 response packets."; } // Test with the actual server default subnet TEST(IPv6UtilsTest, ServerDefaultSubnetMatchesPcpp) { // Server defaults from CMakeLists.txt: // FPTN_SERVER_DEFAULT_NET_ADDRESS_IP6="fc00:1::" // Mask: /112 fptn::common::network::IPv6AddressGenerator generator( fptn::common::network::IPv6Address("fc00:1::"), 112); // Generate a few addresses and verify they match pcpp's format for (int i = 1; i <= 5; i++) { const auto generated = generator.GetNextAddress(); const auto& gen_str = generated.ToString(); // Round-trip through pcpp (simulates what happens on packet receive) const pcpp::IPv6Address pcpp_addr(gen_str); const std::string pcpp_str = pcpp_addr.toString(); EXPECT_EQ(gen_str, pcpp_str) << "Mismatch for generated address #" << i << ": generator='" << gen_str << "' vs pcpp='" << pcpp_str << "'"; } } // Test with various address patterns that exercise zero-compression TEST(IPv6UtilsTest, ToStringFormatsMatchPcppForVariousAddresses) { const std::vector test_addresses = { "::1", "fe80::1", "2001:db8::1", "fc00:1::2", "2001:db8:85a3::8a2e:370:7334", "ff02::1", "::", }; for (const auto& addr_str : test_addresses) { const auto boost_addr = boost::asio::ip::make_address_v6(addr_str); const auto uint128_val = fptn::common::network::ipv6::toUInt128(boost_addr); const std::string utils_str = fptn::common::network::ipv6::toString(uint128_val); const pcpp::IPv6Address pcpp_addr(addr_str); const std::string pcpp_str = pcpp_addr.toString(); EXPECT_EQ(utils_str, pcpp_str) << "Format mismatch for input '" << addr_str << "': utils='" << utils_str << "' vs pcpp='" << pcpp_str << "'"; } } ================================================ FILE: tests/common/network/TunDeviceTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Pavel Shpilev Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include #include #include #include #include #include #include #include #include #include #include // NOLINT(build/include_order) #include "common/network/ip_packet.h" #include "common/network/net_interface.h" // ===== Minimal valid IP packet builders for testing ===== namespace { // Minimal valid IPv4 header (20 bytes): src=10.0.0.1, dst=10.0.0.2 std::vector MakeMinimalIPv4Packet() { return { 0x45, 0x00, 0x00, 0x14, // ver=4, IHL=5, total_len=20 0x00, 0x01, 0x00, 0x00, // id=1, flags=0, frag=0 0x40, 0x00, 0xf5, 0xc2, // TTL=64, proto=HOPOPT, checksum 0x0a, 0x00, 0x00, 0x01, // src: 10.0.0.1 0x0a, 0x00, 0x00, 0x02, // dst: 10.0.0.2 }; } // Minimal valid IPv6 header (40 bytes): src=fc00:1::1, dst=fc00:1::2 std::vector MakeMinimalIPv6Packet() { return { 0x60, 0x00, 0x00, 0x00, // ver=6, traffic class, flow label 0x00, 0x00, 0x3b, 0x40, // payload_len=0, next=NoNext(59), hop=64 // src: fc00:1::1 0xfc, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, // dst: fc00:1::2 0xfc, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, }; } } // namespace // ===== Mock TUN device with shared state for testing GenericTunInterface ===== namespace { struct SharedMockState { std::mutex mutex; std::vector> written_packets; std::queue> read_queue; void RecordWrite(const void* data, int size) { std::scoped_lock lock(mutex); written_packets.emplace_back(static_cast(data), static_cast(data) + size); } int FeedRead(void* buffer, int max_size) { std::scoped_lock lock(mutex); if (read_queue.empty()) { return 0; } const auto& front = read_queue.front(); const int sz = std::min(max_size, static_cast(front.size())); std::memcpy(buffer, front.data(), sz); read_queue.pop(); return sz; } void InjectPacket(std::vector data) { std::scoped_lock lock(mutex); read_queue.push(std::move(data)); } void Clear() { std::scoped_lock lock(mutex); written_packets.clear(); while (!read_queue.empty()) { read_queue.pop(); } } }; // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) std::shared_ptr g_mock_state; class MockTunDevice { public: bool Open(const std::string& name) { name_ = name; return true; } // cppcheck-suppress functionStatic void Close() {} // NOLINT(readability-convert-member-functions-to-static) [[nodiscard]] const std::string& GetName() const { return name_; } // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-functions-to-static) bool ConfigureIPv4(const std::string& /*addr*/, int /*mask*/) { return true; } // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-functions-to-static) bool ConfigureIPv6(const std::string& /*addr*/, int /*mask*/) { return true; } // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-*) void SetNonBlocking(bool /*enabled*/) {} // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-*) void SetMTU(int /*mtu*/) {} // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-*) void BringUp() {} // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-*) void SetStopFlag(const std::atomic* /*running*/) {} // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-functions-to-static) int Read(void* buffer, int size) { return g_mock_state->FeedRead(buffer, size); } // cppcheck-suppress functionStatic // NOLINTNEXTLINE(readability-convert-member-functions-to-static) int Write(const void* data, int size) { g_mock_state->RecordWrite(data, size); return size; } private: std::string name_; }; } // namespace namespace fptn::common::network { template class GenericTunInterface; } using MockTunInterface = fptn::common::network::GenericTunInterface; namespace { class GenericTunInterfaceTest : public ::testing::Test { protected: void SetUp() override { g_mock_state = std::make_shared(); } void TearDown() override { g_mock_state->Clear(); g_mock_state.reset(); } static MockTunInterface::Config MakeConfig() { return MockTunInterface::Config{ .name = "mock0", .ipv4_addr = fptn::common::network::IPv4Address("10.0.0.1"), .ipv4_netmask = 24, .ipv6_addr = fptn::common::network::IPv6Address("fc00:1::1"), .ipv6_netmask = 112, }; } }; } // namespace TEST_F(GenericTunInterfaceTest, StartAndStop) { MockTunInterface iface(MakeConfig()); ASSERT_TRUE(iface.Start()); EXPECT_EQ(iface.Name(), "mock0"); EXPECT_TRUE(iface.Stop()); } TEST_F(GenericTunInterfaceTest, SendIPv4Packet) { MockTunInterface iface(MakeConfig()); ASSERT_TRUE(iface.Start()); auto pkt_data = MakeMinimalIPv4Packet(); auto packet = fptn::common::network::IPPacket::Parse(pkt_data); ASSERT_NE(packet, nullptr); EXPECT_TRUE(iface.Send(std::move(packet))); std::this_thread::sleep_for(std::chrono::milliseconds(10)); auto written = g_mock_state->written_packets; ASSERT_EQ(written.size(), 1U); EXPECT_EQ(written[0].size(), pkt_data.size()); EXPECT_EQ(written[0], pkt_data); iface.Stop(); } TEST_F(GenericTunInterfaceTest, SendIPv6Packet) { MockTunInterface iface(MakeConfig()); ASSERT_TRUE(iface.Start()); auto pkt_data = MakeMinimalIPv6Packet(); auto packet = fptn::common::network::IPPacket::Parse(pkt_data); ASSERT_NE(packet, nullptr); EXPECT_TRUE(iface.Send(std::move(packet))); std::this_thread::sleep_for(std::chrono::milliseconds(10)); auto written = g_mock_state->written_packets; ASSERT_EQ(written.size(), 1U); EXPECT_EQ(written[0].size(), pkt_data.size()); EXPECT_EQ(written[0], pkt_data); iface.Stop(); } TEST_F(GenericTunInterfaceTest, ReceiveIPv4Packet) { MockTunInterface iface(MakeConfig()); std::mutex callback_mutex; std::vector> received; iface.SetRecvIPPacketCallback([&](fptn::common::network::IPPacketPtr packet) { if (packet) { std::scoped_lock lock(callback_mutex); const auto* raw = packet->GetRawPacket(); const auto* data = static_cast(raw->getRawData()); received.emplace_back(data, data + raw->getRawDataLen()); } }); ASSERT_TRUE(iface.Start()); g_mock_state->InjectPacket(MakeMinimalIPv4Packet()); for (int i = 0; i < 100; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); std::scoped_lock lock(callback_mutex); if (!received.empty()) { break; } } iface.Stop(); std::scoped_lock lock(callback_mutex); ASSERT_FALSE(received.empty()); ASSERT_EQ(received.size(), 1U); // cppcheck-suppress containerOutOfBounds EXPECT_EQ(received[0], MakeMinimalIPv4Packet()); } TEST_F(GenericTunInterfaceTest, ReceiveIPv6Packet) { MockTunInterface iface(MakeConfig()); std::mutex callback_mutex; std::vector> received; iface.SetRecvIPPacketCallback([&](fptn::common::network::IPPacketPtr packet) { if (packet) { std::scoped_lock lock(callback_mutex); const auto* raw = packet->GetRawPacket(); const auto* data = static_cast(raw->getRawData()); received.emplace_back(data, data + raw->getRawDataLen()); } }); ASSERT_TRUE(iface.Start()); g_mock_state->InjectPacket(MakeMinimalIPv6Packet()); for (int i = 0; i < 100; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(5)); std::scoped_lock lock(callback_mutex); if (!received.empty()) { break; } } iface.Stop(); std::scoped_lock lock(callback_mutex); ASSERT_FALSE(received.empty()); ASSERT_EQ(received.size(), 1U); // cppcheck-suppress containerOutOfBounds EXPECT_EQ(received[0], MakeMinimalIPv6Packet()); } TEST_F(GenericTunInterfaceTest, SendMultipleMixedPackets) { MockTunInterface iface(MakeConfig()); ASSERT_TRUE(iface.Start()); auto ipv4_data = MakeMinimalIPv4Packet(); auto ipv6_data = MakeMinimalIPv6Packet(); auto pkt4 = fptn::common::network::IPPacket::Parse(ipv4_data); auto pkt6 = fptn::common::network::IPPacket::Parse(ipv6_data); ASSERT_NE(pkt4, nullptr); ASSERT_NE(pkt6, nullptr); EXPECT_TRUE(iface.Send(std::move(pkt4))); EXPECT_TRUE(iface.Send(std::move(pkt6))); std::this_thread::sleep_for(std::chrono::milliseconds(10)); auto written = g_mock_state->written_packets; ASSERT_EQ(written.size(), 2U); EXPECT_EQ(written[0], ipv4_data); EXPECT_EQ(written[1], ipv6_data); iface.Stop(); } TEST_F(GenericTunInterfaceTest, DeviceNameUpdatedAfterStart) { MockTunInterface iface(MakeConfig()); EXPECT_EQ(iface.Name(), "mock0"); ASSERT_TRUE(iface.Start()); EXPECT_EQ(iface.Name(), "mock0"); iface.Stop(); } #ifdef __APPLE__ #include #include #include #include "common/network/tun/darwin_tun_device.h" class DarwinAfHeaderTest : public ::testing::Test { protected: void SetUp() override { ASSERT_EQ(socketpair(AF_UNIX, SOCK_DGRAM, 0, fds_), 0); } void TearDown() override { close(fds_[0]); close(fds_[1]); } int fds_[2] = {-1, -1}; }; TEST_F(DarwinAfHeaderTest, WriteIPv4PrependsCorrectAfHeader) { fptn::common::network::DarwinTunDevice device; ASSERT_TRUE(device.OpenWithFd(fds_[0], "test0")); auto ipv4_pkt = MakeMinimalIPv4Packet(); const int written = device.Write(ipv4_pkt.data(), ipv4_pkt.size()); EXPECT_EQ(written, static_cast(ipv4_pkt.size())); std::uint8_t raw_buf[256] = {}; const ssize_t n = recv(fds_[1], raw_buf, sizeof(raw_buf), 0); ASSERT_GT(n, 4); std::uint32_t af_header = 0; std::memcpy(&af_header, raw_buf, 4); EXPECT_EQ(af_header, htonl(AF_INET)); const auto payload_size = static_cast(n) - 4; EXPECT_EQ(payload_size, ipv4_pkt.size()); EXPECT_EQ(std::memcmp(raw_buf + 4, ipv4_pkt.data(), ipv4_pkt.size()), 0); device.OpenWithFd(-1, ""); } TEST_F(DarwinAfHeaderTest, WriteIPv6PrependsCorrectAfHeader) { fptn::common::network::DarwinTunDevice device; ASSERT_TRUE(device.OpenWithFd(fds_[0], "test0")); auto ipv6_pkt = MakeMinimalIPv6Packet(); const int written = device.Write(ipv6_pkt.data(), ipv6_pkt.size()); EXPECT_EQ(written, static_cast(ipv6_pkt.size())); std::uint8_t raw_buf[256] = {}; const ssize_t n = recv(fds_[1], raw_buf, sizeof(raw_buf), 0); ASSERT_GT(n, 4); std::uint32_t af_header = 0; std::memcpy(&af_header, raw_buf, 4); EXPECT_EQ(af_header, htonl(AF_INET6)); const auto payload_size = static_cast(n) - 4; EXPECT_EQ(payload_size, ipv6_pkt.size()); EXPECT_EQ(std::memcmp(raw_buf + 4, ipv6_pkt.data(), ipv6_pkt.size()), 0); device.OpenWithFd(-1, ""); } TEST_F(DarwinAfHeaderTest, ReadStripsAfHeaderIPv4) { fptn::common::network::DarwinTunDevice device; ASSERT_TRUE(device.OpenWithFd(fds_[0], "test0")); device.SetNonBlocking(true); auto ipv4_pkt = MakeMinimalIPv4Packet(); std::vector raw(4 + ipv4_pkt.size()); std::uint32_t af = htonl(AF_INET); std::memcpy(raw.data(), &af, 4); std::memcpy(raw.data() + 4, ipv4_pkt.data(), ipv4_pkt.size()); ASSERT_EQ(send(fds_[1], raw.data(), raw.size(), 0), static_cast(raw.size())); std::uint8_t read_buf[256] = {}; const int bytes_read = device.Read(read_buf, sizeof(read_buf)); EXPECT_EQ(bytes_read, static_cast(ipv4_pkt.size())); EXPECT_EQ(std::memcmp(read_buf, ipv4_pkt.data(), ipv4_pkt.size()), 0); device.OpenWithFd(-1, ""); } TEST_F(DarwinAfHeaderTest, ReadStripsAfHeaderIPv6) { fptn::common::network::DarwinTunDevice device; ASSERT_TRUE(device.OpenWithFd(fds_[0], "test0")); device.SetNonBlocking(true); auto ipv6_pkt = MakeMinimalIPv6Packet(); std::vector raw(4 + ipv6_pkt.size()); std::uint32_t af = htonl(AF_INET6); std::memcpy(raw.data(), &af, 4); std::memcpy(raw.data() + 4, ipv6_pkt.data(), ipv6_pkt.size()); ASSERT_EQ(send(fds_[1], raw.data(), raw.size(), 0), static_cast(raw.size())); std::uint8_t read_buf[256] = {}; const int bytes_read = device.Read(read_buf, sizeof(read_buf)); EXPECT_EQ(bytes_read, static_cast(ipv6_pkt.size())); EXPECT_EQ(std::memcmp(read_buf, ipv6_pkt.data(), ipv6_pkt.size()), 0); device.OpenWithFd(-1, ""); } TEST_F(DarwinAfHeaderTest, RoundTripIPv6Preserved) { fptn::common::network::DarwinTunDevice writer; fptn::common::network::DarwinTunDevice reader; ASSERT_TRUE(writer.OpenWithFd(fds_[0], "writer0")); ASSERT_TRUE(reader.OpenWithFd(fds_[1], "reader0")); reader.SetNonBlocking(true); auto original = MakeMinimalIPv6Packet(); const int written = writer.Write(original.data(), original.size()); EXPECT_EQ(written, static_cast(original.size())); std::uint8_t read_buf[256] = {}; const int bytes_read = reader.Read(read_buf, sizeof(read_buf)); ASSERT_EQ(bytes_read, static_cast(original.size())); std::vector result(read_buf, read_buf + bytes_read); EXPECT_EQ(result, original); writer.OpenWithFd(-1, ""); reader.OpenWithFd(-1, ""); } #endif // __APPLE__ ================================================ FILE: tests/fptnlib/CMakeLists.txt ================================================ find_package(GTest REQUIRED) find_package(Boost REQUIRED) find_package(OpenSSL REQUIRED) find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(PcapPlusPlus REQUIRED) find_package(nlohmann_json REQUIRED) find_package(jwt-cpp REQUIRED) find_package(Boost REQUIRED COMPONENTS random filesystem) if(WIN32) set(TUNTAP_LIB Wintun rpcrt4) add_definitions(-D_WIN32_WINNT=0x0601) else() set(TUNTAP_LIB tuntap++) endif() set(LIBS GTest::gtest GTest::gtest_main Boost::boost Boost::random Boost::process Boost::filesystem OpenSSL::SSL OpenSSL::Crypto nlohmann_json::nlohmann_json jwt-cpp::jwt-cpp spdlog::spdlog fmt::fmt PcapPlusPlus::PcapPlusPlus fptn-protocol-lib_static ${TUNTAP_LIB}) # --- ApiClient test --- add_executable(ApiClientTest api_client/ApiClientTest.cpp) target_link_libraries(ApiClientTest PRIVATE ${LIBS}) add_test(NAME ApiClientTest COMMAND ApiClientTest) ================================================ FILE: tests/fptnlib/api_client/ApiClientTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2026 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include // NOLINT(build/include_order) #include "fptn-protocol-lib/https/api_client/api_client.h" // TEST(ApiClientTest, GitHubReleasesConnection) { // fptn::protocol::https::ApiClient client("api.github.com", 443, // "api.github.com", // fptn::protocol::https::CensorshipStrategy::kSniRealityModeChrome147); // // auto response = client.Get("/repos/batchar2/fptn/releases/latest", 30); // // EXPECT_EQ(response.code, 200); // EXPECT_FALSE(response.body.empty()); // // auto release_info = response.Json(); // EXPECT_TRUE(release_info.contains("tag_name")); // EXPECT_TRUE(release_info.contains("name")); // EXPECT_TRUE(release_info.contains("html_url")); // } TEST(ApiClientTest, GitHubHandshakeTest) { fptn::protocol::https::ApiClient client("api.github.com", 443, "api.github.com", fptn::protocol::https::CensorshipStrategy::kSni); bool handshake_success = client.TestHandshake(10); EXPECT_TRUE(handshake_success); } // TEST(ApiClientTest, GitHubResponseStructure) { // fptn::protocol::https::ApiClient client("api.github.com", 443, // "api.github.com", fptn::protocol::https::CensorshipStrategy::kSni); // // auto response = client.Get("/repos/batchar2/fptn/releases/latest", 30); // // ASSERT_EQ(response.code, 200); // // auto release_info = response.Json(); // EXPECT_TRUE(release_info.contains("id")); // EXPECT_TRUE(release_info.contains("tag_name")); // EXPECT_TRUE(release_info.contains("assets")); // // if (release_info.contains("assets") && release_info["assets"].is_array()) { // auto assets = release_info["assets"]; // if (!assets.empty()) { // // auto asset = assets[0]; // // EXPECT_TRUE(asset.contains("name")); // // EXPECT_TRUE(asset.contains("browser_download_url")); // } // } // } ================================================ FILE: tests/server/CMakeLists.txt ================================================ find_package(GTest REQUIRED) find_package(Boost REQUIRED) find_package(OpenSSL REQUIRED) find_package(spdlog REQUIRED) find_package(fmt REQUIRED) find_package(PcapPlusPlus REQUIRED) find_package(nlohmann_json REQUIRED) find_package(jwt-cpp REQUIRED) find_package(prometheus-cpp REQUIRED) if(WIN32) set(TUNTAP_LIB Wintun rpcrt4) else() set(TUNTAP_LIB tuntap++) endif() set(LIBS GTest::gtest GTest::gtest_main Boost::boost OpenSSL::SSL OpenSSL::Crypto nlohmann_json::nlohmann_json jwt-cpp::jwt-cpp spdlog::spdlog fmt::fmt PcapPlusPlus::PcapPlusPlus fptn-protocol-lib_static prometheus-cpp::prometheus-cpp ${TUNTAP_LIB}) include_directories(${FPTN_SERVER_PATH}) # --- MetricTest test --- add_executable(MetricTest statistic/MetricTest.cpp ${FPTN_SERVER_PATH}/statistic/metrics.h ${FPTN_SERVER_PATH}/statistic/metrics.cpp) target_link_libraries(MetricTest PRIVATE ${LIBS}) add_test(NAME MetricTest COMMAND MetricTest) # --- AntiScan test --- add_executable(AntiScanTest filter/antiscan/AntiScanTest.cpp ${FPTN_SERVER_PATH}/filter/filters/antiscan/antiscan.h ${FPTN_SERVER_PATH}/filter/filters/antiscan/antiscan.cpp) target_link_libraries(AntiScanTest PRIVATE ${LIBS}) add_test(NAME AntiScanTest COMMAND AntiScanTest) ================================================ FILE: tests/server/filter/antiscan/AntiScanTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include // NOLINT(build/include_order) #include "common/network/ip_address.h" #include "common/network/ip_packet.h" #include "fptn-server/filter/filters/antiscan/antiscan.h" namespace { class MockIPv4Packet : public fptn::common::network::IPPacket { public: explicit MockIPv4Packet(const pcpp::IPv4Address& addr) { ipv4Layer_.setDstIPv4Address(addr); } bool IsIPv4() const noexcept override { return true; } bool IsIPv6() const noexcept override { return false; } pcpp::IPv4Layer* IPv4Layer() noexcept override { return &ipv4Layer_; } private: pcpp::IPv4Layer ipv4Layer_; }; class MockIPv6Packet : public fptn::common::network::IPPacket { public: explicit MockIPv6Packet(const pcpp::IPv6Address& addr) { ipv6_layer_.setDstIPv6Address(addr); } bool IsIPv4() const noexcept override { return false; } bool IsIPv6() const noexcept override { return true; } pcpp::IPv6Layer* IPv6Layer() noexcept override { return &ipv6_layer_; } private: pcpp::IPv6Layer ipv6_layer_; }; /* IPv4 */ // cppcheck-suppress syntaxError TEST(AntiScanTest, BlockScan) { /* IPv4 */ const fptn::common::network::IPv4Address server_ipv4("192.168.1.1"); const fptn::common::network::IPv4Address net_ipv4("192.168.1.0"); const int mask_ipv4 = 24; /* IPv6 */ const fptn::common::network::IPv6Address server_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0001"); const fptn::common::network::IPv6Address net_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0000"); const int mask_ipv6 = 126; fptn::filter::AntiScan anti_scan_filter( /* IPv4 */ server_ipv4, net_ipv4, mask_ipv4, /* IPv6 */ server_ipv6, net_ipv6, mask_ipv6); EXPECT_EQ(anti_scan_filter.apply( std::make_unique(net_ipv4.ToString())), nullptr) << "Packet in the network should be blocked"; EXPECT_EQ(anti_scan_filter.apply(std::make_unique( pcpp::IPv4Address("192.168.1.5"))), nullptr) << "Packet in the network should be blocked"; EXPECT_EQ(anti_scan_filter.apply(std::make_unique( pcpp::IPv4Address("192.168.1.255"))), nullptr) << "Packet in the network should be blocked"; EXPECT_EQ(anti_scan_filter.apply(std::make_unique( pcpp::IPv4Address("255.255.255.255"))), nullptr); } TEST(AntiScanTest, AllowNonScanPacket) { /* IPv4 */ const fptn::common::network::IPv4Address server_ipv4("192.168.1.1"); const fptn::common::network::IPv4Address net_ipv4("192.168.1.0"); const int mask_ipv4 = 24; /* IPv6 */ const fptn::common::network::IPv6Address server_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0001"); const fptn::common::network::IPv6Address net_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0000"); const int mask_ipv6 = 126; fptn::filter::AntiScan anti_scan_filter( /* IPv4 */ server_ipv4, net_ipv4, mask_ipv4, /* IPv6 */ server_ipv6, net_ipv6, mask_ipv6); EXPECT_NE(anti_scan_filter.apply( std::make_unique(server_ipv4.ToString())), nullptr); EXPECT_NE(anti_scan_filter.apply(std::make_unique( pcpp::IPv4Address("192.168.2.1"))), nullptr); EXPECT_NE(anti_scan_filter.apply( std::make_unique(pcpp::IPv4Address("8.8.8.8"))), nullptr); EXPECT_NE(anti_scan_filter.apply(std::make_unique( pcpp::IPv4Address("192.168.0.1"))), nullptr); EXPECT_NE(anti_scan_filter.apply(std::make_unique( pcpp::IPv4Address("192.168.0.255"))), nullptr); } /* IPv6 */ TEST(AntiScanTest, BlockScanIPv6) { /* IPv4 */ const fptn::common::network::IPv4Address server_ipv4("192.168.1.1"); const fptn::common::network::IPv4Address net_ipv4("192.168.1.0"); const int mask_ipv4 = 24; /* IPv6 */ const fptn::common::network::IPv6Address server_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0001"); const fptn::common::network::IPv6Address net_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0000"); const int mask_ipv6 = 120; fptn::filter::AntiScan anti_scan_filter( /* IPv4 */ server_ipv4, net_ipv4, mask_ipv4, /* IPv6 */ server_ipv6, net_ipv6, mask_ipv6); EXPECT_EQ(anti_scan_filter.apply( std::make_unique(net_ipv6.ToString())), nullptr) << "IPv6 packet in the network should be blocked"; EXPECT_EQ(anti_scan_filter.apply(std::make_unique( pcpp::IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:0002"))), nullptr); EXPECT_EQ(anti_scan_filter.apply(std::make_unique( pcpp::IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:00A0"))), nullptr); } TEST(AntiScanTest, AllowNonScanPacketIPv6) { /* IPv4 */ const fptn::common::network::IPv4Address server_ipv4("192.168.1.1"); const fptn::common::network::IPv4Address net_ipv4("192.168.1.0"); const int mask_ipv4 = 24; /* IPv6 */ const fptn::common::network::IPv6Address server_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0001"); const fptn::common::network::IPv6Address net_ipv6( "2001:0db8:85a3:0000:0000:8a2e:0370:0000"); const int mask_ipv6 = 126; fptn::filter::AntiScan anti_scan_filter( /* IPv4 */ server_ipv4, net_ipv4, mask_ipv4, /* IPv6 */ server_ipv6, net_ipv6, mask_ipv6); EXPECT_NE(anti_scan_filter.apply( std::make_unique(server_ipv6.ToString())), nullptr); EXPECT_NE(anti_scan_filter.apply(std::make_unique( pcpp::IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0371:1000"))), nullptr); EXPECT_NE(anti_scan_filter.apply(std::make_unique( pcpp::IPv6Address("2001:0db8:85a3:0000:0000:8a2e:0370:FFFF"))), nullptr); } } // namespace ================================================ FILE: tests/server/statistic/MetricTest.cpp ================================================ /*============================================================================= Copyright (c) 2024-2025 Stas Skokov Distributed under the MIT License (https://opensource.org/licenses/MIT) =============================================================================*/ #include #include // NOLINT(build/include_order) #include "fptn-server/statistic/metrics.h" TEST(MetricsTest, UpdateTraffic) { fptn::statistic::Metrics metrics; metrics.UpdateStatistics(1, "user1", 1024, 2048); const std::string metrics_data = metrics.Collect(); EXPECT_NE(metrics_data.find("fptn_user_incoming_traffic_bytes{session_id=" "\"1\",username=\"user1\"} 1024"), std::string::npos); EXPECT_NE(metrics_data.find("fptn_user_outgoing_traffic_bytes{session_id=" "\"1\",username=\"user1\"} 2048"), std::string::npos); }