From da2e0eef42d5d611b0125de17e467dbddf3d8d71 Mon Sep 17 00:00:00 2001 From: Andrew Guschin Date: Fri, 30 Oct 2026 01:59:28 +0400 Subject: Document subscription formats and ALT runtime integration - Add the Roentgen domain glossary - Record research on 3x-ui subscriptions, runtime integration, and defaults --- GLOSSARY.md | 45 +++++++++ docs/research/3x-ui-subscriptions.md | 161 +++++++++++++++++++++++++++++++ docs/research/alt-runtime-integration.md | 152 +++++++++++++++++++++++++++++ docs/research/linux-tun-xray.md | 132 +++++++++++++++++++++++++ docs/research/public-defaults.md | 95 ++++++++++++++++++ docs/research/simple-ssh-scope.md | 97 +++++++++++++++++++ 6 files changed, 682 insertions(+) create mode 100644 GLOSSARY.md create mode 100644 docs/research/3x-ui-subscriptions.md create mode 100644 docs/research/alt-runtime-integration.md create mode 100644 docs/research/linux-tun-xray.md create mode 100644 docs/research/public-defaults.md create mode 100644 docs/research/simple-ssh-scope.md diff --git a/GLOSSARY.md b/GLOSSARY.md new file mode 100644 index 0000000..f67c7bd --- /dev/null +++ b/GLOSSARY.md @@ -0,0 +1,45 @@ +# Roentgen + +Roentgen controls proxying of the machine on which it runs. + +## Language + +**Subscription**: +A saved URL supplied by 3x-ui and the collection of proxy configurations retrieved from it. + +**Configuration**: +A connection definition retrieved from a subscription that can be selected for proxying traffic. +_Avoid_: Server, node, profile + +**Subscription entry**: +An item received from a subscription, which may contain a usable configuration, unsupported connection data or invalid data. Invalid and unsupported entries remain visible but cannot be selected. + +**Selected configuration**: +The optional configuration explicitly chosen by the user for proxying traffic. It starts unset, and imports never choose one automatically; selection alone does not enable the proxy. + +**Viewed subscription**: +The subscription whose configurations are currently displayed. Changing the viewed subscription does not change the selected configuration. + +**Direct private networks**: +The private address ranges excluded from proxying by default, regardless of whether they are directly connected or which port is used. They include the RFC 1918 IPv4 networks and IPv6 unique-local range `fc00::/7`. + +**Local machine**: +The machine running Roentgen whose traffic it controls, including a server accessed through SSH. + +**TUN mode**: +The mode intended to route the local machine's traffic through the selected configuration. + +**Enabled**: +The user's requested state in which traffic must use TUN and the selected configuration. A proxy failure does not change this request or permit direct fallback. + +**Disabled**: +The user's requested state in which traffic uses ordinary direct routing, regardless of proxy availability. + +**Kill switch**: +The protection that blocks direct fallback while proxying is enabled but cannot carry traffic. + +**Proxy health**: +The observed ability of the selected configuration to carry traffic. Health is separate from the user's enabled or disabled state. + +**Ping**: +The interface's name for an HTTPS delay check carried through a proxy configuration. It measures the proxy path rather than an ICMP round trip. diff --git a/docs/research/3x-ui-subscriptions.md b/docs/research/3x-ui-subscriptions.md new file mode 100644 index 0000000..d7d7d7a --- /dev/null +++ b/docs/research/3x-ui-subscriptions.md @@ -0,0 +1,161 @@ +# What does a 3x-ui subscription contain? + +Research decision ticket for Roentgen. Verified on 2026-10-29. Planning only. + +## Answer and scope + +A 3x-ui subscription URL identifies a subscriber and returns an ordered collection of connection choices. Its ordinary response is a newline-separated share-link list, usually wrapped in Base64. Depending on the configured endpoint and request, it can instead return complete Xray client configurations, a Mihomo YAML profile, an HTML page, or status JSON. It does not define a universal stable identifier for each exported choice. See the [subscription documentation][docs], [HTTP controller][controller], and [export service][service]. + +Roentgen's agreed scope is a Linux CLI/TUI that controls the host on which it runs, including when invoked over SSH. Users add or update a subscription URL. The first configuration is initially selected if one exists, but import never enables the proxy. Proxy on/off is explicit. Enabled operation captures host traffic through TUN and persists through a service. Custom routing comes later. These are product requirements supplied for this investigation, not properties of 3x-ui. + +Recommendation: make the raw link endpoint the initial interoperability contract. Accept plain and Base64 link lists, preserve their order, and report unsupported entries. Treat full Xray JSON as a separate import format with an explicit normalization policy. Do not execute an imported full configuration as the host's runtime configuration. + +## Evidence baseline and limits + +The latest official 3x-ui release checked was [v3.11.0][panel-release], commit `c621ba9014a6aa21fd7c9ecf45655e44e24ebf13`. It was also the repository HEAD retrieved during this investigation. The panel release names Xray v26.10.21. The latest official Xray release checked was [v26.10.29][xray-release], and the source checkout was `0afec66085a85191e9dd2a4a6646c7c9a94688d1`. Source citations below pin those commits. Older deployed panels and forks can differ. + +Method: browsed official documentation and release pages, then read the official repositories' controller, exporters, ordering helpers, default template, external-link handling, and Xray configuration loaders. No live subscription URL or credentials were provided. Findings describe source behavior, not an end-to-end network test against a deployed panel. No application code, tracker, glossary, or runtime configuration changed. + +## Verified response formats and negotiation + +| Response | How the panel selects it | Body and caveats | +| --- | --- | --- | +| Raw links | Administrator-configured `subPath` plus subscriber ID | Each generated link ends with a newline. `subEncrypt=true` applies standard Base64 to the whole UTF-8 list. Otherwise the list is plain text. Both use a text response. | +| Xray JSON | Configured `subJsonPath`, when enabled | One full configuration object when exactly one exists and `subJsonAlwaysArray=false`; otherwise an array. The normal endpoint sends `text/plain`; `?view=raw` sends `application/json` and an attachment filename. | +| Automatically detected JSON | Raw endpoint with enabled JSON auto-detection and matching administrator-configured User-Agent regex | Always an array, with `application/json`. | +| Mihomo YAML | Configured `subClashPath`, when enabled; also `/mihomo/` unless that alias conflicts | Full YAML profile, not Xray JSON or a bare node array. Sends `application/yaml`. | +| Legacy Clash YAML | `/clash-legacy/`, when enabled and not conflicting | Compatibility-filtered profile. Returns HTTP 422 if no compatible proxy exists. | +| HTML | `Accept` containing `text/html`, `?html=1`, or `?view=html` | Subscription information page. Browser output is not evidence that the URL has returned machine-readable configuration. | +| Information JSON | Raw endpoint with `?format=info` | Status/page data without the `links` field. This is not an Xray configuration. | + +These branches, content types, route enablement, and alias collision checks are explicit in the [controller][controller]. JSON cardinality and empty-body handling are in [GetJson][json]. The configured raw and JSON paths are independent settings; a client cannot reliably discover one by appending `/json` to the other. [URL construction][urls] also allows configured public URIs and prefixes. + +Raw endpoint precedence is information query, HTML request, hardware-ID enforcement, Mihomo User-Agent detection, JSON User-Agent detection, then links. `Accept: application/json` alone does not select JSON. Both auto-detection modes default to false; JSON itself defaults to disabled. There is no general `?format=json` switch in these handlers. JSON and YAML `?view=raw` bypass the HTML branch. See [controller][controller] and [setting defaults][settings]. + +Recommended request policy: + +- Use GET with a stable, honest `Roentgen/` User-Agent and an `Accept` value that excludes HTML, such as `text/plain, application/json;q=0.9, */*;q=0.1`. +- Retain the supplied URL and query. Detect known HTML/status selectors and explain why they cannot import; do not silently rewrite unrelated query parameters. +- Detect format from a bounded body as well as Content-Type. Text/plain is valid for both raw links and JSON. Validate the parsed structure so an HTML login page or information JSON cannot become an empty success. +- Never impersonate another client to trigger auto-detection. Requesting an explicitly supplied JSON URL is more predictable than depending on an administrator's User-Agent regex. + +## Verified protocols and encodings + +The raw export dispatch handles the following forms. These are observed panel outputs, not a promise that every protocol will belong in Roentgen's first release. See the [link generators][service] and [raw dispatch][dispatch]. + +| Exported choice | Raw representation | Xray implication | +| --- | --- | --- | +| VLESS | `vless://@host:port?...#remark` | Xray `vless`; credentials, encryption, flow, transport, and TLS/REALITY all matter. | +| VMess | `vmess://` followed by Base64 JSON | Decode the inner JSON separately from the subscription's outer Base64. Fields include `add`, `port`, `id`, `scy`, `net`, `tls`, and `ps`. | +| Trojan | `trojan://@host:port?...#remark` | Xray `trojan`, including security and transport settings. | +| Shadowsocks | `ss://...` | SIP002 uses Base64url of `method:password` for userinfo. Shadowsocks 2022 instead uses percent-encoded method/password userinfo. | +| Hysteria | `hysteria2://...`, or `hysteria://...` for version 1 | Xray's outbound protocol name is `hysteria`; URI version and associated transport settings need translation. | +| WireGuard | `wireguard://@host:port?...#remark` | Includes public key, client addresses, and optional preshared key, MTU, DNS, and keepalive. | +| AmneziaWG | `vpn://` plus unpadded Base64url configuration text | Panel's JSON exporter explicitly skips it; Xray's inspected outbound loader has no AmneziaWG protocol. | +| TUIC | `tuic://:@host:port?...#remark` | Panel's JSON exporter explicitly skips it; Xray's inspected outbound loader has no TUIC protocol. | +| MTProto | `tg://proxy?server=...&port=...&secret=...` | A Telegram proxy link, not an all-traffic Xray outbound. | +| Information entry | `socks://127.0.0.1:1080#...` | Optional synthetic usage/expiry entry. It can appear first or replace every real entry. | + +The optional information entry is especially relevant to "select first". It can be a syntactically valid proxy URI while representing only usage, expiry, or quota information. JSON has an equivalent dummy SOCKS configuration. Arbitrary remarks make detection by display text unreliable. See [information-node generation][info] and [JSON generation][json]. + +Current source has additional differences between the formats. The inbound query admits MASQUE, and the JSON generator supports it, but raw `GetLink` has no MASQUE case. MTProto also has no explicit JSON generation case; the generic code can still append the default direct/block outbounds. Therefore validate that a JSON choice has an actual usable proxy outbound; do not infer equivalence between raw and JSON counts. These are source observations requiring fixtures against a deployed release before making a compatibility guarantee. See [inbound query][query], [dispatch][dispatch], [JSON switch][json-switch], and [Xray loader][xray-loader]. + +A subscriber can also receive administrator-added external links or links fetched by the panel from an external subscription. The raw exporter appends these after local exports. The JSON exporter parses supported external links into proxy outbounds and skips those it cannot parse. The protocol list is therefore not a closed raw-input schema. [External configuration handling][external] + +Recommended decoding policy: strip a UTF-8 BOM if present, accept LF or CRLF and blank lines, and try a recognizable plain link list before one outer Base64 decode. Accept padded/unpadded standard Base64 and Base64url as a compatibility extension, with bounded decoded size. The inspected panel itself emits standard Base64 for the outer wrapper. Parse VMess inner Base64 and protocol-specific userinfo only at their own layer. Percent-decode individual URI components once, preserve IPv6 brackets and credential punctuation, and retain unknown fields for diagnostics. Never repeatedly Base64-decode arbitrary content until something parses. + +## Ordering and identity on refresh + +Verified ordering is more complex than database creation order. Local inbounds start in ascending `sub_sort_index`, then `id`. Hosts-first mode reorders endpoint variants through the Hosts settings. One inbound/client can expand into multiple addresses. External entries follow their own client/sort/index order and append after local entries. JSON also interleaves configured balancer profiles. Optional information entries can precede everything. Preserve the final received order rather than rebuilding a presumed order. See [query][query], [host ordering][host-order], [raw assembly][service], [external entries][external], and [JSON ordering][json]. + +The exported URI and JSON object expose connection credentials and settings, not the database inbound/host/client IDs used for sorting. A subscriber ID identifies the subscription, not one configuration. Remarks are templates and can change with usage or expiry. REALITY generation randomly chooses an SNI and short ID from configured lists for each fetch. Host address expansion can also introduce changes. Consequently, array index, remark, raw URI hash, full JSON hash, and credential UUID alone are each insufficient as a universal stable node identity. This is an inference from the [link generators][service], [REALITY generation][reality], [remark expansion][remarks], and [host expansion][host-sub]. + +Recommended matching policy, with its limits: + +1. Assign each imported occurrence a local opaque ID and keep a separate content digest. Scope matches to the local subscription record, which survives URL edits. +2. Match an unchanged normalized connection definition first. Exclude display remarks, usage metadata, and serialization order from that comparison. Normalize equivalent URI/JSON field shapes deliberately. +3. For remaining entries, allow a weaker match only when a candidate is unique on both sides. A candidate key can include protocol, endpoint host/port, authentication identity, transport kind/path/service, security kind, and server public key. Changes in randomized REALITY SNI/short ID need special treatment; ignore them only for identity matching, never for the connection settings actually applied. +4. If matching is ambiguous, credentials or endpoint rotate, or a selected choice disappears, expose a selection-needs-attention state. Do not guess based on a name or ordinal. Keep old records long enough to explain the change. + +This policy cannot guarantee identity across arbitrary provider edits. Duplicate endpoints and credentials can represent distinct choices; an operator can rotate every observable field. Supporting perfect continuity would require an upstream stable exported identifier or an explicit user mapping. Conservative failure is a product choice, not a 3x-ui guarantee. + +Initial selection and refresh are separate operations. Apply the agreed first-configuration default only on initial successful import. Refresh must preserve the local selection when it can match it and must never transition proxy state from off to on. Whether "first configuration" means the first received entry or the first supported non-information entry remains an explicit product decision. + +## Redirects, failures, and metadata + +The subscription handlers return 404 when no subscription rows match, 500 for internal errors, and 422 for a legacy Clash profile with no compatible proxy. Hardware-ID enforcement can also return 404, with `X-Hwid-*` response headers. It reads `X-HWID`, User-Agent, and device headers. A GET can record a subscription fetch and may consume/register a hardware-ID slot under the panel's policy. HEAD is routed through the same handlers, so do not assume it is a side-effect-free preflight. [HTTP handlers][controller] + +Successful output does not prove a usable proxy exists. Disabled/expired choices, excluded inbounds, closed host bindings, unsupported conversions, synthetic information entries, or inactive external subscriptions can leave empty or unusable results. JSON can return HTTP 200 with an empty body and subscription metadata for some inactive/closed cases. [Raw assembly][service] and [JSON empty handling][json] + +The inspected handlers do not define a redirect-based discovery protocol. Redirects may come from the router, reverse proxy, HTTP-to-HTTPS deployment, or a moved URL. HTTP semantics define `Location`, relative resolution, redirect codes, and 304 handling; no 3x-ui-specific redirect guarantee follows. [RFC 9110, redirection][http-redirect] + +Recommended transport policy: accept HTTP and HTTPS URLs, verify TLS, bound time, body size, decoded size and redirect count, and detect loops. Keep the original configured URL separately from the final fetched URL. Follow redirects under an explicit policy, reject HTTPS-to-HTTP downgrade by default, and do not forward authorization or device-identifying headers across origins without an established policy. Treat subscription URLs and response bodies as secrets because they carry reusable credentials. Report status/host and redacted diagnostics, not the full URL token or body. + +Recommended refresh semantics: fetch, decode, validate, and reconcile a candidate snapshot before an atomic replacement. A timeout, TLS error, non-success status, HTML response, malformed body, unsupported format, or all-unsupported result preserves the last good snapshot and selection. Represent a verified empty result separately from failure; decide whether it clears entries before implementation. A 304 is usable only if Roentgen has a corresponding cached representation and validator. The panel sets no-cache/no-store headers, so do not assume validators or delta updates. [Cache headers][cache] + +Metadata belongs beside the list, not in identity or activation logic. `Subscription-Userinfo` contains `upload`, `download`, `total`, and `expire`; the panel converts expiry milliseconds to Unix seconds and can adjust month-boundary presentation. `Profile-Update-Interval` suggests refresh hours. `Profile-Title` and `Announce` use a `base64:` prefix. `Support-Url` and `Profile-Web-Page-Url` are optional. Routing and client-specific policy headers may also appear. Treat metadata as optional display data and refresh advice. Do not let `Routing`, `Routing-Enable`, or `Hide-Settings` change Roentgen's local policy. [Metadata headers][headers] and [userinfo serialization][userinfo] + +## Xray fields and the host-owned runtime + +A complete subscription JSON object can contain local SOCKS/HTTP listeners, DNS, logging, policy, routing, stats, extra outbounds, mux, and balancer/observatory configuration. The shipped template listens on loopback ports 10808 and 10809; it does not establish Roentgen's host TUN policy. Provider settings can replace DNS and routing. [Default template][template] and [JSON template assembly][json] + +| Connection data to retain | Required interpretation | +| --- | --- | +| `outbounds[].protocol`, `settings` | Protocol-specific endpoint and authentication. Current VLESS/VMess support flat `address`, `port`, `id` and historical `vnext[].users[]` forms. Trojan/Shadowsocks can use server collections. Preserve cipher, encryption, flow, passwords and keys. | +| `streamSettings.network` and transport settings | RAW/TCP, WebSocket path and Host, gRPC service/authority, HTTPUpgrade, XHTTP mode/extra/xmux, KCP and other supported transport options are connection behavior. | +| `streamSettings.security`, `tlsSettings`, `realitySettings` | SNI, fingerprint, ALPN, certificate verification/pins, REALITY public key, short ID, spiderX and post-quantum verification settings must survive normalization. | +| `mux`, `finalmask` and connection dependencies | These can be necessary. Vision flow imposes mux restrictions; XHTTP can carry its own multiplexing. Unknown extensions need a declared support result rather than silent deletion. | +| `tag`, routing references and multiple proxy outbounds | A generated single-node profile commonly uses `proxy`, `direct`, and `block`. Balancer profiles can contain several proxy outbounds and dependencies. Tags are runtime references, not subscription node IDs. | + +Sources: [panel outbound generation][json-switch], [Xray VLESS loader][vless], [VMess loader][vmess], [outbound documentation][outbound-docs], and [transport documentation][transport-docs]. Current source confirms `rawSettings` and the `tcpSettings` alias. [Transport loader][transport-source] + +Recommendation: Roentgen owns the runtime envelope, including TUN, DNS strategy, host routing, loop prevention, management access, service lifecycle and off-state cleanup. Import supported connection definitions into that envelope. Reject complex JSON profiles that cannot be normalized safely rather than silently flattening a balancer or applying provider listeners/routing. Validate the generated candidate with the pinned Xray version before activation. Version compatibility needs an explicit supported matrix. + +Current Xray has a native `tun` inbound on Linux. Its configuration includes interface name, MTU, gateway prefixes, automatic system routes and outbound interface selection. The current Linux source also has an opt-in DNS takeover path using `resolvectl` and `autoSystemDnsToGateway`; the `dns` array itself is documented as Windows-only. Linux route installation and cleanup exist in source. Native TUN support therefore does not establish a universal DNS configuration or prove SSH-safe activation. [TUN documentation][tun-docs], [TUN configuration loader][tun-source], and [Linux implementation][tun-linux] + +Recommendations for the stated host scope: proxy on must apply a tested host-owned capture policy for IPv4, IPv6 and DNS, and preserve the SSH management connection and core egress. Proxy off must remove Roentgen's capture state. Installing or starting the persistent control service must not imply proxy on after an import. Whether the user's explicit on state survives reboot, and how to recover after a failed route change over SSH, need separate lifecycle decisions. No TUN or service implementation is proposed by this report. + +## Open decisions for the parent + +1. Format contract. Raw plain/Base64 first is recommended. Decide whether the first release also accepts full Xray JSON. YAML conversion is a separate capability; an Xray binary cannot consume Mihomo YAML directly. +2. Supported connection matrix. Decide protocols, transports, security extensions and minimum Xray version. Recommend an explicit unsupported status for every entry outside it, including TUIC, AmneziaWG and Telegram MTProto when Xray is the only engine. +3. First-entry semantics. Confirm how the initial-first requirement handles information nodes and unsupported entries. Recommendation: display received order, initially select the first usable connection, and explain skipped entries. This refines the supplied requirement and needs a product decision. +4. Refresh continuity. Approve conservative matching, what happens to a running connection when its definition changes, and behavior when selection disappears. Recommendation: do not select a replacement automatically; never enable through refresh. Keeping an already active old snapshot versus turning off is unresolved. +5. Empty and partial updates. Decide whether a valid empty response clears entries and whether a mixed malformed/valid list partially imports. Recommendation: preserve the last good snapshot on malformed updates and show unsupported entries separately from malformed entries. +6. Redirect and HWID policy. Decide cross-origin redirects, plain HTTP acceptance, and whether Roentgen supplies a persistent host-scoped hardware ID when required. HWID enrollment has server-side effects and is not a generic mandatory 3x-ui requirement. +7. Runtime ownership. Decide whether JSON balancers and provider mux/obfuscation dependencies are supported initially. Custom provider routing remains deferred under the supplied scope. +8. Service semantics. Decide reboot restoration of explicit on/off state, DNS integration, SSH protection and rollback. These are required host-control decisions that a subscription payload does not resolve. + +Before implementation, capture sanitized fixtures for plain and outer-Base64 lists, nested VMess, JSON object/array, information entries, randomized REALITY refreshes, reordered/duplicate entries, external unsupported links, valid empty output, HTML/status JSON, HWID denial and redirect/error responses. Validate supported generated configurations with the chosen Xray release. This is proposed validation work, not tests run during this research. + +[docs]: https://docs.sanaei.dev/docs/config/subscription/ +[panel-release]: https://github.com/MHSanaei/3x-ui/releases/tag/v3.11.0 +[xray-release]: https://github.com/XTLS/Xray-core/releases/tag/v26.10.29 +[controller]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/controller.go +[service]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/service.go +[json]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/json_service.go +[urls]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/service.go#L3323-L3384 +[settings]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/web/service/setting.go +[dispatch]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/service.go#L919-L944 +[info]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/service.go#L429-L608 +[query]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/service.go#L750-L780 +[json-switch]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/json_service.go#L627-L1025 +[external]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/external_config.go +[host-order]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/host_order.go +[reality]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/json_service.go#L886-L912 +[remarks]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/remark_vars.go +[host-sub]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/host_sub.go +[http-redirect]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4 +[cache]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/controller.go#L764-L768 +[headers]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/controller.go#L945-L1015 +[userinfo]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/calendar_expire.go +[template]: https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/default.json +[xray-loader]: https://github.com/XTLS/Xray-core/blob/0afec66085a85191e9dd2a4a6646c7c9a94688d1/infra/conf/xray.go +[vless]: https://github.com/XTLS/Xray-core/blob/0afec66085a85191e9dd2a4a6646c7c9a94688d1/infra/conf/vless.go +[vmess]: https://github.com/XTLS/Xray-core/blob/0afec66085a85191e9dd2a4a6646c7c9a94688d1/infra/conf/vmess.go +[outbound-docs]: https://xtls.github.io/en/config/outbound.html +[transport-docs]: https://xtls.github.io/en/config/transport.html +[transport-source]: https://github.com/XTLS/Xray-core/blob/0afec66085a85191e9dd2a4a6646c7c9a94688d1/infra/conf/transport_internet.go +[tun-docs]: https://xtls.github.io/en/config/inbounds/tun.html +[tun-source]: https://github.com/XTLS/Xray-core/blob/0afec66085a85191e9dd2a4a6646c7c9a94688d1/infra/conf/tun.go +[tun-linux]: https://github.com/XTLS/Xray-core/blob/0afec66085a85191e9dd2a4a6646c7c9a94688d1/proxy/tun/tun_linux.go diff --git a/docs/research/alt-runtime-integration.md b/docs/research/alt-runtime-integration.md new file mode 100644 index 0000000..6900ad8 --- /dev/null +++ b/docs/research/alt-runtime-integration.md @@ -0,0 +1,152 @@ +# System Xray, DNS, and polkit on ALT Linux + +Researched 2026-10-29 for Roentgen's Linux first release. This is a feasibility report and proposed design, not an implemented or tested network configuration. The scope is ALT Linux on the same host as the CLI/TUI, with a persistent system service and the distribution's `/usr/bin/xray`. No managed binary, binary download, container forwarding, or concurrent full-tunnel VPN is proposed. + +## Findings and evidence boundary + +The installed Xray can carry the required host TCP/UDP traffic over native TUN and Trojan TCP TLS. It needs Roentgen to configure all Linux routing and DNS capture. It does not have the automatic route, outbound-interface, or DNS takeover settings described in the earlier report about Xray 26.10.29. A kernel nftables guard independent of both Xray and the controller can retain blocking after either crashes. Protecting boot requires a separate early guard and explicit dependencies on the ALT network providers. Neither ordinary Xray startup nor `network-online.target` supplies that guarantee. These are design conclusions from the sources below, not successful live experiments. + +Read-only inspection confirmed ALT Regular x86-64 with `xray-core-26.3.27-alt2`, systemd `262-alt1`, polkit `127-alt1`, NetworkManager `1.58.1-alt2`, openresolv `3.14.1-alt1`, nftables `1.1.7-alt2`, and iproute2 `6.18.0-alt1`. `/usr/bin/xray version` reports `26.3.27`, build `v1.260327.0`. Resolved, NetworkManager, and polkit are active. `/etc/resolv.conf` is a regular resolvconf-generated file with a non-loopback LAN nameserver. The NSS hosts line includes `dns`, rather than `resolve`. Private names and resolver addresses are deliberately omitted. This establishes the conventional libc DNS path; it does not prove every application uses that path. + +Inspection used RPM metadata/file lists, installed unit files and manuals, service active-state queries, and sanitized resolver metadata. No service, package, interface, route, DNS setting, firewall object, or polkit rule was changed. No agent, authorization request, remote proxy, or speed test was run. Root-only polkit rule directories could not be read without elevation; their effective contents remain unknown. The earlier main-checkout research reports were read as context only. This checkout has no glossary or ADR directory. + +## Pin the installed release, not the current documentation + +Upstream tag `v26.3.27` resolves to `d2758a023cd7f4174a5a5fa4ff66e487d4342ba0`. ALT packaging tag `26.3.27-alt2` resolves to `dfce17e2794d0bc2bba421d8b3383a0ba6117af2`. Its `.gear/tags/list` names that exact upstream commit, and `.gear/rules` exports the upstream tag as the source tar. The spec builds `./main` with vendored dependencies and declares no source patch. `alt2` adds GeoIP/geosite files; it does not add newer TUN automation. This connects the installed package release to the inspected upstream source without assuming that today's branch head matches it. [Upstream tag API](https://api.github.com/repos/XTLS/Xray-core/git/ref/tags/v26.3.27), [ALT refs](https://git.altlinux.org/gears/x/xray-core.git/refs), [ALT source tag mapping][alt-tags], [ALT export rules][alt-rules], [ALT spec][alt-spec]. + +Native TUN first shipped in v26.1.23. At v26.3.27 its JSON settings are exactly `name`, `MTU`, and `userLevel`; omitted name and MTU become `xray0` and `1500`. Use the source spelling `MTU`. The Linux implementation opens `/dev/net/tun`, creates a nonpersistent TUN, sets its MTU, and brings it up. It does not assign addresses or install routes/rules. Its stack registers IPv4, IPv6, TCP, and UDP. It has no ICMP transport, unlike the newer report's description of local echo responses. A synthetic TCP SYN-ACK also does not demonstrate remote reachability. [First TUN release](https://github.com/XTLS/Xray-core/releases/tag/v26.1.23), [pinned settings][tun-conf], [pinned Linux implementation][tun-linux], [pinned stack][tun-stack], [pinned limitations][tun-readme]. + +Do not generate `gateway`, `dns`, `autoSystemRoutingTable`, `autoOutboundsInterface`, `autoSystemDnsToGateway`, or `desc` for this release. Its Linux implementation also has no `XRAY_TUN_FD` handling. The JSON decoder does not call `DisallowUnknownFields`, so a config containing newer fields can appear acceptable while those fields do nothing. Roentgen must validate its own narrow schema and use a version-specific generator. [Settings][tun-conf], [Linux source][tun-linux], [JSON loader][json-loader]. + +Recommend `26.3.27` as the first supported upstream baseline, with the observed `26.3.27-alt2` ALT build as the initial validation target. `26.1.23` is the historical TUN floor, not a claim that every release since then has passed this product's contract. Check RPM identity, absolute binary path and executable ownership, parsed upstream version, TUN device availability, kernel nftables/conntrack and IPv6 support, and required routing commands. Reject older or unknown incompatible versions before activation. Re-check after distribution upgrades and retain blocking if requested enabled. A version comparison alone is insufficient; use an explicit tested compatibility range and the generated-config checks. Nothing here requires a newer Xray or an automatic download. + +The ALT spec installs `/usr/share/xray/geoip.dat` and `geosite.dat` from third-party rule-list projects. They are not needed for the first release's explicit CIDR and DNS policy. Do not make their contents an implicit default routing policy. [ALT spec][alt-spec]. + +## Config generation and validation + +The installed help advertises `/usr/bin/xray run -test -format json -config ` and `run -dump`. Source gives an important qualification. `-test` calls `core.New` before checking the test flag. TUN construction calls its `Init`, which creates the device and starts its stack/link. Thus testing a complete TUN config can alter the network when privileged, or fail on permissions when unprivileged. It is not a pure parser check. `-dump` exits through the config-merge path before core construction, but only checks merging/parsing and may print credentials. [Command implementation][run], [core construction][core], [TUN constructor][tun-handler]. + +Use Roentgen schema validation first. Validate the Trojan/TLS/routing portion with `run -test` on a generated copy without the TUN inbound, restricted to permitted settings, no file logging, no external config URLs, and no privileged capabilities. Validate a complete TUN config only in a disposable network namespace or ALT VM during development. Production activation itself must handle device/config failure under an already installed guard. A safe merge inspection is `run -dump -format json -config ` with protected output, but it cannot certify TUN support or working routing. None of these validation commands was executed on a TUN config during this research. + +Import only the agreed plain or outer-Base64 subscription list of Trojan TCP TLS links. Normalize to a typed profile containing the remote address/port, password, TLS server name and supported TLS options. Require certificate verification. Do not pass provider JSON, command arguments, outbound tags, file paths, routing, DNS, or helper instructions into the privileged runtime. The pinned Trojan parser accepts one server, requires its address, nonzero port and password, and rejects Trojan `flow`. It emits a deprecation warning but still builds this protocol. The client implements UDP encapsulation as well as TCP over its server connection. Real server UDP and IPv6 reachability remain acceptance tests. [Pinned Trojan config][trojan-conf], [pinned client][trojan-client]. + +Custom IP/port/protocol routing and custom DNS are feasible through this core's router and outbounds. TUN initially supplies destination IPs, not hostnames. Domain routing needs deliberately configured DNS knowledge or sniffing and cannot promise domain recognition for arbitrary encrypted traffic. Limit the first release to product-defined rules. User direct-to-Internet rules would relax the stated enabled guarantee and need a separate decision. LAN and SSH exceptions already form explicit direct policy. [Pinned router][router], [TUN handler][tun-handler], [TUN DNS discussion][tun-readme]. + +## DNS on the observed ALT resolver arrangement + +Do not use `resolvectl dns` or `~.` as the first release's solution for this host. An active resolved does not make applications use its stub or its D-Bus resolver. NetworkManager distinguishes its DNS plugin from its `rc-manager`; `resolvconf` is a supported resolver-file manager. Resolved can also consume a resolver file owned elsewhere. The observed file and NSS arrangement therefore does not justify transferring DNS ownership to resolved. [NetworkManager DNS and rc-manager documentation](https://www.networkmanager.dev/docs/api/latest/NetworkManager.conf.html), [resolved resolver-file modes](https://www.freedesktop.org/software/systemd/man/latest/systemd-resolved.service.html). + +Recommend retaining resolvconf ownership and capturing ordinary host unicast DNS in the packet path. Mark TCP and UDP destination port 53 for TUN routing before LAN/direct exceptions, for both families and every ordinary service UID. In Xray, put a DNS rule before ordinary routing rules. Send those flows to a dedicated `freedom` outbound with `settings.redirect` set to an approved resolver IP and port 53, chained with `proxySettings.tag` to the selected Trojan outbound. This forwards whole DNS exchanges instead of resolving only address records. The pinned freedom code overrides the destination and carries TCP/UDP; its outbound dialer dispatches a chained destination through the tagged outbound. This is a source-supported proposed composition, not a tested config recipe. [Freedom config][freedom-conf], [freedom implementation][freedom], [outbound chaining][outbound-handler], [socket marking][sockopt]. + +A LAN-only nameserver often cannot be reached from the Trojan server, which is why simply proxying the original LAN destination is insufficient. Use a remote-reachable resolver chosen by product policy or the operator. Keep the original resolver file unchanged, so explicit disable restores its normal use by removing capture. DNS to hardcoded external servers is captured too. Application DoH, DoT, and DoQ traverse the general proxy policy; Roentgen does not decrypt them. Local loopback stub requests can stay local, but every stub's eventual upstream TCP/UDP query must enter capture. Do not broadly exempt resolved or NetworkManager UIDs. Enforce DNS precedence over the direct LAN exception in both routing and the final egress guard. The guard must drop physical TCP/UDP port-53 egress before any LAN accept, apart from the bounded bootstrap-worker exception. A routing failure must not turn LAN DNS into direct egress. + +This recommendation avoids a resolver-file rollback dependency on controller survival. It still requires testing policy-route relookup of locally originated marked packets, local stub upstream sockets bound to physical links, and queries to local non-loopback addresses that the kernel would deliver through its local table. Support that last case through explicit interception to an owned DNS listener or reject it during preflight. The inspected host's resolver is a separate LAN address, so that local-destination case is not established here. A physical-bound socket that cannot be captured must fail closed, never silently receive a direct exception. + +An alternative is an owned local DNS forwarder plus an exclusive openresolv entry. The installed manual supports `resolvconf -a -x` with input on stdin and deletion with `-d`; only the latest exclusive entry is used. Openresolv distributes entries to libc and configured resolver subscribers. This is supported integration machinery, but Roentgen would need to own the listener, stale-entry recovery and DNS during disabled mode. It would still need capture for applications that ignore the file. Prefer packet capture first; do not overwrite `/etc/resolv.conf` or edit NetworkManager connection profiles as a shortcut. No resolvconf mutation was performed. [Pinned openresolv manual](https://github.com/NetworkConfiguration/openresolv/blob/v3.14.1/resolvconf.8.in), [project resolver-manager explanation](https://roy.marples.name/projects/openresolv). + +The pinned Xray `dns` outbound is another option, but its default `nonIPQuery` behavior rejects non-A/AAAA queries. `nonIPQuery: "skip"` forwards those queries through its outbound connection, while A/AAAA use its built-in DNS client. That requires separate routing/bootstrap controls for both paths. Whole-message forwarding through the redirected, chained freedom outbound is a smaller initial policy and preserves MX/TXT/SRV/HTTPS queries for testing. [Pinned DNS config][dns-conf], [pinned DNS processing][dns-out]. + +Endpoint bootstrap is a separate infrastructure exception. Resolve only the selected proxy hostname through a dedicated service-controlled worker, with direct egress limited to approved bootstrap resolver tuples. Cache endpoint addresses, refresh them with bounded retries and TTL handling, and preserve the TLS server name when connecting to an IP. Update endpoint allowlists before switching addresses and remove old entries safely. Do not give every application a resolver escape, or give Xray unrestricted DNS/direct Internet access. DNS-independent bootstrap IPs avoid recursive dependency on the selected tunnel. Initial subscription retrieval while disabled can use ordinary networking. Enabled refresh uses the working proxy or waits if it is unavailable; it must not create a general direct fallback. + +Local DNS names may stop resolving under forced remote DNS. mDNS/LLMNR use other ports and multicast; define their LAN treatment explicitly. If LAN split DNS is desired, it is an explicit exception to universal ordinary DNS capture. The report recommends no silent LAN DNS exception. + +## Local service boundary and polkit + +Use a root-owned persistent Roentgen system daemon with a small local system-bus API. The CLI/TUI stays unprivileged. The service owns validated desired state, fixed helper invocations, configuration files, policy rules, routes, and its nftables table. Run Xray under a dedicated Roentgen runtime UID with only the capabilities and device access needed for TUN and socket marks. Keep route/firewall management in the controller. This release still needs `CAP_NET_ADMIN` in Xray to create its TUN, so that ownership split is not a security boundary against a compromised core. It is sufficient to separate lifecycle responsibility for crash handling. The observed package unit runs as `xray` with `CAP_NET_ADMIN` and `CAP_NET_BIND_SERVICE`, starts `/etc/xray/`, and uses `Restart=on-abort`. Use a separate product-owned unit/config path that invokes the same `/usr/bin/xray`; do not take over an unrelated packaged Xray instance. Detect competing TUN/firewall ownership. [ALT installed-unit source][alt-unit], [Linux TUN privilege requirements](https://docs.kernel.org/networking/tuntap.html), [socket options][sockopt]. + +For every mutation, derive the subject from the incoming D-Bus message's unique sender name and call polkit `CheckAuthorization` with a `system-bus-name` subject. The system bus owns sender identity and can supply connection credentials. Polkit 127 resolves that bus name to credentials, with pidfd support where available. Never accept a caller-supplied UID/PID/session/name as proof, authorize the daemon's own subject, or trust a client-side `pkcheck` success. Scope the pending operation to the caller connection, exact validated request and current state generation; cancel on disconnect and re-check state before applying it. [D-Bus sender/credentials specification](https://dbus.freedesktop.org/doc/dbus-specification.html), [polkit bus-subject implementation][polkit-bus], [Authority API][polkit-api]. + +Proposed actions, each mapped to fixed typed methods: + +- `org.roentgen.runtime.set-enabled` authorizes `SetEnabled(bool)` and the persisted boot intent, including explicit disable and recovery disable. +- `org.roentgen.runtime.select-config` authorizes selecting an existing stored profile by opaque ID, including a guarded switch while enabled. +- `org.roentgen.config.modify` authorizes bounded typed profile/subscription updates. It grants no arbitrary Xray JSON or filesystem access. +- `org.roentgen.policy.modify` authorizes validated LAN, SSH, resolver and supported routing policy changes. Disallow removing the active management path through an implicit profile update. +- `org.roentgen.diagnostics.download-test` authorizes a manual bounded speed test through the selected profile. Periodic delay checks need no recurring user authorization. + +Expose sanitized status without secrets. Protect credential/subscription export separately if added. Methods never accept executable paths, shell strings, arbitrary systemd units, arbitrary nftables scripts, route commands or privileged file destinations. A bus policy may admit method calls to this one service, but the service must still authorize them. Polkit approves an operation; it neither makes the CLI root nor makes untrusted input safe. Do not use a general `pkexec`, systemd unit-management permission, or blanket privileged arbitrary execution. [Polkit mechanism/action model][polkit-doc127]. + +Ship action XML with explicit `auth_admin` for `allow_any`, `allow_inactive`, and `allow_active`. SSH is not a local active-console authorization case, so setting only `allow_active` would exclude the intended workflow. `auth_admin` requires an administrator identity, not necessarily the caller's password. Avoid `auth_self` and `auth_admin_keep` initially. Cached action authorization does not bind future parameter values, and disabling capture is security-sensitive too. Administrator overrides remain the host administrator's policy. No Roentgen group or permissive JavaScript rule is needed. [Pinned policy semantics][polkit-doc127]. + +ALT's polkit spec splits `50-default.rules` into `polkit-default-rules`, whose description designates wheel members as administrators. That subpackage is absent on the inspected host. Upstream 127 falls back to UID 0 when admin rules yield no identities. Because the installed rule directories were unreadable, do not infer that this host actually uses wheel, root-only auth, a usable root password, or any particular administrator identity. Authentication policy and PAM availability need a real operator test later. This is compatible with choosing polkit instead of a product-specific group. [ALT polkit spec](https://packages.altlinux.org/en/sisyphus/srpms/polkit/specfiles/), [pinned administrator fallback][polkit-backend], [ALT rule template][polkit-alt-default]. + +For interactive SSH with a controlling terminal, use the installed `/usr/bin/pkttyagent` for the CLI's retained system-bus connection via `--system-bus-name --notify-fd `. Wait for the notification FD to close before making the authorized request. The alternative is `--process pid,start-time`; avoid bare PID reuse. Suspend the TUI's raw/full-screen mode while the agent owns the terminal, then restore it. `--fallback` preserves an existing agent and may therefore select an unsuitable GUI agent; do not rely on it to guarantee a terminal prompt. Agent registration can fail and must be reported. Stop the agent when the interaction ends. This path is supported by the installed manual and pinned program, but no agent was started here. [pkttyagent manual](https://www.freedesktop.org/software/polkit/docs/latest/pkttyagent.1.html), [pinned agent][polkit-agent]. + +Only explicit interactive requests set `AllowUserInteraction`. Noninteractive invocations use flags zero, start no agent, and return an authentication-required/denied error without mutating intent. A suitable authorization may already exist under site policy, so noninteractive does not necessarily mean deny. Handle `is_authorized`, challenge, errors, dismissal and cancellation separately; only an affirmative authorization permits mutation. Use a unique cancellation ID, cancel pending checks on user cancel, timeout or disconnect, and serialize the resulting transaction. Cancellation before commit changes nothing. Cancellation after the service has durably accepted a transaction reports its ID/state rather than claiming rollback. Background retries, boot restoration and the 30-second health job operate on previously authorized state and never prompt. [Authority flags/results/cancellation][polkit-api]. + +## Persistent enabled intent and the crash-safe guard + +The minimum process layout is an early one-shot guard, one persistent controller, and one Xray runtime. Xray can keep an isolated loopback probe inbound alive while capture is disabled. A distinct, short-lived bootstrap worker can narrow infrastructure DNS privileges. No separate tun2socks process is required. Polkit and the system bus remain system dependencies, not child processes. + +Store requested enabled/disabled, selected profile snapshot and exception policy in root-owned durable files with atomic replacement, file and directory fsync, strict schemas and permissions. Keep enough guard input on a local early-mounted filesystem, preferably alongside configuration on the root filesystem, rather than depending on late `/var` mounts. Record effective state separately, such as disabled, enabled/retrying, enabled/healthy, or error. Requested enabled must never turn into disabled because Xray cannot start. + +Use an owned `inet` nftables output filter with blocking behavior when enabled. Permit loopback and the owned TUN path, narrow link maintenance, direct LAN policy after DNS capture, recognized management replies, and restricted runtime/bootstrap infrastructure. The runtime escape should require its dedicated UID plus approved endpoint address/port/protocol, not UID alone. Never exempt all root traffic or accept all established connections before the guard, since ordinary direct connections established before enable would survive. Retain the main-table physical routes and use higher-priority UID/mark and management exceptions, followed by policy capture into a separate dual-stack TUN table. [nftables kernel/filter model](https://netfilter.org/projects/nftables/manpage.html), [policy-routing selectors](https://man7.org/linux/man-pages/man8/ip-rule.8.html), [pinned Xray loop warning][tun-readme]. + +Kernel rules outlive the userspace process that installed them. If the core exits, its nonpersistent TUN disappears and policy lookup may fall through to physical routes, but the independent output guard must still drop ordinary physical egress. Controller failure must also leave that guard intact. Do not remove it from `ExecStopPost`, a destructor, a restart hook or ordinary failure cleanup. Use interface-name matching so loss of the TUN does not invalidate the guard. Atomic nftables batches update only Roentgen-owned objects. The transition can be atomic within nftables, but persistence, routing and process startup are separate operations and require ordering and reconciliation. [Kernel TUN lifetime](https://docs.kernel.org/networking/tuntap.html), [atomic nftables replacement](https://wiki.nftables.org/wiki-nftables/index.php/Atomic_rule_replacement). + +On enable, validate input and discover exceptions, install blocking policy, then durably commit enabled intent before reporting acceptance. Start/retry Xray, configure owned routes and DNS capture, and report healthy only after an actual HTTPS exchange through the selected outbound. Failure at any stage leaves enabled/retrying and the guard intact. A crash before the durable commit may leave temporary blocking even though the previous intent was disabled; reconciliation follows the durable record. On explicit disable, prepare removal of capture while still guarded, durably commit disabled, then release the owned guard and stop unneeded TUN processing. Interrupted disable can remain temporarily blocked; reboot uses the committed disabled record. Never claim these multi-resource steps form a single atomic transaction. + +At boot, a dedicated guard unit must run before `network-pre.target`, read the persisted intent and load the guard without network access or authentication. Use deliberate early-unit dependencies, including `DefaultDependencies=no` with the required local mounts and shutdown ordering, and pull in `network-pre.target`. Ordering alone does not propagate startup failure. Add `Requires=` and `After=` dependencies from both ALT `network.service` and NetworkManager to the guard so a failed guard prevents these providers from bringing up ordinary networking. The observed providers already order after `network-pre.target`; relying only on their `Wants=` is insufficient. Controller startup occurs later and cannot release an enabled guard merely because Xray is unavailable. [systemd network-pre semantics](https://github.com/systemd/systemd/blob/v262/man/systemd.special.xml), [requirement/ordering semantics](https://www.freedesktop.org/software/systemd/man/latest/systemd.unit.html). + +Guard state corruption, missing required files after installation, or guard installation failure must produce a blocked boot/network-provider failure, not infer disabled. Define a distinct installation marker so an explicitly initialized disabled record is recognizable. Reserve an authenticated local recovery path. An authorized root operator can always change the host policy; that is outside the crash guarantee. + +The installed ALT nftables unit is itself a conflict requiring packaging attention. Its `ExecStop` flushes the entire ruleset, and `ExecReload` flushes then includes its configuration. An independent Roentgen table would disappear under those operations. Inspect enabled firewall owners and either integrate the guard into their supported lifecycle or reject that arrangement until coordinated. Never promise survival of arbitrary privileged firewall flushes. The report inspected `/usr/lib/systemd/system/nftables.service` without invoking it. Choosing a unique table name does not protect it from a global flush. [Inspected installed ALT unit](/usr/lib/systemd/system/nftables.service). + +Boot protection is conditional on verified local-root ALT boot ordering with networking initially down. Initramfs networking, network-root mounts, preconfigured interfaces, alternate network starters and late firewall flushers can create egress before the guard. Those need initramfs integration or must be unsupported. Ordering before `network-online.target` cannot repair an earlier leak. Verify the actual boot graph and packet trace in an ALT VM before claiming boot enforcement. systemd restart policy can recover crashes, but start-limit exhaustion needs a deliberate bounded retry strategy and a hung process needs health supervision. Neither may release the guard. [Service restart semantics](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html). + +## LAN, SSH, and dual-stack exceptions + +Discover and preserve connected LAN routes and required DHCP, IPv6 neighbor discovery, router discovery and essential ICMP error traffic. The core has no general ICMP transport; useful link control must remain narrowly direct. Do not classify every RFC1918/ULA prefix as locally connected LAN. Expose the actual direct policy to the operator without treating a LAN nameserver as an automatic DNS exemption. + +Protect both existing and new inbound SSH sessions on actual listening addresses and ports, including custom ports. Mark recognized inbound management connections and restore the connection mark into output packet marks before route selection; route replies through the direct physical path. Replies target the client's ephemeral port, so a destination-port-22 output rule is wrong. Connection-tracking marks and route-output mark changes provide the needed mechanisms. Restrict classification to configured host SSH listeners and supported ingress paths. Do not bypass arbitrary outbound SSH merely because its destination port matches. [nftables mark handling](https://wiki.nftables.org/wiki-nftables/index.php/Setting_packet_metainformation), [routing marks](https://man7.org/linux/man-pages/man8/ip-rule.8.html). + +Seed already established SSH flows during enable. They may have missed the new inbound-mark rule, so preserve their exact connection tuples and initial reply path before capture. Read-only listener discovery cannot always distinguish sshd from another process or account for future socket-activated/custom listeners. Require explicit management port/address configuration when discovery is ambiguous. `SSH_CONNECTION` identifies one invoking session and is not sufficient for other sessions or future reconnects. Do not broadly exempt an entire remote client address unless the operator deliberately accepts that extra direct access. + +Apply capture and the guard to IPv4 and IPv6 together, or block ordinary egress for a family that cannot be carried. Ensure selected-server IPv6 destinations and UDP forwarding work, even when the outer Trojan connection uses IPv4. Test MTU, fragmentation, DNS TCP fallback, reverse-path filtering and reply source selection. Do not change global `rp_filter` as a generic workaround. Existing firewall drops can still break SSH; an accept in Roentgen's chain does not override drops in other base chains. Containers, forwarding and concurrent full-tunnel VPNs remain outside the first-release support contract. + +## Delay and manual speed checks + +Keep a service-only SOCKS inbound on loopback, with a separate probe tag forced to the selected Trojan outbound. Do not expose an unauthenticated local listener to every user; use core-supported authentication and protected credentials, or isolate the probe listener. When disabled, omit TUN and host capture but retain or start the selected-config probe runtime. A disabled host stays direct while this one probe still traverses the selected server. This can use the same Xray process with guarded restart/reconfiguration on state changes. [Pinned SOCKS implementation](https://github.com/XTLS/Xray-core/blob/v26.3.27/proxy/socks/server.go), [pinned inbound settings](https://github.com/XTLS/Xray-core/blob/v26.3.27/infra/conf/socks.go), [pinned routing][router]. + +Every 30 seconds, issue one small HTTPS check with a total five-second deadline covering name resolution, proxy/TLS connection and expected response. Resolve the check destination through the proxy, retain TLS verification, bound response bytes and redirects, and prohibit direct fallback. Prevent overlapping runs and tie results to the selected profile generation. Label the measurement as HTTPS delay rather than ICMP latency. While disabled, a failed check only updates health. While enabled, failure retains blocking and schedules retries; it never changes requested intent. Manual download speed uses an explicit request, a bounded duration/byte budget and the same selected path. Never start speed downloads from the periodic timer. + +## Remaining decisions and tests + +The platform, system binary, polkit choice, persistent requested state, enabled failure blocking, automatic retries, direct disabled behavior, and 30-second/five-second check schedule are settled requirements. They need no further approval as product choices. The following decisions remain: + +1. Choose the default remote DNS resolver and bootstrap resolver, whether operators may customize them, and whether LAN-only DNS names are deliberately unsupported or receive an explicit split-DNS exception. +2. Specify what counts as direct LAN, allowed management interfaces/addresses/ports, behavior when listener discovery is ambiguous, and how future SSH listener changes update persisted exceptions. +3. Choose the HTTPS check URL and expected response, the manual speed-test source and budget, retry backoff, and health thresholds. Decide whether failed TCP and UDP checks produce different degraded states. +4. Confirm the supported ALT boot/storage/firewall arrangements, the recovery command and console procedure, and whether initramfs networking is excluded. Decide how to coordinate the distribution nftables unit before installation. +5. Confirm a tested Xray compatibility range and the administrator-authentication UX under this host's effective polkit/PAM policy. Decide whether any later action may cache authorization. No product-specific group is proposed. +6. Define selected-profile removal/refresh behavior while enabled. Recommend retaining the last authorized usable snapshot until replacement is validated, or remaining enabled/blocked if it cannot be retained. Never silently disable or switch to direct traffic. + +Before implementation is considered safe, run destructive networking tests only in a disposable ALT VM with console access. Cover complete config validation, Trojan TCP/TLS and UDP, IPv4/IPv6, UDP/TCP DNS and non-address records, hardcoded/LAN/stub resolver capture, private-name behavior, new and established SSH on custom ports, LAN access, existing direct conntrack flows, endpoint DNS/address changes, controller/Xray SIGKILL, hangs, retry limits, power loss at each persistence boundary, malformed state, enabled and disabled reboot packet traces, firewall reload/stop conflicts, and polkit remote/noninteractive/cancel/agent-absent cases. Also verify no system binary replacement and that probes still use the selected profile while disabled. No live TUN, routing, DNS interception, nftables, boot or authentication acceptance test was performed for this report. + +[alt-tags]: https://git.altlinux.org/gears/x/xray-core.git?p=xray-core.git;a=blob_plain;f=.gear/tags/list;hb=dfce17e2794d0bc2bba421d8b3383a0ba6117af2 +[alt-rules]: https://git.altlinux.org/gears/x/xray-core.git?p=xray-core.git;a=blob_plain;f=.gear/rules;hb=dfce17e2794d0bc2bba421d8b3383a0ba6117af2 +[alt-spec]: https://git.altlinux.org/gears/x/xray-core.git?p=xray-core.git;a=blob_plain;f=xray-core.spec;hb=dfce17e2794d0bc2bba421d8b3383a0ba6117af2 +[alt-unit]: https://git.altlinux.org/gears/x/xray-core.git?p=xray-core.git;a=blob_plain;f=xray.service;hb=dfce17e2794d0bc2bba421d8b3383a0ba6117af2 +[tun-conf]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/infra/conf/tun.go +[tun-linux]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/proxy/tun/tun_linux.go +[tun-stack]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/proxy/tun/stack_gvisor.go +[tun-handler]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/proxy/tun/handler.go +[tun-readme]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/proxy/tun/README.md +[json-loader]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/infra/conf/serial/loader.go +[run]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/main/run.go +[core]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/core/xray.go +[trojan-conf]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/infra/conf/trojan.go +[trojan-client]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/proxy/trojan/client.go +[router]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/infra/conf/router.go +[sockopt]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/transport/internet/sockopt_linux.go +[freedom-conf]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/infra/conf/freedom.go +[freedom]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/proxy/freedom/freedom.go +[outbound-handler]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/app/proxyman/outbound/handler.go +[dns-conf]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/infra/conf/dns_proxy.go +[dns-out]: https://github.com/XTLS/Xray-core/blob/d2758a023cd7f4174a5a5fa4ff66e487d4342ba0/proxy/dns/dns.go +[polkit-doc127]: https://github.com/polkit-org/polkit/blob/127/docs/man/polkit.xml +[polkit-api]: https://github.com/polkit-org/polkit/blob/127/docs/polkit/docbook-interface-org.freedesktop.PolicyKit1.Authority.xml +[polkit-bus]: https://github.com/polkit-org/polkit/blob/127/src/polkit/polkitsystembusname.c +[polkit-agent]: https://github.com/polkit-org/polkit/blob/127/src/programs/pkttyagent.c +[polkit-backend]: https://github.com/polkit-org/polkit/blob/127/src/polkitbackend/polkitbackendduktapeauthority.c +[polkit-alt-default]: https://git.altlinux.org/gears/p/polkit.git?p=polkit.git;a=blob_plain;f=src/polkitbackend/50-default.rules.in;hb=6f4777cb31eb8b30f54bfe86624930f74fc0f2de diff --git a/docs/research/linux-tun-xray.md b/docs/research/linux-tun-xray.md new file mode 100644 index 0000000..eb59d4e --- /dev/null +++ b/docs/research/linux-tun-xray.md @@ -0,0 +1,132 @@ +# How Roentgen can route Linux host traffic through Xray TUN + +Researched 2026-10-29. Planning only. No application code or host network changes were made. Sources are official project documentation, release metadata, and source code. Recommendations below are design inferences, not results of a live networking experiment. + +## Recommendation + +Use native Xray-core TUN as the preferred first-release backend. A companion is **not required** by current Xray. Native TUN shipped in v26.1.23; the latest official release observed is v26.10.29, published at 08:43:27 UTC on the research date. The v26.10.29 tag resolves to `0afec66085a85191e9dd2a4a6646c7c9a94688d1`, which was also the inspected main revision. Pin and validate that release for a prototype rather than assuming every version with TUN has today's automation. The original release explicitly required manual routes. [First TUN release](https://github.com/XTLS/Xray-core/releases/tag/v26.1.23), [current release](https://github.com/XTLS/Xray-core/releases/tag/v26.10.29), [official release API](https://api.github.com/repos/XTLS/Xray-core/releases/latest), [tag reference](https://api.github.com/repos/XTLS/Xray-core/git/ref/tags/v26.10.29). + +For a small supported platform set, use one Xray process plus a Roentgen system service that owns activation and recovery. Prefer Roentgen-managed policy routing over blindly installing Xray's automatic default routes when SSH access must survive. Restrict the initial platform to Linux hosts with systemd, working TUN, and a known DNS manager. Support for other init systems or resolver arrangements is a separate packaging decision. + +The user requirements remain distinct: adding or updating a 3x-ui subscription stores configurations; adding a subscription selects its first configuration if one exists; selection does not enable traffic capture. Whether that means the first received entry or the first usable connection remains a product decision. Only explicit proxy-on activates TUN. Proxy-off restores ordinary networking. Closing the terminal or SSH client leaves an already enabled system service running. Reboot auto-start and failure behavior still need a product decision. + +## What current Xray actually provides + +The official inbound is `protocol: "tun"`. It accepts traffic through a network interface rather than a listening proxy port. The normal distribution imports the TUN package. Its current stack supports TCP, UDP, IPv4, and IPv6. This removes the historical need to put tun2socks or sing-box in front of Xray. [TUN documentation](https://xtls.github.io/en/config/inbounds/tun.html), [distribution imports](https://github.com/XTLS/Xray-core/blob/v26.10.29/main/distro/all/all.go), [stack implementation](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/stack.go). + +Native TUN is still a proxy conversion, not an arbitrary IP VPN. ICMP echo replies are generated locally; other ICMP types are ignored. A successful TCP handshake or ping therefore does not prove that the remote destination is reachable. Define "all traffic" as host TCP/UDP through TUN, with necessary management and link-control exceptions. If arbitrary IP protocols, real remote ICMP, or transparent inbound port forwarding are required, this architecture does not satisfy that meaning. [Pinned TUN limitations](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/README.md#limitation). + +On Linux, `gateway` assigns interface prefixes. `autoSystemRoutingTable` adds destination routes; `autoOutboundsInterface` binds Xray-created sockets away from TUN and defaults to `"auto"` when automatic routes are supplied. An explicit empty value disables that default. The `dns` array in the TUN settings does not configure Linux DNS. `autoSystemDnsToGateway` is a separate, opt-in Linux/FreeBSD field present in the current source but absent from the English inbound page's field list. Source must resolve this documentation gap. [Pinned config parser](https://github.com/XTLS/Xray-core/blob/v26.10.29/infra/conf/tun.go), [inbound field documentation](https://xtls.github.io/en/config/inbounds/tun.html). + +## Minimal architecture options + +| Option | Data path | Work Roentgen still owns | Assessment | +| --- | --- | --- | --- | +| Native Xray with automatic setup | Host TUN enters Xray directly, then the selected outbound | SSH exceptions, DNS policy, preflight, supervised lifecycle and recovery | Fewest processes. Suitable for a constrained single-uplink host after route validation. | +| Native Xray with managed policy routes | Same data path; Roentgen owns route tables, rules and exceptions | More Linux integration, but one owner for host routing and cleanup | Recommended for SSH-safe first release. Keep Xray's automatic route list empty and configure loop prevention explicitly. | +| tun2socks plus Xray | TUN enters tun2socks, then a loopback SOCKS5 inbound in Xray | Route management, resolver integration, loop prevention for both processes, coupled lifecycle | Useful compatibility option for older Xray. No current requirement justifies the extra process. | +| sing-box plus Xray | sing-box TUN and routing feed Xray through loopback SOCKS5 | Two configurations, both processes' bypass paths, SSH and crash recovery | Viable if Roentgen deliberately adopts sing-box's Linux integration. Not needed merely to obtain TUN. | + +These are architecture judgments. Companion capabilities are verified below; no throughput comparison was measured. + +### tun2socks + +The current observed release is v2.7.0. The official project supports TCP/UDP and IPv4/IPv6 through its userspace stack. Its SOCKS5 source implements UDP ASSOCIATE; a loopback TCP SOCKS5 endpoint is suitable, whereas its Unix-domain SOCKS mode does not support UDP. Xray's SOCKS inbound defaults UDP off, so this option requires `udp: true`. Do not replace SOCKS5 with HTTP CONNECT and assume UDP survives. [Release](https://github.com/xjasonlyu/tun2socks/releases/tag/v2.7.0), [project capabilities](https://github.com/xjasonlyu/tun2socks), [pinned SOCKS5 implementation](https://github.com/xjasonlyu/tun2socks/blob/v2.7.0/proxy/socks5/socks5.go), [Xray SOCKS settings](https://xtls.github.io/en/config/inbounds/socks.html). + +tun2socks explicitly leaves DNS setup to the user. Its Linux example creates a TUN and changes routes manually; the documentation warns that a local proxy can loop. Binding tun2socks's SOCKS connection does not bind the separate Xray process's remote sockets. Both must have an escape path, while loopback remains reachable. The example's persistent `ip tuntap add` device also requires explicit teardown after a crash. Do not copy its default-route deletion into an SSH-controlled application. [DNS responsibilities](https://github.com/xjasonlyu/tun2socks/wiki/DNS-Configuration), [Linux example and loop warning](https://github.com/xjasonlyu/tun2socks/wiki/Examples). + +### sing-box + +The current observed release is v1.14.4. Its TUN supports dual-stack addresses, automatic routes, DNS integration, and Linux nftables-based `auto_redirect`. The docs recommend the latter for Docker coexistence. DNS mode fields are marked since 1.14.0; the live page also includes fields marked since 1.15.0, so do not copy the entire current example into a 1.14.4 config. [Release](https://github.com/SagerNet/sing-box/releases/tag/v1.14.4), [TUN options and version annotations](https://sing-box.sagernet.org/configuration/inbound/tun/). + +Its SOCKS outbound supports TCP and UDP. `route.auto_detect_interface`, `route.default_interface`, or outbound binding prevents its own loops, but Roentgen must separately protect Xray's uplink sockets. DNS interception and native resolver configuration still have platform limits. This option adds nftables marks, table/rule identifiers, and potentially NFQUEUE requirements to the compatibility contract. It should be chosen for those routing capabilities after validation, not treated as an invisible packet adapter. [SOCKS outbound](https://sing-box.sagernet.org/configuration/outbound/socks/), [route options](https://sing-box.sagernet.org/configuration/route/), [Linux TUN integration](https://sing-box.sagernet.org/configuration/inbound/tun/). + +## Routing, endpoint loops, and SSH + +Current Xray Linux automatic routes are ordinary destination routes with metric 1. The code does not create a separate policy table or SSH exception. Existing more-specific LAN routes can bypass a default route; an existing lower-metric default can also defeat capture. Automatic uplink selection prefers an IPv4 default before IPv6 and chooses one interface. It watches route/link changes but is not a general policy-routing or multi-uplink controller. [Pinned Linux routing source](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/tun_linux.go). + +Xray's dial controller binds non-loopback sockets to the selected interface and returns an error if binding fails. It covers Xray's own dials, not sshd's reply packets. A proxy endpoint specified by a hostname also needs an independent bootstrap path. Merely exempting one currently resolved IP is fragile when DNS rotates, multiple addresses exist, or transport settings name additional endpoints. [Pinned dial controller](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/handler.go). + +Recommended routing design, inferred from Linux policy-routing and connection-tracking facilities: + +1. Leave the existing main-table uplink routes in place. Allocate collision-checked Roentgen table, rule priorities, and mark bits. Keep the local-table lookup ahead of capture. +2. Install higher-priority exceptions for Xray's dedicated service UID or socket marks, plus management reply traffic. Then direct remaining eligible host traffic into a TUN table for both address families. Never exempt all root traffic. +3. Preserve the current SSH peer's pre-enable route before capture. For robust handling of new connections, mark inbound SSH connections and restore the connection mark on replies before policy route selection. Match the actual listening addresses/ports and incoming path, including custom SSH ports. `SSH_CONNECTION` can seed the current peer but is not a complete service-wide policy. +4. Preserve required loopback, on-link gateway/control traffic, and explicitly supported LAN paths. Show these exceptions to the operator. They are necessary system behavior, even while custom domain/subnet rules remain deferred. + +Linux rules support destination/source, UID ranges, marks, and port selectors. nftables provides connection tracking marks and route-type output chains. The exact priorities, mark masks, and ingress classification need an implementation design and isolated network tests; this report does not prescribe a ready-to-run firewall script. [ip-rule manual](https://man7.org/linux/man-pages/man8/ip-rule.8.html), [nftables manual](https://netfilter.org/projects/nftables/manpage.html). + +Preserving only packets with destination port 22 does not preserve sshd replies, whose destination is the client's ephemeral port. Preserving only existing conntrack flows also misses new SSH sessions. A current-peer host route is a simpler first-release alternative, but it bypasses every connection to that address and cannot guarantee reconnects from a new peer. That limitation must be an explicit user decision. + +## DNS and address families + +Recommended first platform uses active systemd-resolved, version 240 or newer, with Roentgen verifying that applications actually use it. Xray's native `autoSystemDnsToGateway` points resolved to the next address after the first IPv4 gateway, or first IPv6 gateway if there is no IPv4 one. It applies a per-link `~.` domain and DNS default-route flag through `resolvectl`. This is not an instruction to put a public resolver into the Linux TUN `dns` array. [Pinned DNS takeover implementation](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/tun_linux.go), [resolvectl semantics](https://www.freedesktop.org/software/systemd/man/latest/resolvectl.html). + +The takeover checks that the TUN's synthetic resolver address reaches a DNS outbound. It refuses a missing or empty built-in DNS configuration and upstreams that may use the system resolver. This check is a routing probe, not a successful real DNS query. An indirect hostname bootstrap loop can still escape it. [Pinned DNS preflight](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/tun_system_dns.go). + +Use separate policies for endpoint bootstrap and ordinary user DNS. Bootstrap must work before the proxy exists; ordinary DNS should traverse the selected proxy after activation. Configure independent upstream addresses/bootstrap and explicitly route their queries. Xray's DNS module distinguishes routed transports from local transports that bypass its routing. Do not use a local/direct DNS mode for user queries while claiming all DNS goes through the proxy. [Xray DNS transport behavior](https://xtls.github.io/en/config/dns.html). + +Port-53 queries can enter a DNS outbound for processing; DoH/DoT/DoQ are ordinary encrypted connections and must follow the general traffic policy. Decide whether to forward full DNS queries over the proxy or use built-in resolution, and test non-A/AAAA records. DNS outbound behavior must match the pinned core, not older examples. [DNS outbound](https://xtls.github.io/en/config/outbounds/dns.html). + +`~.` does not erase another interface's more-specific split DNS domains. Applications may also bypass the system resolver, and systemd-resolved has multiple `/etc/resolv.conf` modes. A LAN DNS server can evade a plain TUN default route through its connected subnet. For strict capture, cover such port-53 traffic with routing/interception or reject the conflicting setup. NetworkManager can itself bind DNS to a physical connection. [resolved routing and resolver-file modes](https://www.freedesktop.org/software/systemd/man/latest/systemd-resolved.service.html), [NetworkManager DNS management](https://www.networkmanager.dev/docs/api/latest/NetworkManager.conf.html). + +Route both IPv4 and IPv6 or explicitly block unsupported egress for the missing family while enabled. IPv4-only capture with working native IPv6 is a leak. IPv6 destinations can be carried through an IPv4 proxy transport if the selected protocol/server supports them; this must be checked end to end. DHCP, IPv6 neighbor discovery, and management traffic require narrow exceptions. UDP support in the TUN stack does not prove that every imported subscription outbound supports UDP or every remote server can reach IPv6. + +## Privileges, service lifetime, and recovery + +Linux TUN needs `/dev/net/tun` and permission to create/configure the device. Route and firewall operations require network administration privileges. A dedicated system service should own those operations; keep subscription fetching and the terminal UI unprivileged where practical. Bound capability sets and a controlled local command interface are preferable to running the entire TUI as root. Socket binding/mark operations can need additional capabilities depending on kernel and ownership, so do not promise that CAP_NET_ADMIN alone covers every configuration. [Kernel TUN documentation](https://docs.kernel.org/networking/tuntap.html), [Linux capabilities](https://man7.org/linux/man-pages/man7/capabilities.7.html), [systemd capability controls](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html). + +Xray supports a supplied Linux TUN file descriptor through `XRAY_TUN_FD`. In that mode it does not perform the normal owned-device setup. This permits a privileged helper to configure a device and pass its FD to a less privileged core, but endpoint socket binding and DNS permissions remain separate issues. Treat that split as a later hardening option, not a requirement for the smallest release. [Linux FD handling and ownership](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/tun_linux.go), [FD documentation](https://xtls.github.io/en/config/inbounds/tun.html). + +Use a system-level service independent of the SSH login session. Explicit start/stop commands should change desired proxy state; TUI exit must not stop it. systemd restart policy can recover a failed process, and `ExecStopPost` runs after abnormal exits and failed starts as well as ordinary stops. It is the appropriate place for idempotent cleanup. A hung but living core needs health supervision beyond `Restart=on-failure`. [Service restart and cleanup semantics](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html). + +Proposed activation transaction: + +1. Validate the selected profile and generated config without network changes. Record the current resolver manager, routes, SSH peers and existing policy owners. Refuse unsupported conflicts. +2. Prepare loop/management exceptions and recovery ownership records. Start Xray and verify an application exchange through its remote outbound. Establish the resolver path, then activate host capture. Do not use synthetic ping or a TCP handshake alone as the health check. +3. If any step fails, undo only Roentgen-owned changes. Report enabled only after actual capture and DNS checks succeed. +4. On disable, disarm automatic restart of the requested proxy state, remove capture, restore DNS, stop the core, then remove owned exceptions. Order the transition according to the chosen fail-open/fail-closed policy. + +A nonpersistent TUN and its routes normally disappear when the last FD closes, including after SIGKILL. Policy rules and nftables objects are independent and need cleanup. Xray's README explicitly warns that SIGKILL cannot run DNS revert; the source notes that per-link DNS state goes away with a removed link. Do not rely on either alone for externally supplied or persistent devices. The service must reconcile leftovers on failure and before its next activation. A persistent helper-held FD can keep a dead core's interface alive. [Kernel device lifetime](https://docs.kernel.org/networking/tuntap.html), [Xray DNS cleanup caveat](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/README.md#system-dns-on-linux-and-freebsd-autosystemdnstogateway), [Linux Close implementation](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/tun_linux.go). + +A process or machine failure cannot guarantee immediate cleanup by application code. Avoid permanent manager configuration for transient capture; keep an ownership journal, idempotent recovery, and an emergency local disable command. Never flush all firewall rules or restore a stale snapshot of the whole host route table. Strict fail-closed behavior needs a separately owned egress policy that survives core failure while allowing management access. Automatic route removal alone tends toward fail-open and is not a kill switch. + +## Coexistence and supported platform boundary + +Support a conventional host network namespace first. Container-local TUN affects that namespace, not automatically the host; rootless containers and restricted VPS kernels may lack the required device/capabilities. Non-systemd distributions cannot use the proposed system service unchanged. Xray's automatic DNS takeover cannot configure dnsmasq, unbound, static resolver files, or OpenRC arrangements just because a TUN exists. [Xray platform restrictions](https://github.com/XTLS/Xray-core/blob/v26.10.29/proxy/tun/README.md), [kernel device requirements](https://docs.kernel.org/networking/tuntap.html). + +Treat NetworkManager, systemd-networkd, existing VPNs, Docker/Podman bridges, firewalld/UFW, VRFs, and policy-routing rules as preflight inputs. Choose one owner for each Roentgen interface, DNS setting, route and firewall object. Detect address/table/mark/name collisions. Do not disable network managers or replace their physical defaults. Evaluate reverse-path filtering against the resulting routes; prefer consistent tables or a narrowly justified setting over globally disabling it. The kernel documents loose mode for asymmetric routing and `src_valid_mark` for marked reverse-path checks. [Kernel reverse-path settings](https://www.kernel.org/doc/html/latest/networking/ip-sysctl.html), [NetworkManager configuration](https://www.networkmanager.dev/docs/api/latest/NetworkManager.conf.html). + +VPN coexistence needs an explicit policy: reject concurrent full-tunnel VPNs initially, route the proxy transport over the VPN, or bypass it using a named physical uplink. Automatic Xray interface selection does not establish which policy the user wants. Docker forwarding/NAT and traffic in other network namespaces should remain outside the initial host-only guarantee unless tested and deliberately included. + +## Subscription boundary and later routing + +Keep the subscription URL and imported server profiles separate from the privileged host configuration. Current 3x-ui source handles share-link subscription lists, including encoded content. This confirms that subscription handling is application work, not a reason for a TUN companion. Support the chosen exported formats explicitly and validate each imported protocol/transport against the pinned Xray release. Do not run arbitrary downloaded inbounds, routing rules, API listeners, or filesystem paths as root. [3x-ui subscription handling](https://github.com/MHSanaei/3x-ui/blob/c621ba9014a6aa21fd7c9ecf45655e44e24ebf13/internal/sub/external_subscription.go). + +On refresh, preserve the selected profile if it still exists. A failed refresh must not replace the last valid configuration or change off to on. If the active profile disappears or changes, the application needs a documented policy before it switches a live host connection. + +Future domain/subnet policy can live in Xray routing while the Linux layer keeps capturing eligible host traffic. Raw TUN packets contain IP addresses; domain rules need DNS-derived information, FakeDNS, or sniffing, with different reliability and privacy behavior. Reserve the configuration boundary now, but defer the user-facing rules editor. System exceptions for bootstrap and SSH are required in the first release and should not depend on that later feature. [Xray routing](https://xtls.github.io/en/config/routing.html), [FakeDNS](https://xtls.github.io/en/config/fakedns.html). + +## Open user decisions + +- Approve native Xray as the backend and pin v26.10.29 for initial validation, or deliberately support an older installed core through a companion. +- Choose the first supported distributions and resolver arrangement. Recommendation is systemd plus active systemd-resolved; other setups should fail preflight until supported. +- Define "all traffic" as TCP/UDP with visible management/link exceptions. Decide whether LAN destinations remain direct, enter TUN, or are blocked. +- Choose failure behavior. Should proxy failure restore direct internet or block ordinary egress until recovery? Explicit off should restore ordinary networking in either case. +- Choose SSH protection scope. Recommendation is existing and new inbound management connections; a current-peer host route is simpler but narrower. +- Decide whether a previously enabled proxy resumes after reboot. Terminal persistence alone does not answer this. +- Decide whether missing UDP/IPv6 support in a selected server blocks activation or permits an explicit degraded mode that blocks unsupported traffic. +- Decide concurrent VPN policy, DNS upstream/bootstrap policy, and what happens when an active subscription profile changes or disappears. + +## Validation required before implementation is accepted + +No network experiments were run for this research. Validate the pinned binaries in disposable VMs/network namespaces before enabling a remotely administered host: + +- Confirm generated config acceptance and native TUN availability for packaged amd64/arm64 binaries. Test a non-root UI and service privilege boundary. +- Exercise TCP payload exchange, UDP request/reply and QUIC, DNS over UDP/TCP including non-address records, IPv4-only, IPv6-only and dual-stack destinations. Observe physical egress for DNS/IPv6 leakage and endpoint recursion. +- Keep an SSH session active across on/off, reconnect from another peer, use a nonstandard listening port, and test IPv6 SSH. Repeat after core SIGKILL and startup failure. +- Verify cleanup after ordinary stop, SIGKILL, failed DNS takeover, process hang, reboot, and supplied/persistent FD cases. Confirm unrelated routes and firewall objects remain intact. +- Test Wi-Fi/Ethernet changes, DHCP renewals, LAN DNS, resolved split domains, Docker bridges and a conflicting VPN. Either demonstrate the declared coexistence policy or reject activation with a useful reason. +- Verify import/select/refresh while off makes no networking changes; terminal exit preserves on; explicit off wins over restart; a bad refresh leaves the running profile intact. + +These tests decide whether native automatic routes suffice for the declared platform set or whether Roentgen must own policy routing from its first release. The source review establishes that a companion is optional; it does not establish production readiness for any particular host. diff --git a/docs/research/public-defaults.md b/docs/research/public-defaults.md new file mode 100644 index 0000000..dd6dbba --- /dev/null +++ b/docs/research/public-defaults.md @@ -0,0 +1,95 @@ +# Public resolver and diagnostic defaults + +Researched 2026-10-29 for ticket 13, "Which public resolver and diagnostic endpoints can be defaults?" Planning only. These provider values are proposals pending final human review. This report changes no specification, ticket, networking, service or policy. + +## Proposed values + +Use Cloudflare's standard public DNS resolver and its speedtest download API. The current official client documents both the download URL and zero-byte latency requests. That supplies a coherent minimal set without a vendor comparison. [Official client README, pinned revision](https://github.com/cloudflare/speedtest/blob/7183e970bda21c900e7f4bcf9c5416aa08257e27/README.md#measurement-config). + +| Setting | Proposed value | Expected behavior | +| --- | --- | --- | +| Remote DNS | `1.1.1.1:53`, reached through the selected Trojan outbound | Standard DNS; no content filtering | +| Optional second resolver | `1.0.0.1:53`, also through that outbound | Same provider, alternate address; no provider diversity | +| HTTPS Ping URL | `https://speed.cloudflare.com/__down?bytes=0` | GET; expect HTTP `200` and an empty response body | +| Manual Speed URL | `https://speed.cloudflare.com/__down?bytes=10485760` | GET; expect HTTP `200` and 10,485,760 response body bytes if completed | +| Speed byte limit | `10485760` bytes, exactly 10 MiB | Client stops consuming at the limit | +| Speed duration limit | `10` seconds | One deadline includes setup and body consumption; first reached limit stops the test | +| Automatic Ping | Selected configuration only, every `30` seconds; `5` second total deadline | Always through the selected proxy, including while host proxying is disabled | +| Automatic Speed | Never | An explicit manual action starts one bounded download | + +The scheduler and limits above are already approved product requirements. Status and body expectations are source-backed proposals, not observations from a live test. The evidence boundary below matters for the exact `200` expectation. + +## Resolver evidence and routing + +Cloudflare lists `1.1.1.1` and `1.0.0.1` as its standard unfiltered resolver addresses. Its IPv6 alternatives are `2606:4700:4700::1111` and `2606:4700:4700::1001`. Choose one IPv4 resolver for the existing singular Settings field; publish the second as an alternate rather than adding a new list setting. The official documentation describes the address pair as redundancy. [IP addresses](https://developers.cloudflare.com/1.1.1.1/ip-addresses/). + +Port 53 with ordinary DNS is the minimal transport proposal. It does not imply DNS encryption from the Trojan exit to Cloudflare. Cloudflare explicitly distinguishes plaintext DNS from DoT, which it supports on port 853 at these same addresses, with hostname `one.one.one.one`. Adding DoT would be a separate transport decision. [DNS over TLS](https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-tls/). + +Roentgen must direct DNS exchanges, including TCP fallback, through the selected Trojan outbound. Resolver address selection must never permit direct DNS fallback. Resolve the diagnostic hostname through that protected path too. This is an implementation requirement from the approved product scope, not proof that the installed Xray adapter already implements it. The use of a numeric resolver avoids needing DNS to discover that resolver. Resolving the Trojan server's own hostname is a separate bootstrap concern for the runtime design. + +An IPv4 resolver destination does not prevent querying AAAA records. Reaching an IPv6 resolver address requires suitable connectivity from the proxy exit. IPv6 default selection and the installed Xray DNS transport behavior require later implementation validation; this research sent no DNS queries. + +## What the speedtest sources establish + +The official `cloudflare/speedtest` client was inspected at `7183e970bda21c900e7f4bcf9c5416aa08257e27`, the `main` revision returned by GitHub during this research, committed 2026-10-29. Its README names `https://speed.cloudflare.com/__down` as the default GET download API. It explicitly specifies `bytes=0` for latency and defines `bytes` as the download file size. Thus zero-byte GET is an intended latency operation, not an invented convention. [Pinned README](https://github.com/cloudflare/speedtest/blob/7183e970bda21c900e7f4bcf9c5416aa08257e27/README.md#instantiation). + +The client constructs the URL with `URL.searchParams.set`, sets `bytes`, checks `response.ok`, consumes the body, and handles HTTP 429 as a rate-limit failure with retries. Roentgen should use its own bounded reader rather than copy the client's whole-body `text()` read or retry policy. [Pinned BandwidthEngine, lines 437 to 515 and 677 to 699](https://github.com/cloudflare/speedtest/blob/7183e970bda21c900e7f4bcf9c5416aa08257e27/src/engines/BandwidthEngine/BandwidthEngine.ts#L437). + +Cloudflare's launch article links its official Worker template as the benchmark source. The template remains available at `d9b0c5535594a9527d456e2e8c43198dc78569da`, committed 2024-09-20. Its README defines `bytes` as response body size, default zero. Its download handler generates ASCII `0` repeated the requested number of times, clamps at 100,000,000, constructs `new Response(body)` and sets `Content-Type: application/octet-stream` and `Cache-Control: no-store`. Zero produces an empty string. [Launch article](https://blog.cloudflare.com/test-your-home-network-performance/), [pinned template README](https://github.com/cloudflare/worker-speedtest-template/blob/d9b0c5535594a9527d456e2e8c43198dc78569da/README.md#download), [pinned download handler](https://github.com/cloudflare/worker-speedtest-template/blob/d9b0c5535594a9527d456e2e8c43198dc78569da/src/down.js#L24). + +The Fetch standard defines `200` as the default Response status. Consequently the published template returns `200`, an empty body at zero, and N ASCII zero bytes at positive integer N below its clamp. [Fetch ResponseInit](https://fetch.spec.whatwg.org/#responseinit), [Workers Response constructor](https://developers.cloudflare.com/workers/runtime-apis/response/#constructor). + +There is a specific limitation. The template routes GET requests at `/down`; the current client targets `/__down`. The template README also warns that the official service has different internal server timing behavior. These sources support expecting `200` and a zero-length Ping body, but do not establish that today's deployed `/__down` is byte-for-byte the template or has a permanent status/header contract. Do not require ASCII zeros, the template's maximum size, a specific `Content-Length`, or its diagnostic headers from production. No live request was made to either diagnostic URL. [Pinned routes](https://github.com/cloudflare/worker-speedtest-template/blob/d9b0c5535594a9527d456e2e8c43198dc78569da/src/index.js#L9), [template caveat](https://github.com/cloudflare/worker-speedtest-template/blob/d9b0c5535594a9527d456e2e8c43198dc78569da/README.md#installation). + +## Proposed request and check policy + +The following is a Roentgen policy proposal unless a source is explicitly cited. + +Both HTTP settings must parse as absolute `https` URLs with a hostname. Reject userinfo and fragments. Preserve valid paths and queries. Use certificate and hostname verification, with no insecure override. Use GET rather than HEAD. Disable redirects and treat every 3xx as endpoint failure; the user can configure the final HTTPS URL. This keeps one observable destination and avoids measuring redirect chains. HTTPS remains mandatory even inside Trojan. Certificate validation follows [HTTP semantics, section 4.3.4](https://www.rfc-editor.org/rfc/rfc9110.html#section-4.3.4). + +Use an identifiable Roentgen user agent. Send no cookies, referrer, authentication tokens or result-logging requests. Request `Accept-Encoding: identity`, disable automatic decompression and reject other content codings. Disable application response caching and request `Cache-Control: no-cache, no-store`. Use a fresh diagnostic connection for reproducible inclusion of connection establishment. Do not inherit an environment proxy that can bypass the selected outbound. Bound response headers, buffers and every read under the operation's deadline. + +For the default Ping URL, require exactly `200`. Reject a declared nonzero content length. Require end-of-body with zero bytes under the five-second deadline; any delivered body byte is an unexpected-response failure, and the client must abort without draining it. Missing `Content-Length` is acceptable if framing establishes empty completion. A verified TLS handshake or a fast HTTP error alone is not successful Ping. + +Measure Ping with a monotonic clock from the start of the proxied operation to final response headers, then publish success only after empty-body validation. Include proxy connection, DNS if needed, TCP/TLS setup and server response delay. Show milliseconds as HTTPS delay. This differs from the official browser client's `requestStart` to `responseStart` timing and must not be presented as ICMP RTT or an equivalent Cloudflare score. Cloudflare describes Ping in terms of response-start timing. [Speed Test methodology](https://speed.cloudflare.com/about#faqs). + +For an edited Ping URL, URL-only configuration cannot specify arbitrary expected bodies. The minimal generic policy should accept `200` or `204`, discard at most 4 KiB, and require clean completion within the same deadline. Keep the strict `200`/empty rule for the exact default URL. Larger pages fail with an explanation to choose a small endpoint. This generic check confirms bounded HTTPS success, not a body signature. Supporting configurable expected statuses or body matches would require a separate specification decision. + +For the default Speed URL, require `200`, no content coding, and a binary response, expected as `application/octet-stream`. Do not compare payload bytes to zeros. Reject HTML/error responses and HTTP errors rather than report their transfer rate. A completed default response is expected to contain 10,485,760 bytes. If a supplied `Content-Length` contradicts that size, report an unexpected endpoint response. A clean EOF shorter than expected is incomplete, not a completed default test. + +For custom Speed URLs, accept `200` with a nonempty body under the same client limits; flag HTML as an endpoint error. Record any known size and whether EOF, the byte limit or the time limit ended the transfer. An early EOF on a genuinely smaller custom resource is valid but may make the sample too short to represent useful throughput. + +Treat `Content-Length` as metadata, never as the client cap. A larger declared custom resource can still supply a bounded prefix. An absent length is acceptable with valid streaming framing. A truncated declared body before a deliberate cap or clean completion is a transport failure. Do not send Range by default: a server can ignore it, and `206` has separate partial-response semantics. Reject unsolicited `206` and `Content-Range`; adding range support requires validating the returned range. [HTTP content length](https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6), [Range](https://www.rfc-editor.org/rfc/rfc9110.html#section-14.2), [206](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.3.7). + +Stream into a fixed-size scratch buffer. Limit each application read to the remaining byte budget. At the byte limit, deadline or cancellation, abort the response and its transport without draining the remainder or retrying. HTTP libraries, TLS and kernel buffers can read ahead, and packets can already be in flight. The cap therefore bounds consumed response-body bytes, not total link traffic or a billing ceiling. Headers, TLS/Trojan overhead and DNS also consume bytes. + +Start the ten-second deadline before connection setup and keep it active throughout body reads. For nonempty successful data, report download rate as `8 * consumed_body_bytes / elapsed_body_seconds`, using a monotonic clock from final headers to the last counted byte or deadline. Also record total duration, bytes and stop reason. A deliberate byte/time stop can yield a bounded sample; zero-byte timeouts, HTTP/protocol errors and user cancellation must not become successful speeds. Results describe this proxy path to this endpoint with one short transfer. No claim of maximum link capacity follows. + +## Static URL and editable caps + +Save the full Speed URL, including `bytes=10485760`, independently of the two client limits. Do not infer parameter semantics for arbitrary URLs or silently append/replace `bytes`. If the user reduces the cap to 2 MiB, consume at most 2 MiB of the requested 10 MiB body and cancel. If they raise the cap to 20 MiB, the unchanged default URL still finishes at 10 MiB; the cap is a maximum, not a target or promise of a ten-second transfer. Explain that increasing requested payload size requires editing the URL too. + +A provider-aware URL template could instead set `bytes` from the configured cap, but it adds a setting/protocol decision and needs size validation. The fixed URL plus independent limits fits the approved Settings fields and is the recommended minimum. + +Automatic Ping must never scan every configuration, fall back to direct traffic, or activate host TUN merely to probe while disabled. Prevent overlapping runs and bind each result to its configuration generation. Speed remains manual only. No automatic retry should exceed a probe deadline or Speed budget. Report 429 and honor a valid `Retry-After` as a minimum pause for subsequent scheduled requests, without replacing the nominal 30-second cadence with rapid retries. + +## Availability, privacy and use limitations + +Cloudflare explicitly provides no SLA for public 1.1.1.1 and limits dedicated support. Its resolver terms incorporate its Website and Online Services Terms and specify attribution for integrating ISPs or network equipment providers. Name Cloudflare as the selected provider; whether Roentgen falls within those attribution categories is not decided here. [Resolver support](https://developers.cloudflare.com/1.1.1.1/infrastructure/sla-and-support/), [resolver terms](https://developers.cloudflare.com/1.1.1.1/terms-of-use/). + +The Speed Test footer links the same Website Terms. Those terms allow modification, discontinuation and termination, prohibit overburdening services and circumventing limits, and disclaim reliability warranties. The reviewed sources contain no numeric allowance or express guarantee for perpetual third-party Ping every 30 seconds. Public source code and documented client defaults establish technical suitability; they do not confer an SLA or unrestricted hosted-service permission. Accept provider errors and keep URLs editable. [Speed Test footer](https://speed.cloudflare.com/about), [Website Terms, sections 4, 7 and 10](https://www.cloudflare.com/website-terms/). + +The resolver policy allows limited query/debug logs for up to 25 hours, limited APNIC access, retained aggregations, and sampled packet capture of at most 0.05% for troubleshooting and attack mitigation. Do not describe it as "no logging." [Public resolver privacy commitments](https://developers.cloudflare.com/1.1.1.1/privacy/public-dns-resolver/). + +Speed Test's privacy notice says Cloudflare receives the connecting IP, derives location and ASN, truncates IP addresses, and shares anonymized measurement information with partners. This is a separate policy from resolver privacy. [Speed Test privacy notice](https://speed.cloudflare.com/). The official client separately posts final measurements to its logging API; Roentgen need not implement that workflow. A raw download request still reaches the provider and is not guaranteed to avoid server-side logging. [Pinned logging implementation](https://github.com/cloudflare/speedtest/blob/7183e970bda21c900e7f4bcf9c5416aa08257e27/src/logging/logFinalResults.ts#L106). + +Inference from the required proxy path: Cloudflare normally sees the Trojan exit's source IP, and that exit's route determines the Cloudflare edge reached. DNS names remain visible to the resolver; plaintext DNS beyond the exit is visible on that segment. One provider for both diagnostics creates correlated failure. A failed Ping establishes failure of that check, not failure of every Internet destination. + +## Remaining decisions and validation + +1. Human approval of Cloudflare as the specific default provider, including acceptance of the public service's availability and use limits. +2. Accept the fixed Speed URL policy and the generic custom-Ping `200`/`204`, 4 KiB policy, or specify another editable-URL contract before implementation. +3. Keep the single DNS field at `1.1.1.1`, or explicitly add support for a resolver list. Confirm DNS transport through the installed Xray separately; do not introduce direct fallback. +4. Later authorized integration work must verify actual production status/body/headers, HTTP cancellation and proxy routing, including disabled mode. Source inspection cannot establish live availability or compatibility with the installed Xray release. No such tests were run here. + +Research fetched official documentation, GitHub metadata and source only. It made no resolver query, latency request, speed payload download or live proxy test. Root glossary and ADR directory were absent in this checkout. No implementation or tracker edits were made. diff --git a/docs/research/simple-ssh-scope.md b/docs/research/simple-ssh-scope.md new file mode 100644 index 0000000..54b686d --- /dev/null +++ b/docs/research/simple-ssh-scope.md @@ -0,0 +1,97 @@ +# Can SSH protection stay small and general? + +Researched 2026-10-30 for Roentgen's ALT Linux first release. This report proposes a support boundary; it does not record successful networking tests or an approved product decision. + +## Recommendation + +SSH invocation detection can stay small. Universal automatic SSH protection cannot. Recommend explicit, administrator-authorized local management TCP endpoints and one ordinary host routing arrangement. Use `SSH_CONNECTION` only to catch likely mistakes in the invoking CLI, never as authority to create a direct exception. Protect eligible replies on configured endpoints regardless of which CLI, user or SSH session caused activation. Do not discover listeners, enumerate login sessions, trace process ancestry or monitor SSH configuration changes. + +This gives a bounded general TCP reply mechanism that also works with custom SSH ports. It requires accepting a narrower promise: Roentgen preserves direct routing for eligible management replies under the supported arrangement. It cannot promise to detect every remote invocation or preserve every possible SSH access path. If that narrower promise is unacceptable, reconsider remote activation or require an operator-managed management path. Do not quietly expand this work into a discovery subsystem. + +The existing [ALT runtime report](alt-runtime-integration.md) proposes protecting current and future SSH sessions and seeding established flows. This report narrows that proposal. The latest spec's default direct RFC1918 destinations supersede that report's connected-LAN-only proposal. Keep persistent systemd services, the polkit-authorized D-Bus controller, enabled fail-closed behavior, normal service lifecycle and manual-only configuration selection. SSH exceptions do not select a configuration or authorize disabling protection. + +## Evidence and inspection boundary + +Read the uncommitted main-checkout runtime report and first-release spec at `/home/aguschin/Projects/roentgen`, plus research ticket 14. Those files are context, not independently verified upstream evidence. This prepared checkout has no glossary or ADR directory. Read-only RPM metadata here reports OpenSSH server/client `9.6p1-alt7`, iproute2 `6.18.0-alt1`, nftables `1.1.7-alt2` and systemd `262-alt1`. These versions establish installed packages, not effective SSH configuration, firewall behavior or management reachability. + +No host networking, service, polkit or package changes, authentication prompts or live connection tests were performed. No session addresses were collected. Sources below are official manuals and upstream documentation. Current OpenBSD manuals describe OpenSSH semantics; they are not proof of every ALT 9.6p1 implementation detail. Proposed rules and transition behavior require ALT validation before shipping. + +## What the small detector establishes + +OpenSSH documents `SSH_CONNECTION` as four space-separated fields: client IP, client port, server IP and server port. `SSH_TTY` is absent for sessions without a tty, so it is an unsuitable required signal. [OpenSSH environment manual](https://man.openbsd.org/ssh#ENVIRONMENT). + +The unprivileged CLI can parse the tuple into bounded typed IP addresses and ports, reject malformed present values and include an optional hint in its D-Bus request. This is little general parsing code. It should report "SSH connection hint present" rather than "remote session verified". Absence means unknown. A local user can set, clear or copy environment values; wrappers and long-lived shells can lose or retain stale values. Neither presence nor absence establishes comprehensive session discovery. + +The system service has its own environment. It must receive the hint explicitly; it must not look for the caller's connection in the daemon's environment. The bus authenticates the caller connection and supplies process credentials through `GetConnectionCredentials`. It does not authenticate arbitrary method arguments as facts about the caller's SSH transport. [D-Bus credentials specification](https://dbus.freedesktop.org/doc/dbus-specification.html#bus-messages-get-connection-credentials). + +Consequently, polkit authorization for enable is not proof that a submitted tuple belongs to that caller. Finding a matching established TCP socket or conntrack entry would prove that a flow exists, not that this CLI descends from its authenticated SSH login. Stronger association needs additional trusted evidence and race handling. Reading process environments or walking sshd parents is outside this recommendation. + +Keep the daemon's authority in its persisted management policy. A hint may cause refusal or verify coverage by existing policy; it must not add an arbitrary client address, tuple or port. Creating or widening management policy requires the existing separate policy authorization. Even a correctly parsed invented hint then cannot widen egress. An optional exact-flow mode would instead need explicit authorization of that specific exception, rather than calling it trusted autodetection. + +## Separate the guarantees + +1. Detecting one likely SSH invocation needs only the parser above. It offers a best-effort usability check. A direct D-Bus client can omit the hint, so this is not a daemon-enforceable guarantee that all SSH invocations are refused when unsafe. +2. Protecting one exact current TCP flow needs its family and both address/port pairs, correct direction, plus a usable reply route. Replies reverse the tuple and target the client's ephemeral port. An output destination-port-22 rule does not identify them. +3. Protecting other current sessions needs either knowledge of those flows or a policy that classifies all eligible replies on configured endpoints. The invoking environment supplies neither a complete list nor future membership. +4. Protecting new inbound sessions on custom ports needs an enduring endpoint policy before those sessions arrive. One ephemeral client tuple cannot cover reconnects. An endpoint exception preserves routing eligibility; it does not open a listener, change SSH authorization or override another firewall's deny rule. + +OpenSSH permits multiple `Port` and `ListenAddress` values. Command-line options and alternative config files affect effective configuration. `sshd -T` prints the configuration it was asked to evaluate; it is not a registry of all running or future SSH servers. [OpenSSH listener configuration](https://man.openbsd.org/sshd_config#ListenAddress), [OpenSSH daemon options](https://man.openbsd.org/sshd.8#DESCRIPTION). + +systemd may own a listening socket and pass it to a service, including inetd-style activation. Socket and service namespaces can differ. This is another reason that finding an sshd process is insufficient. [systemd socket activation and namespaces](https://www.freedesktop.org/software/systemd/man/latest/systemd.socket.html#Description). + +A port can also belong to a forwarder or another protocol. A remote forwarded port number need not equal the host-visible SSH port. The proposed policy uses the local endpoints seen by the host, and explicitly grants a TCP service exception. It does not attest that packets contain SSH. + +## Minimal proposed endpoint contract + +Support host-terminated TCP in the controller's host network namespace, ordinary conntrack zone, and a conventional direct route in the retained main table. Configure a small bounded set of local management addresses and ports, with an explicit family and optional peer CIDR restriction. A family-wide local-address wildcard must be a deliberate policy choice. Never assume port 22 or infer all listening ports. Never exempt all traffic to the remote client IP or all traffic with a matching source port. + +Use the conntrack original destination endpoint to classify inbound management connections, and exempt only their outgoing reply direction. nftables exposes original/reply tuples, direction, state and marks; route-type output chains support route relookup. Conntrack identity describes a flow, not the owning application. [Official nftables manual, conntrack expressions and chains](https://netfilter.org/projects/nftables/manpage.html). + +The smallest proposed implementation matches those conntrack fields directly in output. It does not need a userspace session list or pre-seeded per-session marks. Set a reserved masked packet mark for eligible replies before general TUN capture, then use a higher-priority policy rule to look up the main table. Recheck the management predicate in the output guard rather than granting egress on an unqualified mark alone. Reserve mark bits and rule priorities with the rest of the runtime. iproute2 documents mark-based rules, explicit priority ordering and table lookup. [Upstream ip-rule manual](https://raw.githubusercontent.com/iproute2/iproute2/main/man/man8/ip-rule.8.in). + +Existing sessions qualify only if conntrack already records the incoming connection in the correct direction. Installing a rule matching only new SYN packets misses old sessions. Output classification avoids that particular race but cannot manufacture trustworthy preexisting conntrack history. The kernel's `nf_conntrack_tcp_loose` setting controls pickup of established connections; it does not certify the original initiator after tracking began late. Do not toggle it as a workaround. [Kernel conntrack documentation](https://www.kernel.org/doc/html/latest/networking/nf_conntrack-sysctl.html). + +Require valid tracking before supported sessions start, such as product rules installed before new management connections. Treat sessions predating installation, untracked flows, missing entries and reversed original tuples as unsupported until reconnected through the installed policy. For a supplied current-session hint, require a corresponding established incoming conntrack entry before acceptance. Do not claim this verifies ownership. Other current sessions are covered only when they satisfy the same tracking and configured-endpoint conditions; no all-session audit is proposed. + +Future connections to the configured endpoints can qualify without process or session discovery. Persist this policy with enabled intent and load it with the early guard, before ordinary networking. Keep it available through controller/Xray restarts and ordinary systemd lifecycle operations. A controller stopped or crashed must not remove the kernel exception. Reboot creates new flows under the same endpoint policy, not preserved TCP sessions. Port/address changes require an explicit policy update before relying on the changed endpoint. Socket activation needs no special discovery if it produces the same supported host-visible TCP flow; cross-namespace, forwarded and NAT-transformed paths are outside the first validation boundary. + +## A tuple is not a saved reply route + +Connection tracking and packet marks do not by themselves save a gateway or interface. `ip route get` can query a route for explicit source, destination, mark and TCP ports without sending a packet. It establishes a current routing decision, not continued reachability. [Upstream ip-route manual](https://raw.githubusercontent.com/iproute2/iproute2/main/man/man8/ip-route.8.in). + +For one hinted flow, query the reply tuple before activation and compare it with the candidate main-table route. Compare source, output interface and next hop. Once bypass rules are prepared, verify the actual marked lookup before capture starts. A query with an unused mark before its rule exists cannot validate that rule. Refuse a detectable mismatch rather than silently assuming that "main" preserves the previous route. This works within the proposed simple topology because Roentgen leaves the direct main-table routes intact. It does not freeze those routes against DHCP, link loss or administrator changes. + +Drop automatic route preservation for arbitrary multihoming, source policy, VRFs, ECMP-dependent paths, network namespaces, host NAT and competing tunnels. Supporting them needs more route context, per-path policy and revalidation. Reverse-path filtering can depend on marks and asymmetric routing; do not globally relax it to rescue an unsupported case. [Kernel reverse-path filtering and source mark semantics](https://docs.kernel.org/networking/ip-sysctl.html#rp-filter). + +The supported guarantee is that Roentgen does not divert eligible replies into TUN or block them in its own guard under the validated arrangement. It cannot guarantee SSH availability through link failure, upstream filtering, route changes or another firewall. Preserve exact tuple scope for a one-session exception; exact historical route pinning is a separate, larger requirement. + +## Short activation algorithm + +This is proposed design, not an executable ruleset. + +1. The CLI parses an optional SSH hint. A malformed present value fails before mutation. Missing data remains unknown; no tty or login-session discovery fallback runs. +2. The controller authorizes the actual D-Bus sender through polkit and validates the existing management policy and supported networking arrangement. The hint cannot modify that policy. +3. When a hint exists, check endpoint coverage, the established incoming conntrack tuple and the direct reply route. Refuse on uncertainty before changing disabled intent or networking. Report the failed prerequisite. Missing hints must never produce a "local invocation verified" claim. +4. Prepare owned management classification, bypass routing and guard exceptions before TUN capture can affect replies. Verify the marked reply lookup at this point. Apply the existing crash-safe enable ordering with the exception policy persisted alongside intent. nftables and routing updates are separate operations and require safe ordering and reconciliation. +5. Keep the guard and configured reply exceptions through retries and ordinary service restarts. Reject policy edits that knowingly remove the hinted active path. An error while already enabled leaves protection armed; it must not automatically disable proxying or restore unrestricted direct traffic. + +Ordinary traffic remains fail-closed. RFC1918 destinations retain the user's default direct policy. Management exceptions do not create a generic established-connection accept. Endpoint port 53 would conflict with the earlier DNS-capture policy; reject that overlap in this narrow contract unless the human explicitly revisits precedence. IPv6 management needs its own validated policy; IPv4 protection is insufficient for an IPv6 SSH hint. + +## Alternatives and effort + +Explicit endpoint policy has bounded implementation components: a typed policy and hint parser, existing polkit policy authorization, a conntrack tuple lookup for the known hint, route queries and topology checks, generated reply classification and masked routing rules, persistence/reconciliation, and ALT integration tests. Bound the topology checks to a documented allowed routing arrangement and reject unfamiliar rules; do not attempt to interpret arbitrary host policy. This is moderate networking integration within the existing runtime, with little SSH-specific code. It is not merely an environment-variable check. + +One known session is narrower in traffic permissions but less useful for persistent services. An explicitly authorized exact tuple avoids listener discovery and protects only that connection. It still needs routing validation, stale-flow handling and removal tied to connection lifetime or bounded expiry. It cannot promise other sessions, a reconnect with a new client port, or access after reboot. Blindly persisting the tuple risks granting a later reused tuple the exception. Treat this as an alternative product contract, not an automatic fallback. + +An operator-managed exception is the smallest Roentgen feature. The administrator prepares both reply routing and firewall compatibility outside Roentgen and retains console recovery. Roentgen may check the known tuple, but it cannot certify all external policy. A firewall accept alone does not keep replies out of TUN. This transfers configuration and availability responsibility to the operator and drops product-managed SSH protection. + +Universal automatic protection has open-ended scope: identifying listeners and their launch configurations, associating callers with authenticated sessions, capturing all old flows, tracking later listeners and changes, and preserving unusual reply routes. It fails the user's condition that detection need little general code. No credible exact line-count estimate follows from these manuals. + +## Human decisions still required + +- Accept configured management endpoints and the conditional flow/routing guarantee, choose the one-session alternative, or make exceptions operator-managed. Endpoint policy is the recommendation, not prior approval. +- Decide the missing-hint policy. Allowing activation offers only best-effort SSH refusal. Requiring an already configured management policy for every activation is stricter and does not depend on detecting SSH. A console-only recovery workflow is another choice. A client assertion that it is local is not proof. An enforceable universal remote/local distinction would reopen scope. +- Choose the actual local ports, address/family and optional peer restrictions. Decide whether exceptions may cover all local addresses and whether other TCP services on those endpoints are an accepted consequence. Configure custom ports explicitly before use. +- Accept the limited routing/conntrack boundary and reconnect requirement for sessions predating tracking, or reconsider automatic protection. Decide how policy changes are performed safely when the environment hint is absent. + +Before shipping, test in disposable ALT environments: old correctly tracked flows, late/missing tracking, multiple sessions, IPv4/IPv6 custom ports, absent/spoofed hints, outbound SSH remaining captured, first server reply during activation, reboot/reconnect, and ordinary controller/Xray stop/restart/failure. Verify tuple direction and physical egress by packet traces. Test route mismatch and competing firewall rejection. These are remaining validation tasks, not experiments performed for this report. -- cgit v1.2.3