# Changelog All notable changes to W-UI are recorded here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/). ## [Unreleased] ### Added - **relay: a WireGuard tunnel behind a CDN that will not carry WebSocket.** *relay* joins a standard WireGuard tunnel's *Transport* menu, with drift's fields. Behind the same front, the tunnel's datagrams travel in the bodies of ordinary POST requests to the transport's path and in the bodies of their answers, one request in flight at a time: the gateway answers at once when something waits for the device, or after 250 ms with padding alone, and the client polls faster while traffic flows and slower, up to every 5 seconds, while it does not. A session opens with drift's HPKE proof, made for relay's own name, and every later request names it by an id only the proof's two ends can work out, and a number. Every body is the session's sealed and padded chunks and is taken whole or not at all, so whoever learns an id can send anything under it and the session goes on as it was; a request a front sends twice gets the answer the gateway kept, and a client whose connection the front drops sends the same bytes again. Whatever proves nothing goes to the operator's site as it came, body included. A device holds 2 relay sessions at most; the transport's key is its own and turns over every 30 days. W-UI's profile at `?format=wui` carries it as an `https://` address. The protocol is specified in the reference under *Stealth transport: relay*, with an nginx block, a test vector and an independent check of its own. - **anchor: a WireGuard tunnel behind a web server of its own, with no domain needed.** *anchor* joins a standard WireGuard tunnel's *Transport* menu. Plain WireGuard stays on the tunnel's port; beside it a gateway on a port of its own is the web server: it ends TLS itself and takes the same tunnel as WebSocket, with no nginx or CDN in front. The session is drift's -- the same HPKE proof, made for anchor's own name, the same sealed and padded chunks, the same checks of WireGuard. What happens before the proof is its own: the decoy is dialled the moment TLS ends, before a byte is read; the request line is decided byte by byte as it arrives; a proof counts only over TLS 1.3 and is opened whatever the path; and whoever is not a device is handed to the operator's site byte for byte, half-closes and resets included, with the gateway lingering over a close as a web server does and never writing a byte of its own. The certificate is the tunnel's own, which the panel makes: an ECDSA P-256 key drawn once and kept, so the pin profiles carry never changes, and a certificate made again with it before it runs out, handed to the running gateway with no session ending; nothing in it says W-UI. The panel's own issued certificate can be served instead on the panel's server, which devices then check as a browser does. The decoy can be given a PROXY v2 header naming the visitor. The port is opened in the firewall, the certificate key is never shown by the API, and W-UI's profile at `?format=wui` carries the pin. The protocol is specified in the reference under *Stealth transport: anchor*, with a test vector and an independent check of its own. - **bare: a WireGuard tunnel over plain HTTP, where TLS itself is cut.** *bare* joins a standard WireGuard tunnel's *Transport* menu, for the network where nothing covered gets through. A gateway takes the same tunnel as WebSocket from the operator's web server over plain HTTP; the session is drift's -- the same HPKE proof, made for bare's own name, the same sealed and padded chunks, the same checks of WireGuard -- and a stranger still sees only the operator's site, which is required here, since its answers cross the network in the clear. The token travels in a cookie of a name the panel draws, never in the target; the answer sets no cookie; the transport's key is its own and turns over every 7 days. A bare front's name and path are never a drift transport's, and a tunnel switched between the two carries no path or key across. A token read off the path opens nothing usable, and a frame written into a session ends it without reaching WireGuard. W-UI's profile at `?format=wui` carries it. The protocol is specified in the reference under *Stealth transport: bare*, with a test vector and an independent check of its own. - **drift: a WireGuard tunnel behind your own web server or a CDN.** *drift* joins a standard WireGuard tunnel's *Transport* menu. Plain WireGuard stays on the tunnel's port; beside it a gateway takes the same tunnel as WebSocket from nginx or a CDN, which ends TLS, so to the network a device is visiting a site. A device opens a session with a proof sealed by HPKE to the transport's own key and bound to its secret, and every request that proves nothing goes to the operator's site as it came: the gateway makes no answer of its own for a stranger. Every message is a sealed chunk, padded like veil's, and a session belongs to its device's WireGuard alone. The form takes the front's name and port, the path (drawn when left empty), the decoy site, the fronts trusted to name the visitor and their header. With no front elsewhere the gateway listens on the loopback alone and its port stays shut; the transport's key is drawn by the panel, turned over every 30 days with the one before still taken, never shown by the API, and given to a remote node with the tunnel. W-UI's profile at `?format=wui` carries it. The protocol is specified in the reference under *Stealth transport: drift*, with a test vector and an independent check of its own. - **veil: a WireGuard tunnel reached where WireGuard is blocked.** A standard WireGuard tunnel now has a *Transport*, as an OpenVPN one does: *UDP*, or *veil*, which keeps plain WireGuard on the tunnel's port and adds veil beside it on a UDP port of its own. A gateway on that port carries the same tunnel in packets that show nothing of WireGuard -- no fixed byte, size or rhythm -- and answers nobody who does not hold a device's secret, so a probe cannot tell anything listens. The tunnel, its port and every `.conf` handed out stay as they were. Each WireGuard device gets a transport secret of its own, drawn with its keys and replaced when they are rotated, and the subscription gives W-UI's own app its profile at `?format=wui`. The port is opened in the firewall, a remote node gets the setting with the tunnel, and the sharing report sees each device at its own address. The protocol is specified in the reference under *Stealth transport: veil*. ## [3.1.2] — 2026-10-26 ### Added - **Which tunnels leave through an outbound or a balancer is chosen on its own form.** An *Interfaces* field on both forms sends everyone on those tunnels out through it. It is saved as a routing rule, so the rules page shows it and narrower rules above it still come first; a tunnel moved to another outbound or balancer leaves the first, and the operator's own rules are never changed. ### Fixed - **An outbound moved to the top stays there.** The top of the outbounds list is the default every customer leaves through. A subscription set to add its outbounds at the top put the built-in ones back above everything each time it was fetched, so an outbound from it moved to the top was moved down again on the next refresh, and everyone left through direct. A fetch now leaves the order as the operator set it, and only a subscription's new outbounds are added near the top, never above the default. ## [3.1.1] — 2026-10-26 ### Fixed - **An L2TP or IKEv2 tunnel knows all of its addresses.** The other addresses an operator adds to a tunnel under *Hosts* went into customers' files but never reached the tunnel itself: an IKEv2 server certificate named only the first address, so a device dialing another was refused the certificate, and an L2TP tunnel bound to its first address only. A change to them now reaches the tunnel. ## [3.1.0] — 2026-10-26 ### Added - **A Bale bot, beside the Telegram one.** Bale is the Iranian messenger whose bot API is Telegram's; it is reached from inside Iran with no relay. The same bot runs there, on its own token and chats set under *Settings → Bale Bot*: the same menu, commands and customer card for administrators, the same answers for a customer, and the same notifications, periodic report and automatic backup, each on its own schedule. A customer is known to it by the new **Bale ID** on their plan, beside the Telegram ID. Messages are written in Bale's own Markdown, since it has no HTML formatting. - **A W-UI node's own customers can be brought in, as a 3x-ui or Pasarguard node's are.** *Bring in users* on a W-UI node's row lists the tunnels and customers that panel had of its own, and brings them in as this panel's: made here with the same keys, ports, addresses and passwords, so nothing a customer holds changes and nobody is disconnected. Their allowance, usage, expiry, device limit, groups and Telegram and Bale IDs come with them; the usage that came over is not counted twice, and their subscription link is this panel's from then on. A tunnel moves with every customer on it, and a customer with a file on an Xray inbound there stays. Both panels need this version. ### Changed - **AmneziaWG costs the server far less.** Every two seconds, each AmneziaWG tunnel read its device twice and wrote its whole configuration back to it, peers and all, whether anything had changed or not -- and amneziawg-go, which most servers run it on, reapplied all of it each time. It now reads the device once and writes only when a customer was added, removed or changed, or the tunnel's own settings were; a device that lost its peers is still put right on the next pass. - **AmneziaWG values are fitted to the server.** The panel reads what this server's way out to the internet carries and sets the tunnel's MTU and padding to fit it, so on a cloud network with a 1450- or 1400-byte link no packet is split on the way. The form says the number under the MTU field; an MTU typed by hand is fitted to instead. ### Fixed - **A deleted tunnel stays deleted.** A pass of the loop that had read the tunnels just before the deletion opened it again, and its device kept the port, so a new tunnel on that port was refused. - **A customer who runs out is shown as out.** The packet that crosses a customer's allowance is dropped by the kernel before any counter sees it, so their stored usage stopped a few bytes short of the limit and they stayed *active* -- cut off, while the panel, the bot and their reseller saw them as running. A customer near their limit whose kernel quota is spent is now marked out of data, and a reset or renewal still lets them back in at once. - **Every byte a customer sends is counted, on a busy server too.** Each customer's stored usage was written into the firewall table, so any customer's traffic changed the table and the panel deleted and wrote it again whole -- making every counter anew at zero. Whatever any customer sent between the last read and the rewrite was never counted: on a test server with twenty thousand customers online, about a third of all traffic. The table is now written whole once, and from then on changed a customer at a time; a customer's traffic changes nothing in it, and a change -- a customer added or removed, switched off, renewed, given another plan -- touches that customer alone, in one transaction, the others' counters untouched. On the same server, twenty thousand customers sending at once and two hundred added mid-run were counted to the byte. - **The panel keeps its two-second pace with many customers.** Each pass read the counters, the quotas and the tables through the nft tool, which reads the whole ruleset back first: at ten thousand customers each read took most of a second and the pass fell behind. They are now read from the kernel directly. On a two-core server the panel holds its pace at twenty thousand customers using half the CPU. - **Usage is written a few hundred rows to a statement.** Every five seconds each customer who moved was four statements, written and compiled one at a time -- eighty thousand with twenty thousand customers online -- and the look for each customer's traffic sample took the wrong index and read every sample there was. It is now one statement for each few hundred rows of each kind, the sample looked for by the index on customer and bucket, to the same values as before (a test writes the same flushes both ways, on SQLite and PostgreSQL, and compares every column). With twenty thousand customers online on a two-core server, a pass now takes about three seconds instead of seven, every byte still counted. - **A customer with several files gets files an app imports.** Their link answered with every configuration written one after another in one text -- for a customer on two tunnels, or with two devices, a file no WireGuard, AmneziaWG or OpenVPN app imports. It now answers with that one file when there is one, and with a zip of them, one a file, when there are several; `?format=base64` of several is their links. A format the link does not serve (`clash`, `singbox`, ...) is refused with the list, rather than answered with the text. - **A WireGuard or AmneziaWG file imports on Android and Windows whatever the customer is called.** Files were named after the customer in their own script, and those apps take only Latin letters, digits and `_ = + . -` in a tunnel's name, at most fifteen: a customer named in Persian could not import their file there. Files are now named that way, after the tunnel for a name in another script. - **Each of a customer's files has a name of its own.** A customer on several tunnels with one device got every file under one name, and a long name lost the device's part when it was cut to fit, so the second import was refused as a tunnel that already exists. The tunnel, the device and the host are now kept whole in the name, and the customer's name is what is cut. - **An IKEv2 certificate downloads as the certificate.** The page's links to a tunnel's authority and a device's certificate returned the device's instructions instead. The zip of a customer's files now holds both certificates too, which the instructions name. - **A balancer can be edited.** The form sent the balancer's id along with its settings, which the API refuses, so every edit failed with "a field this server does not know". - **The balancer page reads in the panel's language and says what it does.** Its column headings and strategy names were English on a Persian panel; the form now has a switch to turn a balancer off and a note, says what each strategy does, points to *Outbounds* when there is no hop to balance over, and names a balancer by its tag rather than its row number. The empty list no longer flashes before the first answer. - **A button in an empty list no longer has a giant faded icon.** The empty state's own large icon was applied to the icon of the button beside it too, on the balancer and DNS pages. - **Restarting the panel no longer drops AmneziaWG customers for a moment.** The tunnel's configuration was written back without its peers when the panel started, removing every customer from the device until the next pass added them again with fresh handshakes. It is now written with the peers the device already holds. - **A customer is never handed a DNS server that does not answer.** While the panel's own resolver is on, every file names its tunnel's gateway as DNS -- including where the resolver could not open port 53 on that gateway, because something else held it or the service could not bind it; those customers connected and resolved nothing (seen on AmneziaWG). A tunnel whose gateway is up but not answered on now hands out its own DNS setting until the resolver answers there. The resolver also no longer tries to answer on the gateways of tunnels on other nodes, which it never could. - **The installer no longer puts the panel on 2096 when you press Enter.** Choosing to name the panel port and then pressing Enter at the port question put the panel on 2096, the number in the documentation, the one a sweep for panels tries first. Enter there now gives a random free port. - **The installer asks for the subscription port, as it asks for the panel's.** Name one, or let it pick a random free one; it is never the panel's own port or one something else already serves. - **A tall dialog's buttons stay on the screen.** On a desktop window the tunnel form and the customer form could run past the bottom of the window, Create and Cancel with them; they now always fit, and the form scrolls inside. - **A 3x-ui user brought in keeps its Telegram ID,** so the bot here knows the customer at once. - **A 3x-ui user on two inbounds is brought in** when the first of them takes no customers here and the second does; before, it was offered as one that could not be. - **A Pasarguard user in two groups that share an inbound** lists that inbound once. - **An AmneziaWG module that does not build no longer stops every protocol after it from installing.** The module package builds the module as it installs; when the kernel could not take it (seen on Ubuntu 22.04 with kernel 5.15.0-46, on a server in Iran) the package stayed half-installed, and apt refused everything after it: OpenVPN, L2TP, IKEv2, PPTP and OpenConnect all failed, and so did the server's own updates. The installer now removes that package at once and carries AmneziaWG on amneziawg-go, as it already did; running the installer again clears a server that hit this before, and the panel's agent does the same before it installs what a new tunnel needs. ## [3.0.0] — 2026-10-19 ### Updating - **An update from 2.6 keeps everything as it was.** Every setting, every customer and every configuration a customer holds is kept byte for byte, and nobody is disconnected: update from the panel's own button, with `w-ui update`, or by running the installer again, as before. - **Panels and nodes on 2.6.4 and 3.0 work together, in either direction,** so they can be updated in any order. A tunnel of a kind new in 3.0 -- IKEv2, L2TP, PPTP, OpenConnect, a proxy inbound -- is made only on a node that is on 3.0 too; an older node is refused with a message rather than half set up. - **Going back to a release before 3.0** (`w-ui` → *Legacy version*) takes down what 3.0 runs beside the panel first: the L2TP agent with any tunnel it carries, and the firewall helper. Their units run the panel's binary with a command an older binary does not know, and an older binary given one starts the whole panel instead -- a second panel on the same tables. For the same reason the installer, given a binary from before 3.0, writes neither. ### Added - **The installer installs every protocol.** Besides WireGuard, OpenVPN, AmneziaWG and L2TP/IPsec, a new install now brings IKEv2 (strongSwan with its EAP plugins), PPTP (`pppd`) and OpenConnect (`ocserv`), each left off until a tunnel uses it, so the first tunnel of each kind starts at once rather than after an install. The automatic install takes all of them without a question; the manual one asks *Install every protocol?* and, on a no, asks for each, xray and sing-box included. `--no-ikev2`, `--no-pptp`, `--no-openconnect` and `--no-proxy-cores` leave one out, and the panel still installs it with the first tunnel of its kind. - **The panel can be watched from outside.** `GET /api/health` answers `200` or `503` with no sign-in, for an uptime checker, under the panel's own path; it is not ok when the loop that applies the database to the kernel has stopped or usage is not being written. `GET /api/metrics`, signed in, gives Prometheus each tunnel up or down, the loop's last run, failed usage writes, the nodes reachable and the backups kept. - **Inbounds are made on a 3x-ui or Pasarguard node from the tunnel form, with everything that panel's own form asks.** The form asks for the server first and then offers, in a list as in 2.6, the protocols that server has at its version: this panel's own for a W-UI server; for 3x-ui VLESS, VMess, Trojan, Shadowsocks, HTTP from 2.0, WireGuard and SOCKS/dokodemo-door from 2.5, mixed and tunnel from 2.8, Hysteria2 from 2.9, MTProto from 3.4, TUIC and AmneziaWG from 3.8, MASQUE from 3.10; for Pasarguard (1.0 and later) VLESS, VMess, Trojan, Shadowsocks, Hysteria2, WireGuard, mixed, SOCKS, HTTP, tunnel and dokodemo-door. OpenVPN and the other kinds of this panel are never offered for them. - Each protocol's own settings, every transport (TCP with its HTTP header, WebSocket, gRPC, HTTPUpgrade, XHTTP, mKCP), TLS with a certificate file or text, ALPN, fingerprint and ECH, Reality with its target, names, short ids and keys, VLESS fallbacks, sniffing, and an advanced JSON field for anything else. - On 3x-ui the inbound's own allowance, expiry and traffic reset, as that panel sets them; on Pasarguard the host its links are written from. - Keys are drawn here: Reality x25519 and short ids, WireGuard, Shadowsocks 2022, MTProto secrets. - The panel's xray must come back running with the new inbound, or the inbound is taken out again and the reason given; the panel's operator's own users never stop for it. - Hysteria2 and MASQUE are served over h3 only, which is what their clients ask for. - A Shadowsocks-2022 inbound on Pasarguard gets a standing user, `wuikeep_` and a number, whose key nobody is given. A Pasarguard node starts that inbound with the users it has, and with none xray takes no users on it afterwards: the first one added failed, and with it that panel's sync of every user on the node, its operator's own included. - Tested on 3x-ui 3.10.2, 3x-ui 2.9.4 and Pasarguard 5.4.1: 34 inbounds of every kind made and checked on each panel, and a customer's traffic through every link a proxy app can dial. - **A 3x-ui or Pasarguard panel's own users can be brought in as customers,** when it is added as a node or any time after, from **Bring in users** on its row: all of them, or the ones ticked. Nothing is made again: each keeps its credentials, its links and the subscription its owner already gave it, which go on working, and this panel's subscription carries the same links beside whatever else the customer is sold -- OpenVPN, WireGuard and the rest from W-UI servers. - Its allowance, what it had spent, its expiry (or its days from first use) and its IP limit come with it. From then on they are this panel's: its own limits there are cleared, and it is switched off there when it runs out here and on again when it is renewed. - A user switched off there by hand stays off, here too. A customer removed here is left on its panel as it was. - On 3x-ui the clients sharing a subscription id are one customer, with a file on each of their inbounds; on Pasarguard a user is one customer, whose links carry every inbound of its groups, and its groups stay its owner's. - A client is sent back to 3x-ui exactly as that panel wrote it, with only its switch and limits changed, so fields this panel does not know are kept. - Users made later there are brought in by asking again; nothing is taken over unasked. Users made by a W-UI panel are never offered. - Tested on 3x-ui 3.10.2, 3x-ui 2.9.4 and Pasarguard 5.4.1 with traffic running through the users' own links the whole time: none of it stopped when they were brought in. - **Two W-UI panels can share a node.** Each installation has a tag of its own, and the clients it makes there carry it (`wui--` on 3x-ui, `wui__` on Pasarguard), so neither removes the other's. - **3x-ui before version 3, and a Pasarguard without API keys, can be nodes too,** signed in with an administrator's username and password. The sign-in is kept between rounds, so the panel's operator is not told of a new one every twenty seconds. On 3x-ui 2.x the older API is used, and a client's links come from that panel's own subscription service. - **The outbound at the top of the list carries the customers on a 3x-ui node too.** It is put in that panel's xray as `wui-out`, with one rule that sends this panel's clients into it and nobody else's: the 3x-ui operator's own users leave as before. Direct at the top takes both away. If that xray does not come back running with it, its config is put back as it was. - **A Pasarguard panel can be a node.** Choose the type Pasarguard panel and give its address and an API key made there. Its cores' inbounds appear as tunnels under the node's tab; each file on one is a Pasarguard user named `wui_` and a number, in a group this panel keeps for that inbound alone, so each file's usage and links stay its own. Pasarguard makes the credentials, its subscription gives the links, and its users' traffic is read into the customer's one total, matching it to the byte. The allowance and expiry are this panel's: a customer who runs out is disabled there. The Pasarguard operator's own users are never touched. Tested against Pasarguard 5.4.1 with its node, with a real client on VLESS, Reality, VMess, Trojan and Shadowsocks. - **A 3x-ui panel can be a node.** Choose the type 3x-ui panel when adding a node and give its access URL and an API token made there. Its inbounds -- VLESS with or without Reality, VMess, Trojan, Shadowsocks 2022 and Hysteria2 -- appear as tunnels under the node's tab within twenty seconds, and customers are put on them as on any tunnel. Each file is a client there named `wui-` and a number; the 3x-ui operator's own clients are never touched. Usage is read from its counters and matches them to the byte; the allowance and expiry are this panel's, and a customer who runs out is switched off there. One user per file is the client's IP limit there. The links are the ones 3x-ui makes, in the customer's one subscription, and follow the inbound when it changes. Tested against 3x-ui 3.10.2 with a real client on every kind. - **Tunnels are grouped by server.** Tabs over the list: All, this server, and each node by its name. - **OpenConnect (Cisco AnyConnect)**, what Cisco Secure Client and openconnect dial. It is a VPN in TLS on any port, 443 by default, with DTLS on UDP. - ocserv is run by the panel's agent, and every sign-in is checked by the agent's own RADIUS server. That gives each device its own address, which is how the kernel counts its traffic. One sign-in is one connection: a second device takes the connection over, the first's address is freed for it, and an ended session cannot come back on its cookie. - Speed limits follow each session's own device. - Camouflage hides the server from anything without its secret where ocserv has it (1.2 and later); on an older ocserv the panel says so instead of claiming it. - The panel's own certificate goes on the tunnel when it covers the address; otherwise the tunnel makes one and renews it. - Device guides for Cisco Secure Client on Windows, macOS, iPhone, Android and ChromeOS, NetworkManager on Linux, and OpenWrt. The sign-in is withheld on the customer's page as for L2TP. - Tested end to end against ocserv 1.1 and 1.3 with real openconnect clients: DTLS, TLS only, camouflage, and one sign-in on two devices. - **PPTP works on Ubuntu 24.04 and later, and on every other system with pppd.** The distributions stopped shipping pptpd, so a PPTP tunnel there could not start. The panel now has its own PPTP server: the control connection on TCP 1723 and the GRE of each call. pppd still does everything PPP, so devices, sign-ins, MPPE and addresses are unchanged, and only `ppp` is installed for it. - A pptpd an earlier version started keeps its calls through the update, and is replaced the next time the tunnel restarts. - Tested on Ubuntu 26.04 (pppd 2.5), Ubuntu 24.04 and Debian 12: Windows, TP-Link, D-Link and MikroTik settings connect with 128-bit MPPE, a device without encryption is refused, one sign-in holds one device, and 30 MB arrives byte for byte. - **IKEv2 speed limits are applied.** Every IKEv2 session now runs through the tunnel's own device, an xfrm interface, so what a customer is sent is shaped there, as on WireGuard. Before, the limits were stored and never applied. On a kernel without xfrm interfaces the tunnel runs as before, and the log says once that it cannot be shaped. - **L2TP, PPTP and IKEv2 can have a port of their own.** The standard port stays open for the VPN built into Windows, iPhone, macOS and Android, which cannot be given another. The tunnel's own port is redirected to it, for the strongSwan app, Linux and routers that take a port, where the standard one is blocked. The form says which clients can use it, and the customer's page shows it. IKEv2's own port works behind NAT too: what starts as plain IKE goes to 500, and IKE with the NAT-T marker and the ESP after it to 4500. - **No two tunnels on one server share a port and transport,** checked in the panel's own records. Asking the machine was not enough: a redirected port has nothing listening on it, and a switched-off tunnel holds nothing. - **The tunnels' ports are let through ufw and firewalld.** With either on, a tunnel's port was dropped there and the tunnel reached nobody. A root helper, `wui-firewall.service`, now opens each tunnel's ports, GRE and ESP included, whenever the tunnels change. It closes only what it opened itself; a rule that was there before is never touched. - **Speed limits apply on every server.** Found on a real Ubuntu 26.04 server, where no customer was shaped: - The first customer's queue was given the handle of the shaping root itself, which the kernel refuses, and the refusal took every other customer on the device with it. The kernel now picks each queue's handle. - iproute2 6.19 names a class's kind differently and gives its rate in bytes a second; the panel read neither, so it took a device's classes for missing and tried to add them again every two seconds. Both are read now, and a rate that is right is no longer rewritten. - A class a failed attempt left without its queue gets one. - **The installer leaves apt working when the AmneziaWG module does not build for a new kernel.** On Ubuntu 24.10 and later such a module failed the kernel's package and left dpkg half-configured, and every install after it failed: OpenVPN, L2TP and the server's own updates. DKMS's own switch now lets the kernel install, `wui-amnezia-kmod` builds the module at boot when it can, and amneziawg-go carries the tunnels when it cannot. A dpkg left half-configured by an earlier run is finished first. - **Found and fixed on real Ubuntu 24.04 and 26.04 servers, with real clients:** - Every L2TP and PPTP session failed on Ubuntu 24.04: its pppd does not know the `lock` option the panel gave it. The option is gone; a session has no serial line to lock. - An IKEv2 tunnel signed in nobody on a server where `ip` gives an xfrm device's id as a hex string: the panel took its own device for a stranger's, the tunnel never opened, and its devices were never handed to strongSwan. Both forms are read. - IKEv2 refused every password on a fresh server where an L2TP tunnel had started strongSwan before IKEv2's plugins were installed. The agent now restarts a strongSwan that has not loaded them. - On a server with more than one address -- a floating address, a second one -- tunnels answered from whichever address the route picked, and a device that dialed another dropped every answer: - PPTP's GRE now comes from the address the device dialed. - OpenVPN over UDP answers from the address it was sent to (`multihome`). - L2TP's xl2tpd answers from the tunnel's address when it is one of this server's; its unencrypted answers had matched no IPsec policy. - The distribution's own xl2tpd, from an init script, was left running on L2TP's port by the installer and the agent; it is stopped now, not only disabled. - amneziawg-go, the fallback for kernels the AmneziaWG module does not build for, could not be built in Iran: Google's servers refuse it. Go now comes from go.dev or a mirror of it, checked against go.dev's own checksum, and the source clone is retried. - Tested on both servers: WireGuard, AmneziaWG, OpenVPN over UDP and TCP, OpenConnect, IKEv2, L2TP and PPTP each connect, reach the internet from the server's address, are held to the customer's speed limit, have what they move counted, and a second device on the same sign-in takes the first one's place. - **`install.sh --local` refuses a binary for another architecture** instead of installing one the shell then tries to run as a script. - The tunnels table tags PPTP as TCP, not UDP, and OpenConnect as TCP + UDP. - **A pasted OpenVPN profile is recognised.** The outbound form's paste box was one line high, and a browser drops the line breaks of anything pasted into one: a whole .ovpn became one long line no reader knew. It now keeps every line, grows as text is pasted, and still imports a link with Enter. A profile with Windows or old-Mac line endings is read too, and its tag starts as its server's name. - **OpenVPN connects better**, without touching the one-sign-in rule: - Windows keeps to the tunnel's DNS (`block-outside-dns`), so a filtered name is not answered by the ISP's resolver. - A UDP device that disconnects says so (`explicit-exit-notify`), and its session ends in five seconds instead of two minutes. - The kernel sizes the socket buffers (`sndbuf`/`rcvbuf 0`), so a fast or distant link is no longer capped. This is pushed to files already handed out too. - OpenVPN Connect lists the profile by the subscription's title and the tunnel instead of an address. - The DNS and exit options are in the file, not pushed, so every file handed out before keeps connecting. This is tested against the server as it is now. - **AmneziaWG, all of it.** - **I1 to I5** are written into the server and every customer's file. Before each handshake the device sends packets that look like QUIC, a DNS query or STUN. The form offers these ready-made, or you can write your own from the tags both the kernel module and the phone apps read. - **Filled in by the panel, in the form, from the start**: choosing AmneziaWG opens the values already drawn for that server, unlike its other AmneziaWG tunnels (no shared identifier or handshake padding, a different packet before the handshake), fitted to the MTU and kept light for a busy tunnel. Modern (AmneziaWG 2) or Compatible, for devices that cannot be updated. Tested end to end with 3000 devices on one tunnel. - **Every obfuscation value is editable, and is checked as AmneziaWG checks it, the refusal naming the field. This includes identifier ranges (AmneziaWG 2.0) and the header protection key. **Draw new values** keeps the operator's I1 to I5. - **AmneziaVPN key**: every AmneziaWG file is also offered as the AmneziaVPN app imports it, a link or a QR code, on the customer's page and in the panel. - **Kernel upgrades no longer take AmneziaWG down**: each new kernel gets its headers so DKMS rebuilds the module, `wui-amnezia-kmod` rebuilds it at boot when that did not happen, and `amneziawg-go` is installed besides, pinned to the same release as the tools. - **S4 is now drawn to fit a 1500-byte link** at the default MTU. - **IKEv2**, the VPN built into Android 12+, iPhone, Mac and Windows, in all four of Android's types: username and password (MSCHAPv2, through a RADIUS server of the panel's own, so Windows works too), key (PSK), certificate (RSA) and certificate in EAP (EAP-TLS). One tunnel per server on UDP 500/4500, beside L2TP; certificates made, issued and renewed by the panel; one sign-in per device, as on every protocol. The customer's page has each device's steps, the server's certificate and the device's `.p12`, with the sign-in withheld like L2TP's. - **Sharing history.** The Sharing page has a **History** tab: every file that has been on the list, and every device held off for going over its plan's users, is kept there -- after it leaves the list, and after the customer is deleted -- with its addresses, its plan and when. On the first start, the log of earlier releases is read back into it: the holds they logged, and files they logged connecting from three or more places inside ten minutes. - **L2TP and PPTP sign-ins are kept off the customer's page.** The username, password and key show as a mask, and each copy button fetches its value on the click -- from the page alone, counted and logged -- and puts it on the clipboard without writing it into the page. What anyone holding the link fetches (raw, download, archive) carries the mask too. - **Search in the device list** of the customer's page, by name, brand or model, in English or Persian. - **PPTP**, as a tunnel of its own, for routers and modems that have nothing else: off until made, one per server on TCP 1723, MS-CHAPv2 and 128-bit MPPE required, and said to be weak wherever it is offered. - **Device guides for 78 routers, modems and phones**, chosen from two lists -- the device, then the connection it can make -- on the customer's page and in the panel. ### Security - **A backup no longer opens the panel it came from.** The key sessions are signed with was in the database, and so in every backup: whoever held one -- from a Telegram chat, a copied file -- could sign in to that panel as its owner, with no password and no second factor. It now lives in `/var/lib/wui/keys/`, which no backup carries and no restore replaces, and 3.0 makes a new one, so every backup taken before carries a key that opens nothing. Everyone signs in once more after the update. - **Backups can be encrypted with a password.** Set one under Settings → Backup and every archive from then on, the ones the Telegram bot sends included, is encrypted with it: named `….tar.gz.enc`, unreadable without the password, and refused whole if it was cut short or changed (AES-256-GCM in chunks bound to their place; the key is scrypt of the password). The password is kept in `/var/lib/wui/keys/`, never in the database or a backup; an encrypted archive restores where the same password is set. Until one is set, backups the bot sends are a high-severity security warning in the panel, and each one's caption in the chat says it is not encrypted. - **An IKEv2 sign-in's exchange is used once.** The state the RADIUS server handed out before a device proved its password was kept after the proof, naming a session already let in; a success sent with it within a minute was let in again without the password. Only strongSwan on the same machine, holding the shared secret, can send one, and each state is now retired as the exchange moves on. - **SQL written by hand cannot reach another reseller's customers.** The narrowing that keeps a reseller to their own customers is a clause added to every query, and a statement written out in full has no clause to add it to; the few that exist narrowed themselves, by hand. Such a statement on the customers' tables is now refused for a reseller unless the code says it narrows itself, so one written later that forgets fails in its tests instead of leaking. - **The database file is the panel's alone.** SQLite made it, and its journals, readable by everyone (`0644`); only the data directory's mode kept other accounts on the server from every customer's keys and every second factor. The panel now makes them `0600` when it starts. ### Fixed - **Leaving a page with an unsaved edit no longer throws it away on Cancel.** The question asked on leaving the settings or engine page had *Save and leave* and *Cancel*, and *Cancel* -- like Escape, or a click beside the dialog -- discarded the edit and left. It now has *Stay*, *Discard and leave* and *Save and leave*, and closing it any way stays. - **A toast that reports success reads as one,** and a warning stands out: seventeen of them, a token issued or a node updated among them, appeared in the grey of plain information. - **Cards, corners and a drawer look as designed.** Four colours and sizes the styles named were never defined, so the resellers' phone cards and the date picker had no border, six corners were square and the filter drawer did not slide; the cyan tag and the card washes had dark-theme colours on the light theme. - **An outbound can be edited from a phone.** The cards had no way to, the edit button being the table's alone. - **Editing a node on mutual TLS shows this panel's authority at once,** instead of an empty box until the mode was changed. - **The Telegram bot sends long reports.** A message is split to fit Telegram's limit, but only at blank lines, so a list of every customer, one a line, was sent whole and refused: on a large panel the reports never arrived. It is now split at line ends too, between characters for a line longer than a page, and a page that ends inside bold or code text closes it and the next opens it again. - **A subscription link keeps working when the service moves.** Found on a live panel, where some customers' links stopped loading: a link handed out before the operator changed the subscription's port, its certificate or its path answered nothing, or not found. - Each address the service leaves -- its port, plain or HTTPS, and its path -- is kept and still answered. Settings → Subscription lists them under *Old addresses still answered*; one is closed once nobody asks for it, and one this panel never recorded can be added. - The panel's own port answers a customer's link too, as it did before the service had its own. It is also what answers when the service's own port cannot open; before, every customer's subscription then answered not found. - A customer's page that could not be made for a reason of the server's -- a busy database, a tunnel restarting -- is said to be so, 503 with a moment to wait, and logged without the link's secret. It was answered not found, which looked to the customer like a dead link and to the operator like nothing at all. - **A customer's page loads in a fraction of what it did.** It carried its styles and its words in both languages, 140 KB the same for every customer, on every visit: now they are two files fetched once and kept by the browser, and what the service sends is compressed for a browser or an app that takes it. A page of 290 KB is 75 KB the first time and less after. - **A QR code on the customer's page shows the moment it is tapped.** It was an image fetched when tapped, and on a phone's network the box stayed empty for a second or more. The page now carries each code as its dots, about 700 bytes for a WireGuard file, and draws it on the phone with no request at all; it can still be saved as an image. Each code is worked out once on the server and kept until the file's keys change. - **A customer with many files gets a page a third of the size.** Each file's setup card carried every device's notes, the device list and a guide for each device, written out whole, and every icon was drawn again wherever it was used: 50 KB a file, 563 KB for a customer of ten WireGuard files. Each is now written once and filled in by the page as it opens, each file carrying only its own values: 215 KB for the same customer, and 27 KB over the network where it was 63. The page looks and works as before. - **What a customer's app shows as used is what they were charged.** Their app adds upload and download up, and traffic whose direction was not told -- a node of an older release reports the total alone, and usage counted before there were two counters -- went into the total and not into either, so the app showed less than the panel. It is now counted as download, in what is recorded from now on and in what the app and the page are told for usage recorded before. The allowance itself was always spent by the total, and is unchanged. - **An update no longer says a panel with a certificate has none.** A certificate kept in `/etc/wui/wui.env`, which the panel reads as well as its unit, went on being served, but the update reported "certificate none — this panel serves plain HTTP" and "SSL Certificate: Skipped". It is found there now, and kept. - **A proxy app is told when a subscription has nothing for it.** v2rayNG, Hiddify and the like were answered with an empty list for a customer whose files have no link form -- OpenVPN, OpenConnect, L2TP and the like -- which they take as the subscription emptied, dropping the servers they had, with no reason given. They are now answered not found with the reason, which they show as a failed update, and the files stay on the customer's page. - **Traffic on a node is a customer's use.** A plan that starts on first use never started for a customer only on nodes -- another W-UI, a 3x-ui, a Pasarguard -- and so never ended, and they never showed as last online. - **The outbound at the top carries customers on a 3x-ui node again.** Its rule there matched the clients' names from before panels had tags, and so nobody: customers there left directly, with no error. It matches this panel's clients and the users brought in, under an outbound of this panel's own, and an untagged one left by an older panel is taken away. - **Users asked in twice at once are brought in once**, as with a button pressed twice. - **A 3x-ui 3.10 node reached by username and password signs in**, carrying the CSRF token those panels ask for; given a token too, it is still reached when the sign-in is refused. - **A Shadowsocks-2022 inbound made on Pasarguard no longer fails that panel's sync of its users, even once**: it is given its standing user before its node starts with it. - **A customer on a Shadowsocks inbound of a 3x-ui node with an older cipher no longer stops that panel.** Each client there needs its cipher; without it xray refused its whole config, and every inbound on it stopped, its operator's own users' included. The panel now checks that xray is still running after it changes anything there. - **A Pasarguard node's links are read for users made before.** Its list of users leaves their subscription link out; it is read for each user who needs it. - **A subscription added to v2rayNG, v2rayN, Hiddify, NekoBox, Streisand and the like shows its configs.** They read a list of links, one a line, and were handed the WireGuard file's text first, which they cannot read, so they added nothing. They are now served links alone, base64 as they expect: a 3x-ui node's links and each WireGuard file as a wireguard:// link. Files with no link form stay on the customer's page and in the other formats; ?format= still chooses. - **A node abroad is reached from a panel in Iran over HTTPS.** Go now offers a post-quantum key exchange first, which makes its TLS hello too big for one packet, and Iran's network drops a split hello between a server inside and one abroad: every call to the node hung and ended in EOF while curl went through. The panel offers the long-standing key exchanges to its nodes. With a TLS name set to get past the filter, a node with a certificate for its IP (Let's Encrypt issues them) is still verified against that IP. - **Each file's setup guide is in its own row** on the customer's page, under the file, instead of one card at the foot of the page asking again which file. - **Outbounds, tested on real servers with every kind of link and file:** - A WireGuard outbound to a server elsewhere carried nothing: its own encrypted packets had the mark that routes traffic into it, so after the handshake they went round inside the tunnel. They now have a mark of their own. - A Trojan, VLESS, VMess or Hysteria2 link that skips the certificate check (`allowInsecure=1`, `insecure=1`) reached nothing: xray no longer skips it. The hop now pins the certificate the server shows when it starts; Hysteria2, whose certificate comes over QUIC, is carried by sing-box, which still skips it. - An OpenVPN profile with its sign-in inline, in an `` block, never signed in. The sign-in is read into the form, and from the profile for outbounds saved before. - While a hop was down, what was routed to it left from this server's own address. It is now refused until the hop is up. - An OpenVPN hop slow to connect was stopped after 20 seconds and started over, so on a slow link it never came up. It is kept while its process runs. - **The sharing notice is sent.** Telegram and mail offered a *sharing* notice that nothing ever sent. It now goes once, when a file first shows on the list. - **One L2TP or PPTP sign-in is one session.** A sign-in used again from another device left both connected on one address; the newer now replaces the older, as OpenVPN does. - **Two devices on one sign-in at once count as two.** Where a protocol hands back a session per device (L2TP, PPTP, OpenVPN with duplicates allowed), the readings cancelled out and the second device went uncounted against the plan's users. Each session is now followed on its own; two count as two only while both are moving traffic, so a phone changing networks is still one. ## [2.6.7] — 2026-10-09 ### Fixed - **Import an Interface takes what Export gives.** The export carried the stored AmneziaWG and OpenVPN blocks whole, and the server refuses a field it does not know, so no exported interface had ever imported. The export is now the create call's own shape -- the OpenVPN transport travelling on its own -- and the import reads that, and exports from earlier versions too. One interface refused, a name or port already taken, is named with the reason and the rest are still made; it used to stop at the first. A file can be chosen instead of pasted, and every export can be downloaded. - **Actions that take every customer take every customer.** The server serves at most 200 customers to a page and answers a larger page with 25; the pages asked for 500. Export All URLs, an interface's own Export All URLs, Attach Existing Clients, Attach Clients To, Detach Clients From and Add Clients To Group stopped at the 25th customer of a tunnel, and the customer pickers on the routing and groups pages listed 25. They read the whole list now, page by page. - **Sub links shows the links.** It read a field the customer list does not carry, so every line was a name and a dash; each selected customer's link is fetched now. - **Import clients asks where they go.** The customers went on the first interface whatever it was, a name already here was always skipped, and the result was one number -- "0" for a list imported back into the panel it came from. The interface and what to do with a taken name (skip, import beside it, replace the plan) are chosen in the dialog, the file can be chosen rather than pasted, and the result says how many were created, replaced, skipped and failed. ### Security - **Built with Go 1.26.9 and golang.org/x/net 0.60.0,** which fix advisories published against the versions 2.6.6 was built with: in net/http and its HTTP/2 support, html/template, crypto/tls, net/textproto and os (GO-2026-6599 to GO-2026-6617). The panel's web server, its customer page and every outbound request it makes are built on them. ## [2.6.6] — 2026-10-08 ### Fixed - **The per-user table counts each device itself, to the byte.** A device's usage was the customer's total shared out by estimate, read off the tunnel's own counters; OpenVPN updates those every ten seconds and the panel reads every two, so most of an OpenVPN customer's traffic was never shared to anyone -- on a live panel, 98 GB across 18 customers. The kernel now counts every device on its own tunnel address, and a customer's usage is the sum of their devices, so the rows of the table and the usage above it are the same bytes counted once. 2.6.5's **Earlier, not split by user** row is gone: the old estimates cannot be corrected and are not kept. A plan whose current period began before this release says from when its table counts (**Split by user since …**) until its next reset. - **A device on a node is charged at the node's usage multiplier**, like the customer's total, and in a way that keeps the two equal to the byte. Before, the total was multiplied and the device rows were not, so on a node with a multiplier other than `1` the table could never add up. - **A device on several hosts is counted once in the table.** A tunnel with spare hosts showed each user once per host, and the table added every one of them: with two spare hosts, three times what they had used. - **No usage is lost when the database is busy or briefly fails.** Usage waited in a queue of 512 updates; while the database was writing the last batch the queue could fill and the rest was dropped, and a batch the database refused was gone. Usage now accumulates without a limit and a refused batch is written by the next one. - **A node reports a customer and their devices from the same moment.** The two were read in separate steps, so a write landing between them put a customer's bytes in one report and their devices' in the next. ## [2.6.5] — 2026-10-08 ### Fixed - **The per-user table on the subscription page adds up to the usage above it.** OpenVPN rewrites its session counters every ten seconds and the panel reads traffic every two, so on most ticks a customer's traffic had no file to land on and was left out of the table: an OpenVPN user showed about a fifth of what they had really used. Such traffic now goes to the customer's files by the shares they last moved in, or to the one file a customer has, and the table follows the usage from now on. Traffic used before this release and never split by user is shown as one row of its own, **Earlier, not split by user**, so the table's total is the usage. - **Resetting a customer's traffic resets their per-user table too.** Resetting one customer, several, a group, an interface or everyone, and a plan's own renewal, set the usage to zero but left each file's share from the period before, so the table showed more than the customer had used. Resetting one customer or several also left **Downloaded** and **Uploaded** at the old period's figures beside a usage of zero. ## [2.6.4] — 2026-10-02 ### Fixed - **Plans that renew now renew.** A customer's **Renews** — daily, weekly or monthly — was stored and never acted on: a plan sold as 50 GB a month ran out after the first month and stayed out. At the start of each period the customer's traffic now goes back to zero, and one cut off for running out is back at once. The allowance, the end date and the switch are not touched. Periods are counted from the last renewal or reset, or from when the plan started, and keep the plan's own day of the month. - **Resetting traffic on an interface brings back who ran out.** Its usage went to zero, but customers cut off for running out of traffic stayed cut off. Resetting all interfaces at once did the same. - **Extending a group leaves plans without an end date alone.** It gave a customer with no end date — an unlimited plan, or one waiting for its first connection — a fixed date, ending the first and starting the second's clock early. - **A Listen IP or domain saved without a port keeps the panel's port.** It moved the panel to port 2096, the subscription service's, after the next restart. - **Settings show every label.** The external traffic report and the subscription service's certificate files showed internal names (`set.externalTrafficInformEnable`) instead of their labels. A test now fails on any label a page uses that a language lacks. - **The overview's tiles open the list they count.** *Depleting*, *Out of data*, *Expired*, *Active* and *Online* opened the whole customer list; they now open it filtered. A group's name on the Groups page opens its members, rather than a search for the group's name. - **A client export imports as it is.** Pasting the exported file into **Import clients** was refused for naming no interface; it now goes on the first one, as a bare list did. - **Deleting an interface that still has customers says so at once**, instead of asking for its name to be typed and then refusing. - **One line for running low.** The table turned a customer orange from 85 % used while the summary and the filter counted them from 80 %; all now use 80 %. ### Changed - **The panel says what each action does.** The interfaces page and its menus, the hosts and routing forms, the engine's advanced section, the Telegram bot and the error messages say *interface*, never *inbound*. On the Clients page: **Add or remove time** / **Add or remove traffic** for the selected users, **Add servers to the users**, **Remove servers from the users**, **Set traffic limit and reset cycle** and **New keys and link**; on an interface's menu, **Take All Clients Off** and **Detach Clients From…**. The search box says it searches the name and the note, which is what it does. - **The customer's own page is one click away**: their name in the list opens it, with every file, its configuration and its QR code, and files added or removed beyond the one per user. - **Interfaces' speed and the reseller limit** shown in 2.6.2 are named as the rest of the panel names them: the default and the bot's button say **Users**, and resellers sort by **Most users**. - **Hosts can be checked from their page**: **Check now** tries each address the way a customer's device would; *Reachable* used to say nothing until someone called the API. - **Save tells the truth.** Outbounds and routing said every change needed a restart; changes there apply within seconds. Settings now name the few that wait for a restart — where the panel and the subscription service listen, their certificates, the trusted proxies and the time zone. - **The documentation is rewritten** page by page against the panel: every page, column, menu, field, limit and default, in English and in Persian, with the Persian pages using the panel's Persian labels. ## [2.6.3] — 2026-10-02 ### Changed - **The installer speaks in colour.** Each step is a cyan heading, what went well is green, a warning yellow and a failure red. Its questions are coloured too: the question in yellow with its default answer in green, each set of choices under a magenta heading with the numbers in green, notes in yellow, and an answer it cannot take in red. The summary at the end colours its labels, its values and the state of each part -- missing or off in red, not installed in yellow -- with the password and the address standing out. The words are the same as before, and a log, a pipe or `NO_COLOR` gets them without any colour. ## [2.6.2] — 2026-10-02 ### Fixed - **A reseller's limit counts users, not customers.** A reseller sold 100 could make a hundred customers for fifty users each -- 5,000 users. The limit now adds up the users each customer's plan is for: a hundred one-user customers and twenty-five four-user customers are both 100, and so is any mix. Raising a customer's users counts against the limit, and lowering them frees what they held. A reseller with a limit can no longer give a customer any number of users at once (device limit 0), which the count could not see. The operators page and the reseller's own account box show users against the limit. Customers a reseller already made are kept; any made unlimited before this release count as none until they are changed. - **Interfaces show their speed.** The Speed column on the interfaces page always showed "—", and sorting by it did nothing. It now shows what the customers on each interface are moving, up and down, the same way the customer list does -- for a node's interfaces too, from that node's reports -- and sorts by it. - **Interfaces are called interfaces.** The interfaces page, its menus, the hosts and routing forms, the customer details, the error messages, the Telegram bot and the report said "inbound" (in Persian "ورودی" or "اینباند") for what the panel calls an interface everywhere else. The bot's `/inbound` command keeps its name. ## [2.6.1] — 2026-10-01 ### Fixed - **A panel can manage its nodes again.** Since v2.0.0 a node refused the token its managing panel holds for it -- "this part of the panel is the owner's" -- on everything the panel does through it: checking the node is up, sending it tunnels and customers, collecting its usage, and asking it to update. A node on v2.0.0 to v2.6.0 showed as unreachable, customers added on the panel never reached it, and its traffic was not counted. This applies to every API token, not only the one a managing panel holds for its node: as before v2.0.0, a token can again manage interfaces and tunnels, routing and outbounds, the engine, hosts, the subscription service and nodes, ask a node or this panel to update, and send the Telegram and mail test messages. Still refused to any token, as in v2.6.0: changing the panel's settings (`PUT /api/settings`; `GET` answers a token only the display preferences, as it does a reseller, and never the bot token or the mail password), backups, tokens, operators, adding, changing or removing a node, the subscription page template, restarting the panel, and the owner's own account and two-factor. - **An update no longer issues an API token or empties install-result.env.** Running the installer over an existing install issued a new token named `installer` every time -- one with full access again from v2.6.1 -- and rewrote `/etc/wui/install-result.env` with an empty password and that new token, so automation reading the file lost the password. An update now leaves the file exactly as the install wrote it (its values, its mode, its owner), issues no token, and on an install older than the file writes none. A fresh install is unchanged: one token, and the file. `w-ui update` and the panel's own **Update** never did either. - **A firewall table removed by something else is always reported.** When another program on the server flushed the whole ruleset just as the panel's own rules were changing, the panel put its table back but did not log that it had been removed. It now says so every time. ### Changed - **A release is published only from a commit CI passed.** The release workflow now waits for CI on `main` to finish for the tagged commit -- up to 30 minutes -- and builds nothing if it finished red, never ran, or is still going by then. It also refuses to publish a release without the signing key, where it used to publish one unsigned with a warning: an unsigned release is one no panel installs from its update button. - **Every update is now tested from every earlier release.** Before a release, the panel is started on databases and backups that v1.0.0 to v2.6.0 really wrote, and must give back every customer, key, link and setting unchanged; an installed earlier release is updated by the installer, by the panel's own button and by `w-ui update`, with a customer connected throughout. ### Updating - **Update every node by hand, once.** A node on v2.0.0 to v2.6.0 refuses the panel's request to update itself too, so the panel's **Update** button cannot reach it. On each node server run: ```bash w-ui update ``` From v2.6.1 on, nodes update from the panel again. The managing panel itself updates as usual; the order does not matter. - **Extra `installer` tokens.** Each earlier update by the installer left one extra `installer` token on the panel. They are left in place -- they are yours -- but nothing uses them: keep the one your automation reads from `install-result.env`, and revoke the rest in **Settings → Security → API Token**, where each shows its prefix and when it was last used. ## [2.6.0] — 2026-09-30 ### Added - **Automatic backup to Telegram, on a time of its own.** Settings → Telegram → Backup: switch it on and choose every day at a time, every week on a day at a time, every few hours, or a crontab line. At that time the bot sends the database backup to the admin chat as a file, captioned with the panel's version, the server's name, when it was taken and its size. Times are in the panel's time zone. ### Fixed - **The automatic backup no longer depends on the report.** It went only with the periodic report, so it came when the report did, and a report that could not be built took the backup with it. - **"Every N hours" survives restarts.** The schedule counted from when the panel started, so a panel restarted (or updated) more often than the interval never sent a backup. Where it counts from is now kept in the database. A backup that fell due while the panel was down is sent once, as soon as it is back; switching it on or changing the time does not send one at once. - **A failed backup is said once, not every minute**, and an archive too large for Telegram (past 50 MB) is no longer left open on the server. - **How many backups to keep applies with scheduled backups off.** The archives the bot sends are kept on the server too, and were pruned by the number the panel started with instead of the one on the settings page. - **A customer named with an underscore no longer stops the report.** The report is sent as Markdown, and Telegram refused the whole message. - A backup time more often than every 10 minutes, or one that never comes round (the 30th of February), is refused on save. Panels that had *Database Backup* switched on keep getting it at the time their report went. ## [2.5.2] — 2026-09-30 ### Fixed - **Speed shows for every customer moving traffic, whatever the collection interval.** A customer's speed was averaged over a fixed six seconds, but how often traffic is read is a setting (the Engine page): read every ten seconds or more, the window was empty most of the time, and only customers asked about just after a collection had a speed -- some connected customers showed one, some did not. The window is now never less than three collections. A node's customers are held to the node's own report interval in the same way. - **A speed no longer stays on screen after the customer stops.** The list refreshes its rows by copying the fields that arrive, and a customer with no speed was sent without one, so the last figure stayed; the speed is now always sent, null while there is none. A customer who is connected but moving nothing -- a phone with its screen off, holding the tunnel open -- counts as online and shows `—` for speed, as the classic panel does. ## [2.5.1] — 2026-09-30 ### Fixed - **The Speed column shows each customer's speed.** It read "—" for customers moving traffic: the speed was worked out in the page from two readings of the stored total, inside the render, so every re-render took a fresh reading and saw no change -- and the stored total moves in steps a flush apart in any case. The server now measures it from the kernel's own counters every tick, averaged over the last few seconds, and the column shows it each way, as the classic panel does: `↑ 150 KB/s / ↓ 2.40 MB/s` in blue, `—` in grey while the customer moves nothing. On a phone it shows on the card only while they are. A customer on another node shows their node's last report, every 20 seconds. ## [2.5.0] — 2026-09-30 ### Added - **Time and traffic for many customers at once.** *more -> Time* and *more -> Traffic* add to, or take back from, every customer selected: months, days and hours (a month is 30 days) or TB, GB and MB (in 1024s, as the panel shows sizes). Before anything is applied the dialog shows what will happen, worked out by the panel for exactly this selection: how many change, come back on, stop, and are left alone and why. The rules: customers with no end date or no traffic limit are left as they are; time moves each customer's own end date -- one that ended five days ago and is given two is still ended, one that ended a day ago runs one more day; traffic moves the allowance and keeps the usage, and brings a customer who had run out back on; customers switched off get it and stay off; plans that start on first connection and have not started are left alone unless asked for, and then grow to the hour and still start on first connection; taking traffic back stops at what the customer has used. Taking back from five or more asks for the count to be typed. Nothing happens with nothing selected. `POST /api/clients/extend`, with `dryRun` for the preview. - **Select every customer, across the pages.** Ticking the header offers *Select all N customers* -- every customer, or every one the filter matches (a group, a status, a search) -- and every bulk action then reaches all of them. Changing the filter clears it. `GET /api/clients/ids`. ### Changed - *Adjust* sets a traffic limit and the renewal; adding time moved to *Time*, which leaves customers with no end date unlimited and plans not started waiting -- the old field gave both a date. - The toolbar's *more* button has a name on phones, where its text is hidden, for screen readers. ## [2.4.1] — 2026-09-29 ### Fixed - **Another program wiping the firewall no longer switches enforcement off in silence.** The panel skipped rewriting its rules when nothing had changed, assuming they were still there. A firewall reload that begins with `flush ruleset` -- Debian's default `/etc/nftables.conf` does, so a plain `systemctl restart nftables` was enough -- or another VPN resetting the routing rules took them away: every limit, every switched-off customer and every route out of the tunnels stopped working while the panel went on as if nothing had happened. It now checks its own tables and routing rules before skipping, and puts them back within two seconds, saying so in the log. Checked on every distribution in CI by flushing the whole ruleset under a running panel. - **A tunnel is refused a range already on the server.** A subnet that overlaps Docker's bridge, another VPN's interface or the server's own network was accepted and took that traffic into the tunnel; the refusal now names the device. - **Cleaning up routing rules touches only the panel's own.** A rule was removed -- and its table flushed -- when its mark was in the panel's range, whatever table it pointed at; now both have to be the panel's. - **PostgreSQL on a server that already uses it.** On RHEL-family systems the installer rewrote the loopback lines of `pg_hba.conf` for every database to scram-sha-256, which locks out any other project whose role keeps an md5 password. It now adds two lines for its own database and role, first, and changes nothing else. - **acme.sh's default authority is no longer changed.** The installer and the menu set Let's Encrypt as the default for every certificate acme.sh keeps on the server, moving other projects' renewals; Let's Encrypt is now named on the panel's own requests only. - **Deletes that an empty value could widen are guarded.** Clean-ups after a failed certificate request, and the uninstall, would have removed the whole acme.sh directory -- other projects' certificates with it -- or a whole system directory had a name come back empty; they now stop instead. The three shell scripts have no ShellCheck warnings left. - **The panel opens faster.** Every page was one 824 kB script loaded before anything showed; pages are now fetched when first opened, and the first load is 202 kB. A tab left open across an update, asking for a page the new build does not have, gets a 404 -- it used to get the app shell with a 200 -- and loads the page afresh, once. The build warning about the bundle's size is gone. - The code has no warnings from `go vet` or staticcheck on Linux or on Windows, where a development copy runs; the parsers staticcheck saw as unused there have tests. ### Added - The docs have a section on sharing a server with other projects: what the panel touches, what it leaves alone, and how it recovers from what others do. ## [2.4.0] — 2026-09-29 ### Added - **An automatic install.** The installer's first question is now how to install: *1) Manual* -- every question, exactly as before and in the same order -- or *2) Automatic*, the default. The automatic install asks three things: the database, a domain (checked at once against this server's address, and blank if there is none), and, without a domain, whether to get a free certificate for the server's IP address. Everything else is chosen the way pressing enter through the manual questions would choose it -- a random free port, a random URL path, a generated administrator and password, a free subscription port, OpenVPN and AmneziaWG -- and the closing summary prints the address, the username, the password, the ports and the path. Nothing after the three questions asks anything: a certificate that cannot be issued is skipped, with port 80 left alone when another project on the server holds it, and can be had later from the menu. `--auto` and `--manual` choose without the question. ## [2.3.6] — 2026-09-29 ### Changed - **The customer list opens oldest first**, in the order customers were added, so a customer's place in it does not move each time another is added. Newest first and the other orders are a choice away as before. ## [2.3.5] — 2026-09-29 ### Fixed - **The security warnings no longer repeat themselves.** The account checks ran over every account and each added the same unnamed line: an owner with resellers saw *Two-factor authentication is off* once per account, and a reseller who had not signed in yet was reported as *The generated password has never been changed* -- a password the installer never generated. They are now about the owner's account, each said once. Panel administrators without a second factor are named together in one line; resellers are not warned about, their second factor being theirs to turn on. ## [2.3.4] — 2026-09-29 ### Fixed - **A restore could lock you out of the panel.** The panel's own port, URL path, domain and certificate files are kept in the database and laid over the service's settings at every start, and a restore brought the archive's back. An archive from another server -- or from this one before its port or path was changed -- put the panel on a port the firewall had never opened, under a path nobody had, or, when the certificate path did not exist on this machine, stopped it starting at all: the service then restarted it into the same failure forever. How this panel is reached -- port, path, domain, certificates, trusted proxies and the subscription service's own listener -- is now always this server's, whatever the archive says; the customers, keys and accounts come from the archive. Reproduced on a real server layout with 2.3.3 (the panel did not come back) and checked with this one (it did). - **A failed database snapshot no longer makes a backup that restores old data.** When SQLite's consistent copy could not be taken, the live file was archived instead -- one that can lack the changes still in its write-ahead log -- and a restore takes that file over the portable dump. The dump now carries the database in that case, and a backup with no copy of the database at all is not written: the failure is reported, including in the Telegram report, instead of a file that a restore would refuse. - **Two backups in the same second no longer overwrite each other.** A restore's safety copy taken in the same second as the backup being restored replaced it with the state being thrown away. - **Backups leave out an update waiting for the update helper**, which, restored on another machine, would have set its helper off. - **Settings -> Backups restores and Restart panel follow the panel** through its restart and reload once it is back, instead of after a guessed six seconds -- which landed on a closed port, or on the old process just before it ended. - **Backups too large for Telegram** (over 50 MB) are not sent into a bare 413 any more: the chat is told the size and to download it from the panel, from both the report and the bot's backup button. - **The guide to moving a panel to another server** said to unpack the archive with `tar -C /`, which puts the database where the panel never looks and skips every check a restore makes; it now uses the restore, and no longer asks for the same port and path on the new server. - The restore confirmation says what comes from the archive -- including the accounts you sign in with afterwards -- and what stays this server's. - The API reference's example for creating a WireGuard interface carried a transport, which WireGuard refuses. ## [2.3.3] — 2026-09-29 ### Fixed - The continuous-integration checks pass again: a declaration in the update download was written the way staticcheck asks, which had stopped the run before the race-detector tests, the PostgreSQL tests, the vulnerability check and the clean installs. All of them pass, the new update helper included on every distribution the installer supports. Nothing changes in what the panel does. ## [2.3.2] — 2026-09-29 ### Fixed - **Updating from the panel works on a real server.** Pressing *Install* answered *internal error* on every server set up by the installer: the panel runs as an unprivileged user, in a sandbox where its own binary is read-only, so it could not replace itself -- and the reason was hidden behind a generic message. The panel now does the half it can: it downloads the release and checks its signature into its own data directory. A small root helper the installer sets up, `wui-update.path`, sees the request and runs the installed binary as root with the new `wui apply-update`, which checks everything again with that binary's own key, refuses anything that is not a newer signed release, puts it in place and restarts the panel. Root only ever runs a build this project signed, and nothing in the panel's directory is trusted beyond its bytes: every file operation stays inside it and follows no link out of it. Rehearsed end to end as a real server runs it -- the panel as its own user, the binary root's -- from the button to the page reloading on the new version. - **A server installed before this release has no helper yet**, and the update dialog now says so, with the one command to run as root (the update script), instead of a button that fails. After that, updates install from the panel. ## [2.3.1] — 2026-09-29 ### Fixed - **Updating from the panel no longer reports a failure that did not happen.** The release is tens of megabytes, and a server whose way to GitHub is slow took longer to fetch it than the panel lets one request run: the update installed, and the page said it had failed. The install now runs in the background and the dialog follows it -- a bar while the release downloads, then the signature check, the install and the restart -- and the page reloads by itself once the panel is back on the new version. Closing the dialog or reloading the page does not stop it; the tag reads *Updating…* until it is done. A second install meanwhile is refused, an interrupted download is named as one, and a panel that does not come back within three minutes is said so, with the command to look. Tested end to end on Linux: a download of a minute and a half, a reload in the middle of it, the restart, and the page landing on the new version still signed in. - **Backup archives are no longer cut off.** An upload was given thirty seconds to arrive and a download a minute to leave; a large archive over a slow link now has thirty minutes. ### Changed - **The overview bar is laid out as the classic panel's**: at the start, a pill with the tunnels' state and this panel's version, and the *Update v…* tag right beside it; at the end, the buttons. - **Backup & Restore works as the classic panel's**: two lines, one button each. *Download backup* takes a fresh archive and saves it to your device in one click -- it used to take one on the server and offer the newest archive as a second step, which could be an older automatic one. *Choose a file* shows the archive's name and size, what will happen, and the one option that matters when moving servers, before anything changes; the upload shows its progress, and the page reloads once the panel is back with the restored data instead of after a guessed seven seconds. A file that is not a backup is refused with the reason, and nothing changes. - Asking a node to update now returns as soon as the node has started, and says so; older nodes still answer as before. ## [2.3.0] — 2026-09-28 ### Added - **"Update v…" on the overview.** While a newer release is published, an amber tag beside the panel's version names it; it opens the update dialog, with the release notes and the button that installs it -- fetched by the panel, checked against the signing key built into it, then a restart. The dot on the version alone was easy to miss. ### Changed - The release list is asked at most every half hour, and after a failure at most every five minutes, instead of on every visit to the overview: GitHub allows sixty questions an hour, and a server that cannot reach it no longer waits on it each time. Opening the update dialog always asks afresh. ### Fixed - Versions are ordered by number: 2.10.0 comes after 2.9.0, a pre-release is behind its release, and a panel ahead of the newest release is no longer offered that release as an update. ## [2.2.2] — 2026-09-28 ### Fixed - **The clients pager is Ant's again.** It sits at the right end under the table and reads left to right in every language: the total, the arrows, the pages and the page size. The page-size box used to stretch across the whole row and push the pager to the other side. Only the current page has a border; the other pages and the arrows are bare until hovered. - "25 / page" keeps the number first in Persian, where "۲۵ / صفحه" used to turn round, and page numbers use the same digits as the total. ## [2.2.1] — 2026-09-28 ### Changed - **A paused reseller's customers read "Paused"**, a short tag that fits the column, and their switch shows off and cannot be flipped, since flipping it could not bring them back. The tag's tooltip says why, and whether the customer's own switch is on -- that is, whether they come back with the reseller. Nothing is written to the customer: their own setting is kept. ## [2.2.0] — 2026-09-28 ### Added - **Resellers, many at once.** A box on every reseller, and one in the header for everything the filters show; the toolbar then acts on the selection: switch on, switch off, extend the term, start the allowance again, set the traffic allowance, set the customer limit, add or remove servers, and delete -- asking, as for one reseller, what becomes of their customers. Switching off asks first and says how many customers go off. Each reseller goes through the same checks as when changed alone; one that cannot be changed does not stop the rest, and the answer names it with the reason. `POST /api/admins/bulk`. - Extending in bulk adds the days to a running term, restarts an ended one from today, adds them to a term on hold, and leaves a reseller with no end date without one. - **Customers: reset traffic and new keys for a selection.** Both ask first; new keys for five or more asks you to type how many. New keys replace every file and the subscription link of each selected customer, one at a time, and name any that failed. ## [2.1.2] — 2026-09-28 ### Fixed - A reseller, a server or a node created switched off came up switched on. The schema defaults "enabled" to true, and an insert took a false for nothing given: a reseller the owner meant to prepare before selling could sell at once, and a server meant to be configured first came up carrying traffic. - A reseller's pause is decided in one place and told in one set of words. The three reasons were spelled out in three places, one of them in different words, beside two helpers nothing called. ### Documentation - The resellers page states the rule exactly: a customer is served when both their own switch and their reseller's standing allow it, with a table of every case, the order the three reasons are checked in, what happens the moment a reseller is switched off, and what they can still do. - Every message a reseller or the Resellers page can show is in the error reference, in English and Persian. - The Telegram channel, [@wuipanel](https://t.me/wuipanel), is linked from the README and from every page of the documentation. ## [2.1.1] — 2026-09-28 ### Fixed - Switching a reseller off or on, or changing their date or allowance, now takes their customers off the tunnels -- or puts them back -- the moment it is saved, rather than on the panel's next pass. Their customers' own switches are still never written: a customer switched off before the pause, or during it, stays off when the reseller comes back. ## [2.1.0] — 2026-09-28 ### Added - The Resellers page has the Clients page's search, filters and sort: search by username, note or group, filter by status and server, and sort by name, customers, traffic used, time left or newest. The summary tiles filter too, and the view is kept in the address. - A customer whose reseller is paused reads *Reseller paused* on the Clients page (*Account paused* to the reseller), and their own subscription page says they are off. They read "Active" while being unable to connect. ### Fixed - A tick that could not read which resellers are paused treated every one as in good standing, putting a switched-off reseller's customers back on the tunnels until the next good read. It now keeps the last answer. - On a phone, a time-left tag in Persian ran its number and its words in the wrong order, and a long one pushed out of its card. - On a switched-off reseller's row the switch and the actions faded with the rest, and read as buttons that could not be pressed. - Sixteen Persian labels were still in English, among them "On hold" and the client dialog's tabs. ## [2.0.0] — 2026-09-28 The panel can be sold on. A second kind of operator signs in beside the owner — a reseller who manages only their own customers, within a ceiling the owner sets — and the whole panel has had a security and accessibility pass. Upgrading keeps everything as it was: the account that administers the panel today becomes its owner, and the database takes its new tables on first start. ### Added - **Resellers.** A reseller signs in with their own username and password and manages only the customers they create, on the servers the owner gives them, within a ceiling of customers, traffic and time. They never reach the machine: no interfaces, nodes, routing, settings or backups, and no sight of anyone else's customers. The separation is enforced by the database on every query, not by the menu. - A **panel administrator** role: every customer on the panel, no ceiling, and nothing about the server itself. - **Resellers** is a page in the main menu, laid out as the customer list is: a summary across the top, and a row per reseller with customers, traffic and time left in the customer list's colours. On a phone each is a card. - The reseller dialog is the customer dialog's shape and fits one screen. The username and password are drawn for you, side by side with a button to draw again, and one copy button puts the address, username and password on the clipboard together. Presets fill a typical reseller plan. - A reseller's term is picked from a calendar — Gregorian or Jalali, as the panel is set — with +1 month, +3 months and +1 year counted from the date already set, or put **on hold**: a number of days that starts at their first sign-in. - The owner files each reseller's customers under a group of their choosing, which the reseller never sees; changing it moves the customers already there. - A reseller's own customer list opens with **Your account**: customers against their limit, traffic left and time left, and the reason when it is paused. - Switching a reseller off stops every customer they hold and signs them out; a term that ends or traffic that runs out stops them the same way, and they may still sign in, read-only, to see why. Nothing is written to their customers, so switching them back on restores exactly what was running. - Removing a reseller asks whether to keep their customers under the owner or delete them too. ### Security - Changing your own password and turning off the second factor are throttled as sign-in is. With a borrowed session either could be used to guess the password, or a six-digit code, as fast as requests could be sent. - A new username chosen on the security page follows the same rules as a new operator's, case-insensitively, so nobody can become "Admin" beside "admin". - A subscription link chosen by hand is unique across the whole panel, and asking for taken ones over and over is stopped, so it cannot be used to find out which links exist. - Customer and device names and notes are held to their columns and to one line. A longer one was a database error on PostgreSQL; a line break in one reached the log, the export and Telegram messages. - After signing in, the panel only follows a `next` address on itself. - A device is removed, and its configuration read, only by an operator who can see its customer. ### Accessibility - Secondary text meets 4.5:1 in both themes; it was 3.6:1 on every page. Red text uses a lighter red on dark surfaces, and filled danger buttons a darker one, so white on them reads. - Gold server chips were unreadable in the light theme (1.9:1), and the upload and download figures failed in one theme or the other. - Tabbing through a table of row actions showed no focus at all: the buttons' own shadow cancelled the focus ring. - Every page names itself in the browser tab and carries a heading for a screen reader. - The × buttons on alerts, search boxes and chips take a tap 24px across without looking any larger, and chips and small links are 24px tall. - The settings selects are named, the suggestions under a free-text field close when focus leaves by keyboard, and its clear button says what it does. ### Fixed - An OpenVPN server carried across a panel upgrade no longer refuses every customer. It kept the mount namespace of the panel that started it, so once the unit's writable paths changed it saw its own directory as read-only and could not write the file OpenVPN makes for each login — every attempt came back as "user authentication failed". The panel now checks that a server it adopts can still write there, and restarts it when it cannot. - A tunnel whose engine died is brought back up. Its driver was kept as long as the interface's settings were unchanged, so the panel went on shaping and billing an interface nobody could reach; an open tunnel that reports itself unhealthy is reopened on the next tick. - The dropdown arrow on free-text pickers had no icon. ## [1.1.0] — 2026-09-25 ### Added - **New keys.** A customer can be issued fresh credentials: a new WireGuard key pair and preshared key, and a new OpenVPN password, so a file that leaked stops working within seconds. The key on their row does everything at once — every file and the subscription link together. The Credentials tab replaces one user's file of one kind on its own, leaving their other files, the other users and the link alone. `POST /api/clients/{id}/rotate-keys` takes `user`, `protocol`, `accountIds` and `subToken`. - **Usage by user and tunnel** on the subscription page: a row per user, a column per tunnel, a total at each edge, for people sharing a plan and its cost. - A customer can be in **several groups**, picked or typed on the client dialog and shown as chips on the list; the groups page adds and removes members one group at a time. - **Add Bulk**, the classic dialog: a quantity, a naming method — random, prefix and number, or prefix, random and postfix — with a live example, and the plan they all share. - Persian is set in **Vazirmatn**, bundled with the panel and carried in the binary for the subscription page. ### Changed - **The subscription service on a port of its own is now the only port that answers.** The panel's port stops serving subscriptions, as the classic panel's does, and the link names the service's port rather than the one the panel was reached on. Links that named the panel's port must be reissued to customers. - **Listen Domain**, when set, is the only host the subscription service answers to; any other name gets a 404. - Each tunnel is charged with what crossed it: every file keeps its own counters and the interfaces page sums a tunnel's files, so a customer on WireGuard and OpenVPN no longer puts one allowance on both. - The QR dialog is a tab per tunnel over one code rather than a panel per file, so a plan for seventeen users is still one screen. - Every dialog is at most one screen tall and scrolls inside, with no scrollbar drawn over its controls; floating menus stay on the screen. - Fail2ban is set up in seconds rather than minutes at install. - Byte figures carry two decimals from a gigabyte up. ### Fixed - The per-tunnel figures add up to the customer's total. They were summed from the tunnels' own counters, which include WireGuard's encryption overhead and ran a few percent above what the kernel charges. - An OpenVPN password that changes now ends the session it was logged in with, rather than leaving the old one good until the customer reconnects. - Behind a relay, a port is a flow and not a device, so one phone reconnecting is no longer reported as sharing. - On the subscription page the language menu drops down again and the live badge shows its dot and word instead of a grey block. - The group picker offers a group made empty on the groups page. - The actions column fits the key that was added to it. ## [1.0.0] — 2026-09-14 The first public release. ### Added - WireGuard, AmneziaWG and OpenVPN interfaces on one server, with kernel quota enforcement, expiry, device limits and per-customer rate limits. - Customers, groups, hosts and host groups, subscription links with 21 page templates, QR codes, config downloads for every client app. Quotas in MB, GB or TB and validity in hours, days or months. - SQLite or PostgreSQL, chosen at install; backups carry a portable dump beside the SQLite snapshot, so an archive from either engine or an older panel restores into any install. `wui backup create|list|restore`. - The installer picks a random port for the subscription service, turns it on with the panel's certificate, and prints it with the rest. - Outbounds with policy routing, balancers, fail-closed default outbound, DNS proxy with pins and upstream routing, engine settings. - Telegram notifications and an interactive Telegram bot for the operator and for customers. - Backups on a schedule, sharing detection, an API with tokens and docs. - An installer that gets a Let's Encrypt certificate for a domain or for the server's own address, renews it unattended, and a `w-ui` management menu. - English and Persian, with dark, ultra-dark and light themes. [Unreleased]: https://github.com/AbolfazlTafakori/w-ui/compare/v3.1.2...HEAD [3.1.2]: https://github.com/AbolfazlTafakori/w-ui/compare/v3.1.1...v3.1.2 [3.1.1]: https://github.com/AbolfazlTafakori/w-ui/compare/v3.1.0...v3.1.1 [3.1.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v3.0.0...v3.1.0 [3.0.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.7...v3.0.0 [2.6.7]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.6...v2.6.7 [2.6.6]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.5...v2.6.6 [2.6.5]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.4...v2.6.5 [2.6.4]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.3...v2.6.4 [2.6.3]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.2...v2.6.3 [2.6.2]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.1...v2.6.2 [2.6.1]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.6.0...v2.6.1 [2.6.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.5.2...v2.6.0 [2.5.2]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.5.1...v2.5.2 [2.5.1]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.5.0...v2.5.1 [2.5.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.4.1...v2.5.0 [2.4.1]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.4.0...v2.4.1 [2.4.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.3.6...v2.4.0 [2.3.6]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.3.5...v2.3.6 [2.3.5]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.3.4...v2.3.5 [2.3.4]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.3.3...v2.3.4 [2.3.3]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.3.2...v2.3.3 [2.3.2]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.3.1...v2.3.2 [2.3.1]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.3.0...v2.3.1 [2.3.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.2.2...v2.3.0 [2.2.2]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.2.1...v2.2.2 [2.2.1]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.2.0...v2.2.1 [2.2.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.1.2...v2.2.0 [2.1.2]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.1.1...v2.1.2 [2.1.1]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.1.0...v2.1.1 [2.1.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v1.1.0...v2.0.0 [1.1.0]: https://github.com/AbolfazlTafakori/w-ui/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/AbolfazlTafakori/w-ui/releases/tag/v1.0.0