after #4083 the staleness window is 30 minutes, which still lets an ip
that stopped connecting a few minutes ago sit in the db blob and keep
the protected slot on the ascending sort. the ip that is actually
connecting right now gets classified as excess and sent to fail2ban,
and never lands in inbound_client_ips.ips so the panel doesnt show it
until you clear the log by hand.
only count ips observed in the current scan toward the limit. db-only
entries stay in the blob for display but dont participate in the ban
decision. live subset still uses the "protect oldest, ban newcomer"
rule.
closes#4091. followup to #4077.
The externalProxy fanout from #4073 did `int(ep["port"].(float64))`
with no ok-check. If any entry is missing port or has the wrong
type it panics, and since this runs in the /sub/<id> handler the
whole subscription returns 500. Skip malformed entries instead.
UI stores v1 and v2 both as "hysteria" with settings.version, but
inbounds that came in from imports / manual SQL can carry the
literal "hysteria2" string and get silently dropped everywhere we
switch on protocol.
Add Hysteria2 constant + IsHysteria helper, use it in the places
that gate on protocol (sub SQL, getLink, genHysteriaLink, clash
buildProxy, json gen, inbound.go validation, xray AddUser).
Existing "hysteria" inbounds are untouched.
closes#4081
On fresh Debian 12+, Ubuntu 24+ and recent RHEL-family minimal images
the fail2ban package ships with `banaction = nftables-multiport` as
the default in /etc/fail2ban/jail.conf but does not pull in the
`nftables` package as a dependency. The first SSH brute-force attempt
hits the default sshd jail and fail2ban logs
stderr: /bin/sh: 1: nft: not found
returned 127 -- HINT on 127: "Command not found"
repeatedly, which users mistake for a 3x-ui regression (see the
discussion on #4083). The 3x-ipl jail itself is unaffected — it uses
an iptables-based action configured in create_iplimit_jails — so this
is only stray noise, but noisy enough to look like a real failure on
first install.
Add `nftables` to the package list in every branch of install_iplimit
so new installs end up with a working default sshd jail out of the
box. Existing installs where `nftables` is already present are a
no-op.
After 60abeaa flipped the excess-IP selector to "oldest wins,
newest loses" (to protect the original/current connections), the
per-client IP table in `inbound_client_ips.ips` never evicted IPs
that stopped connecting. Their stored timestamp stayed ancient, so
on every subsequent run they counted as the "oldest protected"
slot(s) and whichever IP was actually using the config now was
classified as "new excess" and re-banned via fail2ban.
This is exactly the #4077 scenario: two IPs connect once and get
recorded, the ban lifts after the configured duration, the lone
legitimate IP that reconnects gets banned again, and again, and
again — a permanent 3xipl.log loop with no real abuser anywhere.
Fix: when merging the persisted `old` list with the freshly
observed `new` log lines, drop entries whose last-seen timestamp
is older than `ipStaleAfterSeconds` (30 minutes). A client that's
actually still active refreshes its timestamp any time xray emits
a new `accepted` line for a fresh TCP, so the cutoff is far above
even idle streaming sessions; a client that's genuinely gone falls
out of the table in bounded time and frees its slot.
Extracted the merge into `mergeClientIps` so it can be exercised
by unit tests without spinning up the full DB-backed job.
Tests cover:
- stale old entry is dropped (the #4077 regression)
- fresh old entries are still carried forward (access-log rotation
is still backed by the persisted table)
- newer timestamp wins when the same IP appears in both lists
- a clock-skewed old `new` entry can't resurrect a stale IP
- a zero cutoff never over-evicts
Closes#4077
* Fix Hysteria External Proxy + include Hysteria in Clash subscription (#4053)
Two related gaps on the Hysteria side of the subscription layer:
1) `genHysteriaLink` ignored `externalProxy` entirely, so an admin who
pointed a Hysteria inbound at an alternate endpoint (e.g. a CDN
hostname forwarding UDP back to the node) still got a link with the
original server address. Mirror what `genVlessLink` / `genTrojanLink`
already do: fan out one link per entry, substituting `dest` / `port`
and picking up the entry's remark suffix. As a bonus, the salamander
obfs password is now copied into the URL too — the panel-side link
generator already did this, so the subscription output was lagging
behind it.
2) `buildProxy` in `subClashService.go` had a protocol switch with cases
for VMESS / VLESS / Trojan / Shadowsocks and a `default: return nil`.
Hysteria inbounds fell into the default branch and silently vanished
from the Clash YAML. Route Hysteria to a dedicated
`buildHysteriaProxy` helper before the transport/security helpers run
(applyTransport / applySecurity model xray streams, which Hysteria
doesn't use).
`buildHysteriaProxy` reads `inbound.StreamSettings` directly instead
of going through `streamData` / `tlsData`, because those prune
fields (`allowInsecure`, the salamander `finalmask.udp` block) that
the mihomo Hysteria proxy wants preserved. Output shape matches
mihomo's expectations:
type: hysteria2 # or "hysteria" for v1
password / auth-str: <client auth>
sni, alpn, skip-cert-verify, client-fingerprint
obfs: salamander
obfs-password: <finalmask.udp[salamander].settings.password>
The existing `getProxies` fanout over `externalProxy` already plugs in
for Clash, so with Hysteria now recognised, External Proxy entries
also flow through to the Clash output for Hysteria inbounds.
Closes#4053
* gofmt: align map keys in buildHysteriaProxy
---------
Co-authored-by: pwnnex <eternxles@gmail.com>
Update GetXrayVersions filter to accept Xray releases >= 26.3.10 instead of the previous >= 26.4.17. This changes the conditional in web/service/server.go so releases from 26.3.10 onward are included when building the versions list.
`getXraySetting` builds its response as
{ "xraySetting": <db value>, "inboundTags": ..., "outboundTestUrl": ... }
and embeds the raw DB value as the `xraySetting` field without
checking whether the stored value already has that exact shape.
The frontend pulls the textarea content from `result.xraySetting`
and saves it back verbatim. If the DB ever ends up holding the
response-shaped wrapper instead of a real xray config (older
installs where this happened at least once, users who imported a
copy-pasted response into the textarea, a botched migration, etc.),
the next save nests another layer, the one after that nests a
third, and the Vue-side JSON.parse of the resulting blob silently
fails — the Xray Settings page goes blank.
Fix both ends of the round-trip:
* Add `service.UnwrapXrayTemplateConfig`. It peels off any number of
`xraySetting`-keyed layers, leaving a real xray config behind.
The check is conservative: if the outer object already contains
any top-level xray key (`inbounds`, `outbounds`, `routing`, `api`,
`dns`, `log`, `policy`, `stats`), it is returned unchanged, and
there is a depth cap to avoid pathological inputs.
* `SaveXraySetting` unwraps before validation so a round-tripped
wrapper from an already-corrupted page can no longer re-poison
the DB on save.
* `getXraySetting` unwraps on read and, when it finds a wrapper,
rewrites the DB with the corrected value. Existing broken installs
heal themselves on the next visit to the page.
Includes unit tests for the passthrough, single-wrap, multi-wrap,
string-encoded-inner, and false-positive cases.
Co-authored-by: pwnnex <eternxles@gmail.com>
Remove the "Custom GeoSite / GeoIP DAT" section from the main README and all localized READMEs (ar_EG, es_ES, fa_IR, ru_RU, zh_CN). Also apply minor formatting cleanups: normalize language header spacing and remove trailing spaces from the Stargazers badge lines.
* Fix: propagate xhttp xPadding settings into generated subscription links
The four `genXLink` helpers in `sub/subService.go` only copied `path`,
`host` and `mode` out of `xhttpSettings` when building vmess:// /
vless:// / trojan:// / ss:// URLs. Everything else — `xPaddingBytes`,
`xPaddingObfsMode`, `xPaddingKey`, `xPaddingHeader`,
`xPaddingPlacement`, `xPaddingMethod` — was silently dropped.
That meant an admin who set, say, `xPaddingBytes: "80-600"` plus obfs
mode with a custom `xPaddingKey` on the inbound had a server config
that no client could match from the copy-pasted link: the client kept
the xray/sing-box internal defaults (`100-1000`, `x_padding`,
`Referer`), hit the server, and was rejected by
invalid padding (queryInHeader=Referer, key=x_padding) length: 0
The user-visible symptom on OpenWRT / Podkop / sing-box was
"xhttp inbound just won't connect" — no obvious pointer to what was
actually wrong because the link itself *looks* complete.
Fix:
* New helper `applyXhttpPaddingParams(xhttp, params)` writes
`x_padding_bytes=<range>` (flat, sing-box family reads this) and
an `extra=<url-encoded-json>` blob carrying the full set of xhttp
settings (xray-core family reads this). Both encodings are emitted
side-by-side so every mainstream client can pick at least one up.
* All four link generators (`genVmessLink` via the obj map,
`genVlessLink`, `genTrojanLink`, `genShadowsocksLink`) now invoke
the copy.
* Obfs-only fields (`xPaddingKey`, `xPaddingHeader`,
`xPaddingPlacement`, `xPaddingMethod`) are only included when
`xPaddingObfsMode` is actually true and the admin filled them in.
An inbound with no custom padding produces exactly the same URL
as before — existing subscriptions are unaffected.
* Also propagate xhttp xPadding settings into the panel's own Info/QR links
The previous commit covered the subscription service
(sub/subService.go). The admin-panel side — the "Copy URL" / QR /
Info buttons inside inbound details — has four more
xhttp-emitting link generators in `web/assets/js/model/inbound.js`
(`genVmessLink`, `genVLESSLink`, `genTrojanLink`, `genSSLink`) that
had the exact same gap: only `path`, `host` and `mode` were copied.
Mirror the server-side fix on the client:
* Add two static helpers on `Inbound`:
- `Inbound.applyXhttpPaddingToParams(xhttp, params)` for
`vless://` / `trojan://` / `ss://` style URLs — writes
`x_padding_bytes=<range>` (sing-box family) and
`extra=<url-encoded-json>` (xray-core family).
- `Inbound.applyXhttpPaddingToObj(xhttp, obj)` for the VMess base64
JSON body — sets the same fields directly on the object.
* Call them from all four link generators so an admin who enables
obfs mode + a custom `xPaddingKey` / `xPaddingHeader` actually
gets a working URL from the panel.
* Only non-empty fields are emitted, so default inbounds produce
exactly the same URL as before.
Also fixes a latent positional-args bug in
`web/assets/js/model/outbound.js`: both VMess-JSON (L933) and
`fromParamLink` (L975) were calling
`new xHTTPStreamSettings(path, host, mode)` — but the 3rd positional
arg of the constructor is `headers`, not `mode`, so `mode` was
landing in the `headers` slot and the actual `mode` field stayed at
its default. Construct explicitly and set `mode` by name; while
here, also pick up `x_padding_bytes` and the `extra` JSON blob from
the imported URL so the symmetric case of importing a padded link
works too.
---------
Co-authored-by: pwnnex <eternxles@gmail.com>
`genHysteriaLink` was calling `.join(',')` on
`this.stream.tls.settings.echConfigList`, but that field is bound to an
`<a-input>` (single-line string) in `tls_settings.html` and defaults to
`''` in `TlsStreamSettings.Settings`. Calling `.join()` on a string
throws `TypeError: echConfigList.join is not a function`, which breaks
the Info / QR buttons for every hysteria / hysteria2 inbound.
All three sibling link generators (`genVmessLink`, `genVlessLink`,
`genTrojanLink`) already pass the value directly:
params.set("ech", this.stream.tls.settings.echConfigList)
`URLSearchParams.set` will stringify arrays with `,` on its own, so the
same one-liner works for both string and array inputs. Align
`genHysteriaLink` with the other three.
Fixes#4063
Co-authored-by: pwnnex <eternxles@gmail.com>
Update GetXrayVersions in web/service/server.go to select releases newer than 26.4.17 (previously 26.2.6). This relaxes the version filter so releases >= 26.4.17 are included.
Treat the xdns mask type as a multi-value setting and update forms accordingly. Inbound/outbound UdpMask models now return arrays for xdns (inbound: settings.domains, outbound: settings.resolvers) using Array.isArray checks. UI templates were split so 'header-dns' still uses a single domain string, while 'xdns' renders a tags-style <a-select> for multiple entries (domains/resolvers). Conditionals were made explicit (mask.type === ...) instead of using includes(). Changed files: web/assets/js/model/inbound.js, web/assets/js/model/outbound.js, web/html/form/outbound.html, web/html/form/stream/stream_finalmask.html.
Change TunSettings to support separate IPv4/IPv6 MTU values and add gateway, DNS, autoSystemRoutingTable and autoOutboundsInterface properties. Introduces _normalizeMtu to accept legacy single-value or array forms and provide sensible defaults. Update fromJson/toJson to handle new fields and preserve backward compatibility. Update tun form UI to expose MTU IPv4/IPv6 inputs, Gateway/DNS tag selects, Auto Routing Table and Auto Outbounds input.
Expose an ipsBlocked array on Outbound.FreedomSettings and wire it into the outbound form. The constructor now defaults fragment to {} and noises/ipsBlocked to arrays for robustness; fromJson/toJson handle ipsBlocked and omit it when empty. The outbound HTML adds a tag-style <a-select> bound to outbound.settings.ipsBlocked (with comma tokenization and placeholder) so users can enter IP/CIDR/geoip entries.
Update Go toolchain to 1.26.2 and upgrade multiple direct and indirect dependencies for bug fixes, compatibility and improvements. Notable bumps include github.com/mymmrac/telego v1.7.0→v1.8.0, github.com/valyala/fasthttp v1.69.0→v1.70.0, golang.org/x/crypto v0.49.0→v0.50.0, golang.org/x/sys v0.42.0→v0.43.0, golang.org/x/text v0.35.0→v0.36.0, go.mongodb.org/mongo-driver/v2 v2.5.0→v2.5.1, and updates to several mattn, pires, sagernet and golang.org/x/* packages. Regenerated go.sum to reflect the updated module checksums.
Replace legacy KCP buffer options with cwndMultiplier and maxSendingWindow across models and UI. Updated KcpStreamSettings in web/assets/js/model/inbound.js and web/assets/js/model/outbound.js (constructor, fromJson and toJson) to remove congestion/readBuffer/writeBuffer and use cwndMultiplier/maxSendingWindow instead. Updated web/html/form/outbound.html to reflect the new KCP fields in the stream form and to include extensive template formatting/markup cleanup for consistency and readability.
Configure session cookie options centrally in initRouter and remove per-login MaxAge handling. Deleted SetMaxAge helper and its use in the login flow; session.Options are now applied once using basePath with HttpOnly and SameSite defaults, and MaxAge is set only when the stored setting is available and >0. Also make CookieManager.setCookie treat exdays as optional (only add expires when provided) and stop using a hardcoded 150-day expiry for the lang cookie in the JS language manager.
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
Replace the previous flat settings map for VLESS outbound with a vnext/users structure. Encryption is now pulled from inbound settings into the user object, a level field (8) is added, and client id and flow are preserved. Address and port are nested under vnext and outbound.Settings is set to {vnext: [...]}, aligning the outbound format with the expected VLESS schema.
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
* feat: Add NordVPN NordLynx (WireGuard) integration with dedicated UI and backend services.
* remove limit=10 to get all servers
* feat: add city selector to NordVPN modal
* feat: auto-select best server on country/city change
* feat: simplify filter logic and enforce > 7% load
* fix
---------
Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>