mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-08 13:46:08 +00:00
docs: vendor the documentation site into the monorepo
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
name: Docs CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/docs-ci.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/docs-ci.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 11
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: docs/pnpm-lock.yaml
|
||||
|
||||
- run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Typecheck
|
||||
run: pnpm typecheck
|
||||
|
||||
- name: Lint
|
||||
run: pnpm lint
|
||||
|
||||
- name: Test
|
||||
run: pnpm test
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
@@ -0,0 +1,67 @@
|
||||
name: Docs Deploy (GitHub Pages)
|
||||
|
||||
# Static-export deploy of docs/ to GitHub Pages. Pages must be enabled in repo
|
||||
# settings (Source: GitHub Actions) and the docs.sanaei.dev custom domain
|
||||
# attached to this repository. The site URL defaults to the production domain in
|
||||
# docs/lib/shared.ts, so NEXT_PUBLIC_SITE_URL is optional.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'docs/**'
|
||||
- '.github/workflows/docs-deploy.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
concurrency:
|
||||
group: pages
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: docs
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 11
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
cache: pnpm
|
||||
cache-dependency-path: docs/pnpm-lock.yaml
|
||||
- run: pnpm install --frozen-lockfile
|
||||
- name: Build (static export)
|
||||
env:
|
||||
DEPLOY_TARGET: static
|
||||
NEXT_PUBLIC_SITE_URL: ${{ vars.NEXT_PUBLIC_SITE_URL }}
|
||||
run: pnpm build
|
||||
- name: Mirror default locale (en) to the site root
|
||||
# hideLocale makes most English links unprefixed (/, /docs/...), but the
|
||||
# language switcher still targets /en/... . The export emits pages only
|
||||
# under /en/, and there is no i18n middleware on a static host — so copy
|
||||
# the English build to the root (data files included) while KEEPING /en/
|
||||
# in place. That way both /docs/... and /en/docs/... resolve. Other
|
||||
# locales stay under /fa, /ru, /zh.
|
||||
run: cp -a out/en/. out/
|
||||
- uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
path: docs/out
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- id: deployment
|
||||
uses: actions/deploy-pages@v5
|
||||
@@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
@@ -0,0 +1,31 @@
|
||||
# deps
|
||||
/node_modules
|
||||
|
||||
# generated content
|
||||
.source
|
||||
|
||||
# test & build
|
||||
/coverage
|
||||
/.next/
|
||||
/out/
|
||||
/build
|
||||
*.tsbuildinfo
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
/.pnp
|
||||
.pnp.js
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# others
|
||||
.env*.local
|
||||
.vercel
|
||||
next-env.d.ts
|
||||
# npm (this project uses pnpm)
|
||||
package-lock.json
|
||||
|
||||
# claude code (local settings/plans; keep shared project instructions)
|
||||
/.claude/*
|
||||
@@ -0,0 +1,10 @@
|
||||
node_modules
|
||||
.next
|
||||
.source
|
||||
out
|
||||
pnpm-lock.yaml
|
||||
public/openapi.json
|
||||
# Don't let Prettier reflow MDX prose — it merges headings into paragraphs and
|
||||
# collapses lists inside JSX components (Steps/Callout). Author MDX by hand.
|
||||
content/**/*.mdx
|
||||
content/docs/**/reference/api
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
# Contributing to 3x-ui-docs
|
||||
|
||||
Thanks for helping improve the 3x-ui documentation and product site!
|
||||
|
||||
## Prerequisites
|
||||
|
||||
This project uses **[pnpm](https://pnpm.io)** (not npm — `package-lock.json` is
|
||||
gitignored). Install dependencies and start the dev server:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev # http://localhost:3000
|
||||
```
|
||||
|
||||
## Scripts
|
||||
|
||||
| Script | Description |
|
||||
| ---------------- | ----------------------------------------------------- |
|
||||
| `pnpm dev` | Start the dev server |
|
||||
| `pnpm build` | Production build |
|
||||
| `pnpm start` | Serve the production build |
|
||||
| `pnpm typecheck` | Generate MDX/route types and run `tsc --noEmit` |
|
||||
| `pnpm lint` | ESLint (flat config) |
|
||||
| `pnpm format` | Format with Prettier |
|
||||
| `pnpm test` | Run unit tests (Vitest) for `lib/xray/*` pure logic |
|
||||
| `pnpm gen:api` | Generate the API reference from `public/openapi.json` |
|
||||
|
||||
Before opening a pull request, please run `pnpm typecheck`, `pnpm lint`, and
|
||||
`pnpm test` — these are the same checks that CI runs on every PR.
|
||||
|
||||
## License
|
||||
|
||||
By contributing, you agree that your contributions will be licensed under the
|
||||
project's [GPL-3.0](./LICENSE) license.
|
||||
+674
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
<p align="center">
|
||||
<a href="https://docs.sanaei.dev">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="public/logo-dark.png" />
|
||||
<img src="public/logo-light.png" alt="3x-ui" width="180" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h1 align="center">3x-ui Documentation</h1>
|
||||
|
||||
<p align="center">
|
||||
The official documentation and product site for
|
||||
<a href="https://github.com/MHSanaei/3x-ui"><b>3x-ui</b></a> —
|
||||
an advanced web panel for managing Xray-core servers.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.sanaei.dev"><img src="https://img.shields.io/badge/docs-docs.sanaei.dev-22d3ee?style=flat-square" alt="Live site" /></a>
|
||||
<a href="https://github.com/MHSanaei/3x-ui/actions/workflows/docs-ci.yml"><img src="https://github.com/MHSanaei/3x-ui/actions/workflows/docs-ci.yml/badge.svg" alt="CI" /></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/license-GPL--3.0-blue?style=flat-square" alt="License: GPL-3.0" /></a>
|
||||
<img src="https://img.shields.io/badge/Next.js-16-black?style=flat-square&logo=next.js" alt="Next.js 16" />
|
||||
<img src="https://img.shields.io/badge/Fumadocs-16-0ea5e9?style=flat-square" alt="Fumadocs 16" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://docs.sanaei.dev"><b>Read the docs →</b></a>
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
This directory (`docs/` in the [3x-ui](https://github.com/MHSanaei/3x-ui) monorepo) contains
|
||||
the source for [docs.sanaei.dev](https://docs.sanaei.dev) — a static-first documentation and
|
||||
marketing site built with [Fumadocs](https://fumadocs.dev) on Next.js. It has **no backend,
|
||||
no database, and no auth**: every page is prerendered and every tool runs entirely in the
|
||||
browser.
|
||||
|
||||
## What's inside
|
||||
|
||||
The documentation walks you through 3x-ui from first install to day-to-day operation:
|
||||
|
||||
- **Getting Started** — installation, first login, and updating or uninstalling the panel.
|
||||
- **Configuration** — the panel, inbounds, REALITY, transports, clients, subscriptions, and share links.
|
||||
- **Operations** — reverse proxy, multi-node setups, outbounds & routing, backup/restore, the Telegram bot, and security.
|
||||
- **Reference** — environment variables, the database, ports & firewall, and the HTTP API.
|
||||
- **Help** — troubleshooting, FAQ, migration, and how to contribute.
|
||||
|
||||
## Interactive tools
|
||||
|
||||
The site ships with in-browser helpers that generate configuration for you — **no data
|
||||
ever leaves your browser**:
|
||||
|
||||
| Tool | What it does |
|
||||
| ---------------------------- | ------------------------------------------------------- |
|
||||
| **REALITY Config Generator** | Build a valid REALITY inbound configuration. |
|
||||
| **Share Link Inspector** | Decode and inspect `vless://` / `vmess://` share links. |
|
||||
| **Install Command Builder** | Assemble the right install command for your setup. |
|
||||
| **Reverse Proxy Generator** | Generate reverse-proxy configs (Nginx / Caddy). |
|
||||
| **Protocol Wizard** | Pick and configure the right protocol for your needs. |
|
||||
| **Firewall Rules Generator** | Produce firewall rules for your ports. |
|
||||
|
||||
## Tech stack
|
||||
|
||||
| Layer | Technology |
|
||||
| ---------- | ---------------------------------------------------------- |
|
||||
| Framework | [Next.js 16](https://nextjs.org) (App Router) · React 19 |
|
||||
| Docs | [Fumadocs](https://fumadocs.dev) (`-ui` / `-core` / `-mdx`) |
|
||||
| Styling | [Tailwind CSS v4](https://tailwindcss.com) |
|
||||
| Search | [Orama](https://orama.com) static index |
|
||||
| Language | TypeScript (strict) |
|
||||
| Tests | [Vitest](https://vitest.dev) for the pure `lib/xray` logic |
|
||||
| Tooling | pnpm · ESLint 9 · Prettier |
|
||||
|
||||
## Quick start
|
||||
|
||||
This project uses **[pnpm](https://pnpm.io)** (npm lockfiles are gitignored). Run everything
|
||||
from the `docs/` directory:
|
||||
|
||||
```bash
|
||||
cd docs
|
||||
pnpm install
|
||||
pnpm dev # http://localhost:3000
|
||||
```
|
||||
|
||||
Useful scripts:
|
||||
|
||||
| Script | Description |
|
||||
| ---------------- | -------------------------------------------- |
|
||||
| `pnpm dev` | Start the dev server |
|
||||
| `pnpm build` | Production build (also typechecks) |
|
||||
| `pnpm typecheck` | Generate MDX/route types and `tsc --noEmit` |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
| `pnpm test` | Run unit tests (Vitest) |
|
||||
|
||||
See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the full list and project conventions.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
app/ # Next.js App Router — layouts, home, docs, OG images, search, llms.txt
|
||||
components/ # React components — interactive tools, home sections, MDX bindings
|
||||
content/docs/ # MDX documentation, one folder per locale (en · fa · ru · zh)
|
||||
lib/ # source config, i18n, GitHub stats, and the unit-tested lib/xray logic
|
||||
public/ # static assets — logos, favicon, openapi.json, CNAME
|
||||
scripts/ # build-time scripts (API reference generation)
|
||||
source.config.ts # Fumadocs MDX schema & collection config
|
||||
next.config.mjs # Next.js config (static-export gating)
|
||||
proxy.ts # i18n middleware
|
||||
```
|
||||
|
||||
## Internationalization
|
||||
|
||||
Documentation is authored in **English**. Persian (`fa`, RTL), Russian (`ru`), and
|
||||
Chinese (`zh`) locales are wired up; untranslated pages fall back to English so they
|
||||
never 404. English URLs are unprefixed; other locales live under `/fa`, `/ru`, `/zh`.
|
||||
|
||||
## Deployment
|
||||
|
||||
The site builds for two targets:
|
||||
|
||||
- **Vercel / Node** — `pnpm build` (static search index + prerendered OG images).
|
||||
- **GitHub Pages (static export)** — `DEPLOY_TARGET=static pnpm build` → `out/`.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Setup, scripts, and project conventions live in
|
||||
[`CONTRIBUTING.md`](./CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
Licensed under [GPL-3.0](./LICENSE).
|
||||
@@ -0,0 +1,28 @@
|
||||
import { HomeLayout } from 'fumadocs-ui/layouts/home';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
import { HomeLanguageSwitcher } from '@/components/home/language-switcher';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const options = baseOptions(lang);
|
||||
return (
|
||||
<HomeLayout
|
||||
{...options}
|
||||
// Disable fumadocs' built-in popover language switcher here: nested in the
|
||||
// home navbar's Radix NavigationMenu its item clicks don't fire. We inject
|
||||
// an anchor-based one instead (see HomeLanguageSwitcher). Docs keep the
|
||||
// built-in switcher (its sidebar isn't a NavigationMenu, so it works).
|
||||
i18n={false}
|
||||
links={[
|
||||
...(options.links ?? []),
|
||||
{
|
||||
type: 'custom',
|
||||
secondary: true,
|
||||
children: <HomeLanguageSwitcher current={lang} />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
import Link from 'next/link';
|
||||
import { ArrowRight, BookOpen, Heart } from 'lucide-react';
|
||||
import { GitHubIcon, TelegramIcon } from '@/components/icons';
|
||||
import { Logo } from '@/components/logo';
|
||||
import { Features } from '@/components/home/features';
|
||||
import { GitHubStatsRow } from '@/components/home/github-stats';
|
||||
import { InstallCommand } from '@/components/home/install-command';
|
||||
import { getGitHubStats } from '@/lib/github-stats';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { appName, productRepoUrl, deepWikiUrl, telegramChannelUrl, donateUrl } from '@/lib/shared';
|
||||
import { getSiteMessages, type SiteMessages } from '@/lib/site-i18n';
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
const INSTALL_COMMAND =
|
||||
'bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)';
|
||||
|
||||
export default async function HomePage({ params }: PageProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const prefix = lang === 'en' ? '' : `/${lang}`;
|
||||
const m = getSiteMessages(lang);
|
||||
const stats = await getGitHubStats();
|
||||
|
||||
return (
|
||||
<main className="flex flex-1 flex-col">
|
||||
{/* Hero */}
|
||||
<section className="relative overflow-hidden border-b">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 -top-40 h-80 bg-gradient-to-b from-brand/15 to-transparent blur-3xl"
|
||||
aria-hidden
|
||||
/>
|
||||
<div className="mx-auto flex w-full max-w-5xl flex-col items-center px-4 py-20 text-center sm:py-28">
|
||||
<Logo className="h-20 drop-shadow-sm" />
|
||||
<h1 className="mt-6 text-4xl font-bold tracking-tight sm:text-6xl">
|
||||
<span className="bg-gradient-to-r from-cyan-500 to-sky-600 bg-clip-text text-transparent dark:from-cyan-300 dark:to-sky-400">
|
||||
{appName}
|
||||
</span>
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-lg text-fd-muted-foreground sm:text-xl">{m.tagline}</p>
|
||||
|
||||
<div className="mt-8 flex flex-col items-center gap-3 sm:flex-row">
|
||||
<Link
|
||||
href={`${prefix}/docs`}
|
||||
className="inline-flex items-center gap-2 rounded-xl bg-fd-primary px-5 py-3 font-medium text-fd-primary-foreground transition-opacity hover:opacity-90"
|
||||
>
|
||||
{m.getStarted}
|
||||
<ArrowRight className="size-4 rtl:rotate-180" aria-hidden />
|
||||
</Link>
|
||||
<a
|
||||
href={productRepoUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-2 rounded-xl border px-5 py-3 font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
|
||||
>
|
||||
<GitHubIcon className="size-4" />
|
||||
{m.viewOnGitHub}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<InstallCommand
|
||||
command={INSTALL_COMMAND}
|
||||
copyLabel={m.copyCommand}
|
||||
copiedLabel={m.copied}
|
||||
className="mt-8 w-full max-w-2xl"
|
||||
/>
|
||||
|
||||
{/* Build-time stats as the initial render; refreshed live on the client. */}
|
||||
<GitHubStatsRow
|
||||
initial={stats}
|
||||
labels={{ stars: m.stars, forks: m.forks, latest: m.latest }}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Features heading={m.featuresHeading} subtitle={m.featuresSubtitle} items={m.features} />
|
||||
|
||||
<Footer prefix={prefix} m={m} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function Footer({ prefix, m }: { prefix: string; m: SiteMessages }) {
|
||||
return (
|
||||
<footer className="border-t">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col items-center justify-between gap-4 px-4 py-8 text-sm text-fd-muted-foreground sm:flex-row">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Logo className="h-6" />
|
||||
<span>
|
||||
{appName} — {m.licenseBefore}
|
||||
<a
|
||||
href={`${productRepoUrl}/blob/main/LICENSE`}
|
||||
className="underline hover:text-fd-foreground"
|
||||
>
|
||||
GPL-3.0
|
||||
</a>
|
||||
{m.licenseAfter}
|
||||
</span>
|
||||
</div>
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link href={`${prefix}/docs`} className="hover:text-fd-foreground">
|
||||
{m.docs}
|
||||
</Link>
|
||||
<a
|
||||
href={productRepoUrl}
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<GitHubIcon className="size-4" />
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href={deepWikiUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<BookOpen className="size-4" />
|
||||
DeepWiki
|
||||
</a>
|
||||
<a
|
||||
href={telegramChannelUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<TelegramIcon className="size-4" />
|
||||
Telegram
|
||||
</a>
|
||||
<a
|
||||
href={donateUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="inline-flex items-center gap-1.5 hover:text-fd-foreground"
|
||||
>
|
||||
<Heart className="size-4" />
|
||||
{m.donate}
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { getPageImage, getPageMarkdownUrl, source } from '@/lib/source';
|
||||
import {
|
||||
DocsBody,
|
||||
DocsDescription,
|
||||
DocsPage,
|
||||
DocsTitle,
|
||||
MarkdownCopyButton,
|
||||
ViewOptionsPopover,
|
||||
} from 'fumadocs-ui/layouts/docs/page';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { getMDXComponents } from '@/components/mdx';
|
||||
import { OpenAPIPage as BaseOpenAPIPage } from '@/components/openapi-page';
|
||||
import { openapi } from '@/lib/openapi';
|
||||
import type { Metadata } from 'next';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { createRelativeLink } from 'fumadocs-ui/mdx';
|
||||
import { gitConfig } from '@/lib/shared';
|
||||
|
||||
export default async function Page(props: PageProps<'/[lang]/docs/[[...slug]]'>) {
|
||||
const { lang, slug } = await props.params;
|
||||
const page = source.getPage(slug, lang);
|
||||
if (!page) notFound();
|
||||
|
||||
const MDX = page.data.body;
|
||||
const markdownUrl = getPageMarkdownUrl(page).url;
|
||||
const editUrl = `https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${gitConfig.branch}/${gitConfig.docsDir}/${page.path}`;
|
||||
|
||||
// Generated API reference pages carry `_openapi` metadata. Preload the spec
|
||||
// on the server (highlighting included) so the client OpenAPIPage doesn't have
|
||||
// to load it at render time.
|
||||
const isOpenAPI = Boolean((page.data as { _openapi?: unknown })._openapi);
|
||||
const extraComponents: Record<string, unknown> = {};
|
||||
if (isOpenAPI) {
|
||||
const preloaded = await openapi.preloadOpenAPIPage(page);
|
||||
function PreloadedOpenAPIPage(p: ComponentProps<typeof BaseOpenAPIPage>) {
|
||||
return <BaseOpenAPIPage {...p} {...preloaded} />;
|
||||
}
|
||||
extraComponents.OpenAPIPage = PreloadedOpenAPIPage;
|
||||
}
|
||||
|
||||
return (
|
||||
<DocsPage toc={page.data.toc} full={page.data.full}>
|
||||
<DocsTitle>{page.data.title}</DocsTitle>
|
||||
<DocsDescription className="mb-0">{page.data.description}</DocsDescription>
|
||||
<div className="flex flex-row items-center gap-2 border-b pb-6">
|
||||
<MarkdownCopyButton markdownUrl={markdownUrl} />
|
||||
<ViewOptionsPopover markdownUrl={markdownUrl} githubUrl={editUrl} />
|
||||
</div>
|
||||
<DocsBody>
|
||||
<MDX
|
||||
components={getMDXComponents({
|
||||
// allows linking to other pages with relative file paths
|
||||
a: createRelativeLink(source, page),
|
||||
...extraComponents,
|
||||
})}
|
||||
/>
|
||||
</DocsBody>
|
||||
</DocsPage>
|
||||
);
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return source.generateParams();
|
||||
}
|
||||
|
||||
export async function generateMetadata(
|
||||
props: PageProps<'/[lang]/docs/[[...slug]]'>,
|
||||
): Promise<Metadata> {
|
||||
const { lang, slug } = await props.params;
|
||||
const page = source.getPage(slug, lang);
|
||||
if (!page) notFound();
|
||||
|
||||
return {
|
||||
title: page.data.title,
|
||||
description: page.data.description,
|
||||
openGraph: {
|
||||
images: getPageImage(page).url,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||
import { baseOptions } from '@/lib/layout.shared';
|
||||
|
||||
export default async function Layout({ params, children }: LayoutProps<'/[lang]/docs'>) {
|
||||
const { lang } = await params;
|
||||
return (
|
||||
<DocsLayout tree={source.getPageTree(lang)} {...baseOptions(lang)}>
|
||||
{children}
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import '../global.css';
|
||||
import { RootProvider } from 'fumadocs-ui/provider/next';
|
||||
import { Inter, Vazirmatn } from 'next/font/google';
|
||||
import { i18n, localeDirection } from '@/lib/i18n';
|
||||
import { provider } from '@/lib/i18n-ui';
|
||||
import SearchDialog from '@/components/search-dialog';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'], display: 'swap' });
|
||||
// Persian UI font; covers Arabic + Latin glyphs so mixed content renders well.
|
||||
const vazirmatn = Vazirmatn({ subsets: ['arabic'], display: 'swap' });
|
||||
|
||||
export function generateStaticParams() {
|
||||
return i18n.languages.map((lang) => ({ lang }));
|
||||
}
|
||||
|
||||
export default async function LangLayout({ params, children }: LayoutProps<'/[lang]'>) {
|
||||
const { lang } = await params;
|
||||
const dir = localeDirection(lang);
|
||||
const fontClassName = lang === 'fa' ? vazirmatn.className : inter.className;
|
||||
|
||||
return (
|
||||
<html lang={lang} dir={dir} className={fontClassName} suppressHydrationWarning>
|
||||
<body className="flex min-h-screen flex-col" suppressHydrationWarning>
|
||||
<RootProvider i18n={provider(lang)} search={{ SearchDialog }}>
|
||||
{children}
|
||||
</RootProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { createFromSource } from 'fumadocs-core/search/server';
|
||||
|
||||
// Required for `output: 'export'` — the search index is fully static.
|
||||
export const revalidate = false;
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
// Static search index: works under both SSR/Vercel and static export
|
||||
// (`output: 'export'`). The client loads this prebuilt index and searches
|
||||
// in-browser (see the `type: 'static'` search option in app/[lang]/layout.tsx).
|
||||
// All locales currently hold English (fallback) content, and Orama has no
|
||||
// Persian tokenizer, so map every locale to the English tokenizer. When real
|
||||
// translations land, switch ru -> 'russian', zh -> 'mandarin' (with
|
||||
// @orama/tokenizers), etc. See https://docs.orama.com/open-source/supported-languages
|
||||
export const { staticGET: GET } = createFromSource(source, {
|
||||
localeMap: {
|
||||
en: 'english',
|
||||
fa: 'english',
|
||||
ru: 'english',
|
||||
zh: 'english',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'fumadocs-ui/css/neutral.css';
|
||||
@import 'fumadocs-ui/css/preset.css';
|
||||
|
||||
/* 3x-ui brand: cyan / turquoise accent (matches the panel logo). */
|
||||
:root {
|
||||
--color-fd-primary: hsl(190, 95%, 39%);
|
||||
--color-fd-primary-foreground: hsl(0, 0%, 100%);
|
||||
--color-fd-ring: hsl(190, 95%, 39%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-fd-primary: hsl(187, 90%, 55%);
|
||||
--color-fd-primary-foreground: hsl(190, 80%, 8%);
|
||||
--color-fd-ring: hsl(187, 90%, 55%);
|
||||
}
|
||||
|
||||
/* Expose the brand color as Tailwind utilities (bg-brand, text-brand, ...). */
|
||||
@theme {
|
||||
--color-brand: hsl(190, 95%, 39%);
|
||||
--color-brand-foreground: hsl(0, 0%, 100%);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
/* In RTL the scroll-lock padding must be applied to the logical inline-end. */
|
||||
html > body[data-scroll-locked] {
|
||||
margin-inline-end: 0px !important;
|
||||
--removed-body-scroll-bar-size: 0px !important;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Metadata } from 'next';
|
||||
import type { ReactNode } from 'react';
|
||||
import { appName, appTagline, siteUrl } from '@/lib/shared';
|
||||
|
||||
// Global SEO defaults. The real <html>/<body> live in `app/[lang]/layout.tsx`
|
||||
// so we can set `lang`/`dir` per locale (RTL for fa); this root layout is a
|
||||
// pass-through that only carries site-wide metadata.
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL(siteUrl),
|
||||
title: {
|
||||
default: `${appName} — ${appTagline}`,
|
||||
template: `%s — ${appName}`,
|
||||
},
|
||||
description: appTagline,
|
||||
applicationName: appName,
|
||||
openGraph: {
|
||||
siteName: appName,
|
||||
type: 'website',
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
},
|
||||
icons: {
|
||||
icon: '/favicon.png',
|
||||
apple: '/icon.png',
|
||||
},
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { getLLMText, source } from '@/lib/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET() {
|
||||
const scan = source.getPages().map(getLLMText);
|
||||
const scanned = await Promise.all(scan);
|
||||
|
||||
return new Response(scanned.join('\n\n'));
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { getLLMText, getPageMarkdownUrl, source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug?.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return new Response(await getLLMText(page), {
|
||||
headers: {
|
||||
'Content-Type': 'text/markdown',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageMarkdownUrl(page).segments,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { source } from '@/lib/source';
|
||||
import { llms } from 'fumadocs-core/source';
|
||||
|
||||
export const revalidate = false;
|
||||
|
||||
export function GET() {
|
||||
return new Response(llms(source).index());
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { getPageImage, source } from '@/lib/source';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { ImageResponse } from 'next/og';
|
||||
import { generate as DefaultImage } from 'fumadocs-ui/og';
|
||||
import { appName } from '@/lib/shared';
|
||||
|
||||
export const dynamic = 'force-static';
|
||||
export const revalidate = false;
|
||||
|
||||
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
|
||||
const { slug } = await params;
|
||||
const page = source.getPage(slug.slice(0, -1));
|
||||
if (!page) notFound();
|
||||
|
||||
return new ImageResponse(
|
||||
<DefaultImage title={page.data.title} description={page.data.description} site={appName} />,
|
||||
{
|
||||
width: 1200,
|
||||
height: 630,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export function generateStaticParams() {
|
||||
return source.getPages().map((page) => ({
|
||||
lang: page.locale,
|
||||
slug: getPageImage(page).segments,
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { siteUrl } from '@/lib/shared';
|
||||
|
||||
// Required for `output: 'export'`.
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: { userAgent: '*', allow: '/' },
|
||||
sitemap: `${siteUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { MetadataRoute } from 'next';
|
||||
import { source } from '@/lib/source';
|
||||
import { i18n } from '@/lib/i18n';
|
||||
import { siteUrl } from '@/lib/shared';
|
||||
|
||||
// Required for `output: 'export'`.
|
||||
export const dynamic = 'force-static';
|
||||
|
||||
// Locale home pages + the canonical (English) docs pages. Other locales
|
||||
// currently fall back to English content, so we don't list them separately
|
||||
// to avoid duplicate-content entries until real translations exist.
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const entries: MetadataRoute.Sitemap = [];
|
||||
|
||||
for (const lang of i18n.languages) {
|
||||
const prefix = lang === 'en' ? '' : `/${lang}`;
|
||||
entries.push({
|
||||
url: `${siteUrl}${prefix}` || siteUrl,
|
||||
changeFrequency: 'weekly',
|
||||
priority: 1,
|
||||
});
|
||||
}
|
||||
|
||||
for (const page of source.getPages('en')) {
|
||||
entries.push({
|
||||
url: `${siteUrl}${page.url}`,
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
});
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
Boxes,
|
||||
Network,
|
||||
Send,
|
||||
ShieldCheck,
|
||||
TerminalSquare,
|
||||
Users,
|
||||
type LucideIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
// Icons map by position to the localized feature items in lib/site-i18n.ts
|
||||
// (Every major protocol, REALITY, Clients, Multi-node, Telegram, Self-hosted).
|
||||
const ICONS: LucideIcon[] = [Boxes, ShieldCheck, Users, Network, Send, TerminalSquare];
|
||||
|
||||
export function Features({
|
||||
heading,
|
||||
subtitle,
|
||||
items,
|
||||
}: {
|
||||
heading: string;
|
||||
subtitle: string;
|
||||
items: { title: string; description: string }[];
|
||||
}) {
|
||||
return (
|
||||
<section className="mx-auto w-full max-w-6xl px-4 py-16 sm:py-24">
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<h2 className="text-2xl font-bold tracking-tight sm:text-3xl">{heading}</h2>
|
||||
<p className="mt-3 text-fd-muted-foreground">{subtitle}</p>
|
||||
</div>
|
||||
<div className="mt-12 grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{items.map(({ title, description }, i) => {
|
||||
const Icon = ICONS[i] ?? Boxes;
|
||||
return (
|
||||
<div
|
||||
key={title}
|
||||
className="rounded-2xl border bg-fd-card p-6 transition-colors hover:border-fd-primary/40"
|
||||
>
|
||||
<div className="inline-flex size-11 items-center justify-center rounded-xl bg-brand/10 text-brand">
|
||||
<Icon className="size-6" aria-hidden />
|
||||
</div>
|
||||
<h3 className="mt-4 font-semibold">{title}</h3>
|
||||
<p className="mt-2 text-sm text-fd-muted-foreground">{description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { GitFork, Star, Tag } from 'lucide-react';
|
||||
import { fetchGitHubStats, formatCount, type GitHubStats } from '@/lib/github-stats';
|
||||
|
||||
/**
|
||||
* Stars / forks / latest-release row. Renders the build-time numbers
|
||||
* immediately (no layout shift, works without JS), then swaps in live ones
|
||||
* from the GitHub API after hydration.
|
||||
*/
|
||||
export function GitHubStatsRow({
|
||||
initial,
|
||||
labels,
|
||||
}: {
|
||||
initial: GitHubStats;
|
||||
labels: { stars: string; forks: string; latest: string };
|
||||
}) {
|
||||
const [stats, setStats] = useState(initial);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
// Plain fetch, no custom headers — keeps the request preflight-free.
|
||||
void fetchGitHubStats().then((live) => {
|
||||
if (cancelled || !live) return;
|
||||
setStats((prev) => ({ ...live, latestVersion: live.latestVersion || prev.latestVersion }));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<dl className="mt-8 flex flex-wrap items-center justify-center gap-x-8 gap-y-3 text-sm">
|
||||
<Stat icon={<Star className="size-4" aria-hidden />} label={labels.stars}>
|
||||
{formatCount(stats.stars)}
|
||||
</Stat>
|
||||
<Stat icon={<GitFork className="size-4" aria-hidden />} label={labels.forks}>
|
||||
{formatCount(stats.forks)}
|
||||
</Stat>
|
||||
<Stat icon={<Tag className="size-4" aria-hidden />} label={labels.latest}>
|
||||
{stats.latestVersion}
|
||||
</Stat>
|
||||
</dl>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({
|
||||
icon,
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<span className="text-brand">{icon}</span>
|
||||
<dt className="sr-only">{label}</dt>
|
||||
<dd>
|
||||
<span className="font-semibold">{children}</span>{' '}
|
||||
<span className="text-fd-muted-foreground">{label}</span>
|
||||
</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export function InstallCommand({
|
||||
command,
|
||||
className,
|
||||
copyLabel = 'Copy install command',
|
||||
copiedLabel = 'Copied',
|
||||
}: {
|
||||
command: string;
|
||||
className?: string;
|
||||
copyLabel?: string;
|
||||
copiedLabel?: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Clipboard unavailable (insecure context) — silently ignore.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-xl border bg-fd-card py-2.5 pe-2 ps-4 text-sm shadow-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="select-none font-mono text-fd-muted-foreground">$</span>
|
||||
{/* Commands are always LTR, even on RTL pages. */}
|
||||
<code dir="ltr" className="flex-1 overflow-x-auto whitespace-nowrap text-start font-mono">
|
||||
{command}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
aria-label={copied ? copiedLabel : copyLabel}
|
||||
className="inline-flex size-8 shrink-0 items-center justify-center rounded-lg text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground focus-visible:outline-2 focus-visible:outline-fd-ring"
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-4 text-brand" aria-hidden />
|
||||
) : (
|
||||
<Copy className="size-4" aria-hidden />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import Link from 'next/link';
|
||||
import { Languages } from 'lucide-react';
|
||||
import { i18n, locales } from '@/lib/i18n';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
// Home-navbar language switcher.
|
||||
//
|
||||
// fumadocs' built-in popover switcher (`LanguageSelect`) has its item clicks
|
||||
// swallowed when it is nested inside HomeLayout's Radix `NavigationMenu` — the
|
||||
// dropdown opens but selecting a locale never fires `onChange`/`router.push`.
|
||||
// The docs sidebar isn't wrapped in a NavigationMenu, so the built-in one works
|
||||
// there and is kept. Here we use a native `<details>` toggle + real `<Link>`
|
||||
// anchors, which navigate reliably inside the navbar (like the other nav links).
|
||||
//
|
||||
// The home navbar only renders on the landing page, so the targets are simply
|
||||
// each locale's home (`/`, `/fa`, `/ru`, `/zh`).
|
||||
export function HomeLanguageSwitcher({ current }: { current: string }) {
|
||||
return (
|
||||
<details className="group relative [&>summary::-webkit-details-marker]:hidden">
|
||||
<summary
|
||||
aria-label="Choose a language"
|
||||
className="flex cursor-pointer list-none items-center rounded-lg p-1.5 text-fd-muted-foreground transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground group-open:bg-fd-accent"
|
||||
>
|
||||
<Languages className="size-5" />
|
||||
</summary>
|
||||
<div className="absolute end-0 z-50 mt-1.5 flex min-w-40 flex-col gap-0.5 rounded-lg border bg-fd-popover p-1 text-fd-popover-foreground shadow-lg">
|
||||
<p className="p-2 text-xs font-medium text-fd-muted-foreground">Choose a language</p>
|
||||
{locales.map(({ locale, name }) => (
|
||||
<Link
|
||||
key={locale}
|
||||
href={locale === i18n.defaultLanguage ? '/' : `/${locale}`}
|
||||
className={cn(
|
||||
'rounded-md px-2 py-1.5 text-start text-sm transition-colors',
|
||||
locale === current
|
||||
? 'bg-fd-primary/10 text-fd-primary'
|
||||
: 'text-fd-muted-foreground hover:bg-fd-accent hover:text-fd-accent-foreground',
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Brand icons that are not part of lucide-react.
|
||||
|
||||
export function GitHubIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden>
|
||||
<path d="M12 .5C5.73.5.5 5.74.5 12.02c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.56 0-.27-.01-1.16-.02-2.1-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.69-1.28-1.69-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.7 1.26 3.36.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.1-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11.03 11.03 0 0 1 5.8 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.81 1.18 1.84 1.18 3.1 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14 0 1.55-.01 2.8-.01 3.18 0 .31.21.68.8.56A10.53 10.53 0 0 0 23.5 12.02C23.5 5.74 18.27.5 12 .5Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function TelegramIcon({ className }: { className?: string }) {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden>
|
||||
<path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.139-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
// Official 3x-ui logo (media/3x-ui-{light,dark}.png from the upstream repo).
|
||||
// Theme-aware via Tailwind's `dark:` variant. Pass a height class (e.g. `h-6`);
|
||||
// width scales automatically (the artwork is 2:1).
|
||||
export function Logo({ className }: { className?: string }) {
|
||||
return (
|
||||
<>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/logo-light.png" alt="3x-ui" className={cn('w-auto dark:hidden', className)} />
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src="/logo-dark.png" alt="3x-ui" className={cn('hidden w-auto dark:block', className)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
import { Tab, Tabs } from 'fumadocs-ui/components/tabs';
|
||||
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||
import { Mermaid } from '@/components/mdx/mermaid';
|
||||
import { RealityConfigGenerator } from '@/components/tools/reality-config-generator';
|
||||
import { ShareLinkInspector } from '@/components/tools/share-link-inspector';
|
||||
import { InstallCommandBuilder } from '@/components/tools/install-command-builder';
|
||||
import { ReverseProxyGenerator } from '@/components/tools/reverse-proxy-generator';
|
||||
import { ProtocolWizard } from '@/components/tools/protocol-wizard';
|
||||
import { FirewallRulesGenerator } from '@/components/tools/firewall-rules-generator';
|
||||
import { OutboundGenerator } from '@/components/tools/outbound-generator';
|
||||
import { RoutingBuilder } from '@/components/tools/routing-builder';
|
||||
import { SubscriptionBuilder } from '@/components/tools/subscription-builder';
|
||||
import { TelegramSetupHelper } from '@/components/tools/telegram-setup-helper';
|
||||
import { ApiRequestBuilder } from '@/components/tools/api-request-builder';
|
||||
import { OpenAPIPage } from '@/components/openapi-page';
|
||||
import type { MDXComponents } from 'mdx/types';
|
||||
|
||||
export function getMDXComponents(components?: MDXComponents) {
|
||||
return {
|
||||
...defaultMdxComponents,
|
||||
Tab,
|
||||
Tabs,
|
||||
Step,
|
||||
Steps,
|
||||
Mermaid,
|
||||
RealityConfigGenerator,
|
||||
ShareLinkInspector,
|
||||
InstallCommandBuilder,
|
||||
ReverseProxyGenerator,
|
||||
ProtocolWizard,
|
||||
FirewallRulesGenerator,
|
||||
OutboundGenerator,
|
||||
RoutingBuilder,
|
||||
SubscriptionBuilder,
|
||||
TelegramSetupHelper,
|
||||
ApiRequestBuilder,
|
||||
OpenAPIPage,
|
||||
...components,
|
||||
} satisfies MDXComponents;
|
||||
}
|
||||
|
||||
export const useMDXComponents = getMDXComponents;
|
||||
|
||||
declare global {
|
||||
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useId, useState } from 'react';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
// Client-side, theme-aware Mermaid renderer. Mermaid is imported dynamically so
|
||||
// it stays out of the initial bundle and only loads on pages that use a diagram.
|
||||
export function Mermaid({ chart }: { chart: string }) {
|
||||
const rawId = useId();
|
||||
const id = `mmd-${rawId.replace(/[^a-zA-Z0-9]/g, '')}`;
|
||||
const { resolvedTheme } = useTheme();
|
||||
const [svg, setSvg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
void (async () => {
|
||||
const mermaid = (await import('mermaid')).default;
|
||||
mermaid.initialize({
|
||||
startOnLoad: false,
|
||||
securityLevel: 'strict',
|
||||
theme: resolvedTheme === 'dark' ? 'dark' : 'default',
|
||||
fontFamily: 'inherit',
|
||||
});
|
||||
try {
|
||||
const { svg } = await mermaid.render(id, chart.trim());
|
||||
if (active) setSvg(svg);
|
||||
} catch {
|
||||
if (active) setSvg('');
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
active = false;
|
||||
};
|
||||
}, [chart, resolvedTheme, id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="my-6 flex justify-center overflow-x-auto rounded-xl border bg-fd-card p-4 [&_svg]:max-w-full"
|
||||
role="img"
|
||||
aria-label="Architecture diagram"
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { createOpenAPIPage } from 'fumadocs-openapi/ui';
|
||||
|
||||
// The component used by the generated API reference MDX pages.
|
||||
export const OpenAPIPage = createOpenAPIPage();
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { create } from '@orama/orama';
|
||||
import { useDocsSearch } from 'fumadocs-core/search/client';
|
||||
import { oramaStaticClient } from 'fumadocs-core/search/client/orama-static';
|
||||
import {
|
||||
SearchDialog,
|
||||
SearchDialogClose,
|
||||
SearchDialogContent,
|
||||
SearchDialogHeader,
|
||||
SearchDialogIcon,
|
||||
SearchDialogInput,
|
||||
SearchDialogList,
|
||||
SearchDialogOverlay,
|
||||
} from 'fumadocs-ui/components/dialog/search';
|
||||
import { useI18n } from 'fumadocs-ui/contexts/i18n';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface SharedProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
// The static search index is keyed by locale code (en/fa/ru/zh). Fumadocs'
|
||||
// default static dialog feeds those codes to Orama as a tokenizer language, but
|
||||
// Orama only accepts full names ("english") and throws on "en" — which silently
|
||||
// breaks search entirely. All docs content is English (other locales fall back
|
||||
// to it), so re-create the dialog — the documented escape hatch for custom Orama
|
||||
// setups — with an initOrama that always builds an English index.
|
||||
export default function SearchDialogClient(props: SharedProps) {
|
||||
const { locale } = useI18n();
|
||||
const client = useMemo(
|
||||
() =>
|
||||
oramaStaticClient({
|
||||
from: '/api/search',
|
||||
locale,
|
||||
initOrama: () => create({ schema: { _: 'string' }, language: 'english' }),
|
||||
}),
|
||||
[locale],
|
||||
);
|
||||
const { search, setSearch, query } = useDocsSearch({ client });
|
||||
|
||||
return (
|
||||
<SearchDialog search={search} onSearchChange={setSearch} isLoading={query.isLoading} {...props}>
|
||||
<SearchDialogOverlay />
|
||||
<SearchDialogContent>
|
||||
<SearchDialogHeader>
|
||||
<SearchDialogIcon />
|
||||
<SearchDialogInput />
|
||||
<SearchDialogClose />
|
||||
</SearchDialogHeader>
|
||||
<SearchDialogList items={query.data !== 'empty' ? query.data : null} />
|
||||
</SearchDialogContent>
|
||||
</SearchDialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
'use client';
|
||||
|
||||
import { useId, useState } from 'react';
|
||||
import { buildCurl, buildFetchSnippet, type ApiRequestInput, type HttpMethod } from '@/lib/xray/api-client';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
const METHODS: readonly HttpMethod[] = ['GET', 'POST', 'PUT', 'DELETE'];
|
||||
|
||||
export function ApiRequestBuilder() {
|
||||
const [baseUrl, setBaseUrl] = useState('https://panel.example.com:2053');
|
||||
const [token, setToken] = useState('');
|
||||
const [path, setPath] = useState('/panel/api/inbounds/list');
|
||||
const [method, setMethod] = useState<HttpMethod>('GET');
|
||||
const [body, setBody] = useState('');
|
||||
const bodyId = useId();
|
||||
|
||||
const showBody = method === 'POST' || method === 'PUT';
|
||||
const input: ApiRequestInput = { baseUrl, token: token || '<token>', path, method, body };
|
||||
|
||||
function reset() {
|
||||
setBaseUrl('https://panel.example.com:2053');
|
||||
setToken('');
|
||||
setPath('/panel/api/inbounds/list');
|
||||
setMethod('GET');
|
||||
setBody('');
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="API request builder"
|
||||
description="Build an authenticated cURL command or fetch() snippet for any 3x-ui panel API endpoint under /panel/api/*."
|
||||
onReset={reset}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<TextField label="Panel base URL" value={baseUrl} onChange={setBaseUrl} />
|
||||
<TextField
|
||||
label="API token (Bearer)"
|
||||
value={token}
|
||||
onChange={setToken}
|
||||
placeholder="Settings → Security → API Token"
|
||||
/>
|
||||
<TextField label="Endpoint path" value={path} onChange={setPath} />
|
||||
<SelectField
|
||||
label="Method"
|
||||
value={method}
|
||||
onChange={(v) => setMethod(v as HttpMethod)}
|
||||
options={METHODS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showBody ? (
|
||||
<div className="mt-4 flex flex-col gap-1.5">
|
||||
<label htmlFor={bodyId} className="text-sm font-medium">
|
||||
Request body (JSON)
|
||||
</label>
|
||||
<textarea
|
||||
id={bodyId}
|
||||
dir="ltr"
|
||||
value={body}
|
||||
onChange={(e) => setBody(e.target.value)}
|
||||
rows={4}
|
||||
placeholder='{"id": 1}'
|
||||
className="rounded-lg border bg-fd-background px-3 py-2 font-mono text-sm outline-none transition-colors focus-visible:border-fd-primary focus-visible:ring-2 focus-visible:ring-fd-ring/30"
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="cURL" value={buildCurl(input)} />
|
||||
<OutputBlock label="fetch()" value={buildFetchSnippet(input)} />
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildUfwCommands,
|
||||
buildNftablesRuleset,
|
||||
type FirewallOptions,
|
||||
type PortProtocol,
|
||||
type PortRule,
|
||||
} from '@/lib/xray/firewall';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { CheckboxField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
interface Row {
|
||||
label: string;
|
||||
port: string;
|
||||
protocol: PortProtocol;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const DEFAULT_ROWS: Row[] = [
|
||||
{ label: 'panel', port: '2053', protocol: 'tcp', enabled: true },
|
||||
{ label: 'subscription', port: '2096', protocol: 'tcp', enabled: false },
|
||||
{ label: 'inbound (HTTPS)', port: '443', protocol: 'tcp', enabled: true },
|
||||
{ label: 'inbound (UDP)', port: '443', protocol: 'udp', enabled: false },
|
||||
];
|
||||
|
||||
export function FirewallRulesGenerator() {
|
||||
const [allowSsh, setAllowSsh] = useState(true);
|
||||
const [sshPort] = useState('22');
|
||||
const [rows, setRows] = useState<Row[]>(DEFAULT_ROWS);
|
||||
|
||||
function toggle(index: number, enabled: boolean) {
|
||||
setRows((prev) => prev.map((r, i) => (i === index ? { ...r, enabled } : r)));
|
||||
}
|
||||
|
||||
const ports: PortRule[] = rows
|
||||
.filter((r) => r.enabled && Number(r.port) > 0)
|
||||
.map((r) => ({ port: Number(r.port), protocol: r.protocol, label: r.label }));
|
||||
|
||||
const options: FirewallOptions = { ports, allowSsh, sshPort: Number(sshPort) || 22 };
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Firewall rules generator"
|
||||
description="Pick the ports to open and copy ready-made ufw and nftables rules."
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<CheckboxField
|
||||
label={`Allow SSH (port ${sshPort})`}
|
||||
checked={allowSsh}
|
||||
onChange={setAllowSsh}
|
||||
/>
|
||||
{rows.map((row, i) => (
|
||||
<CheckboxField
|
||||
key={`${row.label}-${row.protocol}`}
|
||||
label={`${row.label} — ${row.port}/${row.protocol}`}
|
||||
checked={row.enabled}
|
||||
onChange={(c) => toggle(i, c)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="ufw" value={buildUfwCommands(options)} />
|
||||
<OutputBlock label="nftables (/etc/nftables.conf)" value={buildNftablesRuleset(options)} />
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildScriptCommand,
|
||||
buildDockerRun,
|
||||
buildDockerCompose,
|
||||
type InstallMethod,
|
||||
type InstallOptions,
|
||||
} from '@/lib/xray/install';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField, CheckboxField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
export function InstallCommandBuilder() {
|
||||
const [method, setMethod] = useState<InstallMethod>('script');
|
||||
const [version, setVersion] = useState('');
|
||||
const [enableFail2ban, setEnableFail2ban] = useState(true);
|
||||
const [panelPort, setPanelPort] = useState('');
|
||||
const [webBasePath, setWebBasePath] = useState('');
|
||||
|
||||
const options: InstallOptions = {
|
||||
method,
|
||||
version,
|
||||
enableFail2ban,
|
||||
panelPort,
|
||||
webBasePath,
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Install command builder"
|
||||
description="Build the exact install command for your setup. It is assembled in your browser."
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Method"
|
||||
value={method}
|
||||
onChange={(v) => setMethod(v as InstallMethod)}
|
||||
options={['script', 'docker']}
|
||||
/>
|
||||
<TextField
|
||||
label="Version"
|
||||
value={version}
|
||||
onChange={setVersion}
|
||||
placeholder="latest"
|
||||
hint="blank = latest stable · a tag like v3.4.0 · or dev-latest for the rolling dev build"
|
||||
/>
|
||||
{method === 'docker' ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Panel port"
|
||||
value={panelPort}
|
||||
onChange={setPanelPort}
|
||||
placeholder="2053"
|
||||
/>
|
||||
<TextField
|
||||
label="Web base path"
|
||||
value={webBasePath}
|
||||
onChange={setWebBasePath}
|
||||
placeholder="/panel"
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-3">
|
||||
<CheckboxField
|
||||
label="Enable Fail2ban"
|
||||
checked={enableFail2ban}
|
||||
onChange={setEnableFail2ban}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
{method === 'script' ? (
|
||||
<OutputBlock label="Run on your server" value={buildScriptCommand(options)} />
|
||||
) : (
|
||||
<>
|
||||
<OutputBlock label="docker run" value={buildDockerRun(options)} />
|
||||
<OutputBlock label="docker-compose.yml" value={buildDockerCompose(options)} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildOutboundJson,
|
||||
type OutboundInput,
|
||||
type OutboundKind,
|
||||
type Network,
|
||||
type Security,
|
||||
type ProxyServerInput,
|
||||
type StreamInput,
|
||||
type WireguardInput,
|
||||
} from '@/lib/xray/outbounds';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
const KINDS: readonly OutboundKind[] = [
|
||||
'freedom',
|
||||
'blackhole',
|
||||
'vless',
|
||||
'vmess',
|
||||
'trojan',
|
||||
'shadowsocks',
|
||||
'socks',
|
||||
'http',
|
||||
'wireguard',
|
||||
'warp',
|
||||
];
|
||||
const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
|
||||
const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
|
||||
const FINGERPRINTS = ['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'];
|
||||
const DOMAIN_STRATEGIES = ['AsIs', 'UseIP', 'UseIPv4', 'UseIPv6', 'ForceIP'];
|
||||
|
||||
const PROXY_KINDS = new Set<OutboundKind>([
|
||||
'vless',
|
||||
'vmess',
|
||||
'trojan',
|
||||
'shadowsocks',
|
||||
'socks',
|
||||
'http',
|
||||
]);
|
||||
const STREAM_KINDS = new Set<OutboundKind>(['vless', 'vmess', 'trojan', 'shadowsocks']);
|
||||
const WG_KINDS = new Set<OutboundKind>(['wireguard', 'warp']);
|
||||
|
||||
export function OutboundGenerator() {
|
||||
const [kind, setKind] = useState<OutboundKind>('vless');
|
||||
const [tag, setTag] = useState('proxy');
|
||||
|
||||
// proxy server
|
||||
const [address, setAddress] = useState('example.com');
|
||||
const [port, setPort] = useState('443');
|
||||
const [id, setId] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [method, setMethod] = useState('2022-blake3-aes-128-gcm');
|
||||
const [flow, setFlow] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
|
||||
// stream
|
||||
const [network, setNetwork] = useState<Network>('tcp');
|
||||
const [security, setSecurity] = useState<Security>('reality');
|
||||
const [host, setHost] = useState('');
|
||||
const [path, setPath] = useState('/');
|
||||
const [serviceName, setServiceName] = useState('');
|
||||
const [sni, setSni] = useState('www.microsoft.com');
|
||||
const [fingerprint, setFingerprint] = useState('chrome');
|
||||
const [publicKey, setPublicKey] = useState('');
|
||||
const [shortId, setShortId] = useState('');
|
||||
|
||||
// freedom
|
||||
const [domainStrategy, setDomainStrategy] = useState('AsIs');
|
||||
|
||||
// wireguard
|
||||
const [wgSecretKey, setWgSecretKey] = useState('');
|
||||
const [wgAddress, setWgAddress] = useState('172.16.0.2/32');
|
||||
const [wgPublicKey, setWgPublicKey] = useState('');
|
||||
const [wgEndpoint, setWgEndpoint] = useState('');
|
||||
|
||||
const isProxy = PROXY_KINDS.has(kind);
|
||||
const hasStream = STREAM_KINDS.has(kind);
|
||||
const isWg = WG_KINDS.has(kind);
|
||||
const hasPath = network === 'ws' || network === 'httpupgrade' || network === 'xhttp';
|
||||
|
||||
const server: ProxyServerInput = {
|
||||
address,
|
||||
port: Number(port),
|
||||
id,
|
||||
password,
|
||||
method,
|
||||
flow,
|
||||
username,
|
||||
};
|
||||
|
||||
const stream: StreamInput = {
|
||||
network,
|
||||
security,
|
||||
host,
|
||||
path,
|
||||
serviceName,
|
||||
sni,
|
||||
fingerprint,
|
||||
publicKey,
|
||||
shortId,
|
||||
};
|
||||
|
||||
const wireguard: WireguardInput = {
|
||||
secretKey: wgSecretKey,
|
||||
address: wgAddress
|
||||
.split(',')
|
||||
.map((a) => a.trim())
|
||||
.filter(Boolean),
|
||||
publicKey: wgPublicKey,
|
||||
endpoint: wgEndpoint,
|
||||
};
|
||||
|
||||
const input: OutboundInput = {
|
||||
kind,
|
||||
tag,
|
||||
server: isProxy ? server : undefined,
|
||||
wireguard: isWg ? wireguard : undefined,
|
||||
stream: hasStream ? stream : undefined,
|
||||
domainStrategy: kind === 'freedom' ? domainStrategy : undefined,
|
||||
};
|
||||
|
||||
function reset() {
|
||||
setKind('vless');
|
||||
setTag('proxy');
|
||||
setAddress('example.com');
|
||||
setPort('443');
|
||||
setId('');
|
||||
setPassword('');
|
||||
setMethod('2022-blake3-aes-128-gcm');
|
||||
setFlow('');
|
||||
setUsername('');
|
||||
setNetwork('tcp');
|
||||
setSecurity('reality');
|
||||
setHost('');
|
||||
setPath('/');
|
||||
setServiceName('');
|
||||
setSni('www.microsoft.com');
|
||||
setFingerprint('chrome');
|
||||
setPublicKey('');
|
||||
setShortId('');
|
||||
setDomainStrategy('AsIs');
|
||||
setWgSecretKey('');
|
||||
setWgAddress('172.16.0.2/32');
|
||||
setWgPublicKey('');
|
||||
setWgEndpoint('');
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Outbound config generator"
|
||||
description="Build an Xray outbound object — freedom, blackhole, a proxy protocol, WireGuard, or WARP — to paste into your Xray configuration."
|
||||
onReset={reset}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Kind"
|
||||
value={kind}
|
||||
onChange={(v) => setKind(v as OutboundKind)}
|
||||
options={KINDS}
|
||||
/>
|
||||
<TextField label="Tag" value={kind === 'warp' ? 'warp' : tag} onChange={setTag} />
|
||||
|
||||
{kind === 'freedom' ? (
|
||||
<SelectField
|
||||
label="Domain strategy"
|
||||
value={domainStrategy}
|
||||
onChange={setDomainStrategy}
|
||||
options={DOMAIN_STRATEGIES}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isProxy ? (
|
||||
<>
|
||||
<TextField label="Address" value={address} onChange={setAddress} />
|
||||
<TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
|
||||
{(kind === 'vless' || kind === 'vmess') && (
|
||||
<TextField label="UUID (id)" value={id} onChange={setId} />
|
||||
)}
|
||||
{kind === 'vless' && (
|
||||
<TextField
|
||||
label="Flow"
|
||||
value={flow}
|
||||
onChange={setFlow}
|
||||
placeholder="xtls-rprx-vision (optional)"
|
||||
/>
|
||||
)}
|
||||
{(kind === 'trojan' || kind === 'shadowsocks') && (
|
||||
<TextField label="Password" value={password} onChange={setPassword} />
|
||||
)}
|
||||
{kind === 'shadowsocks' && (
|
||||
<TextField label="Method (cipher)" value={method} onChange={setMethod} />
|
||||
)}
|
||||
{(kind === 'socks' || kind === 'http') && (
|
||||
<>
|
||||
<TextField
|
||||
label="Username"
|
||||
value={username}
|
||||
onChange={setUsername}
|
||||
placeholder="optional"
|
||||
/>
|
||||
<TextField label="Password" value={password} onChange={setPassword} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{isWg ? (
|
||||
<>
|
||||
<TextField
|
||||
label="Private key (secretKey)"
|
||||
value={wgSecretKey}
|
||||
onChange={setWgSecretKey}
|
||||
/>
|
||||
<TextField label="Local address" value={wgAddress} onChange={setWgAddress} />
|
||||
<TextField label="Peer public key" value={wgPublicKey} onChange={setWgPublicKey} />
|
||||
<TextField
|
||||
label="Peer endpoint"
|
||||
value={wgEndpoint}
|
||||
onChange={setWgEndpoint}
|
||||
placeholder={kind === 'warp' ? 'engage.cloudflareclient.com:2408' : 'host:51820'}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{hasStream ? (
|
||||
<div className="mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Transport"
|
||||
value={network}
|
||||
onChange={(v) => setNetwork(v as Network)}
|
||||
options={NETWORKS}
|
||||
/>
|
||||
<SelectField
|
||||
label="Security"
|
||||
value={security}
|
||||
onChange={(v) => setSecurity(v as Security)}
|
||||
options={SECURITIES}
|
||||
/>
|
||||
{hasPath ? (
|
||||
<>
|
||||
<TextField label="Path" value={path} onChange={setPath} />
|
||||
<TextField label="Host" value={host} onChange={setHost} placeholder="optional" />
|
||||
</>
|
||||
) : null}
|
||||
{network === 'grpc' ? (
|
||||
<TextField label="serviceName" value={serviceName} onChange={setServiceName} />
|
||||
) : null}
|
||||
{security !== 'none' ? (
|
||||
<>
|
||||
<TextField label="SNI (serverName)" value={sni} onChange={setSni} />
|
||||
<SelectField
|
||||
label="Fingerprint"
|
||||
value={fingerprint}
|
||||
onChange={setFingerprint}
|
||||
options={FINGERPRINTS}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
{security === 'reality' ? (
|
||||
<>
|
||||
<TextField label="Public key (pbk)" value={publicKey} onChange={setPublicKey} />
|
||||
<TextField label="Short ID (sid)" value={shortId} onChange={setShortId} />
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mt-4">
|
||||
<OutputBlock label="Outbound (Xray JSON)" value={buildOutboundJson(input)} />
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import {
|
||||
recommend,
|
||||
type UseCase,
|
||||
type CensorshipLevel,
|
||||
type ClientSupport,
|
||||
} from '@/lib/xray/protocols';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { SelectField } from './shared/fields';
|
||||
|
||||
const cap = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
|
||||
export function ProtocolWizard() {
|
||||
const [useCase, setUseCase] = useState<UseCase>('general');
|
||||
const [censorship, setCensorship] = useState<CensorshipLevel>('medium');
|
||||
const [clientSupport, setClientSupport] = useState<ClientSupport>('modern');
|
||||
|
||||
const result = recommend({ useCase, censorship, clientSupport });
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Protocol wizard"
|
||||
description="Answer a few questions to get a recommended protocol and transport."
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||
<SelectField
|
||||
label="Primary goal"
|
||||
value={cap(useCase)}
|
||||
onChange={(v) => setUseCase(v.toLowerCase() as UseCase)}
|
||||
options={['Censorship', 'General', 'Speed']}
|
||||
/>
|
||||
<SelectField
|
||||
label="Censorship level"
|
||||
value={cap(censorship)}
|
||||
onChange={(v) => setCensorship(v.toLowerCase() as CensorshipLevel)}
|
||||
options={['High', 'Medium', 'Low']}
|
||||
/>
|
||||
<SelectField
|
||||
label="Client support"
|
||||
value={cap(clientSupport)}
|
||||
onChange={(v) => setClientSupport(v.toLowerCase() as ClientSupport)}
|
||||
options={['Modern', 'Broad']}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 rounded-xl border bg-fd-background p-4">
|
||||
<div className="flex items-center gap-2 text-brand">
|
||||
<Sparkles className="size-4" aria-hidden />
|
||||
<span className="text-sm font-medium">Recommended</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
<Badge>{result.protocol}</Badge>
|
||||
<Badge>{result.transport}</Badge>
|
||||
<Badge>{result.security}</Badge>
|
||||
</div>
|
||||
<p className="mt-3 text-sm text-fd-muted-foreground">{result.rationale}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-3 text-sm">
|
||||
{result.links.map((link) => (
|
||||
<Link key={link.href} href={link.href} className="text-fd-primary hover:underline">
|
||||
{link.title} →
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function Badge({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<span className="rounded-lg bg-brand/10 px-2.5 py-1 text-sm font-medium text-brand">
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { RefreshCw } from 'lucide-react';
|
||||
import {
|
||||
generateX25519KeyPair,
|
||||
isX25519Available,
|
||||
randomShortId,
|
||||
randomUuid,
|
||||
realityClientLink,
|
||||
realityServerInbound,
|
||||
type RealityConfig,
|
||||
type X25519KeyPair,
|
||||
} from '@/lib/xray/reality';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
import { CopyButton } from './shared/copy-button';
|
||||
|
||||
const FINGERPRINTS = ['chrome', 'firefox', 'safari', 'ios', 'android', 'edge', 'random'] as const;
|
||||
|
||||
export function RealityConfigGenerator() {
|
||||
const [address, setAddress] = useState('your-server.com');
|
||||
const [port, setPort] = useState('443');
|
||||
const [dest, setDest] = useState('www.microsoft.com:443');
|
||||
const [sni, setSni] = useState('www.microsoft.com');
|
||||
const [fingerprint, setFingerprint] = useState<string>('chrome');
|
||||
const [uuid, setUuid] = useState('');
|
||||
const [shortId, setShortId] = useState('');
|
||||
const [keys, setKeys] = useState<X25519KeyPair | null>(null);
|
||||
const [unavailable, setUnavailable] = useState(false);
|
||||
|
||||
const regenerate = useCallback(async () => {
|
||||
setUuid(randomUuid());
|
||||
setShortId(randomShortId(4));
|
||||
if (!isX25519Available()) {
|
||||
setUnavailable(true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setKeys(await generateX25519KeyPair());
|
||||
setUnavailable(false);
|
||||
} catch {
|
||||
setUnavailable(true);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Generate keys/identifiers on the client after hydration. This is a genuine
|
||||
// client-only side effect (WebCrypto + randomness), not derived render state.
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
void regenerate();
|
||||
}, [regenerate]);
|
||||
|
||||
const config: RealityConfig | null =
|
||||
keys && uuid
|
||||
? {
|
||||
address,
|
||||
port: Number(port) || 443,
|
||||
uuid,
|
||||
dest,
|
||||
serverNames: [sni],
|
||||
shortIds: [shortId],
|
||||
privateKey: keys.privateKey,
|
||||
publicKey: keys.publicKey,
|
||||
fingerprint,
|
||||
spiderX: '/',
|
||||
flow: 'xtls-rprx-vision',
|
||||
}
|
||||
: null;
|
||||
|
||||
const serverJson = config ? JSON.stringify(realityServerInbound(config), null, 2) : '';
|
||||
const clientLink = config ? realityClientLink(config) : '';
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="REALITY config generator"
|
||||
description="Generate a VLESS + REALITY inbound and client link. Keys are created in your browser — nothing is sent anywhere."
|
||||
onReset={() => void regenerate()}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<TextField
|
||||
label="Server address"
|
||||
value={address}
|
||||
onChange={setAddress}
|
||||
hint="Your domain or IP"
|
||||
/>
|
||||
<TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
|
||||
<TextField
|
||||
label="Dest (camouflage target)"
|
||||
value={dest}
|
||||
onChange={setDest}
|
||||
hint="A real TLS 1.3 site, e.g. www.microsoft.com:443"
|
||||
/>
|
||||
<TextField label="SNI / Server name" value={sni} onChange={setSni} />
|
||||
<SelectField
|
||||
label="Fingerprint"
|
||||
value={fingerprint}
|
||||
onChange={setFingerprint}
|
||||
options={FINGERPRINTS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{unavailable ? (
|
||||
<div className="mt-4 rounded-xl border border-amber-500/40 bg-amber-500/10 p-3 text-sm">
|
||||
Your browser can't generate X25519 keys here. Generate them on the server instead:
|
||||
<div className="mt-2">
|
||||
<OutputBlock label="run on the server" value="xray x25519" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-4 flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium">Generated keys & identifiers</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void regenerate()}
|
||||
className="inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
|
||||
>
|
||||
<RefreshCw className="size-3.5" aria-hidden />
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<KeyRow label="Public key" value={keys?.publicKey ?? ''} />
|
||||
<KeyRow label="Private key" value={keys?.privateKey ?? ''} />
|
||||
<KeyRow label="UUID" value={uuid} />
|
||||
<KeyRow label="Short ID" value={shortId} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="Server inbound (Xray JSON)" value={serverJson} />
|
||||
<OutputBlock label="Client share link" value={clientLink} qr />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function KeyRow({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-lg border bg-fd-background px-3 py-2">
|
||||
<span className="shrink-0 text-xs font-medium text-fd-muted-foreground">{label}</span>
|
||||
<code dir="ltr" className="flex-1 truncate text-start text-xs">
|
||||
{value}
|
||||
</code>
|
||||
<CopyButton value={value} label="" className="px-1.5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildProxyConfig,
|
||||
buildCertCommand,
|
||||
type ProxyServer,
|
||||
type CertTool,
|
||||
type ReverseProxyOptions,
|
||||
} from '@/lib/xray/reverse-proxy';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
export function ReverseProxyGenerator() {
|
||||
const [server, setServer] = useState<ProxyServer>('nginx');
|
||||
const [domain, setDomain] = useState('panel.example.com');
|
||||
const [panelPort, setPanelPort] = useState('2053');
|
||||
const [panelPath, setPanelPath] = useState('/panel');
|
||||
const [certTool, setCertTool] = useState<CertTool>('certbot');
|
||||
|
||||
const options: ReverseProxyOptions = { server, domain, panelPort, panelPath, certTool };
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Reverse-proxy config generator"
|
||||
description="Generate an Nginx or Caddy reverse-proxy config (with WebSocket support) and a matching certificate command."
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Server"
|
||||
value={server}
|
||||
onChange={(v) => setServer(v as ProxyServer)}
|
||||
options={['nginx', 'caddy']}
|
||||
/>
|
||||
<TextField label="Domain" value={domain} onChange={setDomain} />
|
||||
<TextField
|
||||
label="Panel port"
|
||||
value={panelPort}
|
||||
onChange={setPanelPort}
|
||||
inputMode="numeric"
|
||||
/>
|
||||
<TextField label="Panel web base path" value={panelPath} onChange={setPanelPath} />
|
||||
{server === 'nginx' ? (
|
||||
<SelectField
|
||||
label="Certificate tool"
|
||||
value={certTool}
|
||||
onChange={(v) => setCertTool(v as CertTool)}
|
||||
options={['certbot', 'acme.sh']}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock
|
||||
label={server === 'nginx' ? 'nginx server block' : 'Caddyfile'}
|
||||
value={buildProxyConfig(options)}
|
||||
/>
|
||||
{server === 'nginx' ? (
|
||||
<OutputBlock label="Obtain a certificate" value={buildCertCommand(options)} />
|
||||
) : (
|
||||
<p className="text-sm text-fd-muted-foreground">
|
||||
Caddy obtains and renews TLS certificates automatically — no extra command needed.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildRoutingJson,
|
||||
type DomainStrategy,
|
||||
type RoutingInput,
|
||||
type RuleNetwork,
|
||||
type Strategy,
|
||||
} from '@/lib/xray/routing';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
interface BalancerRow {
|
||||
tag: string;
|
||||
selector: string;
|
||||
strategy: Strategy;
|
||||
fallbackTag: string;
|
||||
}
|
||||
|
||||
interface RuleRow {
|
||||
domain: string;
|
||||
ip: string;
|
||||
port: string;
|
||||
network: string;
|
||||
inboundTag: string;
|
||||
targetKind: 'outbound' | 'balancer';
|
||||
targetTag: string;
|
||||
}
|
||||
|
||||
const STRATEGIES: readonly Strategy[] = ['random', 'roundRobin', 'leastPing', 'leastLoad'];
|
||||
const NETWORKS = ['any', 'tcp', 'udp', 'tcp,udp'];
|
||||
const TARGET_KINDS = ['outbound', 'balancer'];
|
||||
const DOMAIN_STRATEGIES: readonly DomainStrategy[] = ['AsIs', 'IPIfNonMatch', 'IPOnDemand'];
|
||||
|
||||
const DEFAULT_BALANCERS: BalancerRow[] = [
|
||||
{ tag: 'balancer', selector: 'proxy', strategy: 'leastPing', fallbackTag: '' },
|
||||
];
|
||||
const DEFAULT_RULES: RuleRow[] = [
|
||||
{ domain: 'geosite:category-ads-all', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'block' },
|
||||
{ domain: '', ip: 'geoip:private', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: 'direct' },
|
||||
];
|
||||
|
||||
function list(s: string): string[] {
|
||||
return s
|
||||
.split(',')
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
const addBtn =
|
||||
'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
|
||||
|
||||
export function RoutingBuilder() {
|
||||
const [domainStrategy, setDomainStrategy] = useState<DomainStrategy>('IPIfNonMatch');
|
||||
const [balancers, setBalancers] = useState<BalancerRow[]>(DEFAULT_BALANCERS);
|
||||
const [rules, setRules] = useState<RuleRow[]>(DEFAULT_RULES);
|
||||
|
||||
function patchBalancer(i: number, patch: Partial<BalancerRow>) {
|
||||
setBalancers((prev) => prev.map((b, j) => (i === j ? { ...b, ...patch } : b)));
|
||||
}
|
||||
function patchRule(i: number, patch: Partial<RuleRow>) {
|
||||
setRules((prev) => prev.map((r, j) => (i === j ? { ...r, ...patch } : r)));
|
||||
}
|
||||
|
||||
const input: RoutingInput = {
|
||||
domainStrategy,
|
||||
balancers: balancers
|
||||
.filter((b) => b.tag.trim())
|
||||
.map((b) => ({
|
||||
tag: b.tag.trim(),
|
||||
selector: list(b.selector),
|
||||
strategy: b.strategy,
|
||||
fallbackTag: b.fallbackTag.trim() || undefined,
|
||||
})),
|
||||
rules: rules
|
||||
.filter((r) => r.targetTag.trim())
|
||||
.map((r) => ({
|
||||
domain: list(r.domain),
|
||||
ip: list(r.ip),
|
||||
port: r.port.trim() || undefined,
|
||||
network: r.network === 'any' ? undefined : (r.network as RuleNetwork),
|
||||
inboundTag: list(r.inboundTag),
|
||||
target: { kind: r.targetKind, tag: r.targetTag.trim() },
|
||||
})),
|
||||
};
|
||||
|
||||
function reset() {
|
||||
setDomainStrategy('IPIfNonMatch');
|
||||
setBalancers(DEFAULT_BALANCERS);
|
||||
setRules(DEFAULT_RULES);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Balancer & routing builder"
|
||||
description="Compose Xray balancers and routing rules, then copy the routing block (with a matching observatory for leastPing/leastLoad)."
|
||||
onReset={reset}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Domain strategy"
|
||||
value={domainStrategy}
|
||||
onChange={(v) => setDomainStrategy(v as DomainStrategy)}
|
||||
options={DOMAIN_STRATEGIES}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold">Balancers</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() =>
|
||||
setBalancers((p) => [...p, { tag: '', selector: '', strategy: 'random', fallbackTag: '' }])
|
||||
}
|
||||
>
|
||||
Add balancer
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-3">
|
||||
{balancers.map((b, i) => (
|
||||
<div key={i} className="rounded-xl border p-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<TextField label="Tag" value={b.tag} onChange={(v) => patchBalancer(i, { tag: v })} />
|
||||
<TextField
|
||||
label="Selector (comma-separated prefixes)"
|
||||
value={b.selector}
|
||||
onChange={(v) => patchBalancer(i, { selector: v })}
|
||||
/>
|
||||
<SelectField
|
||||
label="Strategy"
|
||||
value={b.strategy}
|
||||
onChange={(v) => patchBalancer(i, { strategy: v as Strategy })}
|
||||
options={STRATEGIES}
|
||||
/>
|
||||
<TextField
|
||||
label="Fallback tag"
|
||||
value={b.fallbackTag}
|
||||
onChange={(v) => patchBalancer(i, { fallbackTag: v })}
|
||||
placeholder="optional"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() => setBalancers((p) => p.filter((_, j) => j !== i))}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold">Rules</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() =>
|
||||
setRules((p) => [
|
||||
...p,
|
||||
{ domain: '', ip: '', port: '', network: 'any', inboundTag: '', targetKind: 'outbound', targetTag: '' },
|
||||
])
|
||||
}
|
||||
>
|
||||
Add rule
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-3">
|
||||
{rules.map((r, i) => (
|
||||
<div key={i} className="rounded-xl border p-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<TextField label="Domain (comma)" value={r.domain} onChange={(v) => patchRule(i, { domain: v })} placeholder="geosite:google, example.com" />
|
||||
<TextField label="IP (comma)" value={r.ip} onChange={(v) => patchRule(i, { ip: v })} placeholder="geoip:cn, 1.1.1.1" />
|
||||
<TextField label="Port" value={r.port} onChange={(v) => patchRule(i, { port: v })} placeholder="443 or 1000-2000" />
|
||||
<SelectField label="Network" value={r.network} onChange={(v) => patchRule(i, { network: v })} options={NETWORKS} />
|
||||
<TextField label="Inbound tag (comma)" value={r.inboundTag} onChange={(v) => patchRule(i, { inboundTag: v })} placeholder="optional" />
|
||||
<SelectField label="Target kind" value={r.targetKind} onChange={(v) => patchRule(i, { targetKind: v as 'outbound' | 'balancer' })} options={TARGET_KINDS} />
|
||||
<TextField label="Target tag" value={r.targetTag} onChange={(v) => patchRule(i, { targetTag: v })} />
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() => setRules((p) => p.filter((_, j) => j !== i))}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<OutputBlock label="Routing block (Xray JSON)" value={buildRoutingJson(input)} />
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo, useState } from 'react';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { parseLink, type ParsedLink } from '@/lib/xray/links';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
|
||||
type Result = { ok: true; data: ParsedLink } | { ok: false; error: string } | null;
|
||||
|
||||
export function ShareLinkInspector() {
|
||||
const [input, setInput] = useState('');
|
||||
|
||||
const result: Result = useMemo(() => {
|
||||
const value = input.trim();
|
||||
if (!value) return null;
|
||||
try {
|
||||
return { ok: true, data: parseLink(value) };
|
||||
} catch (e) {
|
||||
return { ok: false, error: (e as Error).message };
|
||||
}
|
||||
}, [input]);
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Share-link inspector"
|
||||
description="Paste a vless / vmess / trojan / ss link to decode every parameter. It is parsed entirely in your browser — nothing is sent over the network."
|
||||
onReset={input ? () => setInput('') : undefined}
|
||||
>
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="vless://uuid@host:443?security=reality&pbk=...#name"
|
||||
dir="ltr"
|
||||
rows={3}
|
||||
spellCheck={false}
|
||||
className="w-full resize-y rounded-lg border bg-fd-background px-3 py-2 font-mono text-sm outline-none transition-colors focus-visible:border-fd-primary focus-visible:ring-2 focus-visible:ring-fd-ring/30"
|
||||
/>
|
||||
|
||||
{result && !result.ok ? (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-lg border border-red-500/40 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400">
|
||||
<AlertCircle className="size-4 shrink-0" aria-hidden />
|
||||
<span>{result.error}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{result && result.ok ? <ResultTable data={result.data} /> : null}
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
|
||||
function ResultTable({ data }: { data: ParsedLink }) {
|
||||
const rows: [string, string][] = [
|
||||
['Protocol', data.protocol],
|
||||
['Name', data.name],
|
||||
['Address', data.address],
|
||||
['Port', String(data.port)],
|
||||
[data.protocol === 'trojan' ? 'Password' : 'ID / credential', data.credential],
|
||||
...Object.entries(data.params),
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="mt-3 overflow-hidden rounded-xl border">
|
||||
<table className="w-full text-sm">
|
||||
<tbody>
|
||||
{rows.map(([key, value], i) => (
|
||||
<tr key={`${key}-${i}`} className="border-b last:border-b-0">
|
||||
<th
|
||||
scope="row"
|
||||
className="w-1/3 bg-fd-muted/40 px-3 py-2 text-start align-top font-medium text-fd-muted-foreground"
|
||||
>
|
||||
{key}
|
||||
</th>
|
||||
<td dir="ltr" className="break-all px-3 py-2 text-start font-mono">
|
||||
{value || <span className="text-fd-muted-foreground">—</span>}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Check, Copy } from 'lucide-react';
|
||||
import { cn } from '@/lib/cn';
|
||||
|
||||
export function CopyButton({
|
||||
value,
|
||||
label = 'Copy',
|
||||
className,
|
||||
}: {
|
||||
value: string;
|
||||
label?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
|
||||
async function copy() {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch {
|
||||
// Clipboard unavailable (insecure context) — ignore.
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={copy}
|
||||
aria-label={copied ? 'Copied' : label}
|
||||
className={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground focus-visible:outline-2 focus-visible:outline-fd-ring',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{copied ? (
|
||||
<Check className="size-3.5 text-brand" aria-hidden />
|
||||
) : (
|
||||
<Copy className="size-3.5" aria-hidden />
|
||||
)}
|
||||
<span>{copied ? 'Copied' : label}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
'use client';
|
||||
|
||||
import { useId } from 'react';
|
||||
|
||||
const inputClass =
|
||||
'rounded-lg border bg-fd-background px-3 py-2 text-sm outline-none transition-colors focus-visible:border-fd-primary focus-visible:ring-2 focus-visible:ring-fd-ring/30';
|
||||
|
||||
export function TextField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
hint,
|
||||
inputMode,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
hint?: string;
|
||||
inputMode?: 'numeric' | 'text';
|
||||
}) {
|
||||
const id = useId();
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor={id} className="text-sm font-medium">
|
||||
{label}
|
||||
</label>
|
||||
{/* Values are technical (hosts, links) and stay LTR even on RTL pages. */}
|
||||
<input
|
||||
id={id}
|
||||
dir="ltr"
|
||||
inputMode={inputMode}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={inputClass}
|
||||
/>
|
||||
{hint ? <span className="text-xs text-fd-muted-foreground">{hint}</span> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckboxField({
|
||||
label,
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
}) {
|
||||
const id = useId();
|
||||
return (
|
||||
<label htmlFor={id} className="inline-flex cursor-pointer items-center gap-2 text-sm">
|
||||
<input
|
||||
id={id}
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
className="size-4 accent-fd-primary"
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
export function SelectField({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
options: readonly string[];
|
||||
}) {
|
||||
const id = useId();
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor={id} className="text-sm font-medium">
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
id={id}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={inputClass}
|
||||
>
|
||||
{options.map((opt) => (
|
||||
<option key={opt} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
'use client';
|
||||
|
||||
import QRCode from 'react-qr-code';
|
||||
import { CopyButton } from './copy-button';
|
||||
|
||||
export function OutputBlock({
|
||||
label,
|
||||
value,
|
||||
qr = false,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
qr?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="overflow-hidden rounded-xl border">
|
||||
<div className="flex items-center justify-between gap-2 border-b bg-fd-muted/40 px-3 py-2">
|
||||
<span className="text-xs font-medium text-fd-muted-foreground">{label}</span>
|
||||
<CopyButton value={value} />
|
||||
</div>
|
||||
<pre dir="ltr" className="max-h-80 overflow-auto p-3 text-start text-xs leading-relaxed">
|
||||
<code>{value}</code>
|
||||
</pre>
|
||||
{qr && value ? (
|
||||
<div className="flex justify-center border-t bg-white p-4">
|
||||
<QRCode value={value} size={180} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
buildSubscriptionUrls,
|
||||
buildShareLinks,
|
||||
buildBase64Subscription,
|
||||
buildJsonSubscription,
|
||||
type SubClient,
|
||||
type SubUrlInput,
|
||||
} from '@/lib/xray/subscription';
|
||||
import type { Network, Security } from '@/lib/xray/outbounds';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField, SelectField, CheckboxField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
type ClientProtocol = 'vless' | 'vmess' | 'trojan' | 'ss';
|
||||
|
||||
interface ClientRow {
|
||||
protocol: ClientProtocol;
|
||||
remark: string;
|
||||
address: string;
|
||||
port: string;
|
||||
credential: string; // id (vless/vmess) or password (trojan/ss)
|
||||
method: string; // ss
|
||||
network: Network;
|
||||
security: Security;
|
||||
sni: string;
|
||||
}
|
||||
|
||||
const PROTOCOLS: readonly ClientProtocol[] = ['vless', 'vmess', 'trojan', 'ss'];
|
||||
const NETWORKS: readonly Network[] = ['tcp', 'kcp', 'ws', 'grpc', 'httpupgrade', 'xhttp'];
|
||||
const SECURITIES: readonly Security[] = ['none', 'tls', 'reality'];
|
||||
|
||||
const addBtn =
|
||||
'inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground';
|
||||
|
||||
const DEFAULT_CLIENTS: ClientRow[] = [
|
||||
{
|
||||
protocol: 'vless',
|
||||
remark: 'HK-01',
|
||||
address: 'a.example.com',
|
||||
port: '443',
|
||||
credential: '11111111-2222-3333-4444-555555555555',
|
||||
method: '',
|
||||
network: 'tcp',
|
||||
security: 'reality',
|
||||
sni: 'www.microsoft.com',
|
||||
},
|
||||
];
|
||||
|
||||
function toClient(r: ClientRow): SubClient {
|
||||
const isUuid = r.protocol === 'vless' || r.protocol === 'vmess';
|
||||
return {
|
||||
protocol: r.protocol,
|
||||
remark: r.remark,
|
||||
address: r.address,
|
||||
port: Number(r.port),
|
||||
id: isUuid ? r.credential : undefined,
|
||||
password: isUuid ? undefined : r.credential,
|
||||
method: r.protocol === 'ss' ? r.method : undefined,
|
||||
network: r.network,
|
||||
security: r.security,
|
||||
sni: r.sni || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function SubscriptionBuilder() {
|
||||
const [scheme, setScheme] = useState<'http' | 'https'>('https');
|
||||
const [host, setHost] = useState('sub.example.com');
|
||||
const [port, setPort] = useState('2096');
|
||||
const [subPath, setSubPath] = useState('/sub/');
|
||||
const [jsonPath, setJsonPath] = useState('/json/');
|
||||
const [subId, setSubId] = useState('user-1');
|
||||
const [behindProxy, setBehindProxy] = useState(false);
|
||||
const [clients, setClients] = useState<ClientRow[]>(DEFAULT_CLIENTS);
|
||||
|
||||
function patch(i: number, p: Partial<ClientRow>) {
|
||||
setClients((prev) => prev.map((c, j) => (i === j ? { ...c, ...p } : c)));
|
||||
}
|
||||
|
||||
const urlInput: SubUrlInput = { scheme, host, port: Number(port), subPath, jsonPath, subId, behindProxy };
|
||||
const urls = buildSubscriptionUrls(urlInput);
|
||||
const subClients = clients.filter((c) => c.address.trim()).map(toClient);
|
||||
|
||||
function reset() {
|
||||
setScheme('https');
|
||||
setHost('sub.example.com');
|
||||
setPort('2096');
|
||||
setSubPath('/sub/');
|
||||
setJsonPath('/json/');
|
||||
setSubId('user-1');
|
||||
setBehindProxy(false);
|
||||
setClients(DEFAULT_CLIENTS);
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Subscription & sub-JSON builder"
|
||||
description="Build the subscription URLs and preview both body formats — the Base64 link list and the JSON (Xray-json) config."
|
||||
onReset={reset}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Scheme"
|
||||
value={scheme}
|
||||
onChange={(v) => setScheme(v as 'http' | 'https')}
|
||||
options={['https', 'http']}
|
||||
/>
|
||||
<TextField label="Host" value={host} onChange={setHost} />
|
||||
<TextField label="Port" value={port} onChange={setPort} inputMode="numeric" />
|
||||
<TextField label="Sub ID" value={subId} onChange={setSubId} />
|
||||
<TextField label="Sub path" value={subPath} onChange={setSubPath} />
|
||||
<TextField label="JSON path" value={jsonPath} onChange={setJsonPath} />
|
||||
<CheckboxField
|
||||
label="Behind a reverse proxy (omit the port)"
|
||||
checked={behindProxy}
|
||||
onChange={setBehindProxy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="Base64 subscription URL" value={urls.base64} qr />
|
||||
<OutputBlock label="JSON subscription URL" value={urls.json} />
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex items-center justify-between">
|
||||
<h4 className="text-sm font-semibold">Clients in this subscription</h4>
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() =>
|
||||
setClients((p) => [
|
||||
...p,
|
||||
{
|
||||
protocol: 'vless',
|
||||
remark: '',
|
||||
address: '',
|
||||
port: '443',
|
||||
credential: '',
|
||||
method: '',
|
||||
network: 'tcp',
|
||||
security: 'reality',
|
||||
sni: '',
|
||||
},
|
||||
])
|
||||
}
|
||||
>
|
||||
Add client
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-col gap-3">
|
||||
{clients.map((c, i) => (
|
||||
<div key={i} className="rounded-xl border p-3">
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<SelectField
|
||||
label="Protocol"
|
||||
value={c.protocol}
|
||||
onChange={(v) => patch(i, { protocol: v as ClientProtocol })}
|
||||
options={PROTOCOLS}
|
||||
/>
|
||||
<TextField label="Remark" value={c.remark} onChange={(v) => patch(i, { remark: v })} />
|
||||
<TextField label="Address" value={c.address} onChange={(v) => patch(i, { address: v })} />
|
||||
<TextField label="Port" value={c.port} onChange={(v) => patch(i, { port: v })} inputMode="numeric" />
|
||||
<TextField
|
||||
label={c.protocol === 'vless' || c.protocol === 'vmess' ? 'UUID (id)' : 'Password'}
|
||||
value={c.credential}
|
||||
onChange={(v) => patch(i, { credential: v })}
|
||||
/>
|
||||
{c.protocol === 'ss' ? (
|
||||
<TextField label="Method" value={c.method} onChange={(v) => patch(i, { method: v })} />
|
||||
) : null}
|
||||
<SelectField
|
||||
label="Transport"
|
||||
value={c.network}
|
||||
onChange={(v) => patch(i, { network: v as Network })}
|
||||
options={NETWORKS}
|
||||
/>
|
||||
<SelectField
|
||||
label="Security"
|
||||
value={c.security}
|
||||
onChange={(v) => patch(i, { security: v as Security })}
|
||||
options={SECURITIES}
|
||||
/>
|
||||
{c.security !== 'none' ? (
|
||||
<TextField label="SNI" value={c.sni} onChange={(v) => patch(i, { sni: v })} />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className={addBtn}
|
||||
onClick={() => setClients((p) => p.filter((_, j) => j !== i))}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="Subscription links (decoded body)" value={buildShareLinks(subClients).join('\n')} />
|
||||
<OutputBlock label="Base64 body" value={buildBase64Subscription(subClients)} />
|
||||
<OutputBlock label="JSON subscription (preview)" value={buildJsonSubscription(subClients)} />
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
validateBotToken,
|
||||
parseAdminIds,
|
||||
validateRunTime,
|
||||
telegramApiBase,
|
||||
buildBotConfigSummary,
|
||||
} from '@/lib/xray/telegram';
|
||||
import { ToolFrame } from './tool-frame';
|
||||
import { TextField } from './shared/fields';
|
||||
import { OutputBlock } from './shared/output-block';
|
||||
|
||||
function Status({ ok, children }: { ok: boolean; children: ReactNode }) {
|
||||
return (
|
||||
<p
|
||||
className={`text-xs ${ok ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'}`}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function TelegramSetupHelper() {
|
||||
const [token, setToken] = useState('');
|
||||
const [adminIds, setAdminIds] = useState('');
|
||||
const [runTime, setRunTime] = useState('@daily');
|
||||
|
||||
const tokenV = validateBotToken(token);
|
||||
const idsV = parseAdminIds(adminIds);
|
||||
const cronV = validateRunTime(runTime);
|
||||
const summary = buildBotConfigSummary({ token, adminIds, runTime });
|
||||
|
||||
const settingsText = [
|
||||
`tgBotEnable = true`,
|
||||
`tgBotToken = ${summary.tgBotToken || '<token>'}`,
|
||||
`tgBotChatId = ${summary.tgBotChatId || '<admin ids>'}`,
|
||||
`tgRunTime = ${summary.tgRunTime || '@daily'}`,
|
||||
].join('\n');
|
||||
|
||||
function reset() {
|
||||
setToken('');
|
||||
setAdminIds('');
|
||||
setRunTime('@daily');
|
||||
}
|
||||
|
||||
return (
|
||||
<ToolFrame
|
||||
title="Telegram bot setup helper"
|
||||
description="Validate your bot token, admin IDs, and report schedule, then copy the panel settings."
|
||||
onReset={reset}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div>
|
||||
<TextField
|
||||
label="Bot token (from @BotFather)"
|
||||
value={token}
|
||||
onChange={setToken}
|
||||
placeholder="123456789:AA..."
|
||||
/>
|
||||
{token ? (
|
||||
tokenV.valid ? (
|
||||
<Status ok>Valid — bot id {tokenV.botId}</Status>
|
||||
) : (
|
||||
<Status ok={false}>{tokenV.error}</Status>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label="Admin chat IDs (comma-separated)"
|
||||
value={adminIds}
|
||||
onChange={setAdminIds}
|
||||
placeholder="111111111, 222222222"
|
||||
/>
|
||||
{adminIds ? (
|
||||
idsV.invalid.length > 0 ? (
|
||||
<Status ok={false}>Not numeric: {idsV.invalid.join(', ')}</Status>
|
||||
) : (
|
||||
<Status ok>
|
||||
{idsV.ids.length} admin id{idsV.ids.length === 1 ? '' : 's'}
|
||||
</Status>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
<div>
|
||||
<TextField
|
||||
label="Report schedule (tgRunTime)"
|
||||
value={runTime}
|
||||
onChange={setRunTime}
|
||||
placeholder="@daily, @every 8h, or a cron expression"
|
||||
/>
|
||||
{runTime ? (
|
||||
cronV.valid ? (
|
||||
<Status ok>Valid ({cronV.kind})</Status>
|
||||
) : (
|
||||
<Status ok={false}>{cronV.error}</Status>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 grid grid-cols-1 gap-4">
|
||||
<OutputBlock label="Panel settings" value={settingsText} />
|
||||
<OutputBlock
|
||||
label="Bot API base (keep secret)"
|
||||
value={tokenV.valid ? telegramApiBase(token) : 'https://api.telegram.org/bot<token>'}
|
||||
/>
|
||||
</div>
|
||||
</ToolFrame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function ToolFrame({
|
||||
title,
|
||||
description,
|
||||
onReset,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
onReset?: () => void;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section
|
||||
role="group"
|
||||
aria-label={title}
|
||||
className="not-prose my-6 overflow-hidden rounded-2xl border bg-fd-card text-fd-foreground"
|
||||
>
|
||||
<header className="flex items-start justify-between gap-3 border-b px-4 py-3">
|
||||
<div>
|
||||
<h3 className="font-semibold">{title}</h3>
|
||||
{description ? (
|
||||
<p className="mt-0.5 text-sm text-fd-muted-foreground">{description}</p>
|
||||
) : null}
|
||||
</div>
|
||||
{onReset ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onReset}
|
||||
className="inline-flex shrink-0 items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors hover:bg-fd-accent hover:text-fd-accent-foreground"
|
||||
>
|
||||
<RotateCcw className="size-3.5" aria-hidden />
|
||||
Reset
|
||||
</button>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="p-4">{children}</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Clients
|
||||
description: Manage 3x-ui clients — credentials, traffic and expiry limits, IP limits, groups, bulk actions, external links, and online status.
|
||||
icon: Users
|
||||
---
|
||||
|
||||
A **client** is a single user, identified by a unique **email**. In the current
|
||||
panel, clients are first-class records that can be attached to **multiple
|
||||
inbounds** at once, with per-client traffic accounting.
|
||||
|
||||
## Client fields
|
||||
|
||||
| Field | Applies to | Meaning |
|
||||
| -------------- | --------------------- | ------------------------------------------------------------------ |
|
||||
| **Email** | all | Unique identifier used for accounting and lookups. |
|
||||
| **ID (UUID)** | VLESS, VMess | The client credential. |
|
||||
| **Password** | Trojan, Shadowsocks | The client credential. |
|
||||
| **Auth** | Hysteria2 | The client credential. |
|
||||
| **Flow** | VLESS | XTLS flow, e.g. `xtls-rprx-vision`. |
|
||||
| **Limit IP** | all | Max simultaneous source IPs (enforced via Fail2ban). |
|
||||
| **Total (GB)** | all | Traffic quota; the client is disabled when exhausted. |
|
||||
| **Expiry** | all | Date after which the client stops working. |
|
||||
| **Reset** | all | Auto-renew period in **days** (rolls the quota over). |
|
||||
| **Telegram ID**| all | Links the client to a Telegram user for self-service/notifications.|
|
||||
| **Sub ID** | all | Subscription identifier grouping this client's links. |
|
||||
| **Group** | all | Optional client group for organization and bulk filtering. |
|
||||
| **Comment** | all | Free-text note. |
|
||||
|
||||
<Callout type="info">
|
||||
Reaching the **traffic** or **expiry** limit disables the client; the panel can
|
||||
restart Xray automatically when clients are auto-disabled
|
||||
(`restartXrayOnClientDisable`, on by default).
|
||||
</Callout>
|
||||
|
||||
## Limits and IP control
|
||||
|
||||
- **Traffic / expiry** caps disable the client when hit; a **Reset** period
|
||||
auto-renews the quota.
|
||||
- **Limit IP** caps simultaneous source IPs. Enforcement relies on Fail2ban —
|
||||
see [Security](/docs/operations/security). You can view a client's recent IPs
|
||||
and clear them from the client's actions.
|
||||
- **Online status** and **last-online** times are tracked per client (and per
|
||||
node in multi-node setups).
|
||||
|
||||
## Share links and external links
|
||||
|
||||
Every client has share links and a QR code for its inbounds, plus a combined
|
||||
[subscription](/docs/config/subscription). You can also attach **external
|
||||
links** to a client — extra `vless://`, `vmess://`, `trojan://`, `ss://`,
|
||||
`hysteria2://`, or `wireguard://` links, or a remote subscription URL — so they
|
||||
appear alongside the panel-generated ones in the client's subscription.
|
||||
|
||||
To inspect exactly what a link contains, paste it into the
|
||||
[share-link inspector](/docs/config/share-links).
|
||||
|
||||
## Bulk actions
|
||||
|
||||
For managing many clients at once, the panel supports bulk **create, enable,
|
||||
disable, delete, attach/detach** (to inbounds), **reset traffic**, and
|
||||
**adjust** (add days / add bytes / set flow). Maintenance actions also let you
|
||||
delete **depleted** clients (quota/expiry exhausted) and **orphaned** clients
|
||||
(not attached to any inbound).
|
||||
|
||||
<Callout type="warn">
|
||||
A client's share link contains its credential. Treat links and QR codes like
|
||||
passwords, and rotate the credential if one leaks.
|
||||
</Callout>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Inbounds & Protocols
|
||||
description: Create inbounds in 3x-ui — protocols, transports, traffic reset and expiry, and fallbacks that serve multiple protocols on one port.
|
||||
icon: ArrowDownToLine
|
||||
---
|
||||
|
||||
An **inbound** is a listener that accepts client connections on a port using a
|
||||
particular protocol and transport. Most of your day-to-day work is creating and
|
||||
managing inbounds and the clients inside them.
|
||||
|
||||
## Create an inbound
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Add an inbound
|
||||
|
||||
Open **Inbounds → Add**, give it a remark, pick a **protocol**, and choose a
|
||||
**port** and listen address.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Choose a transport and security
|
||||
|
||||
Pick the transport (TCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, …) and the security
|
||||
layer (none, TLS, or REALITY). See [Transports](/docs/config/transports) and
|
||||
[REALITY](/docs/config/reality).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add clients
|
||||
|
||||
Add one or more clients, each with its own credential, limits, and share link.
|
||||
See [Clients](/docs/config/clients).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Set traffic limit, expiry, and reset
|
||||
|
||||
Optionally cap total traffic and set an expiry date for the inbound, and choose a
|
||||
periodic **traffic reset** schedule: `never` (default), `hourly`, `daily`,
|
||||
`weekly`, or `monthly`.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Supported protocols
|
||||
|
||||
The inbound editor accepts these protocols:
|
||||
|
||||
| Protocol | Notes |
|
||||
| ---------------------- | ------------------------------------------------------------------------ |
|
||||
| **VLESS** | Lightweight; the basis for REALITY + XTLS-Vision. Recommended. |
|
||||
| **VMess** | Older but very widely supported by clients. |
|
||||
| **Trojan** | TLS-based; supports XTLS and fallbacks. |
|
||||
| **Shadowsocks** | Includes Shadowsocks-2022 (`2022-blake3-*`) ciphers. |
|
||||
| **WireGuard** | Modern tunnel. |
|
||||
| **Hysteria2** | Selected as `hysteria`; the panel emits `hysteria2://` links. |
|
||||
| **HTTP** | HTTP proxy. |
|
||||
| **Mixed (SOCKS/HTTP)** | A combined SOCKS + HTTP listener. |
|
||||
| **Dokodemo-door / Tunnel** | Port forwarding / traffic redirect. |
|
||||
| **MTProto** | Telegram MTProto proxy, served by a bundled `mtg` process (not Xray). |
|
||||
|
||||
<Callout type="info">
|
||||
Hysteria2 isn't a separate protocol internally — it's the `hysteria` protocol
|
||||
with the transport version set to 2, and the panel generates `hysteria2://`
|
||||
share links for it.
|
||||
</Callout>
|
||||
|
||||
## Fallbacks — multiple protocols on one port
|
||||
|
||||
Fallbacks let a single TLS port (e.g. `443`) serve more than one protocol — for
|
||||
example VLESS **and** Trojan — by routing unmatched handshakes to a child
|
||||
inbound. In 3x-ui, fallbacks are managed in the panel (a master inbound's
|
||||
**Fallbacks** list) rather than hand-written into JSON.
|
||||
|
||||
Fallbacks are available only when the master inbound is:
|
||||
|
||||
- **VLESS** or **Trojan**,
|
||||
- on the raw **TCP** transport,
|
||||
- with **TLS** or **REALITY** security.
|
||||
|
||||
Each fallback rule targets a child inbound and can match on `path`, `alpn`, and
|
||||
`dest`. Client share links for a fallback child are automatically rewritten to
|
||||
advertise the master's address, port, and TLS.
|
||||
|
||||
## Not sure which to pick?
|
||||
|
||||
Use the wizard to get a recommendation based on your goals and clients:
|
||||
|
||||
<ProtocolWizard />
|
||||
|
||||
<Callout type="info">
|
||||
For censorship resistance with modern clients, **VLESS + REALITY +
|
||||
XTLS-Vision** is the usual best choice — continue to
|
||||
[REALITY](/docs/config/reality).
|
||||
</Callout>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "Configuration",
|
||||
"icon": "Settings",
|
||||
"pages": [
|
||||
"panel",
|
||||
"ssl-certificates",
|
||||
"inbounds",
|
||||
"reality",
|
||||
"transports",
|
||||
"clients",
|
||||
"subscription",
|
||||
"share-links"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Panel Settings
|
||||
description: Every 3x-ui panel setting — web server, TLS, display, security, and notifications — with defaults from the source.
|
||||
icon: SlidersHorizontal
|
||||
---
|
||||
|
||||
**Panel Settings** controls how the panel itself is served and secured (separate
|
||||
from your inbounds and clients). Settings are stored as key/value pairs; the
|
||||
defaults below come straight from the panel source. Secrets (tokens, passwords)
|
||||
are shown only as a "set / not set" indicator and are never returned to the
|
||||
browser in full.
|
||||
|
||||
## Web server
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| ------------------- | ----------------------- | ----------------------------------------------------------------------- |
|
||||
| `webPort` | `2053` | Panel port (1–65535). The `XUI_PORT` env var overrides it at runtime. |
|
||||
| `webListen` | _(all interfaces)_ | Bind the panel to a specific IP. |
|
||||
| `webBasePath` | `/` | URL path the panel is served under (always normalized to `/…/`). |
|
||||
| `webCertFile` / `webKeyFile` | _(none)_ | TLS certificate + key. When both are set, the panel serves **HTTPS**. |
|
||||
| `sessionMaxAge` | `360` | Session lifetime in **minutes** (default 6 hours). |
|
||||
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IPs/CIDRs whose forwarded headers (real client IP) are trusted. |
|
||||
| `panelOutbound` | _(none)_ | Route the panel's own egress (update checks, Telegram, geo/sub fetches) through a named Xray outbound. |
|
||||
|
||||
After changing the port or base path, the panel URL becomes
|
||||
`http(s)://<server>:<port><web-base-path>`. You can preset the base path on first
|
||||
launch with [`XUI_INIT_WEB_BASE_PATH`](/docs/reference/env-vars).
|
||||
|
||||
### TLS
|
||||
|
||||
Serving the panel over HTTPS protects your credentials in transit. Either set
|
||||
`webCertFile` + `webKeyFile` — the [`x-ui` SSL menu](/docs/config/ssl-certificates)
|
||||
can obtain a Let's Encrypt certificate for you — or terminate TLS at a
|
||||
[reverse proxy](/docs/operations/reverse-proxy).
|
||||
|
||||
<Callout type="warn">
|
||||
Never expose the panel over plain HTTP on the public internet. Use TLS, a
|
||||
non-default port, and a long random web base path.
|
||||
</Callout>
|
||||
|
||||
## Display
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| ---------------- | ------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| `pageSize` | `25` | Rows per page in lists (`0` disables pagination). |
|
||||
| `expireDiff` | `0` | Days before expiry to start warning. |
|
||||
| `trafficDiff` | `0` | Percent of quota remaining at which to start warning. |
|
||||
| `remarkTemplate` | `{{INBOUND}}-{{EMAIL}}\|📊{{TRAFFIC_LEFT}}\|⏳{{DAYS_LEFT}}D` | Default client remark template (see [Share links](/docs/config/share-links#remark-template-variables)). |
|
||||
| `timeLocation` | `Local` | Time zone for stats and expiry. |
|
||||
| `datepicker` | `gregorian` | Calendar for date inputs (Gregorian or Jalali/Persian). |
|
||||
|
||||
## Security & authentication
|
||||
|
||||
Credentials, two-factor auth, the brute-force limiter, sessions, and LDAP are
|
||||
covered in [First login](/docs/guide/first-login) and
|
||||
[Security](/docs/operations/security). In short:
|
||||
|
||||
- Passwords are stored as **bcrypt** hashes; changing them logs out all sessions.
|
||||
- **2FA (TOTP)** can be required at login.
|
||||
- An **LDAP** fallback can authenticate users when the local password check fails.
|
||||
- API access uses **API tokens** managed under Panel Settings (see the
|
||||
[API reference](/docs/reference/api/api-tokens)).
|
||||
|
||||
## Notifications & subscription
|
||||
|
||||
These have their own settings groups and pages:
|
||||
|
||||
<Cards>
|
||||
<Card title="Telegram bot" href="/docs/operations/telegram-bot" description="Token, chat IDs, alerts, and reports." />
|
||||
<Card title="Subscription" href="/docs/config/subscription" description="Subscription server, formats, and paths." />
|
||||
<Card title="Security" href="/docs/operations/security" description="2FA, IP limits, and hardening." />
|
||||
</Cards>
|
||||
|
||||
<Callout type="info">
|
||||
Email (SMTP) notifications are also configurable (host, port, encryption,
|
||||
recipients) with the same event types as the Telegram bot.
|
||||
</Callout>
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: REALITY
|
||||
description: Set up a VLESS + REALITY inbound with XTLS-Vision in 3x-ui — keys, short IDs, SNI, fingerprints, and common pitfalls.
|
||||
icon: ShieldCheck
|
||||
---
|
||||
|
||||
**REALITY** is an Xray transport security that disguises your proxy as ordinary
|
||||
traffic to a real, popular website. Unlike classic TLS, your server needs **no
|
||||
certificate of its own** — it borrows the TLS handshake of the target site
|
||||
(`dest`). Combined with the **XTLS-Vision** flow, it is fast and resistant to
|
||||
deep-packet inspection.
|
||||
|
||||
REALITY is used with **VLESS** (and Trojan). The recommended flow is
|
||||
`xtls-rprx-vision`.
|
||||
|
||||
## Key settings
|
||||
|
||||
When you choose **REALITY** as the security mode on a VLESS inbound, 3x-ui
|
||||
exposes these fields:
|
||||
|
||||
| Field | What it is |
|
||||
| ------------------------ | ------------------------------------------------------------------ |
|
||||
| **Dest (target)** | A real TLS site to impersonate, e.g. `www.microsoft.com:443`. |
|
||||
| **SNI / Server Names** | The hostname(s) clients send; must match the target's certificate. |
|
||||
| **Public / Private key** | An **x25519** keypair. The private key stays on the server. |
|
||||
| **Short IDs** | Hex strings used to authenticate clients (you can have several). |
|
||||
| **Flow** | Set to `xtls-rprx-vision`. |
|
||||
| **Fingerprint (uTLS)** | The client TLS fingerprint to mimic, e.g. `chrome`. |
|
||||
|
||||
The private key is generated with Xray's `x25519` utility (the panel can
|
||||
generate the pair for you):
|
||||
|
||||
```bash title="generate an x25519 keypair"
|
||||
xray x25519
|
||||
```
|
||||
|
||||
## Set it up in the panel
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Create a VLESS inbound
|
||||
|
||||
Add a new inbound, choose protocol **VLESS**, and set **Security** to
|
||||
**reality**.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Choose a target (dest) and SNI
|
||||
|
||||
Pick a reputable site that supports TLS 1.3 and HTTP/2 and is reachable from your
|
||||
server and your clients (for example `www.microsoft.com:443`). Set the server
|
||||
names / SNI to match that site's certificate.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Generate keys and short IDs
|
||||
|
||||
Generate the x25519 keypair and one or more short IDs. Keep the **private key**
|
||||
secret; clients only ever receive the **public key**.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Set the flow and fingerprint
|
||||
|
||||
Use the `xtls-rprx-vision` flow and a common uTLS fingerprint such as `chrome`.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Add a client and share the link
|
||||
|
||||
Create a client, then use its share link or QR code in a compatible app
|
||||
(v2rayNG, Hiddify, Mihomo, and others).
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## What the configuration looks like
|
||||
|
||||
On the server, a REALITY inbound's `streamSettings` looks roughly like this:
|
||||
|
||||
```json title="server inbound (excerpt)"
|
||||
{
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"dest": "www.microsoft.com:443",
|
||||
"serverNames": ["www.microsoft.com"],
|
||||
"privateKey": "<x25519 private key>",
|
||||
"shortIds": ["<hex short id>"],
|
||||
"fingerprint": "chrome"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The matching client share link carries the **public** parameters:
|
||||
|
||||
```text title="vless:// (excerpt)"
|
||||
vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni=www.microsoft.com&fp=chrome&spx=%2F&flow=xtls-rprx-vision#my-reality
|
||||
```
|
||||
|
||||
- `pbk` — REALITY **public** key
|
||||
- `sid` — short ID (matches one on the server)
|
||||
- `sni` — server name (matches the target's certificate)
|
||||
- `fp` — client fingerprint
|
||||
- `spx` — spiderX path
|
||||
- `flow` — `xtls-rprx-vision`
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
<Callout type="warn">
|
||||
|
||||
- **Bad target.** The `dest` must be a real site that supports **TLS 1.3** and
|
||||
**HTTP/2**, is reachable, and isn't blocked in your region. Pick a site you do
|
||||
not own and that sees lots of traffic.
|
||||
- **SNI mismatch.** The SNI / server names must match the target's real
|
||||
certificate, or the handshake gives the disguise away.
|
||||
- **Leaked private key.** Only ever distribute the **public** key to clients.
|
||||
- **Wrong flow.** REALITY + XTLS-Vision needs `flow = xtls-rprx-vision` on both
|
||||
the inbound client entry and the share link.
|
||||
|
||||
</Callout>
|
||||
|
||||
## Generate a config
|
||||
|
||||
Use the generator below to create a fresh X25519 keypair, UUID, and short ID,
|
||||
then copy the server inbound JSON and the client share link. Everything is
|
||||
computed **in your browser** — no keys or links are sent anywhere.
|
||||
|
||||
<RealityConfigGenerator />
|
||||
|
||||
<Callout type="info">
|
||||
The **private key** belongs only on your server. Share the generated
|
||||
`vless://` link (which contains the **public** key) with clients.
|
||||
</Callout>
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Share Links
|
||||
description: 3x-ui share-link formats (vless, vmess, trojan, ss, hysteria2, mtproto), the remark template variables, and an in-browser link inspector.
|
||||
icon: Link
|
||||
---
|
||||
|
||||
3x-ui generates a **share link** (and QR code) for each client. Client apps such
|
||||
as v2rayNG, Hiddify, and Mihomo import these links to configure themselves.
|
||||
|
||||
## Link formats
|
||||
|
||||
| Scheme | Shape |
|
||||
| -------------- | --------------------------------------------------------------- |
|
||||
| `vless://` | `vless://<uuid>@<host>:<port>?<params>#<remark>` |
|
||||
| `vmess://` | `vmess://<base64-json>` (a base64-encoded JSON object) |
|
||||
| `trojan://` | `trojan://<password>@<host>:<port>?<params>#<remark>` |
|
||||
| `ss://` | `ss://<userinfo>@<host>:<port>?<params>#<remark>` (SIP002; Shadowsocks-2022 uses percent-encoded userinfo) |
|
||||
| `hysteria2://` | `hysteria2://<auth>@<host>:<port>?<params>#<remark>` |
|
||||
| `tg://proxy` | `tg://proxy?server=…&port=…&secret=…` (MTProto) |
|
||||
|
||||
The query parameters carry the transport and security settings — `security`,
|
||||
`sni`, `fp`, `pbk`, `sid`, `spx`, `flow`, `type`, `path`, `host`, `alpn`, and
|
||||
more.
|
||||
|
||||
## Inspect a link
|
||||
|
||||
Paste any share link to decode every field. Parsing happens **entirely in your
|
||||
browser** — the link is never sent over the network.
|
||||
|
||||
<ShareLinkInspector />
|
||||
|
||||
<Callout type="warn">
|
||||
Share links contain everything needed to connect as a client, including the
|
||||
client's credential. Treat them like passwords.
|
||||
</Callout>
|
||||
|
||||
## Remark template variables
|
||||
|
||||
The text after `#` in each link (the **remark**) is generated from a template
|
||||
you control in Panel Settings (`remarkTemplate`). The default is:
|
||||
|
||||
```text
|
||||
{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D
|
||||
```
|
||||
|
||||
Tokens use `{{UPPER_CASE}}` syntax. The template is split on `|` into segments;
|
||||
a segment whose only value is the unlimited marker `∞` (for `TRAFFIC_LEFT`,
|
||||
`TRAFFIC_TOTAL`, `DAYS_LEFT`, or `TIME_LEFT`) is dropped, so unlimited clients
|
||||
don't show empty decorations.
|
||||
|
||||
### Available tokens
|
||||
|
||||
| Token | Value |
|
||||
| ----- | ----- |
|
||||
| `{{EMAIL}}` / `{{USERNAME}}` | Client email (identifier) |
|
||||
| `{{INBOUND}}` | Inbound remark |
|
||||
| `{{HOST}}` | Host-row remark (managed hosts) |
|
||||
| `{{ID}}` / `{{SHORT_ID}}` | Client UUID / its first 8 chars |
|
||||
| `{{TELEGRAM_ID}}` · `{{SUB_ID}}` · `{{COMMENT}}` | Telegram ID, subscription ID, comment |
|
||||
| `{{STATUS}}` / `{{STATUS_EMOJI}}` | `active`/`expired`/`depleted`/`disabled` (or ✅⏳🚫) |
|
||||
| `{{DAYS_LEFT}}` / `{{TIME_LEFT}}` | Days, or `Xd Xh Xm`, remaining (`∞` if unlimited) |
|
||||
| `{{EXPIRE_DATE}}` / `{{JALALI_EXPIRE_DATE}}` / `{{EXPIRE_UNIX}}` | Expiry as Gregorian / Jalali date / Unix seconds |
|
||||
| `{{CREATED_UNIX}}` | Creation time (Unix seconds) |
|
||||
| `{{TRAFFIC_USED}}` / `{{TRAFFIC_LEFT}}` / `{{TRAFFIC_TOTAL}}` | Human-readable usage (`∞` if unlimited) |
|
||||
| `{{TRAFFIC_USED_BYTES}}` / `{{TRAFFIC_LEFT_BYTES}}` / `{{TRAFFIC_TOTAL_BYTES}}` | Same, in bytes |
|
||||
| `{{UP}}` / `{{DOWN}}` | Upload / download (human-readable) |
|
||||
| `{{RESET_DAYS}}` · `{{USAGE_PERCENTAGE}}` | Reset period (days) · used percent |
|
||||
| `{{PROTOCOL}}` / `{{TRANSPORT}}` / `{{SECURITY}}` | e.g. `VLESS` / `ws` / `REALITY` |
|
||||
|
||||
<Callout type="info">
|
||||
Usage tokens (traffic, days, status) appear in the subscription **body** but
|
||||
are stripped from the display/QR view, so a shared QR doesn't leak a client's
|
||||
remaining quota. Date tokens follow the `datepicker` setting (Gregorian or
|
||||
Jalali).
|
||||
</Callout>
|
||||
|
||||
## Related
|
||||
|
||||
<Cards>
|
||||
<Card title="REALITY" href="/docs/config/reality" description="Generate a VLESS + REALITY config and link." />
|
||||
<Card title="Subscription" href="/docs/config/subscription" description="Serve all of a client's links from one URL." />
|
||||
</Cards>
|
||||
@@ -0,0 +1,181 @@
|
||||
---
|
||||
title: SSL Certificates
|
||||
description: Get and renew TLS certificates for the 3x-ui panel and inbounds — with the x-ui ACME menu (domain or bare IP), a Cloudflare DNS-01 wildcard, or manual Certbot.
|
||||
icon: ShieldCheck
|
||||
---
|
||||
|
||||
A TLS certificate lets you serve the **panel** over HTTPS (so your login and API
|
||||
traffic are encrypted) and terminate TLS on **inbounds** (VLESS-TLS, Trojan,
|
||||
Shadowsocks-TLS, and friends). There are three ways to obtain one:
|
||||
|
||||
- **The `x-ui` menu** — built-in [ACME](https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment)
|
||||
client. Easiest for a single domain or a bare IP.
|
||||
- **Cloudflare DNS-01** — also from the menu; needed for **wildcard** certs or
|
||||
when port 80 is blocked / the server sits behind Cloudflare's proxy.
|
||||
- **Manual Certbot** — if you'd rather manage `acme.sh`/Certbot yourself.
|
||||
|
||||
<Callout type="info">
|
||||
If you put the panel behind Nginx or Caddy, let the proxy handle the
|
||||
certificate instead — see [Reverse proxy](/docs/operations/reverse-proxy).
|
||||
[REALITY](/docs/config/reality) inbounds need **no** certificate at all; they
|
||||
borrow a real site's TLS. This page is for the panel and for classic TLS
|
||||
inbounds.
|
||||
</Callout>
|
||||
|
||||
## The `x-ui` SSL menu (Let's Encrypt)
|
||||
|
||||
Run `x-ui` and choose **`20` — SSL Certificate Management**. It drives
|
||||
[acme.sh](https://github.com/acmesh-official/acme.sh) and offers:
|
||||
|
||||
| Option | What it does |
|
||||
| ------------------------------ | ------------------------------------------------------------------- |
|
||||
| Get SSL (Domain) | Issue a certificate for a domain via HTTP validation. |
|
||||
| Get SSL for IP Address | Issue a short-lived (6-day, auto-renewing) cert for a **bare IP**. |
|
||||
| Revoke | Revoke an existing certificate. |
|
||||
| Force Renew | Renew now, before expiry. |
|
||||
| Show Existing Domains | List certificates already on the server. |
|
||||
| Set Cert paths for the panel | Point the panel's TLS at an issued cert (sets the fields for you). |
|
||||
|
||||
### Issue a certificate for a domain
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Point the domain at the server
|
||||
|
||||
Create an `A` (and/or `AAAA`) record for your domain that resolves to this
|
||||
server's public IP. Validation fails until DNS has propagated.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Free up port 80
|
||||
|
||||
HTTP validation needs **port 80** reachable from the internet and not already in
|
||||
use. Stop anything bound to it for the duration, and allow it through the
|
||||
[firewall](/docs/reference/ports-firewall).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Run the issuer
|
||||
|
||||
`x-ui` → `20` → **Get SSL (Domain)**, then enter the domain. acme.sh requests
|
||||
the certificate and saves it under `/root/cert/<domain>/` as `fullchain.pem`
|
||||
(the certificate chain) and `privkey.pem` (the private key).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Wire it into the panel
|
||||
|
||||
Choose **Set Cert paths for the panel** to fill in `webCertFile` and
|
||||
`webKeyFile` and restart the panel, or set them yourself in
|
||||
[Panel Settings](/docs/config/panel#tls). The panel serves HTTPS as soon as both
|
||||
are set.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
### Issue a certificate for a bare IP
|
||||
|
||||
No domain? Choose **Get SSL for IP Address** to obtain a short-lived
|
||||
certificate (valid ~6 days, renewed automatically) bound to the server's IP.
|
||||
Useful for reaching the panel over HTTPS before you've set up a domain.
|
||||
|
||||
## Cloudflare (DNS-01 wildcard)
|
||||
|
||||
DNS validation proves you control the domain by creating a TXT record instead of
|
||||
answering on port 80 — so it works **behind Cloudflare's proxy**, on servers
|
||||
where port 80 is blocked, and for **wildcard** certificates (`*.example.com`).
|
||||
|
||||
Your domain's DNS must be managed by Cloudflare, and you need one of:
|
||||
|
||||
- a **scoped API token** with the `Zone:DNS:Edit` permission (recommended), or
|
||||
- your account **email + Global API Key**.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Create a scoped API token
|
||||
|
||||
In the Cloudflare dashboard go to **My Profile → API Tokens →
|
||||
[Create Token](https://dash.cloudflare.com/profile/api-tokens)**, pick the
|
||||
**Edit zone DNS** template, scope it to the zone you're issuing for, and create
|
||||
it. Copy the token — it's shown only once.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Run the Cloudflare issuer
|
||||
|
||||
`x-ui` → **`21` — Cloudflare SSL Certificate**. When asked, choose **`t`** for an
|
||||
API token (the default) or **`g`** for the Global API Key, then enter your
|
||||
domain (and, for the Global API Key, your account email and key). acme.sh creates
|
||||
the TXT record, validates, and cleans it up.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Point the panel at it
|
||||
|
||||
As with the domain flow, use **Set Cert paths for the panel** (menu `20`) or set
|
||||
`webCertFile` / `webKeyFile` in [Panel Settings](/docs/config/panel#tls).
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Prefer a scoped token over the Global API Key — it only grants DNS edits on the
|
||||
zone you choose, so a leak can't touch the rest of your Cloudflare account.
|
||||
</Callout>
|
||||
|
||||
## Manual (Certbot)
|
||||
|
||||
If you'd rather not use the menu, issue a certificate with Certbot's standalone
|
||||
plugin (again, this needs port 80 free and the domain resolving to the server):
|
||||
|
||||
```bash
|
||||
apt-get install certbot -y
|
||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
Certbot writes the certificate to `/etc/letsencrypt/live/yourdomain.com/`
|
||||
(`fullchain.pem` and `privkey.pem`). Point the panel at those two files in
|
||||
[Panel Settings](/docs/config/panel#tls), and set up renewal — `certbot renew`
|
||||
runs on a systemd timer by default.
|
||||
|
||||
## Using the certificate
|
||||
|
||||
- **Panel** — set `webCertFile` (the full chain) and `webKeyFile` (the private
|
||||
key) in [Panel Settings](/docs/config/panel#tls). Both must be set for the
|
||||
panel to switch to HTTPS. Menu option **`11` — View Current Settings** prints
|
||||
the paths currently in use.
|
||||
- **Inbounds** — when you enable TLS on an inbound, reference the same
|
||||
certificate and key files (or paste their contents) in the inbound's TLS
|
||||
settings. See [Inbounds](/docs/config/inbounds) and
|
||||
[Transports](/docs/config/transports).
|
||||
|
||||
<Callout type="warn">
|
||||
Certificates expire (Let's Encrypt: 90 days; IP certs: ~6 days). The menu and
|
||||
Certbot both renew automatically, but the panel keeps reading the **files** at
|
||||
their fixed paths — so renew **in place** rather than moving the files, and the
|
||||
panel picks up the new cert on its next restart. **Force Renew** (menu `20`)
|
||||
triggers a renewal on demand.
|
||||
</Callout>
|
||||
|
||||
## Next steps
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="Panel Settings"
|
||||
href="/docs/config/panel#tls"
|
||||
description="webCertFile / webKeyFile and the rest of the web-server settings."
|
||||
/>
|
||||
<Card
|
||||
title="Reverse proxy"
|
||||
href="/docs/operations/reverse-proxy"
|
||||
description="Let Nginx or Caddy terminate TLS for you instead."
|
||||
/>
|
||||
<Card
|
||||
title="REALITY"
|
||||
href="/docs/config/reality"
|
||||
description="Stealth TLS for inbounds — no certificate required."
|
||||
/>
|
||||
</Cards>
|
||||
@@ -0,0 +1,86 @@
|
||||
---
|
||||
title: Subscription
|
||||
description: Run the 3x-ui subscription server — base64/JSON/Clash formats, ports and paths, TLS, response headers, and custom templates.
|
||||
icon: Rss
|
||||
---
|
||||
|
||||
A **subscription** is a single URL that returns all of a client's
|
||||
configurations. Client apps refresh it periodically, so when you change an
|
||||
inbound, clients pick up the change automatically. The subscription server runs
|
||||
as a **separate** server from the panel.
|
||||
|
||||
## Enable and configure
|
||||
|
||||
The subscription server is **on by default** (`subEnable`). Configure it in the
|
||||
panel's subscription settings:
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| ------------- | ------- | --------------------------------------------------------------- |
|
||||
| `subPort` | `2096` | Listen port (separate from the panel). |
|
||||
| `subListen` | _(all)_ | Bind address. |
|
||||
| `subPath` | `/sub/` | Base path for raw subscription URLs. |
|
||||
| `subDomain` | _(none)_| Public host; if set, the server only answers for that Host. |
|
||||
| `subCertFile` / `subKeyFile` | _(none)_ | TLS cert + key — when set, the server serves **HTTPS**. |
|
||||
| `subEncrypt` | `true` | Base64-encode the raw subscription body. |
|
||||
| `subUpdates` | `12` | Suggested refresh interval (hours) sent to clients. |
|
||||
|
||||
A subscription URL looks like:
|
||||
|
||||
```text
|
||||
https://<sub-host>:<sub-port>/sub/<sub-id>
|
||||
```
|
||||
|
||||
where `<sub-id>` is the client's **Sub ID**.
|
||||
|
||||
The same Sub ID is served in several formats on different paths — the **Base64**
|
||||
list at `subPath` and the **JSON** (Xray-json) config at the JSON path. Build the
|
||||
URLs and preview both bodies here:
|
||||
|
||||
<SubscriptionBuilder />
|
||||
|
||||
## Output formats
|
||||
|
||||
The **format is chosen by path**, each with its own enable toggle:
|
||||
|
||||
| Format | Path | Enabled by | Output |
|
||||
| --------------------- | --------- | ---------------- | --------------------------------------------------- |
|
||||
| **Raw links** | `/sub/` | always (if on) | A list of `vless://`, `vmess://`, … links (base64-encoded when `subEncrypt` is on). |
|
||||
| **JSON** | `/json/` | `subJsonEnable` | Full Xray client config(s). |
|
||||
| **Clash / Mihomo** | `/clash/` | `subClashEnable` | YAML profile. |
|
||||
|
||||
Only enabled inbounds using **VLESS, VMess, Trojan, Shadowsocks, or Hysteria2**
|
||||
appear in a subscription, ordered by their sub-sort index. Requesting `/sub/`
|
||||
with an `Accept: text/html` header (or `?html=1`) returns a human-readable info
|
||||
page instead of the raw body.
|
||||
|
||||
### Base64 vs JSON
|
||||
|
||||
The **Base64** body is just the newline-joined share links, standard-base64
|
||||
encoded (toggle with `subEncrypt`). The **JSON** body wraps each client in a
|
||||
complete Xray client config — a fixed skeleton (local mixed/HTTP inbounds, DNS,
|
||||
routing, policy) plus a `proxy` outbound pointing at the inbound. 3x-ui emits a
|
||||
**single config object for one client and an array for several**, uses the flat
|
||||
outbound `settings` form (`address`/`port`/`id`, `level: 8`), and strips
|
||||
`sockopt` from `streamSettings`.
|
||||
|
||||
## Response headers
|
||||
|
||||
Subscriptions return standard headers that compatible apps read:
|
||||
|
||||
- **`Subscription-Userinfo`** — `upload`, `download`, `total` (bytes; `total=0`
|
||||
means unlimited) and `expire` (Unix seconds).
|
||||
- **`Profile-Update-Interval`** — refresh interval in hours (`subUpdates`).
|
||||
- **`Profile-Title`**, **`Support-Url`**, **`Profile-Web-Page-Url`**,
|
||||
**`Announce`** — optional branding shown by some clients.
|
||||
|
||||
## Custom page templates
|
||||
|
||||
Point `subThemeDir` at a folder containing a custom info-page template to brand
|
||||
the HTML subscription page. The per-client remark on each link is fully
|
||||
templated — see [Share links → remark variables](/docs/config/share-links#remark-template-variables).
|
||||
|
||||
<Callout type="info">
|
||||
Put the subscription server behind TLS (set `subCertFile`/`subKeyFile`, or a
|
||||
[reverse proxy](/docs/operations/reverse-proxy)) so subscription contents
|
||||
aren't exposed in transit.
|
||||
</Callout>
|
||||
@@ -0,0 +1,199 @@
|
||||
---
|
||||
title: Transports & Security
|
||||
description: Every transport 3x-ui exposes — TCP, mKCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, Hysteria — with their settings, plus FinalMask obfuscation, sockopt, TLS/REALITY, XTLS-Vision, and VLESS encryption.
|
||||
icon: Network
|
||||
---
|
||||
|
||||
A **transport** decides how packets are carried between client and server, a
|
||||
**security** layer decides how they're encrypted and disguised, and **FinalMask**
|
||||
can obfuscate what's left. The panel only offers valid combinations; this page
|
||||
lists every transport's settings and the rules the panel enforces.
|
||||
|
||||
## Transports
|
||||
|
||||
Pick the transport (the inbound's `network`) in the inbound/outbound form. Each
|
||||
network writes its own settings key on the wire (`tcpSettings`, `kcpSettings`, …).
|
||||
|
||||
| Transport | Settings key | When to use it |
|
||||
| --------------- | --------------------- | ---------------------------------------------------------------------- |
|
||||
| **TCP (Raw)** | `tcpSettings` | Lowest overhead. The basis for REALITY + XTLS-Vision and fallbacks; optional HTTP/1.1 header camouflage. |
|
||||
| **mKCP** | `kcpSettings` | Reliable protocol over **UDP** — trades bandwidth for lower latency on lossy links. Carries no TLS/REALITY. |
|
||||
| **WebSocket** | `wsSettings` | Works through CDNs and HTTP reverse proxies; very compatible. |
|
||||
| **gRPC** | `grpcSettings` | HTTP/2-based; multiplexes well and proxies cleanly through Nginx. |
|
||||
| **HTTPUpgrade** | `httpupgradeSettings` | CDN-friendly HTTP/1.1 `Upgrade`; lighter than full WebSocket. |
|
||||
| **XHTTP** | `xhttpSettings` | Modern stream-multiplexed HTTP transport; CDN-friendly and REALITY-capable. |
|
||||
| **Hysteria** | `hysteriaSettings` | QUIC-based transport — only for the **Hysteria2** protocol. |
|
||||
|
||||
<Callout type="info">
|
||||
**WireGuard** and **Tunnel** (dokodemo-door) inbounds expose no transport
|
||||
selector — their stream carries only security/sockopt. Earlier panels also
|
||||
exposed a raw **HTTP/2 (`http`)** transport; it has been superseded by **XHTTP**
|
||||
and is no longer selectable.
|
||||
</Callout>
|
||||
|
||||
### TCP (Raw) — `tcpSettings`
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| ------------------------------ | ------- | ----------------------------------------------------------------------- |
|
||||
| `acceptProxyProtocol` | `false` | Accept the PROXY protocol from an upstream proxy so the real client IP is preserved. |
|
||||
| `header.type` | `none` | `none`, or `http` for HTTP/1.1 camouflage. |
|
||||
| `header.request` / `response` | — | When `type: http`: method, path, version and a header map that mimic a normal HTTP exchange. |
|
||||
|
||||
### mKCP — `kcpSettings`
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| ------------------ | ----------- | ---------------------------------------------------------------- |
|
||||
| `mtu` | `1350` | Maximum transmission unit, in bytes (576–1460). |
|
||||
| `tti` | `20` | Transmission time interval, in ms (10–100). Lower = more responsive, more overhead. |
|
||||
| `uplinkCapacity` | `5` | Upload bandwidth budget, in **MB/s**. |
|
||||
| `downlinkCapacity` | `20` | Download bandwidth budget, in **MB/s**. |
|
||||
| `cwndMultiplier` | `1` | Congestion-window multiplier; raise to push harder on good links. |
|
||||
| `maxSendingWindow` | `2097152` | Upper bound on in-flight packets. |
|
||||
|
||||
<Callout type="info">
|
||||
mKCP can't carry TLS or REALITY. To disguise it, add a **FinalMask** UDP mask —
|
||||
the `mkcp-legacy` mask reproduces the classic header obfuscation that older Xray
|
||||
stored in `kcpSettings.header`/`seed` (those fields no longer exist here).
|
||||
</Callout>
|
||||
|
||||
### WebSocket — `wsSettings`
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| --------------------- | ------- | ---------------------------------------------------------------- |
|
||||
| `path` | `/` | Request path — route on it when several services share one host. |
|
||||
| `host` | _(none)_| `Host` header override (useful behind a CDN). |
|
||||
| `headers` | `{}` | Extra request headers. |
|
||||
| `heartbeatPeriod` | `0` | Seconds between keepalive pings; `0` disables them. |
|
||||
| `acceptProxyProtocol` | `false` | Accept the PROXY protocol from an upstream. |
|
||||
|
||||
### gRPC — `grpcSettings`
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| ------------- | ------- | --------------------------------------------------------- |
|
||||
| `serviceName` | _(none)_| gRPC service path; acts like a secret route. |
|
||||
| `authority` | _(none)_| `:authority` pseudo-header override. |
|
||||
| `multiMode` | `false` | Multiplex several streams over one connection. |
|
||||
|
||||
### HTTPUpgrade — `httpupgradeSettings`
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| --------------------- | ------- | --------------------------------------------- |
|
||||
| `path` | `/` | Request path. |
|
||||
| `host` | _(none)_| `Host` header override. |
|
||||
| `headers` | `{}` | Extra request headers. |
|
||||
| `acceptProxyProtocol` | `false` | Accept the PROXY protocol from an upstream. |
|
||||
|
||||
HTTPUpgrade is a one-shot HTTP/1.1 `Upgrade` with no WebSocket framing — there's
|
||||
no heartbeat field.
|
||||
|
||||
### XHTTP — `xhttpSettings`
|
||||
|
||||
XHTTP (SplitHTTP) has a large field set; the panel fills sensible defaults. The
|
||||
ones you'll usually touch:
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| ---------------------- | ----------- | ---------------------------------------------------------------------- |
|
||||
| `path` | `/` | Request path. |
|
||||
| `host` | _(none)_ | `Host` header override. |
|
||||
| `mode` | `auto` | `auto`, `packet-up`, `stream-up`, or `stream-one`. `packet-up` is the most CDN-compatible; `stream-*` are lower latency. |
|
||||
| `xPaddingBytes` | `100-1000` | Random padding range that blurs packet sizes. |
|
||||
| `scMaxBufferedPosts` | `30` | Server-side buffer for uploaded POSTs. |
|
||||
| `scStreamUpServerSecs` | `20-80` | Stream-up server window (dash range). |
|
||||
| `xmux` (`enableXmux`) | _(off)_ | Connection multiplexing — `maxConcurrency` `16-32`, `maxConnections` `6`, … Turn on for high concurrency. |
|
||||
|
||||
Session-ID fields (`sessionIDPlacement`, `sessionIDKey`, `sessionIDTable`,
|
||||
`sessionIDLength`) and the `scMin/MaxEachPostBytes` knobs are advanced; leave them
|
||||
empty unless you're matching a specific upstream.
|
||||
|
||||
### Hysteria — `hysteriaSettings`
|
||||
|
||||
Only valid when the protocol is **Hysteria2**.
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| ---------------- | ------- | ----------------------------------------------------------------------- |
|
||||
| `version` | `2` | Hysteria protocol version. |
|
||||
| `auth` | _(none)_| Shared authentication string. |
|
||||
| `udpIdleTimeout` | `60` | Seconds (2–600) before idle UDP sessions are dropped. |
|
||||
| `masquerade` | — | Disguise as an HTTP/3 server: `type` `proxy`/`file`/`string` with `url`/`dir`/`content`, plus `headers` and `statusCode`. |
|
||||
|
||||
## FinalMask — late-layer obfuscation
|
||||
|
||||
**FinalMask** wraps traffic **after** the transport and security layers, so it can
|
||||
disguise transports that don't carry TLS (like mKCP) or add a second skin on top of
|
||||
TLS. Masks are configured per direction:
|
||||
|
||||
- **TCP masks** — `fragment`, `sudoku`, `header-custom`.
|
||||
- **UDP masks** — `salamander`, `mkcp-legacy`, `header-custom`, `xdns`, `xicmp`,
|
||||
`noise`, `sudoku`, `realm`. (`mkcp-legacy` reproduces the old mKCP header
|
||||
obfuscation.)
|
||||
- **QUIC params** — congestion control (`reno`, `bbr`, `brutal`, `force-brutal`),
|
||||
Brutal up/down rates, `udpHop` (rotate the QUIC port across a range to dodge
|
||||
port blocking), and receive-window tuning.
|
||||
|
||||
FinalMask replaces the per-transport `header`/`seed` obfuscation that older Xray
|
||||
builds exposed.
|
||||
|
||||
## sockopt — low-level socket options
|
||||
|
||||
`sockopt` rides alongside any transport and tunes the underlying socket. The most
|
||||
useful fields:
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| --------------------- | ------- | ---------------------------------------------------------------- |
|
||||
| `tcpFastOpen` | `false` | Enable TCP Fast Open. |
|
||||
| `tcpcongestion` | `bbr` | Congestion control: `bbr`, `cubic`, or `reno`. |
|
||||
| `tproxy` | `off` | Transparent proxy mode: `off`, `redirect`, or `tproxy`. |
|
||||
| `domainStrategy` | `AsIs` | How addresses resolve (`UseIP`, `ForceIPv4`, …). |
|
||||
| `dialerProxy` | _(none)_| Chain this outbound's dialing through another outbound tag. |
|
||||
| `interface` | _(none)_| Bind to a specific network interface. |
|
||||
| `mark` | `0` | SO_MARK for policy routing (`0` = unset). |
|
||||
|
||||
Numeric fields left at `0` are omitted on the wire so Xray keeps OS defaults.
|
||||
Advanced entries (`happyEyeballs`, `customSockopt[]`, keepalive timers) are
|
||||
available for special cases.
|
||||
|
||||
## Security
|
||||
|
||||
The security layer is one of **`none`**, **`tls`**, or **`reality`**, with these
|
||||
eligibility rules:
|
||||
|
||||
| Security | Eligible transports | Eligible protocols |
|
||||
| ----------- | -------------------------------------------- | --------------------------------------------------- |
|
||||
| **TLS** | `tcp`, `ws`, `grpc`, `httpupgrade`, `xhttp` | VLESS, VMess, Trojan, Shadowsocks (Hysteria2 is always TLS) |
|
||||
| **REALITY** | `tcp`, `grpc`, `xhttp` | VLESS, Trojan |
|
||||
|
||||
mKCP and Hysteria don't take a separate TLS/REALITY layer — mKCP runs plaintext
|
||||
(obfuscate with FinalMask), and Hysteria is QUIC/TLS by design. REALITY disguises
|
||||
your server as a real TLS site and needs no certificate — see
|
||||
[REALITY](/docs/config/reality).
|
||||
|
||||
## XTLS-Vision flow
|
||||
|
||||
The `xtls-rprx-vision` flow is fast and DPI-resistant. It's available for
|
||||
**VLESS** when either:
|
||||
|
||||
- the transport is raw **TCP** with **TLS** or **REALITY** security (classic
|
||||
XTLS-Vision), or
|
||||
- the transport is **XHTTP** with VLESS encryption enabled (see below).
|
||||
|
||||
Set the flow on the VLESS **client**, not the inbound. With classic Vision on
|
||||
TCP, the panel can also offer a **Vision seed** once a client uses the flow.
|
||||
|
||||
## VLESS encryption (ML-KEM)
|
||||
|
||||
VLESS supports post-quantum **encryption** (ML-KEM / `mlkem768x25519`), stored in
|
||||
the inbound's `decryption` (server) and clients' `encryption` (for link
|
||||
generation). When enabled, it unlocks the Vision flow over XHTTP. Generate the
|
||||
keys from the panel's VLESS settings.
|
||||
|
||||
## Shadowsocks ciphers
|
||||
|
||||
Shadowsocks inbounds support both classic ciphers and **Shadowsocks-2022**
|
||||
(method names starting with `2022-blake3-`). Most ciphers are multi-user;
|
||||
`2022-blake3-chacha20-poly1305` is single-user.
|
||||
|
||||
<Callout type="info">
|
||||
Transports and security must match on both ends. The client's share link
|
||||
encodes them (`type=ws`, `security=reality`, `flow=xtls-rprx-vision`, …) —
|
||||
decode any link with the [share-link inspector](/docs/config/share-links).
|
||||
</Callout>
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
title: First Login
|
||||
description: Find your generated 3x-ui credentials, reach the panel, enable two-factor auth, and harden it before exposing anything.
|
||||
icon: KeyRound
|
||||
---
|
||||
|
||||
After installation, your first job is to log in and **secure the panel** before
|
||||
exposing anything else.
|
||||
|
||||
## Reach the panel
|
||||
|
||||
The panel is served at:
|
||||
|
||||
```text
|
||||
http://<your-server-ip>:<port>/<web-base-path>
|
||||
```
|
||||
|
||||
The default port is **2053** and the default base path is `/` — but a script
|
||||
install **randomly generates** the username, password, **port**, and web base
|
||||
path, so check your actual values.
|
||||
|
||||
### Find your credentials
|
||||
|
||||
A script install prints a credential summary when it finishes and also writes it
|
||||
to a root-only file:
|
||||
|
||||
```bash title="/etc/x-ui/install-result.env (mode 600)"
|
||||
XUI_USERNAME=...
|
||||
XUI_PASSWORD=...
|
||||
XUI_PANEL_PORT=...
|
||||
XUI_WEB_BASE_PATH=...
|
||||
XUI_ACCESS_URL=...
|
||||
XUI_API_TOKEN=...
|
||||
XUI_DB_TYPE=sqlite
|
||||
```
|
||||
|
||||
If you missed them, use the management tools:
|
||||
|
||||
```bash
|
||||
x-ui # menu → 11 (View Current Settings)
|
||||
x-ui settings # or the one-shot form
|
||||
```
|
||||
|
||||
For **Docker**, read the generated credentials from the container logs, or run
|
||||
`docker exec -it <container> x-ui setting -show`.
|
||||
|
||||
<Callout type="warn">
|
||||
If your panel still uses the default `admin` / `admin` (the panel warns when it
|
||||
does), change it immediately — before creating any inbounds.
|
||||
</Callout>
|
||||
|
||||
## Change credentials, port, and path
|
||||
|
||||
A non-default port and a long, random **web base path** make the panel much
|
||||
harder to find. Change them from **Panel Settings** in the UI, or from the
|
||||
`x-ui` menu:
|
||||
|
||||
- **7 — Reset Username & Password** (optionally disabling 2FA at the same time)
|
||||
- **8 — Reset Web Base Path** (randomizes it)
|
||||
- **10 — Change Port**
|
||||
|
||||
Changing your username or password **logs out all existing sessions** and, if
|
||||
two-factor auth was on, disables it.
|
||||
|
||||
## Two-factor authentication (2FA)
|
||||
|
||||
3x-ui supports TOTP two-factor auth (compatible with Google Authenticator, Aegis,
|
||||
etc.). Enable it in **Panel Settings** — once enabled, the login page asks for a
|
||||
6-digit code in addition to your password, and turning it on forces everyone to
|
||||
log in again. You can disable it from the menu's **Reset Username & Password**
|
||||
step or with `x-ui setting -resetTwoFactor`.
|
||||
|
||||
## Built-in login protection
|
||||
|
||||
- **Brute-force limiter:** after **5** failed logins from the same IP/username
|
||||
within 5 minutes, that combination is blocked for **15 minutes**.
|
||||
- **Generic errors:** the login page reports "wrong username or password" for
|
||||
both bad credentials and bad 2FA codes, so it leaks nothing.
|
||||
- **Sessions** last `sessionMaxAge` minutes (default **360** = 6 hours) and are
|
||||
invalidated when you change credentials.
|
||||
- **LDAP** can be enabled as an auth fallback in Panel Settings.
|
||||
|
||||
## Essential hardening checklist
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Set strong, unique credentials
|
||||
|
||||
Replace the generated (or `admin/admin`) username and password with strong values.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Use a non-default port and random base path
|
||||
|
||||
Move the panel off `2053` and serve it under a long random path.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Enable two-factor authentication
|
||||
|
||||
Turn on 2FA so a leaked password alone can't grant access.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Put the panel behind TLS
|
||||
|
||||
Use a valid certificate (via the `x-ui` menu's SSL management, or a reverse
|
||||
proxy) so the panel is only reachable over HTTPS.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Restrict access with a firewall
|
||||
|
||||
Open only the ports you actually need, and consider limiting panel access by IP.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Want the panel on a clean domain with automatic HTTPS? See
|
||||
[Reverse proxy](/docs/operations/reverse-proxy). For deeper hardening, see
|
||||
[Security](/docs/operations/security).
|
||||
</Callout>
|
||||
@@ -0,0 +1,71 @@
|
||||
---
|
||||
title: Meet 3x-ui
|
||||
description: A web panel for Xray-core — manage inbounds, protocols, clients, and subscriptions from your browser instead of editing JSON by hand.
|
||||
icon: Info
|
||||
---
|
||||
|
||||
**3x-ui** is a web control panel that sits on top of
|
||||
[Xray-core](https://github.com/XTLS/Xray-core), the proxy engine that actually
|
||||
moves your traffic. Instead of writing and reloading Xray's JSON configuration
|
||||
by hand, you manage everything — inbounds, protocols, clients, certificates,
|
||||
subscriptions — from a browser dashboard.
|
||||
|
||||
## How the pieces fit together
|
||||
|
||||
<Mermaid
|
||||
chart={`
|
||||
flowchart LR
|
||||
Admin["Admin browser"] -->|HTTPS panel| Panel["3x-ui panel"]
|
||||
Panel -->|writes config, reloads| Xray["Xray-core"]
|
||||
Panel --- DB[("SQLite or PostgreSQL")]
|
||||
Clients["Client apps"] -->|VLESS / VMess / Trojan / ...| Xray
|
||||
Xray -->|proxied traffic| Internet["Internet"]
|
||||
`}
|
||||
/>
|
||||
|
||||
- **The panel** is the management layer: it stores your configuration in a
|
||||
database, renders the dashboard, exposes a REST API, and writes the live Xray
|
||||
configuration.
|
||||
- **Xray-core** is the data plane: it terminates client connections on your
|
||||
**inbounds** and forwards traffic to its destination.
|
||||
- **Client apps** (such as v2rayNG, Clash/Mihomo, Hiddify, and others) connect
|
||||
using a share link or a subscription that the panel generates for each client.
|
||||
|
||||
## What it gives you
|
||||
|
||||
- A dashboard for **inbounds** across every major protocol — VLESS, VMess,
|
||||
Trojan, Shadowsocks, WireGuard, Hysteria2, SOCKS, HTTP, and Dokodemo-door.
|
||||
- First-class **REALITY** and **XTLS-Vision** support for stealthy, fast
|
||||
transports.
|
||||
- **Per-client** traffic quotas, expiry dates, IP limits, online status, and
|
||||
one-click share links / QR codes.
|
||||
- **Subscriptions** in VLESS, Clash/Mihomo, and JSON formats.
|
||||
- Operational tooling: **multi-node** management, a **Telegram bot**, backups,
|
||||
Fail2ban-based IP limiting, and a documented REST API.
|
||||
|
||||
## Under the hood
|
||||
|
||||
| Layer | Technology |
|
||||
| ------------ | -------------------------------------------- |
|
||||
| Backend | Go with the Gin web framework |
|
||||
| Frontend | TypeScript / React |
|
||||
| Database | SQLite (default) or PostgreSQL |
|
||||
| Proxy engine | Xray-core (bundled and managed by the panel) |
|
||||
|
||||
The default SQLite database lives at `/etc/x-ui/x-ui.db`, and the panel listens
|
||||
on port **2053** by default. Both are configurable — see
|
||||
[First login](/docs/guide/first-login) and the environment variable reference.
|
||||
|
||||
## Who it's for
|
||||
|
||||
3x-ui is aimed at anyone running their own Xray server: from a single personal
|
||||
VPS to operators managing many nodes and clients. If you want the power of
|
||||
Xray-core without living in JSON config files, this is for you.
|
||||
|
||||
<Callout type="info">
|
||||
3x-ui is an enhanced fork of the original X-UI project, adding broader protocol
|
||||
support, improved stability, per-client traffic accounting, multi-node
|
||||
management, and many quality-of-life features.
|
||||
</Callout>
|
||||
|
||||
Ready to install? Continue to [Installation](/docs/guide/installation).
|
||||
@@ -0,0 +1,148 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Install 3x-ui via the official script (stable, pinned, or dev-latest), unattended/cloud-init, or Docker — and choose SQLite or PostgreSQL.
|
||||
icon: Download
|
||||
---
|
||||
|
||||
3x-ui runs on a wide range of Linux distributions — Ubuntu, Debian, Armbian,
|
||||
Fedora, CentOS, RHEL, AlmaLinux, Rocky Linux, Oracle Linux, Amazon Linux,
|
||||
Virtuozzo, Arch, Manjaro, openSUSE (Tumbleweed/Leap), Alpine — and Windows,
|
||||
across `amd64`, `386`, `arm64`, `armv7`, `armv6`, `armv5`, and `s390x`.
|
||||
|
||||
<Callout type="warn">
|
||||
Run the script installer as **root** (or with `sudo`). It installs a service,
|
||||
sets up the `x-ui` management command, and enables the panel on boot.
|
||||
</Callout>
|
||||
|
||||
<Tabs items={['Script', 'Docker', 'Manual']}>
|
||||
|
||||
<Tab value="Script">
|
||||
|
||||
The official script is the recommended path. During installation it generates a
|
||||
**random** username, password, and access (web base) path, sets up the service,
|
||||
and installs the `x-ui` management command.
|
||||
|
||||
```bash title="latest stable"
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
Install a **specific version** by appending its tag:
|
||||
|
||||
```bash title="pinned version"
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v3.4.0
|
||||
```
|
||||
|
||||
Install the rolling **dev** build (the latest per-commit pre-release from `main`
|
||||
— not a stable release) by passing `dev-latest`:
|
||||
|
||||
```bash title="rolling dev build"
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) dev-latest
|
||||
```
|
||||
|
||||
When it finishes, note the printed login details and run `x-ui` to open the
|
||||
[management menu](/docs/guide/update-uninstall#the-x-ui-management-menu), then
|
||||
continue to [First login](/docs/guide/first-login).
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="Docker">
|
||||
|
||||
The default Compose setup uses SQLite. Clone the repo (or copy its
|
||||
`docker-compose.yml` and `Dockerfile`) and start it:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
To run with the bundled **PostgreSQL** service, uncomment the two `XUI_DB_*`
|
||||
lines in `docker-compose.yml` and start with the profile:
|
||||
|
||||
```bash
|
||||
docker compose --profile postgres up -d
|
||||
```
|
||||
|
||||
Prefer the prebuilt image? It's published to the GitHub Container Registry. The
|
||||
image bundles Fail2ban (for [IP limits](/docs/operations/security)), which bans
|
||||
with `iptables` and therefore needs `NET_ADMIN` (and `NET_RAW` for IPv6) —
|
||||
otherwise bans are logged but never applied:
|
||||
|
||||
```bash title="docker run"
|
||||
docker run -d \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=NET_RAW \
|
||||
-e XUI_ENABLE_FAIL2BAN=true \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--network=host \
|
||||
--restart=unless-stopped \
|
||||
--name 3x-ui \
|
||||
ghcr.io/mhsanaei/3x-ui:latest
|
||||
```
|
||||
|
||||
The `db/` volume holds the SQLite database (`/etc/x-ui/x-ui.db`) and `cert/`
|
||||
holds TLS certificates, so your data survives upgrades.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab value="Manual">
|
||||
|
||||
For advanced users, download a release archive for your architecture from the
|
||||
[releases page](https://github.com/MHSanaei/3x-ui/releases), extract it, and run
|
||||
the binary as a systemd service. The install script automates exactly these
|
||||
steps, so it's preferred unless you have a specific reason to install by hand.
|
||||
|
||||
</Tab>
|
||||
|
||||
</Tabs>
|
||||
|
||||
## Build your install command
|
||||
|
||||
Tailor the command to your setup:
|
||||
|
||||
<InstallCommandBuilder />
|
||||
|
||||
## Choose a database
|
||||
|
||||
You pick the storage backend at install time:
|
||||
|
||||
- **SQLite** (default) — a single file at `/etc/x-ui/x-ui.db`. Zero setup.
|
||||
- **PostgreSQL** — for high client counts or multi-node setups. The installer
|
||||
can install it locally or use a DSN you provide.
|
||||
|
||||
See [Database](/docs/reference/database) for details and SQLite→PostgreSQL
|
||||
migration.
|
||||
|
||||
## Unattended / cloud-init
|
||||
|
||||
The installer also runs **non-interactively** for automation. Set
|
||||
`XUI_NONINTERACTIVE=1` (or run with no TTY) and it installs end-to-end with zero
|
||||
prompts, generating random credentials and writing them to
|
||||
`/etc/x-ui/install-result.env`:
|
||||
|
||||
```bash
|
||||
XUI_NONINTERACTIVE=1 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
The repo's [`deploy/`](https://github.com/MHSanaei/3x-ui/tree/main/deploy)
|
||||
directory has ready-made **cloud-init** user-data for unattended installs on any
|
||||
cloud (Hetzner, AWS, DigitalOcean, Vultr, GCP, Azure, Oracle).
|
||||
|
||||
## Next steps
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="First login"
|
||||
href="/docs/guide/first-login"
|
||||
description="Reach the panel and secure it."
|
||||
/>
|
||||
<Card
|
||||
title="Update & uninstall"
|
||||
href="/docs/guide/update-uninstall"
|
||||
description="The x-ui menu, updates, and removal."
|
||||
/>
|
||||
<Card
|
||||
title="REALITY"
|
||||
href="/docs/config/reality"
|
||||
description="Configure your first stealthy inbound."
|
||||
/>
|
||||
</Cards>
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Getting Started",
|
||||
"icon": "Rocket",
|
||||
"pages": ["index", "installation", "first-login", "update-uninstall"]
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Update & Uninstall
|
||||
description: Manage 3x-ui with the x-ui menu and CLI — update (stable, dev, or legacy), change settings, and uninstall cleanly.
|
||||
icon: RefreshCw
|
||||
---
|
||||
|
||||
After a script install, the `x-ui` command is your control center. Run it with
|
||||
no arguments for the interactive menu, or pass a subcommand for a one-shot action.
|
||||
|
||||
```bash
|
||||
x-ui
|
||||
```
|
||||
|
||||
## The `x-ui` management menu
|
||||
|
||||
The menu (items `0`–`28`) shows the panel/Xray status at the top, then:
|
||||
|
||||
| # | Item | What it does |
|
||||
| ----- | -------------------------------------- | --------------------------------------------------------- |
|
||||
| 1 | Install | (Re)install from the remote script |
|
||||
| 2 | Update | Update to the latest **stable** release |
|
||||
| 3 | Update to Dev Channel (latest commit) | Update to the rolling `dev-latest` build |
|
||||
| 4 | Update Menu | Update just the `x-ui` menu script |
|
||||
| 5 | Legacy Version | Install a specific older version (prompts for a tag) |
|
||||
| 6 | Uninstall | Remove 3x-ui (see below) |
|
||||
| 7 | Reset Username & Password | Set new credentials; optionally disable 2FA |
|
||||
| 8 | Reset Web Base Path | Randomize the web base path |
|
||||
| 9 | Reset Settings | Reset panel settings (your account is preserved) |
|
||||
| 10 | Change Port | Change the panel port |
|
||||
| 11 | View Current Settings | Show username, port, web base path, cert paths |
|
||||
| 12–14 | Start / Stop / Restart | Control the panel service |
|
||||
| 15 | Restart Xray | Reload only Xray-core |
|
||||
| 16 | Check Status | Service status |
|
||||
| 17 | Logs Management | View debug logs / clear logs |
|
||||
| 18–19 | Enable / Disable Autostart | Toggle start-on-boot |
|
||||
| 20 | SSL Certificate Management | Let's Encrypt (domain or IP), custom paths, renew/revoke |
|
||||
| 21 | Cloudflare SSL Certificate | DNS-01 wildcard cert via Cloudflare |
|
||||
| 22 | IP Limit Management | Fail2ban-based per-client IP limits |
|
||||
| 23 | Firewall Management | `ufw` install and port rules |
|
||||
| 24 | SSH Port Forwarding Management | Bind the panel to localhost and tunnel over SSH |
|
||||
| 25 | PostgreSQL Management | Install/migrate/manage PostgreSQL |
|
||||
| 26 | Enable BBR | Toggle the BBR congestion-control sysctl |
|
||||
| 27 | Update Geo Files | Update geoip/geosite data (Loyalsoldier, IR, RU) |
|
||||
| 28 | Speedtest by Ookla | Run an Ookla speed test |
|
||||
| 0 | Exit | — |
|
||||
|
||||
Some of these have their own pages: [SSL certificates](/docs/config/ssl-certificates)
|
||||
(items 20–21), [Security](/docs/operations/security) (IP limits, firewall),
|
||||
[Reverse proxy](/docs/operations/reverse-proxy) and [Panel settings](/docs/config/panel)
|
||||
(TLS), and [Database](/docs/reference/database) (PostgreSQL).
|
||||
|
||||
## CLI subcommands
|
||||
|
||||
For scripts and quick actions, `x-ui` also takes a subcommand directly:
|
||||
|
||||
| Command | Action |
|
||||
| -------------------------- | --------------------------------------------------- |
|
||||
| `x-ui start` / `stop` / `restart` | Control the service |
|
||||
| `x-ui restart-xray` | Reload only Xray-core |
|
||||
| `x-ui status` | Show status |
|
||||
| `x-ui settings` | Show current settings |
|
||||
| `x-ui enable` / `disable` | Toggle autostart on boot |
|
||||
| `x-ui log` | Tail the debug log |
|
||||
| `x-ui banlog` | Show Fail2ban ban log |
|
||||
| `x-ui update` | Update to the latest stable release |
|
||||
| `x-ui update-dev` | Update to the rolling `dev-latest` build |
|
||||
| `x-ui legacy` | Install a specific older version (prompts) |
|
||||
| `x-ui update-all-geofiles` | Update all geo files, restart if changed |
|
||||
| `x-ui migrate-db --dsn …` | Migrate SQLite → PostgreSQL (see [Database](/docs/reference/database)) |
|
||||
| `x-ui install` / `uninstall` | Install / uninstall |
|
||||
|
||||
## Updating
|
||||
|
||||
- **Stable:** menu option **2** or `x-ui update`. Re-running the install script
|
||||
also updates in place.
|
||||
- **Dev channel:** menu option **3** or `x-ui update-dev` — the rolling
|
||||
`dev-latest` per-commit build (not a stable release).
|
||||
- **A specific older version:** menu option **5** (Legacy Version).
|
||||
|
||||
Updating preserves your database and settings. Take a
|
||||
[backup](/docs/operations/backup-restore) before a major-version jump.
|
||||
|
||||
<Callout type="info">
|
||||
Docker users update differently — pull the new image and recreate the
|
||||
container (`docker compose pull && docker compose up -d`) rather than using the
|
||||
`x-ui` update commands.
|
||||
</Callout>
|
||||
|
||||
## Uninstalling
|
||||
|
||||
Menu option **6** or `x-ui uninstall`. It stops and disables the service, removes
|
||||
the service unit, and deletes `/etc/x-ui/` and the install folder. If the panel
|
||||
used a locally-installed PostgreSQL, it offers to purge that too (a separate,
|
||||
irreversible confirmation).
|
||||
|
||||
<Callout type="warn">
|
||||
Uninstalling removes the database (`/etc/x-ui/x-ui.db`) and your configuration.
|
||||
[Back up](/docs/operations/backup-restore) first if you might need it.
|
||||
</Callout>
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Contributing
|
||||
description: How to contribute to 3x-ui and to translate this documentation.
|
||||
icon: GitPullRequestArrow
|
||||
---
|
||||
|
||||
3x-ui is community-driven. Contributions to the panel and to these docs are
|
||||
welcome.
|
||||
|
||||
## Support the developer
|
||||
|
||||
3x-ui is free and open source, built and maintained in the open. If it's useful
|
||||
to you, consider supporting continued development:
|
||||
|
||||
- **Donate** at [donate.sanaei.dev](https://donate.sanaei.dev/) — the page shows
|
||||
current funding **goals and targets** you can help reach.
|
||||
- **Star** the [repository](https://github.com/MHSanaei/3x-ui) and share the
|
||||
project.
|
||||
- **Join** the Telegram channel [@XrayUI](https://t.me/XrayUI) to follow news and
|
||||
help others.
|
||||
|
||||
## Contribute to 3x-ui
|
||||
|
||||
- Read the project's `CONTRIBUTING.md` in the
|
||||
[repository](https://github.com/MHSanaei/3x-ui).
|
||||
- Open issues for bugs and feature requests with clear reproduction steps.
|
||||
- Use Conventional Commits and keep pull requests focused.
|
||||
|
||||
## Translate the documentation
|
||||
|
||||
This site is built for translation. Content lives under `content/docs/<locale>/`
|
||||
(`en`, `fa`, `ru`, `zh`), and untranslated pages **fall back to English**, so you
|
||||
can translate incrementally.
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Copy a page
|
||||
|
||||
Copy a page from `content/docs/en/...` to the same path under your locale, e.g.
|
||||
`content/docs/fa/guide/installation.mdx`.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Translate the prose only
|
||||
|
||||
Translate the body and the frontmatter `title`/`description`. **Do not** translate
|
||||
code, commands, environment variable names, protocol names, or share links.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Mind direction
|
||||
|
||||
Persian (`fa`) renders right-to-left. Keep code blocks and links left-to-right
|
||||
(the layout already handles this), and check the page in both directions.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
<Callout type="info">
|
||||
Missing a page in your locale is fine — it falls back to English rather than
|
||||
404ing. Translate the highest-traffic pages first (installation, first login,
|
||||
REALITY).
|
||||
</Callout>
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: FAQ
|
||||
description: Frequently asked questions about 3x-ui — licensing, supported systems, databases, and clients.
|
||||
icon: CircleQuestionMark
|
||||
---
|
||||
|
||||
## Is 3x-ui free and open source?
|
||||
|
||||
Yes. 3x-ui is open source under the GPL-3.0 license. The source is on
|
||||
[GitHub](https://github.com/MHSanaei/3x-ui).
|
||||
|
||||
## What systems does it run on?
|
||||
|
||||
Most major Linux distributions (Ubuntu, Debian, CentOS/RHEL and derivatives,
|
||||
Fedora, Arch, Alpine, and more) across `amd64`, `arm64`, and other architectures.
|
||||
It also runs as a Docker container. See [Installation](/docs/guide/installation).
|
||||
|
||||
## SQLite or PostgreSQL?
|
||||
|
||||
SQLite is the default and is fine for most deployments. PostgreSQL is available
|
||||
for larger setups via `XUI_DB_TYPE=postgres` and `XUI_DB_DSN` — see the
|
||||
[environment variables](/docs/reference/env-vars).
|
||||
|
||||
## Which client apps work with it?
|
||||
|
||||
Any Xray-compatible client — for example v2rayNG, Hiddify, and Clash/Mihomo.
|
||||
Import a client's share link or QR code, or use a
|
||||
[subscription](/docs/config/subscription).
|
||||
|
||||
## How is 3x-ui different from x-ui?
|
||||
|
||||
3x-ui is an enhanced fork of the original X-UI project. It adds broader protocol
|
||||
support, improved stability, per-client traffic accounting, multi-node
|
||||
management, a 13-language UI, and many quality-of-life features.
|
||||
|
||||
## How do I update?
|
||||
|
||||
Re-run the install script (it updates in place), or pull the new Docker image.
|
||||
See [Installation](/docs/guide/installation) and the
|
||||
[releases page](https://github.com/MHSanaei/3x-ui/releases).
|
||||
|
||||
## Where can I get help?
|
||||
|
||||
Join the official Telegram channel [@XrayUI](https://t.me/XrayUI) for
|
||||
announcements and community support, or open an issue on
|
||||
[GitHub](https://github.com/MHSanaei/3x-ui/issues). For common problems, start
|
||||
with [Troubleshooting](/docs/help/troubleshooting).
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Help",
|
||||
"icon": "LifeBuoy",
|
||||
"pages": ["troubleshooting", "faq", "migration", "contributing"]
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
title: Migration
|
||||
description: Migrate to 3x-ui from x-ui, between servers, or between 3x-ui versions.
|
||||
icon: ArrowRightLeft
|
||||
---
|
||||
|
||||
## Between servers
|
||||
|
||||
Moving to a new server is a database move:
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Back up the old server
|
||||
|
||||
Copy the database (default `/etc/x-ui/x-ui.db`) and your certificates. See
|
||||
[Backup & restore](/docs/operations/backup-restore).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Install 3x-ui on the new server
|
||||
|
||||
Use the same install method and a compatible version. See
|
||||
[Installation](/docs/guide/installation).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Restore the database
|
||||
|
||||
Stop the panel, put the database in place, restore certificates, and start the
|
||||
panel. Update any IP/domain-specific settings.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Between 3x-ui versions
|
||||
|
||||
Within the same backend, upgrading is usually just re-running the installer or
|
||||
pulling a new image — the panel migrates its own database. Across **major**
|
||||
versions, read the [release notes](https://github.com/MHSanaei/3x-ui/releases)
|
||||
first and take a backup.
|
||||
|
||||
## From x-ui
|
||||
|
||||
Importing an older x-ui panel's database directly into 3x-ui is **not
|
||||
supported** — the schemas differ. Install 3x-ui fresh and recreate
|
||||
inbounds/clients, exporting share links from the old panel as you go.
|
||||
|
||||
<Callout type="warn">
|
||||
Always take a backup before any migration, and keep the old server running
|
||||
until you've verified the new one.
|
||||
</Callout>
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
description: Fix common 3x-ui problems — the panel won't start, 502 errors, certificate issues, and clients that can't connect.
|
||||
icon: Wrench
|
||||
---
|
||||
|
||||
A checklist for the most common issues. When in doubt, raise the log level
|
||||
(`XUI_LOG_LEVEL=debug`) and check the panel and Xray logs.
|
||||
|
||||
## The panel won't start
|
||||
|
||||
- Check the service status and logs (`x-ui` menu → status/logs).
|
||||
- Make sure the **panel port isn't already in use** by another service.
|
||||
- Verify the database path is writable (default `/etc/x-ui/x-ui.db`).
|
||||
|
||||
## 502 / panel unreachable behind a proxy
|
||||
|
||||
- Confirm the panel is actually listening on the upstream port (e.g. `2053`).
|
||||
- Ensure your [reverse proxy](/docs/operations/reverse-proxy) passes WebSocket
|
||||
upgrade headers and points at the correct port and **web base path**.
|
||||
- Check the firewall isn't blocking the proxy → panel connection.
|
||||
|
||||
## Certificate problems
|
||||
|
||||
- The domain's DNS must point at the server before issuing a certificate.
|
||||
- Ports 80/443 must be reachable for HTTP/TLS validation (or use DNS validation).
|
||||
- For REALITY, remember it needs **no certificate** — the issue is usually a bad
|
||||
`dest`/SNI instead (see [REALITY pitfalls](/docs/config/reality)).
|
||||
|
||||
## A client can't connect
|
||||
|
||||
- Decode the client's link with the
|
||||
[share-link inspector](/docs/config/share-links) and confirm every parameter.
|
||||
- Check the **transport and security match** on both ends.
|
||||
- Make sure the client hasn't hit its **traffic, expiry, or IP limit**.
|
||||
- Confirm the inbound port is open in the firewall.
|
||||
|
||||
<Callout type="info">
|
||||
Still stuck? Search the
|
||||
[GitHub issues](https://github.com/MHSanaei/3x-ui/issues) — your symptom has
|
||||
probably been seen before.
|
||||
</Callout>
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
title: 3x-ui Documentation
|
||||
description: Official documentation for 3x-ui — an advanced web panel for managing Xray-core servers, proxies, clients, and subscriptions.
|
||||
icon: House
|
||||
---
|
||||
|
||||
**3x-ui** is an open-source web panel for deploying and managing
|
||||
[Xray-core](https://github.com/XTLS/Xray-core) servers. It gives you a modern
|
||||
dashboard for inbounds, protocols, clients, traffic accounting, subscriptions,
|
||||
and more — without hand-editing Xray JSON on the command line.
|
||||
|
||||
This site is the official documentation and a set of **interactive, in-browser
|
||||
tools** (config generators) that run entirely on your device — no data ever
|
||||
leaves the page.
|
||||
|
||||
## Start here
|
||||
|
||||
<Cards>
|
||||
<Card
|
||||
title="What is 3x-ui?"
|
||||
href="/docs/guide"
|
||||
description="The big picture: Xray-core, the panel, and who it's for."
|
||||
/>
|
||||
<Card
|
||||
title="Installation"
|
||||
href="/docs/guide/installation"
|
||||
description="Install via script, Docker, or manually."
|
||||
/>
|
||||
<Card
|
||||
title="First login"
|
||||
href="/docs/guide/first-login"
|
||||
description="Reach the panel and harden it before anything else."
|
||||
/>
|
||||
<Card
|
||||
title="REALITY"
|
||||
href="/docs/config/reality"
|
||||
description="Set up VLESS + REALITY with XTLS-Vision."
|
||||
/>
|
||||
</Cards>
|
||||
|
||||
## Highlights
|
||||
|
||||
- **Every major protocol** — VLESS, VMess, Trojan, Shadowsocks, WireGuard,
|
||||
Hysteria2, SOCKS, HTTP, and Dokodemo-door.
|
||||
- **REALITY & XTLS-Vision** — modern, censorship-resistant transports.
|
||||
- **Per-client controls** — traffic quotas, expiry dates, IP limits, share
|
||||
links, and QR codes.
|
||||
- **Subscriptions** — VLESS, Clash/Mihomo, and JSON formats.
|
||||
- **Operations** — multi-node management, Telegram bot, backups, and a REST API.
|
||||
|
||||
<Callout type="info">
|
||||
New to Xray? Read [What is 3x-ui?](/docs/guide) first — it explains how the panel, Xray-core, and
|
||||
your client apps fit together.
|
||||
</Callout>
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"pages": ["index", "guide", "config", "operations", "reference", "help"]
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
title: Backup & Restore
|
||||
description: Back up and restore your 3x-ui database and certificates, manually or via the Telegram bot.
|
||||
icon: DatabaseBackup
|
||||
---
|
||||
|
||||
Your entire configuration — inbounds, clients, settings — lives in the panel's
|
||||
database. Back it up regularly so you can recover or migrate.
|
||||
|
||||
## What to back up
|
||||
|
||||
- **The database** — SQLite at `/etc/x-ui/x-ui.db` by default (or your
|
||||
PostgreSQL database if you use that backend).
|
||||
- **Certificates** — anything under `/root/cert/` (or wherever you store TLS
|
||||
certs).
|
||||
|
||||
## Manual backup
|
||||
|
||||
You can download a backup from the panel's overview, or copy the database file
|
||||
directly from the server:
|
||||
|
||||
```bash title="copy the SQLite database"
|
||||
cp /etc/x-ui/x-ui.db /root/x-ui-backup-$(date +%F).db
|
||||
```
|
||||
|
||||
To restore, stop the panel, put the database back in place, and start it again.
|
||||
|
||||
<Callout type="warn">
|
||||
Restore a backup onto the **same major version** it came from when possible.
|
||||
Across major upgrades, let the panel run its migrations rather than forcing an
|
||||
old schema.
|
||||
</Callout>
|
||||
|
||||
## Telegram backup
|
||||
|
||||
If you've configured the [Telegram bot](/docs/operations/telegram-bot), enable
|
||||
**`tgBotBackup`** to attach a backup to the periodic report (on the `tgRunTime`
|
||||
schedule, default daily). The bot sends both the **database** and the **Xray
|
||||
`config.json`** to your admin chat, so you always have an off-server copy. Admins
|
||||
can also request a backup on demand from the bot's menu.
|
||||
|
||||
## SQLite dump / restore
|
||||
|
||||
The `x-ui migrate-db` command converts the SQLite database to and from a plain
|
||||
SQL text dump (handy for inspection or transferring between machines):
|
||||
|
||||
```bash
|
||||
x-ui migrate-db --dump /root/x-ui.sql # SQLite -> SQL text
|
||||
x-ui migrate-db --restore /root/x-ui.sql # SQL text -> SQLite
|
||||
```
|
||||
|
||||
To move to PostgreSQL instead, see [Database](/docs/reference/database).
|
||||
|
||||
<Callout type="info">
|
||||
Whatever method you use, store backups **off the server** and test a restore
|
||||
occasionally — an untested backup isn't a backup.
|
||||
</Callout>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"title": "Operations",
|
||||
"icon": "ServerCog",
|
||||
"pages": [
|
||||
"reverse-proxy",
|
||||
"multi-node",
|
||||
"outbounds-routing",
|
||||
"backup-restore",
|
||||
"telegram-bot",
|
||||
"security"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: Multi-node & Managed Hosts
|
||||
description: Manage multiple 3x-ui panels from one master, with API-token or mTLS trust, heartbeats, and per-inbound host overrides for subscriptions.
|
||||
icon: Boxes
|
||||
---
|
||||
|
||||
3x-ui can manage **multiple servers** from a single master panel, and override
|
||||
how each inbound is advertised in subscriptions with **managed hosts**.
|
||||
|
||||
## Nodes
|
||||
|
||||
A **node** is another 3x-ui panel that your master panel manages over the node's
|
||||
API. The master polls each node and shows its status, versions, CPU/memory,
|
||||
uptime, and traffic in one place.
|
||||
|
||||
### Add a node
|
||||
|
||||
Provide the node's connection details:
|
||||
|
||||
| Field | Notes |
|
||||
| ----------------- | --------------------------------------------------------------------- |
|
||||
| **Name** | Unique label (e.g. `de-fra-1`). |
|
||||
| **Scheme** | `https` (default) or `http`. |
|
||||
| **Address / Port**| The node panel's host and port. |
|
||||
| **Base path** | The node's web base path. |
|
||||
| **API token** | A Bearer token created on the node (not needed in mTLS mode). |
|
||||
| **TLS verify** | `verify` (default), `skip`, `pin` (pin a cert SHA-256), or `mtls`. |
|
||||
| **Inbound sync** | `all` inbounds, or `selected` by tag. |
|
||||
| **Outbound tag** | Optionally reach the node **through** a named outbound (egress bridge).|
|
||||
|
||||
The master verifies reachability when you add or test a node. It then sends a
|
||||
**heartbeat** every few seconds, updating the node's status (`online` / `offline`)
|
||||
and emitting `node.up` / `node.down` events (see the
|
||||
[Telegram bot](/docs/operations/telegram-bot)).
|
||||
|
||||
<Callout type="info">
|
||||
Nodes are identified by a stable per-panel GUID, so a node keeps its identity
|
||||
across restarts. A node can itself manage further nodes — the master surfaces
|
||||
those as read-only **transitive** sub-nodes (Node 1 → Node 2 → Node 3).
|
||||
</Callout>
|
||||
|
||||
### Mutual TLS (mTLS) between master and node
|
||||
|
||||
For the strongest trust, use `tlsVerifyMode = mtls` (requires `https`):
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Get the master's CA
|
||||
|
||||
On the master, fetch its node-auth CA certificate (the CA private key never
|
||||
leaves the panel).
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Trust it on the node
|
||||
|
||||
Paste that CA into the node's "trusted CA" setting. It takes effect on the node's
|
||||
next restart.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Switch the node to mTLS
|
||||
|
||||
Set the node's TLS verify mode to `mtls`. The master now presents a client
|
||||
certificate instead of an API token.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Managed hosts
|
||||
|
||||
A **managed host** is an override endpoint attached to an inbound. At
|
||||
subscription time, each enabled host renders an additional share link / proxy
|
||||
with its own address, port, TLS, SNI, host header, path, and more — superseding
|
||||
the older "external proxy" list. Use them to:
|
||||
|
||||
- front an inbound through a **CDN** (Cloudflare) with a different address/SNI,
|
||||
- advertise **multiple domains** or per-region endpoints for one inbound,
|
||||
- tweak ALPN, fingerprint, ECH, or mux per endpoint.
|
||||
|
||||
Each host has a remark (which supports the same
|
||||
[template variables](/docs/config/share-links#remark-template-variables)), an
|
||||
enable toggle, a sort order, and can be **excluded from specific subscription
|
||||
formats** or **scoped to specific nodes**.
|
||||
|
||||
<Callout type="info">
|
||||
Hosts whose address/port point at a CDN let you keep the real server address
|
||||
private while clients connect through the CDN edge.
|
||||
</Callout>
|
||||
|
||||
## Related
|
||||
|
||||
<Cards>
|
||||
<Card title="Outbounds & routing" href="/docs/operations/outbounds-routing" description="WARP, NordVPN, outbound subscriptions, and routing." />
|
||||
<Card title="Subscription" href="/docs/config/subscription" description="How hosts shape subscription output." />
|
||||
</Cards>
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
title: Outbounds & Routing
|
||||
description: Shape egress in 3x-ui — WARP and NordVPN outbounds, outbound subscriptions (server pools), routing rules, and load balancers.
|
||||
icon: Route
|
||||
---
|
||||
|
||||
Inbounds accept clients; **outbounds** decide where their traffic goes next.
|
||||
3x-ui can route traffic through Cloudflare WARP, NordVPN, or arbitrary outbound
|
||||
pools imported from a subscription, and select between them with routing rules
|
||||
and balancers.
|
||||
|
||||
## Editing outbounds & routing
|
||||
|
||||
Outbounds, routing rules, balancers, DNS, and logging all live in the **Xray
|
||||
configuration** (the config template you edit under Xray Settings). There's no
|
||||
separate per-rule UI — you edit the JSON, and the panel reloads Xray. The panel
|
||||
also offers an **outbound connectivity test** and a **route test** (ask the
|
||||
running core which outbound a given destination would use).
|
||||
|
||||
## Build an outbound
|
||||
|
||||
Every outbound is a JSON object with up to four parts: a **`tag`** (referenced by
|
||||
routing rules and balancers), a **`protocol`**, protocol-specific **`settings`**,
|
||||
and — for proxy protocols — **`streamSettings`** that must match the remote
|
||||
inbound's transport and security. Two outbounds are almost always present:
|
||||
|
||||
- **`freedom`** sends traffic straight to its destination — the default egress.
|
||||
Optionally set a `domainStrategy` (e.g. `UseIP`) to control how hostnames resolve.
|
||||
- **`blackhole`** drops traffic. Route unwanted destinations (ads, torrents) here.
|
||||
|
||||
```json title="freedom + blackhole"
|
||||
{
|
||||
"outbounds": [
|
||||
{ "tag": "direct", "protocol": "freedom", "settings": {} },
|
||||
{ "tag": "block", "protocol": "blackhole", "settings": {} }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A **proxy** outbound (VLESS, VMess, Trojan, Shadowsocks) forwards to another
|
||||
server — handy for chaining or sending select traffic abroad. Mind the wire
|
||||
shapes 3x-ui uses: **VLESS is the flat form** (`address`/`port`/`id`/`flow`/
|
||||
`encryption`), **VMess uses `settings.vnext[]`**, and **Trojan/Shadowsocks use
|
||||
`settings.servers[]`**. The `streamSettings` must mirror the destination's
|
||||
[transport and security](/docs/config/transports).
|
||||
|
||||
Assemble any outbound below and paste the JSON into **Xray Settings → Outbounds**:
|
||||
|
||||
<OutboundGenerator />
|
||||
|
||||
## Cloudflare WARP
|
||||
|
||||
WARP lets your server egress through Cloudflare's network. 3x-ui can register a
|
||||
WARP account for you and wire it into a WireGuard outbound tagged **`warp`**:
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Add a `warp`-tagged outbound
|
||||
|
||||
Create a WireGuard outbound with the tag `warp` in your Xray config.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Register WARP
|
||||
|
||||
From the panel's WARP controls, register an account. 3x-ui fills the outbound's
|
||||
keys, addresses, reserved bytes, and peer endpoint automatically.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### (Optional) auto-rotate the IP
|
||||
|
||||
Set a WARP update interval (in **days**) to periodically rotate the WARP IP. A
|
||||
free license can also be applied.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
Route the traffic you want (for example specific domains) to the `warp` outbound
|
||||
with a routing rule.
|
||||
|
||||
## NordVPN
|
||||
|
||||
3x-ui can fetch NordVPN (NordLynx/WireGuard) credentials from an access token (or
|
||||
accept a private key directly) and list countries/servers, so you can build a
|
||||
NordVPN outbound.
|
||||
|
||||
## Outbound subscriptions (server pools)
|
||||
|
||||
An **outbound subscription** imports a remote share-link subscription and injects
|
||||
its servers as **outbounds** into the running Xray config — without touching your
|
||||
saved template. This is the recommended way to subscribe to a *pool* of servers.
|
||||
|
||||
| Field | Default | Meaning |
|
||||
| ---------------- | ------- | -------------------------------------------------------------- |
|
||||
| `url` | — | The remote subscription URL (SSRF-guarded). |
|
||||
| `tagPrefix` | auto | Prefix for generated outbound tags (e.g. `hk-`); blank = `subN-`. |
|
||||
| `updateInterval` | `600` | Refresh interval in **seconds**. |
|
||||
| `prepend` | `false` | Place these outbounds before your manual ones. |
|
||||
| `priority` | `0` | Merge order (lower first). |
|
||||
|
||||
Imported outbounds get **stable tags**: the same server keeps the same tag across
|
||||
refreshes, so exact-tag routing/balancer selectors stay pinned — while
|
||||
prefix/wildcard selectors (e.g. `hk-*`) automatically pick up new servers as the
|
||||
pool changes. Supported link schemes: `vmess`, `vless`, `trojan`, `ss`,
|
||||
`hysteria2` (`hy2`), and `wireguard` (`wg`). The panel refreshes enabled
|
||||
subscriptions on a timer and reloads Xray when something changes.
|
||||
|
||||
## Routing rules
|
||||
|
||||
**Routing rules** decide which outbound (or balancer) each connection uses. Each
|
||||
rule is a `field`-type matcher: set any of `domain`, `ip`, `port`, `network`,
|
||||
`protocol`, `inboundTag`, `sourceIP`, … and point it at an **`outboundTag`** or a
|
||||
**`balancerTag`**. Rules are evaluated **top-to-bottom — the first match wins**, so
|
||||
put specific rules above general ones.
|
||||
|
||||
```json title="route ads to blackhole, private IPs direct"
|
||||
{
|
||||
"routing": {
|
||||
"domainStrategy": "IPIfNonMatch",
|
||||
"rules": [
|
||||
{ "type": "field", "domain": ["geosite:category-ads-all"], "outboundTag": "block" },
|
||||
{ "type": "field", "ip": ["geoip:private"], "outboundTag": "direct" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Balancers
|
||||
|
||||
A **balancer** groups outbounds by a **selector** (tag prefixes, including the
|
||||
wildcard pools from outbound subscriptions) and spreads or fails traffic over them
|
||||
with a **strategy**:
|
||||
|
||||
| Strategy | Picks… | Needs a monitor |
|
||||
| ------------- | ----------------------------------------------- | ------------------------ |
|
||||
| `random` | a random member per connection | no |
|
||||
| `roundRobin` | members in rotation | no |
|
||||
| `leastPing` | the lowest-latency member | **`observatory`** |
|
||||
| `leastLoad` | the most stable member by sampled load | **`burstObservatory`** |
|
||||
|
||||
Reference a balancer from a rule via `balancerTag`. `leastPing` and `leastLoad`
|
||||
need a health monitor, which Xray places at the **top level** of the config
|
||||
(`observatory` / `burstObservatory`, **not** inside `routing`). The panel can
|
||||
report balancer status and **override** a balancer to a specific outbound for
|
||||
testing.
|
||||
|
||||
Build the routing block — rules, balancers, and the matching observatory — here:
|
||||
|
||||
<RoutingBuilder />
|
||||
|
||||
<Callout type="warn">
|
||||
Outbounds that reach external services are fetched with SSRF protection — by
|
||||
default private/internal addresses are blocked unless you explicitly allow them
|
||||
per source.
|
||||
</Callout>
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
title: Reverse Proxy
|
||||
description: Put the 3x-ui panel and subscription behind Nginx or Caddy with Let's Encrypt TLS.
|
||||
icon: Waypoints
|
||||
---
|
||||
|
||||
A reverse proxy lets you serve the panel and subscription on a clean domain with
|
||||
automatic HTTPS, and hide the real ports behind ports 80/443. Prefer to let the
|
||||
panel terminate TLS directly? Get a certificate with the
|
||||
[`x-ui` SSL menu](/docs/config/ssl-certificates) instead.
|
||||
|
||||
## Generate a config
|
||||
|
||||
<ReverseProxyGenerator />
|
||||
|
||||
<Callout type="info">
|
||||
The panel uses WebSockets for live updates, so the proxy must pass the
|
||||
`Upgrade`/`Connection` headers (the Nginx output above already does). Caddy
|
||||
handles WebSocket upgrades automatically.
|
||||
</Callout>
|
||||
|
||||
## Nginx + certificate
|
||||
|
||||
With Nginx, obtain a certificate with `certbot` (or `acme.sh`) and reference it
|
||||
in the server block:
|
||||
|
||||
```bash title="certbot"
|
||||
certbot certonly --nginx -d panel.example.com
|
||||
```
|
||||
|
||||
Reload Nginx after installing the certificate, and set up automatic renewal
|
||||
(`certbot renew` runs on a timer by default).
|
||||
|
||||
## Caddy
|
||||
|
||||
Caddy obtains and renews certificates for you — point a Caddyfile at the panel
|
||||
and it just works:
|
||||
|
||||
```text title="Caddyfile"
|
||||
panel.example.com {
|
||||
reverse_proxy 127.0.0.1:2053
|
||||
}
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Keep the panel's **web base path** even behind a proxy; defense in depth.
|
||||
- If you terminate TLS at the proxy, you may want `XUI_SKIP_HSTS=true` on the
|
||||
panel — see the [environment variables reference](/docs/reference/env-vars).
|
||||
- Proxy the [subscription](/docs/config/subscription) server too, so its contents
|
||||
are served over HTTPS.
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: Security
|
||||
description: Harden 3x-ui — panel auth and 2FA, Fail2ban IP limits, firewall rules, BBR, and staying current.
|
||||
icon: ShieldCheck
|
||||
---
|
||||
|
||||
A proxy panel is a high-value target. A few layers of hardening go a long way.
|
||||
|
||||
## Panel hardening
|
||||
|
||||
- Strong, unique credentials and **two-factor auth** (TOTP).
|
||||
- A non-default panel port and a long, random web base path.
|
||||
- TLS on the panel (directly or via a [reverse proxy](/docs/operations/reverse-proxy)).
|
||||
- The built-in **login limiter** blocks an IP/username after 5 failed attempts
|
||||
in 5 minutes (for 15 minutes), and the panel can route its own egress through
|
||||
an outbound.
|
||||
|
||||
See [First login](/docs/guide/first-login) for the full checklist.
|
||||
|
||||
## Fail2ban & IP limits
|
||||
|
||||
Set an **IP limit** per client (see [Clients](/docs/config/clients)) to cap the
|
||||
number of simultaneous source IPs. Enforcement is handled by **Fail2ban**, which
|
||||
3x-ui installs and configures for you (enabled by default on script installs, and
|
||||
on Docker via `XUI_ENABLE_FAIL2BAN=true`).
|
||||
|
||||
Manage it from the `x-ui` menu (**22 — IP Limit Management**): install/configure,
|
||||
change the ban duration (default **30 minutes**), ban/unban an IP, view ban logs,
|
||||
and check status. Under the hood:
|
||||
|
||||
- The jail is named **`3x-ipl`**; ban logs live at `/var/log/x-ui/3xipl.log` and
|
||||
`/var/log/x-ui/3xipl-banned.log` (also via `x-ui banlog`).
|
||||
- Bans cover all TCP/UDP **except** your SSH and panel ports, so a ban can't lock
|
||||
you out of the server or panel.
|
||||
|
||||
<Callout type="warn">
|
||||
On Docker, Fail2ban bans with `iptables`, which needs the `NET_ADMIN` (and
|
||||
`NET_RAW`) capability — `docker-compose.yml` grants them. With a bare
|
||||
`docker run`, add `--cap-add=NET_ADMIN --cap-add=NET_RAW` or bans are logged
|
||||
but never applied.
|
||||
</Callout>
|
||||
|
||||
## Firewall
|
||||
|
||||
Open only the ports you actually use: SSH, the panel port, the subscription
|
||||
port, and your inbound ports. The `x-ui` menu (**23 — Firewall Management**) wraps
|
||||
`ufw`, or generate rules here:
|
||||
|
||||
<FirewallRulesGenerator />
|
||||
|
||||
<Callout type="warn">
|
||||
Make sure SSH stays allowed before enabling a default-deny firewall, or you can
|
||||
lock yourself out. Test with a second session open.
|
||||
</Callout>
|
||||
|
||||
## Network tuning (BBR)
|
||||
|
||||
The `x-ui` menu (**26 — Enable BBR**) toggles Google's BBR congestion control
|
||||
(`net.ipv4.tcp_congestion_control = bbr`, `net.core.default_qdisc = fq`), which
|
||||
often improves throughput on congested links.
|
||||
|
||||
## Keep current
|
||||
|
||||
Update 3x-ui and Xray-core regularly — security fixes land in new releases. Watch
|
||||
the [releases page](https://github.com/MHSanaei/3x-ui/releases) and see
|
||||
[Update & uninstall](/docs/guide/update-uninstall).
|
||||
@@ -0,0 +1,112 @@
|
||||
---
|
||||
title: Telegram Bot
|
||||
description: Connect a Telegram bot to 3x-ui for commands, periodic reports, event alerts (login, CPU, node up/down), backups, and client self-service.
|
||||
icon: Send
|
||||
---
|
||||
|
||||
3x-ui can drive a Telegram bot for monitoring, alerts, backups, and remote
|
||||
management. Admins get full control; regular users (linked by Telegram ID) can
|
||||
check their own usage and links.
|
||||
|
||||
<Callout type="info">
|
||||
Looking for news and community support? Join the official Telegram channel
|
||||
[@XrayUI](https://t.me/XrayUI). That's separate from the bot below, which you
|
||||
run yourself to manage your own panel.
|
||||
</Callout>
|
||||
|
||||
## Set it up
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### Create a bot
|
||||
|
||||
Message [@BotFather](https://t.me/BotFather), send `/newbot`, and copy the **bot
|
||||
token**.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Find your Telegram ID
|
||||
|
||||
Get your numeric Telegram user ID (the bot's own `/id` command reports it once
|
||||
connected). This is your **admin** ID.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Configure the panel
|
||||
|
||||
In Panel Settings, enable the Telegram bot and set the **token** and **admin chat
|
||||
ID(s)** (comma-separated). Save, then message your bot.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
Validate your token, admin IDs, and report schedule before pasting them into the
|
||||
panel:
|
||||
|
||||
<TelegramSetupHelper />
|
||||
|
||||
## Commands
|
||||
|
||||
These appear in the Telegram command menu: `/start`, `/help`, `/status`, `/id`.
|
||||
Additional commands:
|
||||
|
||||
| Command | Who | Action |
|
||||
| ------------------ | ------ | ------------------------------------------------------------ |
|
||||
| `/start`, `/help` | anyone | Greeting and the menu of inline buttons |
|
||||
| `/status` | anyone | Confirm the bot is alive |
|
||||
| `/id` | anyone | Show your Telegram numeric ID |
|
||||
| `/usage <arg>` | both | Admins search clients; users look up their own usage |
|
||||
| `/inbound <remark>`| admin | Show an inbound's details |
|
||||
| `/restart` | admin | Restart Xray |
|
||||
|
||||
Admins also get inline-button flows for server usage, sorted traffic reports,
|
||||
resetting traffic, DB backups, ban logs, listing inbounds/clients, online
|
||||
clients, "depleting soon", and a full **add-client** wizard. Regular users get
|
||||
buttons for their own usage, subscription links, individual links, and QR codes.
|
||||
|
||||
## Reports & alerts
|
||||
|
||||
- **Periodic report** — on the `tgRunTime` schedule (default `@daily`), the bot
|
||||
sends admins server usage (host, versions, uptime, load, memory, online
|
||||
clients, traffic), a list of exhausted/expiring clients, and — if
|
||||
`tgBotBackup` is on — a database + Xray config backup. Clients linked by
|
||||
Telegram ID get their own expiry/quota warnings.
|
||||
- **Event alerts** — selected by `tgEnabledEvents` (default `login.attempt,cpu.high`):
|
||||
|
||||
| Event | When |
|
||||
| --------------- | ------------------------------------------------------- |
|
||||
| `login.attempt` | A panel login succeeds or fails (with IP and username) |
|
||||
| `cpu.high` | CPU exceeds `tgCpu` percent (default 80) |
|
||||
| `memory.high` | Memory exceeds `tgMemory` percent (default 80) |
|
||||
| `xray.crash` | Xray-core crashes |
|
||||
| `outbound.down` / `outbound.up` | An outbound goes down / recovers |
|
||||
| `node.down` / `node.up` | A node goes offline / comes back |
|
||||
|
||||
The warning lead times come from `expireDiff` (days before expiry) and
|
||||
`trafficDiff` (GB of quota remaining); both default to `0` (off).
|
||||
|
||||
## Settings
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| -------------- | -------------------------- | ---------------------------------------------- |
|
||||
| `tgBotEnable` | `false` | Master on/off. |
|
||||
| `tgBotToken` | _(secret)_ | Bot API token. |
|
||||
| `tgBotChatId` | _(none)_ | Comma-separated **admin** Telegram IDs. |
|
||||
| `tgBotProxy` | _(none)_ | `socks5://`, `http://`, or `https://` proxy. |
|
||||
| `tgBotAPIServer` | _(default)_ | Custom Telegram Bot API server. |
|
||||
| `tgRunTime` | `@daily` | Report schedule (cron / `@daily` / `@every …`).|
|
||||
| `tgBotBackup` | `false` | Attach a DB backup to the periodic report. |
|
||||
| `tgCpu` / `tgMemory` | `80` / `80` | CPU / memory alert thresholds (percent). |
|
||||
| `tgLang` | `en-US` | Bot language. |
|
||||
| `tgEnabledEvents` | `login.attempt,cpu.high`| Which events to deliver. |
|
||||
|
||||
<Callout type="warn">
|
||||
The bot token controls your bot — keep it secret and only add **trusted** admin
|
||||
chat IDs. Login alerts never include passwords.
|
||||
</Callout>
|
||||
|
||||
<Callout type="info">
|
||||
Email (SMTP) notifications mirror the same events (`smtpEnabledEvents`) if you'd
|
||||
rather receive alerts by email — configure SMTP in Panel Settings.
|
||||
</Callout>
|
||||
@@ -0,0 +1,79 @@
|
||||
---
|
||||
title: API Tokens
|
||||
description: >-
|
||||
Manage Bearer tokens used for programmatic auth (bots, central panels acting
|
||||
on this node, CI). Each token has a unique name and an enabled flag — disable
|
||||
to revoke without deleting, delete to revoke permanently. Tokens are stored as
|
||||
SHA-256 hashes and the plaintext is returned only once, in the create response
|
||||
— it cannot be retrieved afterwards, so copy it then. Send one as
|
||||
<code>Authorization: Bearer <token></code> on any /panel/api/* request —
|
||||
the token is a full-admin credential.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every API token, enabled or not. The token value is never returned
|
||||
— only metadata.
|
||||
url: >-
|
||||
#list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
|
||||
- depth: 2
|
||||
title: >-
|
||||
Mint a new API token. Name must be unique and 1-64 characters; the token
|
||||
string is server-generated and returned only in this response — it is
|
||||
stored hashed and cannot be retrieved later.
|
||||
url: >-
|
||||
#mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
|
||||
- depth: 2
|
||||
title: >-
|
||||
Permanently delete a token. Any caller using it stops authenticating
|
||||
immediately.
|
||||
url: >-
|
||||
#permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
||||
- depth: 2
|
||||
title: >-
|
||||
Toggle a token enabled/disabled without deleting it. Disabled tokens are
|
||||
rejected by checkAPIAuth on the next request.
|
||||
url: >-
|
||||
#toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every API token, enabled or not. The token value is never
|
||||
returned — only metadata.
|
||||
id: >-
|
||||
list-every-api-token-enabled-or-not-the-token-value-is-never-returned--only-metadata
|
||||
- content: >-
|
||||
Mint a new API token. Name must be unique and 1-64 characters; the
|
||||
token string is server-generated and returned only in this response —
|
||||
it is stored hashed and cannot be retrieved later.
|
||||
id: >-
|
||||
mint-a-new-api-token-name-must-be-unique-and-1-64-characters-the-token-string-is-server-generated-and-returned-only-in-this-response--it-is-stored-hashed-and-cannot-be-retrieved-later
|
||||
- content: >-
|
||||
Permanently delete a token. Any caller using it stops authenticating
|
||||
immediately.
|
||||
id: >-
|
||||
permanently-delete-a-token-any-caller-using-it-stops-authenticating-immediately
|
||||
- content: >-
|
||||
Toggle a token enabled/disabled without deleting it. Disabled tokens
|
||||
are rejected by checkAPIAuth on the next request.
|
||||
id: >-
|
||||
toggle-a-token-enableddisabled-without-deleting-it-disabled-tokens-are-rejected-by-checkapiauth-on-the-next-request
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/setting/apiTokens","method":"get"},{"path":"/panel/api/setting/apiTokens/create","method":"post"},{"path":"/panel/api/setting/apiTokens/delete/{id}","method":"post"},{"path":"/panel/api/setting/apiTokens/setEnabled/{id}","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
---
|
||||
title: Authentication
|
||||
description: >-
|
||||
Two authentication modes are supported. UI sessions use a cookie set by the
|
||||
login endpoint. Programmatic clients (bots, scripts, remote panels)
|
||||
authenticate with a Bearer token taken from Settings → Security → API Token.
|
||||
Both work for every endpoint under /panel/api/*.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Authenticate with username + password and receive a session cookie.
|
||||
Required before any cookie-based API call.
|
||||
url: >-
|
||||
#authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
|
||||
- depth: 2
|
||||
title: Clear the session cookie. Requires the CSRF header for browser sessions.
|
||||
url: '#clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Mint a CSRF token for the current session. The SPA replays it in the
|
||||
X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
|
||||
this — the middleware short-circuits CSRF for authenticated API
|
||||
requests.
|
||||
url: >-
|
||||
#mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests
|
||||
- depth: 2
|
||||
title: >-
|
||||
Returns whether 2FA is enabled on the panel — used by the login page to
|
||||
decide whether to show the OTP field.
|
||||
url: >-
|
||||
#returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Authenticate with username + password and receive a session cookie.
|
||||
Required before any cookie-based API call.
|
||||
id: >-
|
||||
authenticate-with-username--password-and-receive-a-session-cookie-required-before-any-cookie-based-api-call
|
||||
- content: >-
|
||||
Clear the session cookie. Requires the CSRF header for browser
|
||||
sessions.
|
||||
id: clear-the-session-cookie-requires-the-csrf-header-for-browser-sessions
|
||||
- content: >-
|
||||
Mint a CSRF token for the current session. The SPA replays it in the
|
||||
X-CSRF-Token header on unsafe requests. Bearer-token callers can skip
|
||||
this — the middleware short-circuits CSRF for authenticated API
|
||||
requests.
|
||||
id: >-
|
||||
mint-a-csrf-token-for-the-current-session-the-spa-replays-it-in-the-x-csrf-token-header-on-unsafe-requests-bearer-token-callers-can-skip-this--the-middleware-short-circuits-csrf-for-authenticated-api-requests
|
||||
- content: >-
|
||||
Returns whether 2FA is enabled on the panel — used by the login page
|
||||
to decide whether to show the OTP field.
|
||||
id: >-
|
||||
returns-whether-2fa-is-enabled-on-the-panel--used-by-the-login-page-to-decide-whether-to-show-the-otp-field
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/login","method":"post"},{"path":"/logout","method":"post"},{"path":"/csrf-token","method":"get"},{"path":"/getTwoFactorEnable","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
---
|
||||
title: Backup
|
||||
description: Operations that interact with the configured Telegram bot.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Send a fresh DB backup to every Telegram chat configured as an admin
|
||||
recipient. No body, no params.
|
||||
url: >-
|
||||
#send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Send a fresh DB backup to every Telegram chat configured as an admin
|
||||
recipient. No body, no params.
|
||||
id: >-
|
||||
send-a-fresh-db-backup-to-every-telegram-chat-configured-as-an-admin-recipient-no-body-no-params
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/backuptotgbot","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,627 @@
|
||||
---
|
||||
title: Clients
|
||||
description: >-
|
||||
Manage clients as first-class entities that can be attached to one or more
|
||||
inbounds. A single client row drives the settings.clients entry in every
|
||||
inbound it belongs to. Endpoints live under /panel/api/clients.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every client with its attached inbound IDs and traffic record. The
|
||||
reverse field, if set, is returned as a nested JSON object (legacy
|
||||
JSON-encoded-string form is still accepted on write).
|
||||
url: >-
|
||||
#list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write
|
||||
- depth: 2
|
||||
title: >-
|
||||
Filter, sort, and paginate clients on the server. Each item is a slim
|
||||
row (no uuid/password/auth/flow/security/reverse/tgId) so the clients
|
||||
page can ship 25-ish rows in a few KB instead of the full table. The
|
||||
response also includes a summary computed across the full DB row set so
|
||||
dashboard counters stay stable as the user paginates or filters. Page
|
||||
size capped at 200; fetch /get/:email to obtain the full per-client
|
||||
payload for an edit/info modal.
|
||||
url: >-
|
||||
#filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal
|
||||
- depth: 2
|
||||
title: >-
|
||||
Fetch one client by email, including the inbound IDs and external config
|
||||
IDs it is attached to.
|
||||
url: >-
|
||||
#fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess, password
|
||||
for Trojan/Shadowsocks, auth for Hysteria) are generated server-side
|
||||
when omitted, so callers can send only the universal fields.
|
||||
url: >-
|
||||
#create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- depth: 2
|
||||
title: >-
|
||||
Update an existing client by email. Changes propagate to every attached
|
||||
inbound. Body is the JSON client payload — supply the full set of fields
|
||||
you want to keep (the server replaces the row, it does not patch).
|
||||
url: >-
|
||||
#update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete a client by email. Removes it from every attached inbound and
|
||||
drops its traffic record unless keepTraffic=1 is passed.
|
||||
url: >-
|
||||
#delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
||||
- depth: 2
|
||||
title: >-
|
||||
Attach an existing client to one or more additional inbounds. Body is
|
||||
JSON.
|
||||
url: >-
|
||||
#attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
- depth: 2
|
||||
title: Detach a client from one or more inbounds without deleting the client.
|
||||
url: '#detach-a-client-from-one-or-more-inbounds-without-deleting-the-client'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace a client's external links (per-client share links and remote
|
||||
subscription URLs surfaced in their subscription). Sends the full set;
|
||||
the server replaces all rows.
|
||||
url: >-
|
||||
#replace-a-clients-external-links-per-client-share-links-and-remote-subscription-urls-surfaced-in-their-subscription-sends-the-full-set-the-server-replaces-all-rows
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reset the up/down counters for every client globally. Quotas and expiry
|
||||
are not affected. Triggers an Xray restart if any counter actually
|
||||
moved.
|
||||
url: >-
|
||||
#reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete every client whose traffic quota is exhausted (used >= total,
|
||||
when reset is disabled) or whose expiry has passed. Returns the deleted
|
||||
count and triggers an Xray restart when any client was on a running
|
||||
inbound.
|
||||
url: >-
|
||||
#delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete every client that is not attached to any inbound, along with its
|
||||
traffic record, IP log, and external links. Useful for clearing clients
|
||||
left unattached after their inbounds were removed. Returns the deleted
|
||||
count. Cannot be undone.
|
||||
url: >-
|
||||
#delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every client as a {client, inboundIds} array — the same shape
|
||||
/bulkCreate and /import accept — so the payload round-trips straight
|
||||
back through /import. Clients with no inbound attachment are included
|
||||
with an empty inboundIds list. The UI shows this in a CodeMirror viewer
|
||||
(copy / download); programmatic callers get the array in obj.
|
||||
url: >-
|
||||
#return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||||
- depth: 2
|
||||
title: >-
|
||||
Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
string-encoded array produced by /export ([{client, inboundIds}]). Items
|
||||
with inboundIds are created and attached to those inbounds; items with
|
||||
an empty inboundIds list are restored as unattached client records.
|
||||
Existing emails are never overwritten — they are returned in skipped.
|
||||
Triggers a single Xray restart at the end if any target inbound was
|
||||
running.
|
||||
url: >-
|
||||
#import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running
|
||||
- depth: 2
|
||||
title: >-
|
||||
Shift expiry and/or traffic quota for many clients in one call.
|
||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||||
corresponding field — bulk extend never converts unlimited to limited.
|
||||
The optional flow directive sets the XTLS flow on every client: "none"
|
||||
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where the
|
||||
inbound supports it (omit or "" to leave it unchanged). Returns the
|
||||
adjusted count and per-email skip reasons.
|
||||
url: >-
|
||||
#shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons
|
||||
- depth: 2
|
||||
title: >-
|
||||
Enable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to add each user. Note that enabling a
|
||||
client whose quota is exhausted or whose expiry has passed only flips
|
||||
the flag — the traffic loop will disable it again on the next tick.
|
||||
Returns the changed count and per-email skip reasons.
|
||||
url: >-
|
||||
#enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- depth: 2
|
||||
title: >-
|
||||
Disable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to remove each user. Returns the
|
||||
changed count and per-email skip reasons.
|
||||
url: >-
|
||||
#disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete many clients in one call. The server processes the list
|
||||
sequentially so each delete sees the committed state of the previous one
|
||||
— avoids the race the per-email fan-out had on the panel side. Pass
|
||||
keepTraffic=true to retain the xray_client_traffic rows after deletion.
|
||||
url: >-
|
||||
#delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create many clients in one call. Body is a JSON array of {client,
|
||||
inboundIds} payloads — the same shape /add accepts. Items are processed
|
||||
sequentially; per-email skip reasons are returned for items that fail
|
||||
(e.g., duplicate email). Triggers a single Xray restart at the end if
|
||||
any inbound was running.
|
||||
url: >-
|
||||
#create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running
|
||||
- depth: 2
|
||||
title: >-
|
||||
Add many clients to a group in one call. Updates clients.group_name and
|
||||
patches the matching client entry inside every owning inbound's settings
|
||||
JSON in a single transaction. If the group name does not yet exist (in
|
||||
client_groups or as a derived label), it is auto-created as a persistent
|
||||
group. To clear the group label, use /groups/bulkRemove instead.
|
||||
url: >-
|
||||
#add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead
|
||||
- depth: 2
|
||||
title: >-
|
||||
Clear the group label on many clients in one call. Inverse of
|
||||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||||
cleared from clients.group_name and from each owning inbound's settings
|
||||
JSON. Groups become empty if all their members are removed.
|
||||
url: >-
|
||||
#clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed
|
||||
- depth: 2
|
||||
title: >-
|
||||
Attach many existing clients to many inbounds in one call. Each client
|
||||
keeps its identity (email/UUID/password/subId) and a shared traffic row;
|
||||
all clients are added to a target inbound in a single AddInboundClient
|
||||
call. Clients already present on a target are reported under skipped.
|
||||
Returns per-email attached/skipped/errors lists and triggers a single
|
||||
Xray restart if any target inbound was running.
|
||||
url: >-
|
||||
#attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- depth: 2
|
||||
title: >-
|
||||
Mirror of bulkAttach: detach many existing clients from many inbounds in
|
||||
one call. For each email, intersects the client's current inbounds with
|
||||
the requested set and detaches from those only; (email, inbound) pairs
|
||||
where the client is not currently attached are silently no-ops. Emails
|
||||
not attached to any of the requested inbounds are reported under
|
||||
skipped. Client records are kept even if they become orphaned — use
|
||||
bulkDel for full removal. Returns per-email detached/skipped/errors
|
||||
lists and triggers a single Xray restart if any target inbound was
|
||||
running.
|
||||
url: >-
|
||||
#mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- depth: 2
|
||||
title: >-
|
||||
Zero up/down counters for many clients in one call. Loops the
|
||||
single-reset path so each client is re-enabled across its attached
|
||||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||||
successfully reset clients.
|
||||
url: >-
|
||||
#zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients
|
||||
- depth: 2
|
||||
title: >-
|
||||
List all client groups with their member counts. Merges persisted groups
|
||||
(rows in client_groups, including empty placeholders) with the distinct
|
||||
group_name values currently set on clients. Sorted alphabetically
|
||||
(case-insensitive).
|
||||
url: >-
|
||||
#list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return just the email list of clients that currently belong to the given
|
||||
group. Useful for fanning a single bulk action over an entire group
|
||||
without round-tripping the full client list.
|
||||
url: >-
|
||||
#return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new empty (placeholder) group. The group becomes selectable in
|
||||
client forms and the filter drawer even before any client is added to
|
||||
it. Errors if a group with the same name already exists.
|
||||
url: >-
|
||||
#create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists
|
||||
- depth: 2
|
||||
title: >-
|
||||
Rename a group. The new name is applied to the client_groups row AND
|
||||
propagated to every matching client (both clients.group_name and the
|
||||
client entry inside every owning inbound's settings JSON) in a single
|
||||
transaction. Returns the number of clients whose label was updated.
|
||||
url: >-
|
||||
#rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated
|
||||
- depth: 2
|
||||
title: >-
|
||||
Remove a group. Deletes the client_groups row and clears the group label
|
||||
from every matching client (both clients.group_name and the inbound
|
||||
settings JSON). The clients themselves are NOT deleted — use /bulkDel
|
||||
after filtering by group for that. Returns the count of clients whose
|
||||
label was cleared.
|
||||
url: >-
|
||||
#remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared
|
||||
- depth: 2
|
||||
title: >-
|
||||
Zero out a single client’s up/down counters. Re-enables the client
|
||||
across every attached inbound and pushes the change to Xray (or the
|
||||
remote node) so depleted users can connect again immediately.
|
||||
url: >-
|
||||
#zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately
|
||||
- depth: 2
|
||||
title: >-
|
||||
Manually adjust a client’s upload + download counters. Useful for
|
||||
migrations from external accounting systems.
|
||||
url: >-
|
||||
#manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
||||
- depth: 2
|
||||
title: >-
|
||||
List source IPs that have connected with the given client’s credentials.
|
||||
Returns an array of "ip (timestamp)" strings.
|
||||
url: >-
|
||||
#list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
||||
- depth: 2
|
||||
title: Reset the recorded IP list for a client.
|
||||
url: '#reset-the-recorded-ip-list-for-a-client'
|
||||
- depth: 2
|
||||
title: >-
|
||||
List the emails of currently connected clients (last seen within the
|
||||
heartbeat window), deduped across every node.
|
||||
url: >-
|
||||
#list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
||||
- depth: 2
|
||||
title: >-
|
||||
Online client emails grouped by the panelGuid of the node that
|
||||
physically hosts each client. The local panel uses its own GUID; each
|
||||
node (at any depth in a chain) uses its GUID. Lets the inbounds page
|
||||
attribute online status to the real node instead of the intermediate one
|
||||
it syncs through.
|
||||
url: >-
|
||||
#online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through
|
||||
- depth: 2
|
||||
title: >-
|
||||
Per-client source IPs grouped by the panelGuid of the node that observed
|
||||
them. Lets the central panel attribute and enforce per-client IP limits
|
||||
using the real visitor IPs each node sees, instead of the address of the
|
||||
intermediate panel it syncs through.
|
||||
url: >-
|
||||
#per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through
|
||||
- depth: 2
|
||||
title: >-
|
||||
Inbound tags that carried traffic within the heartbeat window, grouped
|
||||
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
||||
inbounds page only marks a multi-inbound client online on the inbounds
|
||||
it actually used. Nodes that do not report per-inbound activity are
|
||||
absent.
|
||||
url: >-
|
||||
#inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent
|
||||
- depth: 2
|
||||
title: Map of client email → last-seen unix timestamp.
|
||||
url: '#map-of-client-email--last-seen-unix-timestamp'
|
||||
- depth: 2
|
||||
title: Traffic counters for a client identified by email.
|
||||
url: '#traffic-counters-for-a-client-identified-by-email'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||||
external proxy. Empty array when the subId has no enabled clients.
|
||||
url: >-
|
||||
#return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every URL for one client across all attached inbounds — the same
|
||||
strings the Copy URL button copies in the panel UI. Supported protocols:
|
||||
vmess, vless, trojan, shadowsocks, hysteria. If
|
||||
streamSettings.externalProxy is set, returns one URL per external proxy.
|
||||
Protocols without a URL form (socks, http, mixed, wireguard, dokodemo,
|
||||
tunnel) contribute nothing.
|
||||
url: >-
|
||||
#return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every client with its attached inbound IDs and traffic record.
|
||||
The reverse field, if set, is returned as a nested JSON object (legacy
|
||||
JSON-encoded-string form is still accepted on write).
|
||||
id: >-
|
||||
list-every-client-with-its-attached-inbound-ids-and-traffic-record-the-reverse-field-if-set-is-returned-as-a-nested-json-object-legacy-json-encoded-string-form-is-still-accepted-on-write
|
||||
- content: >-
|
||||
Filter, sort, and paginate clients on the server. Each item is a slim
|
||||
row (no uuid/password/auth/flow/security/reverse/tgId) so the clients
|
||||
page can ship 25-ish rows in a few KB instead of the full table. The
|
||||
response also includes a summary computed across the full DB row set
|
||||
so dashboard counters stay stable as the user paginates or filters.
|
||||
Page size capped at 200; fetch /get/:email to obtain the full
|
||||
per-client payload for an edit/info modal.
|
||||
id: >-
|
||||
filter-sort-and-paginate-clients-on-the-server-each-item-is-a-slim-row-no-uuidpasswordauthflowsecurityreversetgid-so-the-clients-page-can-ship-25-ish-rows-in-a-few-kb-instead-of-the-full-table-the-response-also-includes-a-summary-computed-across-the-full-db-row-set-so-dashboard-counters-stay-stable-as-the-user-paginates-or-filters-page-size-capped-at-200-fetch-getemail-to-obtain-the-full-per-client-payload-for-an-editinfo-modal
|
||||
- content: >-
|
||||
Fetch one client by email, including the inbound IDs and external
|
||||
config IDs it is attached to.
|
||||
id: >-
|
||||
fetch-one-client-by-email-including-the-inbound-ids-and-external-config-ids-it-is-attached-to
|
||||
- content: >-
|
||||
Create a new client and attach it to one or more inbounds in a single
|
||||
call. Body is JSON. Per-protocol secrets (UUID for VLESS/VMess,
|
||||
password for Trojan/Shadowsocks, auth for Hysteria) are generated
|
||||
server-side when omitted, so callers can send only the universal
|
||||
fields.
|
||||
id: >-
|
||||
create-a-new-client-and-attach-it-to-one-or-more-inbounds-in-a-single-call-body-is-json-per-protocol-secrets-uuid-for-vlessvmess-password-for-trojanshadowsocks-auth-for-hysteria-are-generated-server-side-when-omitted-so-callers-can-send-only-the-universal-fields
|
||||
- content: >-
|
||||
Update an existing client by email. Changes propagate to every
|
||||
attached inbound. Body is the JSON client payload — supply the full
|
||||
set of fields you want to keep (the server replaces the row, it does
|
||||
not patch).
|
||||
id: >-
|
||||
update-an-existing-client-by-email-changes-propagate-to-every-attached-inbound-body-is-the-json-client-payload--supply-the-full-set-of-fields-you-want-to-keep-the-server-replaces-the-row-it-does-not-patch
|
||||
- content: >-
|
||||
Delete a client by email. Removes it from every attached inbound and
|
||||
drops its traffic record unless keepTraffic=1 is passed.
|
||||
id: >-
|
||||
delete-a-client-by-email-removes-it-from-every-attached-inbound-and-drops-its-traffic-record-unless-keeptraffic1-is-passed
|
||||
- content: >-
|
||||
Attach an existing client to one or more additional inbounds. Body is
|
||||
JSON.
|
||||
id: >-
|
||||
attach-an-existing-client-to-one-or-more-additional-inbounds-body-is-json
|
||||
- content: Detach a client from one or more inbounds without deleting the client.
|
||||
id: detach-a-client-from-one-or-more-inbounds-without-deleting-the-client
|
||||
- content: >-
|
||||
Replace a client's external links (per-client share links and remote
|
||||
subscription URLs surfaced in their subscription). Sends the full set;
|
||||
the server replaces all rows.
|
||||
id: >-
|
||||
replace-a-clients-external-links-per-client-share-links-and-remote-subscription-urls-surfaced-in-their-subscription-sends-the-full-set-the-server-replaces-all-rows
|
||||
- content: >-
|
||||
Reset the up/down counters for every client globally. Quotas and
|
||||
expiry are not affected. Triggers an Xray restart if any counter
|
||||
actually moved.
|
||||
id: >-
|
||||
reset-the-updown-counters-for-every-client-globally-quotas-and-expiry-are-not-affected-triggers-an-xray-restart-if-any-counter-actually-moved
|
||||
- content: >-
|
||||
Delete every client whose traffic quota is exhausted (used >= total,
|
||||
when reset is disabled) or whose expiry has passed. Returns the
|
||||
deleted count and triggers an Xray restart when any client was on a
|
||||
running inbound.
|
||||
id: >-
|
||||
delete-every-client-whose-traffic-quota-is-exhausted-used--total-when-reset-is-disabled-or-whose-expiry-has-passed-returns-the-deleted-count-and-triggers-an-xray-restart-when-any-client-was-on-a-running-inbound
|
||||
- content: >-
|
||||
Delete every client that is not attached to any inbound, along with
|
||||
its traffic record, IP log, and external links. Useful for clearing
|
||||
clients left unattached after their inbounds were removed. Returns the
|
||||
deleted count. Cannot be undone.
|
||||
id: >-
|
||||
delete-every-client-that-is-not-attached-to-any-inbound-along-with-its-traffic-record-ip-log-and-external-links-useful-for-clearing-clients-left-unattached-after-their-inbounds-were-removed-returns-the-deleted-count-cannot-be-undone
|
||||
- content: >-
|
||||
Return every client as a {client, inboundIds} array — the same shape
|
||||
/bulkCreate and /import accept — so the payload round-trips straight
|
||||
back through /import. Clients with no inbound attachment are included
|
||||
with an empty inboundIds list. The UI shows this in a CodeMirror
|
||||
viewer (copy / download); programmatic callers get the array in obj.
|
||||
id: >-
|
||||
return-every-client-as-a-client-inboundids-array--the-same-shape-bulkcreate-and-import-accept--so-the-payload-round-trips-straight-back-through-import-clients-with-no-inbound-attachment-are-included-with-an-empty-inboundids-list-the-ui-shows-this-in-a-codemirror-viewer-copy--download-programmatic-callers-get-the-array-in-obj
|
||||
- content: >-
|
||||
Import clients from a JSON body { "data": "<json>" }, where data is a
|
||||
string-encoded array produced by /export ([{client, inboundIds}]).
|
||||
Items with inboundIds are created and attached to those inbounds;
|
||||
items with an empty inboundIds list are restored as unattached client
|
||||
records. Existing emails are never overwritten — they are returned in
|
||||
skipped. Triggers a single Xray restart at the end if any target
|
||||
inbound was running.
|
||||
id: >-
|
||||
import-clients-from-a-json-body--data-json--where-data-is-a-string-encoded-array-produced-by-export-client-inboundids-items-with-inboundids-are-created-and-attached-to-those-inbounds-items-with-an-empty-inboundids-list-are-restored-as-unattached-client-records-existing-emails-are-never-overwritten--they-are-returned-in-skipped-triggers-a-single-xray-restart-at-the-end-if-any-target-inbound-was-running
|
||||
- content: >-
|
||||
Shift expiry and/or traffic quota for many clients in one call.
|
||||
addDays/addBytes may be negative. Clients with unlimited expiry
|
||||
(expiryTime=0) or unlimited traffic (totalGB=0) are skipped for the
|
||||
corresponding field — bulk extend never converts unlimited to limited.
|
||||
The optional flow directive sets the XTLS flow on every client: "none"
|
||||
clears it, "xtls-rprx-vision"/"xtls-rprx-vision-udp443" set it where
|
||||
the inbound supports it (omit or "" to leave it unchanged). Returns
|
||||
the adjusted count and per-email skip reasons.
|
||||
id: >-
|
||||
shift-expiry-andor-traffic-quota-for-many-clients-in-one-call-adddaysaddbytes-may-be-negative-clients-with-unlimited-expiry-expirytime0-or-unlimited-traffic-totalgb0-are-skipped-for-the-corresponding-field--bulk-extend-never-converts-unlimited-to-limited-the-optional-flow-directive-sets-the-xtls-flow-on-every-client-none-clears-it-xtls-rprx-visionxtls-rprx-vision-udp443-set-it-where-the-inbound-supports-it-omit-or--to-leave-it-unchanged-returns-the-adjusted-count-and-per-email-skip-reasons
|
||||
- content: >-
|
||||
Enable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to add each user. Note that enabling
|
||||
a client whose quota is exhausted or whose expiry has passed only
|
||||
flips the flag — the traffic loop will disable it again on the next
|
||||
tick. Returns the changed count and per-email skip reasons.
|
||||
id: >-
|
||||
enable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-add-each-user-note-that-enabling-a-client-whose-quota-is-exhausted-or-whose-expiry-has-passed-only-flips-the-flag--the-traffic-loop-will-disable-it-again-on-the-next-tick-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- content: >-
|
||||
Disable many clients in one call. Emails are grouped by inbound and
|
||||
applied with a single read-modify-write per inbound; the running Xray
|
||||
(local or remote node) is updated to remove each user. Returns the
|
||||
changed count and per-email skip reasons.
|
||||
id: >-
|
||||
disable-many-clients-in-one-call-emails-are-grouped-by-inbound-and-applied-with-a-single-read-modify-write-per-inbound-the-running-xray-local-or-remote-node-is-updated-to-remove-each-user-returns-the-changed-count-and-per-email-skip-reasons
|
||||
- content: >-
|
||||
Delete many clients in one call. The server processes the list
|
||||
sequentially so each delete sees the committed state of the previous
|
||||
one — avoids the race the per-email fan-out had on the panel side.
|
||||
Pass keepTraffic=true to retain the xray_client_traffic rows after
|
||||
deletion.
|
||||
id: >-
|
||||
delete-many-clients-in-one-call-the-server-processes-the-list-sequentially-so-each-delete-sees-the-committed-state-of-the-previous-one--avoids-the-race-the-per-email-fan-out-had-on-the-panel-side-pass-keeptraffictrue-to-retain-the-xray_client_traffic-rows-after-deletion
|
||||
- content: >-
|
||||
Create many clients in one call. Body is a JSON array of {client,
|
||||
inboundIds} payloads — the same shape /add accepts. Items are
|
||||
processed sequentially; per-email skip reasons are returned for items
|
||||
that fail (e.g., duplicate email). Triggers a single Xray restart at
|
||||
the end if any inbound was running.
|
||||
id: >-
|
||||
create-many-clients-in-one-call-body-is-a-json-array-of-client-inboundids-payloads--the-same-shape-add-accepts-items-are-processed-sequentially-per-email-skip-reasons-are-returned-for-items-that-fail-eg-duplicate-email-triggers-a-single-xray-restart-at-the-end-if-any-inbound-was-running
|
||||
- content: >-
|
||||
Add many clients to a group in one call. Updates clients.group_name
|
||||
and patches the matching client entry inside every owning inbound's
|
||||
settings JSON in a single transaction. If the group name does not yet
|
||||
exist (in client_groups or as a derived label), it is auto-created as
|
||||
a persistent group. To clear the group label, use /groups/bulkRemove
|
||||
instead.
|
||||
id: >-
|
||||
add-many-clients-to-a-group-in-one-call-updates-clientsgroup_name-and-patches-the-matching-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-if-the-group-name-does-not-yet-exist-in-client_groups-or-as-a-derived-label-it-is-auto-created-as-a-persistent-group-to-clear-the-group-label-use-groupsbulkremove-instead
|
||||
- content: >-
|
||||
Clear the group label on many clients in one call. Inverse of
|
||||
/groups/bulkAdd. Clients themselves are kept — only the group label is
|
||||
cleared from clients.group_name and from each owning inbound's
|
||||
settings JSON. Groups become empty if all their members are removed.
|
||||
id: >-
|
||||
clear-the-group-label-on-many-clients-in-one-call-inverse-of-groupsbulkadd-clients-themselves-are-kept--only-the-group-label-is-cleared-from-clientsgroup_name-and-from-each-owning-inbounds-settings-json-groups-become-empty-if-all-their-members-are-removed
|
||||
- content: >-
|
||||
Attach many existing clients to many inbounds in one call. Each client
|
||||
keeps its identity (email/UUID/password/subId) and a shared traffic
|
||||
row; all clients are added to a target inbound in a single
|
||||
AddInboundClient call. Clients already present on a target are
|
||||
reported under skipped. Returns per-email attached/skipped/errors
|
||||
lists and triggers a single Xray restart if any target inbound was
|
||||
running.
|
||||
id: >-
|
||||
attach-many-existing-clients-to-many-inbounds-in-one-call-each-client-keeps-its-identity-emailuuidpasswordsubid-and-a-shared-traffic-row-all-clients-are-added-to-a-target-inbound-in-a-single-addinboundclient-call-clients-already-present-on-a-target-are-reported-under-skipped-returns-per-email-attachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- content: >-
|
||||
Mirror of bulkAttach: detach many existing clients from many inbounds
|
||||
in one call. For each email, intersects the client's current inbounds
|
||||
with the requested set and detaches from those only; (email, inbound)
|
||||
pairs where the client is not currently attached are silently no-ops.
|
||||
Emails not attached to any of the requested inbounds are reported
|
||||
under skipped. Client records are kept even if they become orphaned —
|
||||
use bulkDel for full removal. Returns per-email
|
||||
detached/skipped/errors lists and triggers a single Xray restart if
|
||||
any target inbound was running.
|
||||
id: >-
|
||||
mirror-of-bulkattach-detach-many-existing-clients-from-many-inbounds-in-one-call-for-each-email-intersects-the-clients-current-inbounds-with-the-requested-set-and-detaches-from-those-only-email-inbound-pairs-where-the-client-is-not-currently-attached-are-silently-no-ops-emails-not-attached-to-any-of-the-requested-inbounds-are-reported-under-skipped-client-records-are-kept-even-if-they-become-orphaned--use-bulkdel-for-full-removal-returns-per-email-detachedskippederrors-lists-and-triggers-a-single-xray-restart-if-any-target-inbound-was-running
|
||||
- content: >-
|
||||
Zero up/down counters for many clients in one call. Loops the
|
||||
single-reset path so each client is re-enabled across its attached
|
||||
inbounds and pushed to Xray/remote nodes. Returns the count of
|
||||
successfully reset clients.
|
||||
id: >-
|
||||
zero-updown-counters-for-many-clients-in-one-call-loops-the-single-reset-path-so-each-client-is-re-enabled-across-its-attached-inbounds-and-pushed-to-xrayremote-nodes-returns-the-count-of-successfully-reset-clients
|
||||
- content: >-
|
||||
List all client groups with their member counts. Merges persisted
|
||||
groups (rows in client_groups, including empty placeholders) with the
|
||||
distinct group_name values currently set on clients. Sorted
|
||||
alphabetically (case-insensitive).
|
||||
id: >-
|
||||
list-all-client-groups-with-their-member-counts-merges-persisted-groups-rows-in-client_groups-including-empty-placeholders-with-the-distinct-group_name-values-currently-set-on-clients-sorted-alphabetically-case-insensitive
|
||||
- content: >-
|
||||
Return just the email list of clients that currently belong to the
|
||||
given group. Useful for fanning a single bulk action over an entire
|
||||
group without round-tripping the full client list.
|
||||
id: >-
|
||||
return-just-the-email-list-of-clients-that-currently-belong-to-the-given-group-useful-for-fanning-a-single-bulk-action-over-an-entire-group-without-round-tripping-the-full-client-list
|
||||
- content: >-
|
||||
Create a new empty (placeholder) group. The group becomes selectable
|
||||
in client forms and the filter drawer even before any client is added
|
||||
to it. Errors if a group with the same name already exists.
|
||||
id: >-
|
||||
create-a-new-empty-placeholder-group-the-group-becomes-selectable-in-client-forms-and-the-filter-drawer-even-before-any-client-is-added-to-it-errors-if-a-group-with-the-same-name-already-exists
|
||||
- content: >-
|
||||
Rename a group. The new name is applied to the client_groups row AND
|
||||
propagated to every matching client (both clients.group_name and the
|
||||
client entry inside every owning inbound's settings JSON) in a single
|
||||
transaction. Returns the number of clients whose label was updated.
|
||||
id: >-
|
||||
rename-a-group-the-new-name-is-applied-to-the-client_groups-row-and-propagated-to-every-matching-client-both-clientsgroup_name-and-the-client-entry-inside-every-owning-inbounds-settings-json-in-a-single-transaction-returns-the-number-of-clients-whose-label-was-updated
|
||||
- content: >-
|
||||
Remove a group. Deletes the client_groups row and clears the group
|
||||
label from every matching client (both clients.group_name and the
|
||||
inbound settings JSON). The clients themselves are NOT deleted — use
|
||||
/bulkDel after filtering by group for that. Returns the count of
|
||||
clients whose label was cleared.
|
||||
id: >-
|
||||
remove-a-group-deletes-the-client_groups-row-and-clears-the-group-label-from-every-matching-client-both-clientsgroup_name-and-the-inbound-settings-json-the-clients-themselves-are-not-deleted--use-bulkdel-after-filtering-by-group-for-that-returns-the-count-of-clients-whose-label-was-cleared
|
||||
- content: >-
|
||||
Zero out a single client’s up/down counters. Re-enables the client
|
||||
across every attached inbound and pushes the change to Xray (or the
|
||||
remote node) so depleted users can connect again immediately.
|
||||
id: >-
|
||||
zero-out-a-single-clients-updown-counters-re-enables-the-client-across-every-attached-inbound-and-pushes-the-change-to-xray-or-the-remote-node-so-depleted-users-can-connect-again-immediately
|
||||
- content: >-
|
||||
Manually adjust a client’s upload + download counters. Useful for
|
||||
migrations from external accounting systems.
|
||||
id: >-
|
||||
manually-adjust-a-clients-upload--download-counters-useful-for-migrations-from-external-accounting-systems
|
||||
- content: >-
|
||||
List source IPs that have connected with the given client’s
|
||||
credentials. Returns an array of "ip (timestamp)" strings.
|
||||
id: >-
|
||||
list-source-ips-that-have-connected-with-the-given-clients-credentials-returns-an-array-of-ip-timestamp-strings
|
||||
- content: Reset the recorded IP list for a client.
|
||||
id: reset-the-recorded-ip-list-for-a-client
|
||||
- content: >-
|
||||
List the emails of currently connected clients (last seen within the
|
||||
heartbeat window), deduped across every node.
|
||||
id: >-
|
||||
list-the-emails-of-currently-connected-clients-last-seen-within-the-heartbeat-window-deduped-across-every-node
|
||||
- content: >-
|
||||
Online client emails grouped by the panelGuid of the node that
|
||||
physically hosts each client. The local panel uses its own GUID; each
|
||||
node (at any depth in a chain) uses its GUID. Lets the inbounds page
|
||||
attribute online status to the real node instead of the intermediate
|
||||
one it syncs through.
|
||||
id: >-
|
||||
online-client-emails-grouped-by-the-panelguid-of-the-node-that-physically-hosts-each-client-the-local-panel-uses-its-own-guid-each-node-at-any-depth-in-a-chain-uses-its-guid-lets-the-inbounds-page-attribute-online-status-to-the-real-node-instead-of-the-intermediate-one-it-syncs-through
|
||||
- content: >-
|
||||
Per-client source IPs grouped by the panelGuid of the node that
|
||||
observed them. Lets the central panel attribute and enforce per-client
|
||||
IP limits using the real visitor IPs each node sees, instead of the
|
||||
address of the intermediate panel it syncs through.
|
||||
id: >-
|
||||
per-client-source-ips-grouped-by-the-panelguid-of-the-node-that-observed-them-lets-the-central-panel-attribute-and-enforce-per-client-ip-limits-using-the-real-visitor-ips-each-node-sees-instead-of-the-address-of-the-intermediate-panel-it-syncs-through
|
||||
- content: >-
|
||||
Inbound tags that carried traffic within the heartbeat window, grouped
|
||||
by the hosting node's panelGuid. Pairs with onlinesByGuid so the
|
||||
inbounds page only marks a multi-inbound client online on the inbounds
|
||||
it actually used. Nodes that do not report per-inbound activity are
|
||||
absent.
|
||||
id: >-
|
||||
inbound-tags-that-carried-traffic-within-the-heartbeat-window-grouped-by-the-hosting-nodes-panelguid-pairs-with-onlinesbyguid-so-the-inbounds-page-only-marks-a-multi-inbound-client-online-on-the-inbounds-it-actually-used-nodes-that-do-not-report-per-inbound-activity-are-absent
|
||||
- content: Map of client email → last-seen unix timestamp.
|
||||
id: map-of-client-email--last-seen-unix-timestamp
|
||||
- content: Traffic counters for a client identified by email.
|
||||
id: traffic-counters-for-a-client-identified-by-email
|
||||
- content: >-
|
||||
Return every protocol URL (vless://, vmess://, trojan://, ss://,
|
||||
hysteria://, hy2://) for clients matching the subscription ID. Same
|
||||
result set as /sub/<subId>, but as a JSON array — no base64. When an
|
||||
inbound has streamSettings.externalProxy set, one URL is emitted per
|
||||
external proxy. Empty array when the subId has no enabled clients.
|
||||
id: >-
|
||||
return-every-protocol-url-vless-vmess-trojan-ss-hysteria-hy2-for-clients-matching-the-subscription-id-same-result-set-as-subsubid-but-as-a-json-array--no-base64-when-an-inbound-has-streamsettingsexternalproxy-set-one-url-is-emitted-per-external-proxy-empty-array-when-the-subid-has-no-enabled-clients
|
||||
- content: >-
|
||||
Return every URL for one client across all attached inbounds — the
|
||||
same strings the Copy URL button copies in the panel UI. Supported
|
||||
protocols: vmess, vless, trojan, shadowsocks, hysteria. If
|
||||
streamSettings.externalProxy is set, returns one URL per external
|
||||
proxy. Protocols without a URL form (socks, http, mixed, wireguard,
|
||||
dokodemo, tunnel) contribute nothing.
|
||||
id: >-
|
||||
return-every-url-for-one-client-across-all-attached-inbounds--the-same-strings-the-copy-url-button-copies-in-the-panel-ui-supported-protocols-vmess-vless-trojan-shadowsocks-hysteria-if-streamsettingsexternalproxy-is-set-returns-one-url-per-external-proxy-protocols-without-a-url-form-socks-http-mixed-wireguard-dokodemo-tunnel-contribute-nothing
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/clients/list","method":"get"},{"path":"/panel/api/clients/list/paged","method":"get"},{"path":"/panel/api/clients/get/{email}","method":"get"},{"path":"/panel/api/clients/add","method":"post"},{"path":"/panel/api/clients/update/{email}","method":"post"},{"path":"/panel/api/clients/del/{email}","method":"post"},{"path":"/panel/api/clients/{email}/attach","method":"post"},{"path":"/panel/api/clients/{email}/detach","method":"post"},{"path":"/panel/api/clients/{email}/externalLinks","method":"post"},{"path":"/panel/api/clients/resetAllTraffics","method":"post"},{"path":"/panel/api/clients/delDepleted","method":"post"},{"path":"/panel/api/clients/delOrphans","method":"post"},{"path":"/panel/api/clients/export","method":"get"},{"path":"/panel/api/clients/import","method":"post"},{"path":"/panel/api/clients/bulkAdjust","method":"post"},{"path":"/panel/api/clients/bulkEnable","method":"post"},{"path":"/panel/api/clients/bulkDisable","method":"post"},{"path":"/panel/api/clients/bulkDel","method":"post"},{"path":"/panel/api/clients/bulkCreate","method":"post"},{"path":"/panel/api/clients/groups/bulkAdd","method":"post"},{"path":"/panel/api/clients/groups/bulkRemove","method":"post"},{"path":"/panel/api/clients/bulkAttach","method":"post"},{"path":"/panel/api/clients/bulkDetach","method":"post"},{"path":"/panel/api/clients/bulkResetTraffic","method":"post"},{"path":"/panel/api/clients/groups","method":"get"},{"path":"/panel/api/clients/groups/{name}/emails","method":"get"},{"path":"/panel/api/clients/groups/create","method":"post"},{"path":"/panel/api/clients/groups/rename","method":"post"},{"path":"/panel/api/clients/groups/delete","method":"post"},{"path":"/panel/api/clients/resetTraffic/{email}","method":"post"},{"path":"/panel/api/clients/updateTraffic/{email}","method":"post"},{"path":"/panel/api/clients/ips/{email}","method":"post"},{"path":"/panel/api/clients/clearIps/{email}","method":"post"},{"path":"/panel/api/clients/onlines","method":"post"},{"path":"/panel/api/clients/onlinesByGuid","method":"post"},{"path":"/panel/api/clients/clientIpsByGuid","method":"post"},{"path":"/panel/api/clients/activeInbounds","method":"post"},{"path":"/panel/api/clients/lastOnline","method":"post"},{"path":"/panel/api/clients/traffic/{email}","method":"get"},{"path":"/panel/api/clients/subLinks/{subId}","method":"get"},{"path":"/panel/api/clients/links/{email}","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
title: Hosts
|
||||
description: >-
|
||||
Per-inbound override endpoints. Each enabled host renders one extra
|
||||
subscription link/proxy with its own address/port/TLS, superseding the legacy
|
||||
externalProxy array. All endpoints under /panel/api/hosts.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every host across all inbounds, grouped by inbound then ordered by
|
||||
sort order.
|
||||
url: >-
|
||||
#list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
|
||||
- depth: 2
|
||||
title: Fetch a single host by ID.
|
||||
url: '#fetch-a-single-host-by-id'
|
||||
- depth: 2
|
||||
title: Fetch one inbound's hosts, ordered by sort order then id.
|
||||
url: '#fetch-one-inbounds-hosts-ordered-by-sort-order-then-id'
|
||||
- depth: 2
|
||||
title: Distinct, sorted set of tags used across all hosts.
|
||||
url: '#distinct-sorted-set-of-tags-used-across-all-hosts'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a host on an inbound. inboundId and remark are required; security
|
||||
defaults to "same" (inherit the inbound).
|
||||
url: >-
|
||||
#create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace a host’s content. The inbound and sort order are immutable here
|
||||
(use /reorder for ordering).
|
||||
url: >-
|
||||
#replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
|
||||
- depth: 2
|
||||
title: Delete a host.
|
||||
url: '#delete-a-host'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Enable or disable a single host (disabled hosts are skipped in
|
||||
subscriptions).
|
||||
url: >-
|
||||
#enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
|
||||
- depth: 2
|
||||
title: Set host sort order by the position of each id in the array.
|
||||
url: '#set-host-sort-order-by-the-position-of-each-id-in-the-array'
|
||||
- depth: 2
|
||||
title: Enable or disable many hosts in one call.
|
||||
url: '#enable-or-disable-many-hosts-in-one-call'
|
||||
- depth: 2
|
||||
title: Delete many hosts in one call.
|
||||
url: '#delete-many-hosts-in-one-call'
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every host across all inbounds, grouped by inbound then ordered
|
||||
by sort order.
|
||||
id: >-
|
||||
list-every-host-across-all-inbounds-grouped-by-inbound-then-ordered-by-sort-order
|
||||
- content: Fetch a single host by ID.
|
||||
id: fetch-a-single-host-by-id
|
||||
- content: Fetch one inbound's hosts, ordered by sort order then id.
|
||||
id: fetch-one-inbounds-hosts-ordered-by-sort-order-then-id
|
||||
- content: Distinct, sorted set of tags used across all hosts.
|
||||
id: distinct-sorted-set-of-tags-used-across-all-hosts
|
||||
- content: >-
|
||||
Create a host on an inbound. inboundId and remark are required;
|
||||
security defaults to "same" (inherit the inbound).
|
||||
id: >-
|
||||
create-a-host-on-an-inbound-inboundid-and-remark-are-required-security-defaults-to-same-inherit-the-inbound
|
||||
- content: >-
|
||||
Replace a host’s content. The inbound and sort order are immutable
|
||||
here (use /reorder for ordering).
|
||||
id: >-
|
||||
replace-a-hosts-content-the-inbound-and-sort-order-are-immutable-here-use-reorder-for-ordering
|
||||
- content: Delete a host.
|
||||
id: delete-a-host
|
||||
- content: >-
|
||||
Enable or disable a single host (disabled hosts are skipped in
|
||||
subscriptions).
|
||||
id: >-
|
||||
enable-or-disable-a-single-host-disabled-hosts-are-skipped-in-subscriptions
|
||||
- content: Set host sort order by the position of each id in the array.
|
||||
id: set-host-sort-order-by-the-position-of-each-id-in-the-array
|
||||
- content: Enable or disable many hosts in one call.
|
||||
id: enable-or-disable-many-hosts-in-one-call
|
||||
- content: Delete many hosts in one call.
|
||||
id: delete-many-hosts-in-one-call
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/hosts/list","method":"get"},{"path":"/panel/api/hosts/get/{id}","method":"get"},{"path":"/panel/api/hosts/byInbound/{inboundId}","method":"get"},{"path":"/panel/api/hosts/tags","method":"get"},{"path":"/panel/api/hosts/add","method":"post"},{"path":"/panel/api/hosts/update/{id}","method":"post"},{"path":"/panel/api/hosts/del/{id}","method":"post"},{"path":"/panel/api/hosts/setEnable/{id}","method":"post"},{"path":"/panel/api/hosts/reorder","method":"post"},{"path":"/panel/api/hosts/bulk/setEnable","method":"post"},{"path":"/panel/api/hosts/bulk/del","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
---
|
||||
title: Inbounds
|
||||
description: >-
|
||||
Manage inbound configurations and their clients. All endpoints live under
|
||||
/panel/api/inbounds and require a logged-in session or Bearer token.
|
||||
Link-generating endpoints honour forwarded headers only when the request comes
|
||||
from a configured trusted proxy.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every inbound owned by the authenticated user, including each
|
||||
inbound’s clientStats traffic counters. settings, streamSettings, and
|
||||
sniffing are returned as nested JSON objects (no escaped strings);
|
||||
legacy callers that send them back as JSON-encoded strings are still
|
||||
accepted on write.
|
||||
url: >-
|
||||
#list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write
|
||||
- depth: 2
|
||||
title: >-
|
||||
Same shape as /list but with settings.clients[] stripped down to {email,
|
||||
enable, comment} and ClientStats not enriched with UUID/SubId. Use this
|
||||
for list pages; fetch /get/:id when you need the full per-client payload
|
||||
(uuid, password, flow, ...).
|
||||
url: >-
|
||||
#same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-
|
||||
- depth: 2
|
||||
title: >-
|
||||
Lightweight picker projection of the authenticated user’s inbounds.
|
||||
Returns id, remark, tag, protocol, port, a server-computed
|
||||
tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
|
||||
XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
|
||||
Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the
|
||||
client UI to generate a valid Shadowsocks 2022 PSK). Use this for
|
||||
dropdowns and attach pickers — it skips settings, streamSettings, and
|
||||
clientStats so the payload stays small even on panels with thousands of
|
||||
clients.
|
||||
url: >-
|
||||
#lightweight-picker-projection-of-the-authenticated-users-inbounds-returns-id-remark-tag-protocol-port-a-server-computed-tlsflowcapable-flag-true-for-vless-on-tcp-with-tls-or-reality-or-on-xhttp-with-vless-encryption--vlessenc-enabled-and-ssmethod-the-shadowsocks-cipher-empty-for-non-shadowsocks-inbounds--used-by-the-client-ui-to-generate-a-valid-shadowsocks-2022-psk-use-this-for-dropdowns-and-attach-pickers--it-skips-settings-streamsettings-and-clientstats-so-the-payload-stays-small-even-on-panels-with-thousands-of-clients
|
||||
- depth: 2
|
||||
title: Fetch a single inbound by numeric ID.
|
||||
url: '#fetch-a-single-inbound-by-numeric-id'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create a new inbound. Send the full inbound payload (protocol, port,
|
||||
settings, streamSettings, sniffing, remark, expiryTime, total, enable).
|
||||
settings, streamSettings, and sniffing may be sent as nested JSON
|
||||
objects (preferred) or as JSON-encoded strings (legacy).
|
||||
url: >-
|
||||
#create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy
|
||||
- depth: 2
|
||||
title: Delete an inbound by ID. Also removes its associated client stats rows.
|
||||
url: '#delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete many inbounds in one call. Processes the list sequentially;
|
||||
failures are reported per id and the rest still proceed. Restarts xray
|
||||
at most once.
|
||||
url: >-
|
||||
#delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
||||
inbounds with thousands of clients — prefer /setEnable for enable-only
|
||||
flips.
|
||||
url: >-
|
||||
#replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
|
||||
- depth: 2
|
||||
title: >-
|
||||
Toggle only the enable flag without serialising the whole settings JSON.
|
||||
Recommended for UI switches on large inbounds.
|
||||
url: >-
|
||||
#toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds
|
||||
- depth: 2
|
||||
title: >-
|
||||
Zero out upload + download counters for a single inbound. Does not touch
|
||||
per-client counters.
|
||||
url: >-
|
||||
#zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
|
||||
- depth: 2
|
||||
title: >-
|
||||
Remove every client attached to a single inbound while keeping the
|
||||
inbound itself. Collects emails from settings.clients[] and feeds them
|
||||
into the optimized bulk-delete path (runtime user removal + traffic-row
|
||||
cleanup + SyncInbound). Destructive and cannot be undone.
|
||||
url: >-
|
||||
#remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reset upload + download counters on every inbound. Destructive —
|
||||
accounting history is lost.
|
||||
url: >-
|
||||
#reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
|
||||
- depth: 2
|
||||
title: >-
|
||||
Bulk-import an inbound from a JSON blob (e.g. one exported via the UI).
|
||||
The body uses form encoding with a single "data" field.
|
||||
url: >-
|
||||
#bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field
|
||||
- depth: 2
|
||||
title: >-
|
||||
Receive a master panel's aggregated per-client usage, keyed by the
|
||||
master's GUID. Stored in a side table used only for the UI display
|
||||
overlay and local quota enforcement — never folded into the local
|
||||
counters that masters poll, so delta accounting stays intact. Called
|
||||
panel-to-panel by the node traffic sync job.
|
||||
url: >-
|
||||
#receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job
|
||||
- depth: 2
|
||||
title: >-
|
||||
List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
||||
inbound. Each rule links one child inbound (the dest) to optional
|
||||
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
||||
inbound's listen+port is used.
|
||||
url: >-
|
||||
#list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used
|
||||
- depth: 2
|
||||
title: >-
|
||||
Replace the entire fallback list for a master inbound. Body is JSON.
|
||||
Triggers an Xray restart.
|
||||
url: >-
|
||||
#replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every inbound owned by the authenticated user, including each
|
||||
inbound’s clientStats traffic counters. settings, streamSettings, and
|
||||
sniffing are returned as nested JSON objects (no escaped strings);
|
||||
legacy callers that send them back as JSON-encoded strings are still
|
||||
accepted on write.
|
||||
id: >-
|
||||
list-every-inbound-owned-by-the-authenticated-user-including-each-inbounds-clientstats-traffic-counters-settings-streamsettings-and-sniffing-are-returned-as-nested-json-objects-no-escaped-strings-legacy-callers-that-send-them-back-as-json-encoded-strings-are-still-accepted-on-write
|
||||
- content: >-
|
||||
Same shape as /list but with settings.clients[] stripped down to
|
||||
{email, enable, comment} and ClientStats not enriched with UUID/SubId.
|
||||
Use this for list pages; fetch /get/:id when you need the full
|
||||
per-client payload (uuid, password, flow, ...).
|
||||
id: >-
|
||||
same-shape-as-list-but-with-settingsclients-stripped-down-to-email-enable-comment-and-clientstats-not-enriched-with-uuidsubid-use-this-for-list-pages-fetch-getid-when-you-need-the-full-per-client-payload-uuid-password-flow-
|
||||
- content: >-
|
||||
Lightweight picker projection of the authenticated user’s inbounds.
|
||||
Returns id, remark, tag, protocol, port, a server-computed
|
||||
tlsFlowCapable flag (true for VLESS on TCP with tls or reality, or on
|
||||
XHTTP with VLESS encryption / vlessenc enabled), and ssMethod (the
|
||||
Shadowsocks cipher, empty for non-Shadowsocks inbounds — used by the
|
||||
client UI to generate a valid Shadowsocks 2022 PSK). Use this for
|
||||
dropdowns and attach pickers — it skips settings, streamSettings, and
|
||||
clientStats so the payload stays small even on panels with thousands
|
||||
of clients.
|
||||
id: >-
|
||||
lightweight-picker-projection-of-the-authenticated-users-inbounds-returns-id-remark-tag-protocol-port-a-server-computed-tlsflowcapable-flag-true-for-vless-on-tcp-with-tls-or-reality-or-on-xhttp-with-vless-encryption--vlessenc-enabled-and-ssmethod-the-shadowsocks-cipher-empty-for-non-shadowsocks-inbounds--used-by-the-client-ui-to-generate-a-valid-shadowsocks-2022-psk-use-this-for-dropdowns-and-attach-pickers--it-skips-settings-streamsettings-and-clientstats-so-the-payload-stays-small-even-on-panels-with-thousands-of-clients
|
||||
- content: Fetch a single inbound by numeric ID.
|
||||
id: fetch-a-single-inbound-by-numeric-id
|
||||
- content: >-
|
||||
Create a new inbound. Send the full inbound payload (protocol, port,
|
||||
settings, streamSettings, sniffing, remark, expiryTime, total,
|
||||
enable). settings, streamSettings, and sniffing may be sent as nested
|
||||
JSON objects (preferred) or as JSON-encoded strings (legacy).
|
||||
id: >-
|
||||
create-a-new-inbound-send-the-full-inbound-payload-protocol-port-settings-streamsettings-sniffing-remark-expirytime-total-enable-settings-streamsettings-and-sniffing-may-be-sent-as-nested-json-objects-preferred-or-as-json-encoded-strings-legacy
|
||||
- content: >-
|
||||
Delete an inbound by ID. Also removes its associated client stats
|
||||
rows.
|
||||
id: delete-an-inbound-by-id-also-removes-its-associated-client-stats-rows
|
||||
- content: >-
|
||||
Delete many inbounds in one call. Processes the list sequentially;
|
||||
failures are reported per id and the rest still proceed. Restarts xray
|
||||
at most once.
|
||||
id: >-
|
||||
delete-many-inbounds-in-one-call-processes-the-list-sequentially-failures-are-reported-per-id-and-the-rest-still-proceed-restarts-xray-at-most-once
|
||||
- content: >-
|
||||
Replace an inbound’s configuration. Body shape mirrors /add. Heavy on
|
||||
inbounds with thousands of clients — prefer /setEnable for enable-only
|
||||
flips.
|
||||
id: >-
|
||||
replace-an-inbounds-configuration-body-shape-mirrors-add-heavy-on-inbounds-with-thousands-of-clients--prefer-setenable-for-enable-only-flips
|
||||
- content: >-
|
||||
Toggle only the enable flag without serialising the whole settings
|
||||
JSON. Recommended for UI switches on large inbounds.
|
||||
id: >-
|
||||
toggle-only-the-enable-flag-without-serialising-the-whole-settings-json-recommended-for-ui-switches-on-large-inbounds
|
||||
- content: >-
|
||||
Zero out upload + download counters for a single inbound. Does not
|
||||
touch per-client counters.
|
||||
id: >-
|
||||
zero-out-upload--download-counters-for-a-single-inbound-does-not-touch-per-client-counters
|
||||
- content: >-
|
||||
Remove every client attached to a single inbound while keeping the
|
||||
inbound itself. Collects emails from settings.clients[] and feeds them
|
||||
into the optimized bulk-delete path (runtime user removal +
|
||||
traffic-row cleanup + SyncInbound). Destructive and cannot be undone.
|
||||
id: >-
|
||||
remove-every-client-attached-to-a-single-inbound-while-keeping-the-inbound-itself-collects-emails-from-settingsclients-and-feeds-them-into-the-optimized-bulk-delete-path-runtime-user-removal--traffic-row-cleanup--syncinbound-destructive-and-cannot-be-undone
|
||||
- content: >-
|
||||
Reset upload + download counters on every inbound. Destructive —
|
||||
accounting history is lost.
|
||||
id: >-
|
||||
reset-upload--download-counters-on-every-inbound-destructive--accounting-history-is-lost
|
||||
- content: >-
|
||||
Bulk-import an inbound from a JSON blob (e.g. one exported via the
|
||||
UI). The body uses form encoding with a single "data" field.
|
||||
id: >-
|
||||
bulk-import-an-inbound-from-a-json-blob-eg-one-exported-via-the-ui-the-body-uses-form-encoding-with-a-single-data-field
|
||||
- content: >-
|
||||
Receive a master panel's aggregated per-client usage, keyed by the
|
||||
master's GUID. Stored in a side table used only for the UI display
|
||||
overlay and local quota enforcement — never folded into the local
|
||||
counters that masters poll, so delta accounting stays intact. Called
|
||||
panel-to-panel by the node traffic sync job.
|
||||
id: >-
|
||||
receive-a-master-panels-aggregated-per-client-usage-keyed-by-the-masters-guid-stored-in-a-side-table-used-only-for-the-ui-display-overlay-and-local-quota-enforcement--never-folded-into-the-local-counters-that-masters-poll-so-delta-accounting-stays-intact-called-panel-to-panel-by-the-node-traffic-sync-job
|
||||
- content: >-
|
||||
List the fallback rules attached to a master VLESS/Trojan TCP-TLS
|
||||
inbound. Each rule links one child inbound (the dest) to optional
|
||||
SNI/ALPN/path/dest/xver match criteria. When dest is empty the child
|
||||
inbound's listen+port is used.
|
||||
id: >-
|
||||
list-the-fallback-rules-attached-to-a-master-vlesstrojan-tcp-tls-inbound-each-rule-links-one-child-inbound-the-dest-to-optional-snialpnpathdestxver-match-criteria-when-dest-is-empty-the-child-inbounds-listenport-is-used
|
||||
- content: >-
|
||||
Replace the entire fallback list for a master inbound. Body is JSON.
|
||||
Triggers an Xray restart.
|
||||
id: >-
|
||||
replace-the-entire-fallback-list-for-a-master-inbound-body-is-json-triggers-an-xray-restart
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/inbounds/list","method":"get"},{"path":"/panel/api/inbounds/list/slim","method":"get"},{"path":"/panel/api/inbounds/options","method":"get"},{"path":"/panel/api/inbounds/get/{id}","method":"get"},{"path":"/panel/api/inbounds/add","method":"post"},{"path":"/panel/api/inbounds/del/{id}","method":"post"},{"path":"/panel/api/inbounds/bulkDel","method":"post"},{"path":"/panel/api/inbounds/update/{id}","method":"post"},{"path":"/panel/api/inbounds/setEnable/{id}","method":"post"},{"path":"/panel/api/inbounds/{id}/resetTraffic","method":"post"},{"path":"/panel/api/inbounds/{id}/delAllClients","method":"post"},{"path":"/panel/api/inbounds/resetAllTraffics","method":"post"},{"path":"/panel/api/inbounds/import","method":"post"},{"path":"/panel/api/inbounds/pushClientTraffics","method":"post"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"get"},{"path":"/panel/api/inbounds/{id}/fallbacks","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
---
|
||||
title: API Reference
|
||||
description: The 3x-ui panel REST API — authentication, inbounds, clients, server stats, and more, generated from the OpenAPI spec.
|
||||
icon: Webhook
|
||||
---
|
||||
|
||||
The 3x-ui panel exposes a REST API for automation. These pages are generated from
|
||||
the panel's OpenAPI specification, so they always match the documented schema.
|
||||
|
||||
## Authentication
|
||||
|
||||
Requests authenticate with either a **session cookie** (from `POST /login`) or a
|
||||
**Bearer token**:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
Create API tokens in the panel; they are full-admin credentials, so store them
|
||||
securely. See [API Tokens](/docs/reference/api/api-tokens).
|
||||
|
||||
Build an authenticated request for any endpoint below:
|
||||
|
||||
<ApiRequestBuilder />
|
||||
|
||||
## Browse by area
|
||||
|
||||
<Cards>
|
||||
<Card title="Authentication" href="/docs/reference/api/authentication" />
|
||||
<Card title="Inbounds" href="/docs/reference/api/inbounds" />
|
||||
<Card title="Clients" href="/docs/reference/api/clients" />
|
||||
<Card title="Server" href="/docs/reference/api/server" />
|
||||
<Card title="Settings" href="/docs/reference/api/settings" />
|
||||
<Card title="Xray Settings" href="/docs/reference/api/xray-settings" />
|
||||
</Cards>
|
||||
|
||||
<Callout type="info">
|
||||
This reference is regenerated from `public/openapi.json` with `pnpm gen:api`.
|
||||
Don't edit the generated tag pages by hand.
|
||||
</Callout>
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"title": "API Reference",
|
||||
"icon": "Webhook",
|
||||
"pages": [
|
||||
"index",
|
||||
"authentication",
|
||||
"api-tokens",
|
||||
"inbounds",
|
||||
"clients",
|
||||
"server",
|
||||
"settings",
|
||||
"xray-settings",
|
||||
"subscription-server",
|
||||
"hosts",
|
||||
"nodes",
|
||||
"backup",
|
||||
"websocket"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
---
|
||||
title: Nodes
|
||||
description: >-
|
||||
Manage remote 3x-ui panels acting as nodes for a central panel. All endpoints
|
||||
under /panel/api/nodes.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
List every configured node with its connection details, health, and last
|
||||
heartbeat patch.
|
||||
url: >-
|
||||
#list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
|
||||
- depth: 2
|
||||
title: >-
|
||||
This panel's node-auth CA certificate (public, PEM) to paste into a
|
||||
node's mTLS trust setting. Lazily mints the CA and the master client
|
||||
cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
|
||||
url: >-
|
||||
#this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node
|
||||
- depth: 2
|
||||
title: >-
|
||||
Set the CA certificate this panel trusts for incoming node-API client
|
||||
certificates (this panel acting as a node). Paste the managing panel's
|
||||
CA (from nodes/mtls/ca). An empty caCert disables it. A non-empty value
|
||||
must be a PEM certificate. Applied on the next panel restart.
|
||||
url: >-
|
||||
#set-the-ca-certificate-this-panel-trusts-for-incoming-node-api-client-certificates-this-panel-acting-as-a-node-paste-the-managing-panels-ca-from-nodesmtlsca-an-empty-cacert-disables-it-a-non-empty-value-must-be-a-pem-certificate-applied-on-the-next-panel-restart
|
||||
- depth: 2
|
||||
title: Fetch a single node by ID.
|
||||
url: '#fetch-a-single-node-by-id'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||
node). Used by the inbound form's "Set Cert from Panel" so a
|
||||
node-assigned inbound gets paths that exist on the node, not the central
|
||||
panel.
|
||||
url: >-
|
||||
#fetch-a-nodes-own-web-tls-certificatekey-file-paths-proxied-to-the-node-used-by-the-inbound-forms-set-cert-from-panel-so-a-node-assigned-inbound-gets-paths-that-exist-on-the-node-not-the-central-panel
|
||||
- depth: 2
|
||||
title: >-
|
||||
Register a new remote node. Provide its URL, apiToken, and optional
|
||||
remark / allowPrivateAddress flag.
|
||||
url: >-
|
||||
#register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag
|
||||
- depth: 2
|
||||
title: Replace a node’s connection details. Same body shape as /add.
|
||||
url: '#replace-a-nodes-connection-details-same-body-shape-as-add'
|
||||
- depth: 2
|
||||
title: Delete a node. Inbounds bound to it are not auto-migrated.
|
||||
url: '#delete-a-node-inbounds-bound-to-it-are-not-auto-migrated'
|
||||
- depth: 2
|
||||
title: Pause or resume traffic sync with this node.
|
||||
url: '#pause-or-resume-traffic-sync-with-this-node'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Probe a node without saving it. Uses the body as connection details and
|
||||
returns the same heartbeat snapshot a registered node would have.
|
||||
url: >-
|
||||
#probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have
|
||||
- depth: 2
|
||||
title: >-
|
||||
Connect to the node over HTTPS without verifying its certificate and
|
||||
return the leaf certificate's SHA-256 (base64). Used by the Add/Edit
|
||||
Node dialog to fetch and pin a self-signed certificate. Uses the same
|
||||
body as /test.
|
||||
url: >-
|
||||
#connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test
|
||||
- depth: 2
|
||||
title: >-
|
||||
Use unsaved node connection details to list the remote inbounds
|
||||
available for selective import.
|
||||
url: >-
|
||||
#use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
|
||||
- depth: 2
|
||||
title: Probe an existing node, updating its cached health state.
|
||||
url: '#probe-an-existing-node-updating-its-cached-health-state'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Trigger the official panel self-updater on each given node (downloads
|
||||
the latest release and restarts). Only enabled, online nodes are
|
||||
updated; offline/disabled ones are reported as skipped. Set "dev": true
|
||||
to move the nodes to the rolling per-commit dev channel instead of the
|
||||
latest stable release. Returns a per-node result list.
|
||||
url: >-
|
||||
#trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list
|
||||
- depth: 2
|
||||
title: >-
|
||||
Aggregated metric history for a node — same shape as /server/history,
|
||||
scoped to one node.
|
||||
url: >-
|
||||
#aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
List every configured node with its connection details, health, and
|
||||
last heartbeat patch.
|
||||
id: >-
|
||||
list-every-configured-node-with-its-connection-details-health-and-last-heartbeat-patch
|
||||
- content: >-
|
||||
This panel's node-auth CA certificate (public, PEM) to paste into a
|
||||
node's mTLS trust setting. Lazily mints the CA and the master client
|
||||
cert on first call. Pair with setting tlsVerifyMode=mtls on the node.
|
||||
id: >-
|
||||
this-panels-node-auth-ca-certificate-public-pem-to-paste-into-a-nodes-mtls-trust-setting-lazily-mints-the-ca-and-the-master-client-cert-on-first-call-pair-with-setting-tlsverifymodemtls-on-the-node
|
||||
- content: >-
|
||||
Set the CA certificate this panel trusts for incoming node-API client
|
||||
certificates (this panel acting as a node). Paste the managing panel's
|
||||
CA (from nodes/mtls/ca). An empty caCert disables it. A non-empty
|
||||
value must be a PEM certificate. Applied on the next panel restart.
|
||||
id: >-
|
||||
set-the-ca-certificate-this-panel-trusts-for-incoming-node-api-client-certificates-this-panel-acting-as-a-node-paste-the-managing-panels-ca-from-nodesmtlsca-an-empty-cacert-disables-it-a-non-empty-value-must-be-a-pem-certificate-applied-on-the-next-panel-restart
|
||||
- content: Fetch a single node by ID.
|
||||
id: fetch-a-single-node-by-id
|
||||
- content: >-
|
||||
Fetch a node's own web TLS certificate/key file paths (proxied to the
|
||||
node). Used by the inbound form's "Set Cert from Panel" so a
|
||||
node-assigned inbound gets paths that exist on the node, not the
|
||||
central panel.
|
||||
id: >-
|
||||
fetch-a-nodes-own-web-tls-certificatekey-file-paths-proxied-to-the-node-used-by-the-inbound-forms-set-cert-from-panel-so-a-node-assigned-inbound-gets-paths-that-exist-on-the-node-not-the-central-panel
|
||||
- content: >-
|
||||
Register a new remote node. Provide its URL, apiToken, and optional
|
||||
remark / allowPrivateAddress flag.
|
||||
id: >-
|
||||
register-a-new-remote-node-provide-its-url-apitoken-and-optional-remark--allowprivateaddress-flag
|
||||
- content: Replace a node’s connection details. Same body shape as /add.
|
||||
id: replace-a-nodes-connection-details-same-body-shape-as-add
|
||||
- content: Delete a node. Inbounds bound to it are not auto-migrated.
|
||||
id: delete-a-node-inbounds-bound-to-it-are-not-auto-migrated
|
||||
- content: Pause or resume traffic sync with this node.
|
||||
id: pause-or-resume-traffic-sync-with-this-node
|
||||
- content: >-
|
||||
Probe a node without saving it. Uses the body as connection details
|
||||
and returns the same heartbeat snapshot a registered node would have.
|
||||
id: >-
|
||||
probe-a-node-without-saving-it-uses-the-body-as-connection-details-and-returns-the-same-heartbeat-snapshot-a-registered-node-would-have
|
||||
- content: >-
|
||||
Connect to the node over HTTPS without verifying its certificate and
|
||||
return the leaf certificate's SHA-256 (base64). Used by the Add/Edit
|
||||
Node dialog to fetch and pin a self-signed certificate. Uses the same
|
||||
body as /test.
|
||||
id: >-
|
||||
connect-to-the-node-over-https-without-verifying-its-certificate-and-return-the-leaf-certificates-sha-256-base64-used-by-the-addedit-node-dialog-to-fetch-and-pin-a-self-signed-certificate-uses-the-same-body-as-test
|
||||
- content: >-
|
||||
Use unsaved node connection details to list the remote inbounds
|
||||
available for selective import.
|
||||
id: >-
|
||||
use-unsaved-node-connection-details-to-list-the-remote-inbounds-available-for-selective-import
|
||||
- content: Probe an existing node, updating its cached health state.
|
||||
id: probe-an-existing-node-updating-its-cached-health-state
|
||||
- content: >-
|
||||
Trigger the official panel self-updater on each given node (downloads
|
||||
the latest release and restarts). Only enabled, online nodes are
|
||||
updated; offline/disabled ones are reported as skipped. Set "dev":
|
||||
true to move the nodes to the rolling per-commit dev channel instead
|
||||
of the latest stable release. Returns a per-node result list.
|
||||
id: >-
|
||||
trigger-the-official-panel-self-updater-on-each-given-node-downloads-the-latest-release-and-restarts-only-enabled-online-nodes-are-updated-offlinedisabled-ones-are-reported-as-skipped-set-dev-true-to-move-the-nodes-to-the-rolling-per-commit-dev-channel-instead-of-the-latest-stable-release-returns-a-per-node-result-list
|
||||
- content: >-
|
||||
Aggregated metric history for a node — same shape as /server/history,
|
||||
scoped to one node.
|
||||
id: >-
|
||||
aggregated-metric-history-for-a-node--same-shape-as-serverhistory-scoped-to-one-node
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/nodes/list","method":"get"},{"path":"/panel/api/nodes/mtls/ca","method":"post"},{"path":"/panel/api/nodes/mtls/trustCA","method":"post"},{"path":"/panel/api/nodes/get/{id}","method":"get"},{"path":"/panel/api/nodes/webCert/{id}","method":"get"},{"path":"/panel/api/nodes/add","method":"post"},{"path":"/panel/api/nodes/update/{id}","method":"post"},{"path":"/panel/api/nodes/del/{id}","method":"post"},{"path":"/panel/api/nodes/setEnable/{id}","method":"post"},{"path":"/panel/api/nodes/test","method":"post"},{"path":"/panel/api/nodes/certFingerprint","method":"post"},{"path":"/panel/api/nodes/inbounds","method":"post"},{"path":"/panel/api/nodes/probe/{id}","method":"post"},{"path":"/panel/api/nodes/updatePanel","method":"post"},{"path":"/panel/api/nodes/history/{id}/{metric}/{bucket}","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
---
|
||||
title: Server
|
||||
description: >-
|
||||
System status, log retrieval, certificate generators, Xray binary management,
|
||||
and backup/restore. All under /panel/api/server.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||
averages, open connections, Xray state. Cached and refreshed every 2
|
||||
seconds in the background.
|
||||
url: >-
|
||||
#real-time-machine-snapshot-cpu-memory-swap-disk-network-io-load-averages-open-connections-xray-state-cached-and-refreshed-every-2-seconds-in-the-background
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reports whether per-client IP limits can be enforced on this host. The
|
||||
panel uses it to gate the "IP Limit" field, since enforcement depends on
|
||||
Fail2ban being installed.
|
||||
url: >-
|
||||
#reports-whether-per-client-ip-limits-can-be-enforced-on-this-host-the-panel-uses-it-to-gate-the-ip-limit-field-since-enforcement-depends-on-fail2ban-being-installed
|
||||
- depth: 2
|
||||
title: >-
|
||||
Legacy: aggregated CPU history. Use /history/cpu/:bucket instead — same
|
||||
data with a uniform {t, v} shape.
|
||||
url: >-
|
||||
#legacy-aggregated-cpu-history-use-historycpubucket-instead--same-data-with-a-uniform-t-v-shape
|
||||
- depth: 2
|
||||
title: >-
|
||||
Aggregated time-series for one metric. Returns an array of {t, v}
|
||||
samples covering the last ~6 hours.
|
||||
url: >-
|
||||
#aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours
|
||||
- depth: 2
|
||||
title: >-
|
||||
Xray runtime metrics state — whether the xray config has a `metrics`
|
||||
block, which expvar keys are flowing, and the current snapshot values
|
||||
for each. Returns an empty state when metrics are not configured.
|
||||
url: >-
|
||||
#xray-runtime-metrics-state--whether-the-xray-config-has-a-metrics-block-which-expvar-keys-are-flowing-and-the-current-snapshot-values-for-each-returns-an-empty-state-when-metrics-are-not-configured
|
||||
- depth: 2
|
||||
title: >-
|
||||
Time-series history for one Xray runtime metric over the last ~6 hours.
|
||||
Same {t, v} shape as /history/:metric/:bucket.
|
||||
url: >-
|
||||
#time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket
|
||||
- depth: 2
|
||||
title: >-
|
||||
Latest snapshot from the Xray observatory — per-outbound latency, health
|
||||
status, and last-probe time. Only populated when the Xray config has an
|
||||
observatory configured.
|
||||
url: >-
|
||||
#latest-snapshot-from-the-xray-observatory--per-outbound-latency-health-status-and-last-probe-time-only-populated-when-the-xray-config-has-an-observatory-configured
|
||||
- depth: 2
|
||||
title: >-
|
||||
Time-series of observatory probe results for one outbound tag. Same {t,
|
||||
v} shape as the other history endpoints.
|
||||
url: >-
|
||||
#time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints
|
||||
- depth: 2
|
||||
title: List Xray binary versions available for install on this host.
|
||||
url: '#list-xray-binary-versions-available-for-install-on-this-host'
|
||||
- depth: 2
|
||||
title: Check whether a newer 3x-ui release is available on GitHub.
|
||||
url: '#check-whether-a-newer-3x-ui-release-is-available-on-github'
|
||||
- depth: 2
|
||||
title: Return the assembled Xray config that’s currently running on this host.
|
||||
url: '#return-the-assembled-xray-config-thats-currently-running-on-this-host'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Stream the SQLite database file as an attachment. Use as a manual
|
||||
backup.
|
||||
url: '#stream-the-sqlite-database-file-as-an-attachment-use-as-a-manual-backup'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Stream a cross-engine migration file as an attachment: a .dump (SQL
|
||||
text) on SQLite, or a .db SQLite database built from the live data on
|
||||
PostgreSQL.
|
||||
url: >-
|
||||
#stream-a-cross-engine-migration-file-as-an-attachment-a-dump-sql-text-on-sqlite-or-a-db-sqlite-database-built-from-the-live-data-on-postgresql
|
||||
- depth: 2
|
||||
title: Generate a fresh UUID v4. Convenience helper for client IDs.
|
||||
url: '#generate-a-fresh-uuid-v4-convenience-helper-for-client-ids'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return this panel's own web TLS certificate and key file paths. The
|
||||
central panel calls it on a node (via the node API token) so "Set Cert
|
||||
from Panel" fills a node-assigned inbound with paths that exist on the
|
||||
node.
|
||||
url: >-
|
||||
#return-this-panels-own-web-tls-certificate-and-key-file-paths-the-central-panel-calls-it-on-a-node-via-the-node-api-token-so-set-cert-from-panel-fills-a-node-assigned-inbound-with-paths-that-exist-on-the-node
|
||||
- depth: 2
|
||||
title: >-
|
||||
Read-only summaries (guid, parentGuid, name, address, status, versions)
|
||||
of the nodes this panel manages. A parent panel calls it on a node (via
|
||||
the node API token) to surface transitive sub-nodes in a chained
|
||||
topology. Counts are computed by the parent, not returned here.
|
||||
url: >-
|
||||
#read-only-summaries-guid-parentguid-name-address-status-versions-of-the-nodes-this-panel-manages-a-parent-panel-calls-it-on-a-node-via-the-node-api-token-to-surface-transitive-sub-nodes-in-a-chained-topology-counts-are-computed-by-the-parent-not-returned-here
|
||||
- depth: 2
|
||||
title: Generate a new X25519 keypair for Reality.
|
||||
url: '#generate-a-new-x25519-keypair-for-reality'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||
{privateKey, publicKey, seed}.
|
||||
url: >-
|
||||
#generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns
|
||||
{clientKey, serverKey}.
|
||||
url: >-
|
||||
#generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate VLESS encryption auth options. Returns an auths array each with
|
||||
id, label, encryption, and decryption fields.
|
||||
url: >-
|
||||
#generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields
|
||||
- depth: 2
|
||||
title: Stop the Xray binary. All proxies go offline immediately.
|
||||
url: '#stop-the-xray-binary-all-proxies-go-offline-immediately'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reload Xray with the current config. Typically required after structural
|
||||
inbound or routing changes.
|
||||
url: >-
|
||||
#reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes
|
||||
- depth: 2
|
||||
title: >-
|
||||
Download and install the specified Xray version. Pass "latest" for the
|
||||
newest release.
|
||||
url: >-
|
||||
#download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release
|
||||
- depth: 2
|
||||
title: >-
|
||||
Self-update the panel to the latest version. The server restarts on
|
||||
success.
|
||||
url: >-
|
||||
#self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
||||
- depth: 2
|
||||
title: >-
|
||||
Toggle the panel update channel between stable and the rolling
|
||||
per-commit dev release. Only effective on dev builds.
|
||||
url: >-
|
||||
#toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds
|
||||
- depth: 2
|
||||
title: >-
|
||||
Refresh the default GeoIP / GeoSite data files. Body can include a
|
||||
fileName, or use the /:fileName variant.
|
||||
url: >-
|
||||
#refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant
|
||||
- depth: 2
|
||||
title: Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).
|
||||
url: '#refresh-a-single-geo-file-by-filename-eg-geoipdat-geositedat'
|
||||
- depth: 2
|
||||
title: Return the last N lines of the panel’s own log.
|
||||
url: '#return-the-last-n-lines-of-the-panels-own-log'
|
||||
- depth: 2
|
||||
title: Return the last N lines of the Xray process log.
|
||||
url: '#return-the-last-n-lines-of-the-xray-process-log'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Restore the panel DB from an uploaded SQLite file (multipart form, field
|
||||
name "db"). The panel restarts after restore. Destructive.
|
||||
url: >-
|
||||
#restore-the-panel-db-from-an-uploaded-sqlite-file-multipart-form-field-name-db-the-panel-restarts-after-restore-destructive
|
||||
- depth: 2
|
||||
title: >-
|
||||
Generate a new ECH (Encrypted Client Hello) keypair and config list for
|
||||
the given SNI.
|
||||
url: >-
|
||||
#generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni
|
||||
- depth: 2
|
||||
title: >-
|
||||
Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||
(pinnedPeerCertSha256). Provide either a server file path or inline
|
||||
PEM/DER content.
|
||||
url: >-
|
||||
#compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content
|
||||
- depth: 2
|
||||
title: >-
|
||||
Run `xray tls ping` against a remote server and return its live
|
||||
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
||||
url: >-
|
||||
#run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256
|
||||
- depth: 2
|
||||
title: >-
|
||||
Fetch the fully aggregated inbound_client_ips database table. Used by
|
||||
nodes to sync recently active IPs across the cluster.
|
||||
url: >-
|
||||
#fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster
|
||||
- depth: 2
|
||||
title: >-
|
||||
Submit a list of recently active IP timestamps. The panel merges them
|
||||
with the existing database to maintain a unified global IP-limit view.
|
||||
url: >-
|
||||
#submit-a-list-of-recently-active-ip-timestamps-the-panel-merges-them-with-the-existing-database-to-maintain-a-unified-global-ip-limit-view
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Real-time machine snapshot: CPU, memory, swap, disk, network IO, load
|
||||
averages, open connections, Xray state. Cached and refreshed every 2
|
||||
seconds in the background.
|
||||
id: >-
|
||||
real-time-machine-snapshot-cpu-memory-swap-disk-network-io-load-averages-open-connections-xray-state-cached-and-refreshed-every-2-seconds-in-the-background
|
||||
- content: >-
|
||||
Reports whether per-client IP limits can be enforced on this host. The
|
||||
panel uses it to gate the "IP Limit" field, since enforcement depends
|
||||
on Fail2ban being installed.
|
||||
id: >-
|
||||
reports-whether-per-client-ip-limits-can-be-enforced-on-this-host-the-panel-uses-it-to-gate-the-ip-limit-field-since-enforcement-depends-on-fail2ban-being-installed
|
||||
- content: >-
|
||||
Legacy: aggregated CPU history. Use /history/cpu/:bucket instead —
|
||||
same data with a uniform {t, v} shape.
|
||||
id: >-
|
||||
legacy-aggregated-cpu-history-use-historycpubucket-instead--same-data-with-a-uniform-t-v-shape
|
||||
- content: >-
|
||||
Aggregated time-series for one metric. Returns an array of {t, v}
|
||||
samples covering the last ~6 hours.
|
||||
id: >-
|
||||
aggregated-time-series-for-one-metric-returns-an-array-of-t-v-samples-covering-the-last-6-hours
|
||||
- content: >-
|
||||
Xray runtime metrics state — whether the xray config has a `metrics`
|
||||
block, which expvar keys are flowing, and the current snapshot values
|
||||
for each. Returns an empty state when metrics are not configured.
|
||||
id: >-
|
||||
xray-runtime-metrics-state--whether-the-xray-config-has-a-metrics-block-which-expvar-keys-are-flowing-and-the-current-snapshot-values-for-each-returns-an-empty-state-when-metrics-are-not-configured
|
||||
- content: >-
|
||||
Time-series history for one Xray runtime metric over the last ~6
|
||||
hours. Same {t, v} shape as /history/:metric/:bucket.
|
||||
id: >-
|
||||
time-series-history-for-one-xray-runtime-metric-over-the-last-6-hours-same-t-v-shape-as-historymetricbucket
|
||||
- content: >-
|
||||
Latest snapshot from the Xray observatory — per-outbound latency,
|
||||
health status, and last-probe time. Only populated when the Xray
|
||||
config has an observatory configured.
|
||||
id: >-
|
||||
latest-snapshot-from-the-xray-observatory--per-outbound-latency-health-status-and-last-probe-time-only-populated-when-the-xray-config-has-an-observatory-configured
|
||||
- content: >-
|
||||
Time-series of observatory probe results for one outbound tag. Same
|
||||
{t, v} shape as the other history endpoints.
|
||||
id: >-
|
||||
time-series-of-observatory-probe-results-for-one-outbound-tag-same-t-v-shape-as-the-other-history-endpoints
|
||||
- content: List Xray binary versions available for install on this host.
|
||||
id: list-xray-binary-versions-available-for-install-on-this-host
|
||||
- content: Check whether a newer 3x-ui release is available on GitHub.
|
||||
id: check-whether-a-newer-3x-ui-release-is-available-on-github
|
||||
- content: >-
|
||||
Return the assembled Xray config that’s currently running on this
|
||||
host.
|
||||
id: return-the-assembled-xray-config-thats-currently-running-on-this-host
|
||||
- content: >-
|
||||
Stream the SQLite database file as an attachment. Use as a manual
|
||||
backup.
|
||||
id: >-
|
||||
stream-the-sqlite-database-file-as-an-attachment-use-as-a-manual-backup
|
||||
- content: >-
|
||||
Stream a cross-engine migration file as an attachment: a .dump (SQL
|
||||
text) on SQLite, or a .db SQLite database built from the live data on
|
||||
PostgreSQL.
|
||||
id: >-
|
||||
stream-a-cross-engine-migration-file-as-an-attachment-a-dump-sql-text-on-sqlite-or-a-db-sqlite-database-built-from-the-live-data-on-postgresql
|
||||
- content: Generate a fresh UUID v4. Convenience helper for client IDs.
|
||||
id: generate-a-fresh-uuid-v4-convenience-helper-for-client-ids
|
||||
- content: >-
|
||||
Return this panel's own web TLS certificate and key file paths. The
|
||||
central panel calls it on a node (via the node API token) so "Set Cert
|
||||
from Panel" fills a node-assigned inbound with paths that exist on the
|
||||
node.
|
||||
id: >-
|
||||
return-this-panels-own-web-tls-certificate-and-key-file-paths-the-central-panel-calls-it-on-a-node-via-the-node-api-token-so-set-cert-from-panel-fills-a-node-assigned-inbound-with-paths-that-exist-on-the-node
|
||||
- content: >-
|
||||
Read-only summaries (guid, parentGuid, name, address, status,
|
||||
versions) of the nodes this panel manages. A parent panel calls it on
|
||||
a node (via the node API token) to surface transitive sub-nodes in a
|
||||
chained topology. Counts are computed by the parent, not returned
|
||||
here.
|
||||
id: >-
|
||||
read-only-summaries-guid-parentguid-name-address-status-versions-of-the-nodes-this-panel-manages-a-parent-panel-calls-it-on-a-node-via-the-node-api-token-to-surface-transitive-sub-nodes-in-a-chained-topology-counts-are-computed-by-the-parent-not-returned-here
|
||||
- content: Generate a new X25519 keypair for Reality.
|
||||
id: generate-a-new-x25519-keypair-for-reality
|
||||
- content: >-
|
||||
Generate a new ML-DSA-65 keypair (post-quantum signature). Returns
|
||||
{privateKey, publicKey, seed}.
|
||||
id: >-
|
||||
generate-a-new-ml-dsa-65-keypair-post-quantum-signature-returns-privatekey-publickey-seed
|
||||
- content: >-
|
||||
Generate a new ML-KEM-768 keypair (post-quantum KEM). Returns
|
||||
{clientKey, serverKey}.
|
||||
id: >-
|
||||
generate-a-new-ml-kem-768-keypair-post-quantum-kem-returns-clientkey-serverkey
|
||||
- content: >-
|
||||
Generate VLESS encryption auth options. Returns an auths array each
|
||||
with id, label, encryption, and decryption fields.
|
||||
id: >-
|
||||
generate-vless-encryption-auth-options-returns-an-auths-array-each-with-id-label-encryption-and-decryption-fields
|
||||
- content: Stop the Xray binary. All proxies go offline immediately.
|
||||
id: stop-the-xray-binary-all-proxies-go-offline-immediately
|
||||
- content: >-
|
||||
Reload Xray with the current config. Typically required after
|
||||
structural inbound or routing changes.
|
||||
id: >-
|
||||
reload-xray-with-the-current-config-typically-required-after-structural-inbound-or-routing-changes
|
||||
- content: >-
|
||||
Download and install the specified Xray version. Pass "latest" for the
|
||||
newest release.
|
||||
id: >-
|
||||
download-and-install-the-specified-xray-version-pass-latest-for-the-newest-release
|
||||
- content: >-
|
||||
Self-update the panel to the latest version. The server restarts on
|
||||
success.
|
||||
id: >-
|
||||
self-update-the-panel-to-the-latest-version-the-server-restarts-on-success
|
||||
- content: >-
|
||||
Toggle the panel update channel between stable and the rolling
|
||||
per-commit dev release. Only effective on dev builds.
|
||||
id: >-
|
||||
toggle-the-panel-update-channel-between-stable-and-the-rolling-per-commit-dev-release-only-effective-on-dev-builds
|
||||
- content: >-
|
||||
Refresh the default GeoIP / GeoSite data files. Body can include a
|
||||
fileName, or use the /:fileName variant.
|
||||
id: >-
|
||||
refresh-the-default-geoip--geosite-data-files-body-can-include-a-filename-or-use-the-filename-variant
|
||||
- content: Refresh a single Geo file by filename (e.g. geoip.dat, geosite.dat).
|
||||
id: refresh-a-single-geo-file-by-filename-eg-geoipdat-geositedat
|
||||
- content: Return the last N lines of the panel’s own log.
|
||||
id: return-the-last-n-lines-of-the-panels-own-log
|
||||
- content: Return the last N lines of the Xray process log.
|
||||
id: return-the-last-n-lines-of-the-xray-process-log
|
||||
- content: >-
|
||||
Restore the panel DB from an uploaded SQLite file (multipart form,
|
||||
field name "db"). The panel restarts after restore. Destructive.
|
||||
id: >-
|
||||
restore-the-panel-db-from-an-uploaded-sqlite-file-multipart-form-field-name-db-the-panel-restarts-after-restore-destructive
|
||||
- content: >-
|
||||
Generate a new ECH (Encrypted Client Hello) keypair and config list
|
||||
for the given SNI.
|
||||
id: >-
|
||||
generate-a-new-ech-encrypted-client-hello-keypair-and-config-list-for-the-given-sni
|
||||
- content: >-
|
||||
Compute the hex SHA-256 of a certificate (DER) for pinning
|
||||
(pinnedPeerCertSha256). Provide either a server file path or inline
|
||||
PEM/DER content.
|
||||
id: >-
|
||||
compute-the-hex-sha-256-of-a-certificate-der-for-pinning-pinnedpeercertsha256-provide-either-a-server-file-path-or-inline-pemder-content
|
||||
- content: >-
|
||||
Run `xray tls ping` against a remote server and return its live
|
||||
leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).
|
||||
id: >-
|
||||
run-xray-tls-ping-against-a-remote-server-and-return-its-live-leaf-certificate-sha-256-hashes-for-pinning-pinnedpeercertsha256
|
||||
- content: >-
|
||||
Fetch the fully aggregated inbound_client_ips database table. Used by
|
||||
nodes to sync recently active IPs across the cluster.
|
||||
id: >-
|
||||
fetch-the-fully-aggregated-inbound_client_ips-database-table-used-by-nodes-to-sync-recently-active-ips-across-the-cluster
|
||||
- content: >-
|
||||
Submit a list of recently active IP timestamps. The panel merges them
|
||||
with the existing database to maintain a unified global IP-limit view.
|
||||
id: >-
|
||||
submit-a-list-of-recently-active-ip-timestamps-the-panel-merges-them-with-the-existing-database-to-maintain-a-unified-global-ip-limit-view
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/server/status","method":"get"},{"path":"/panel/api/server/fail2banStatus","method":"get"},{"path":"/panel/api/server/cpuHistory/{bucket}","method":"get"},{"path":"/panel/api/server/history/{metric}/{bucket}","method":"get"},{"path":"/panel/api/server/xrayMetricsState","method":"get"},{"path":"/panel/api/server/xrayMetricsHistory/{metric}/{bucket}","method":"get"},{"path":"/panel/api/server/xrayObservatory","method":"get"},{"path":"/panel/api/server/xrayObservatoryHistory/{tag}/{bucket}","method":"get"},{"path":"/panel/api/server/getXrayVersion","method":"get"},{"path":"/panel/api/server/getPanelUpdateInfo","method":"get"},{"path":"/panel/api/server/getConfigJson","method":"get"},{"path":"/panel/api/server/getDb","method":"get"},{"path":"/panel/api/server/getMigration","method":"get"},{"path":"/panel/api/server/getNewUUID","method":"get"},{"path":"/panel/api/server/getWebCertFiles","method":"get"},{"path":"/panel/api/server/descendants","method":"get"},{"path":"/panel/api/server/getNewX25519Cert","method":"get"},{"path":"/panel/api/server/getNewmldsa65","method":"get"},{"path":"/panel/api/server/getNewmlkem768","method":"get"},{"path":"/panel/api/server/getNewVlessEnc","method":"get"},{"path":"/panel/api/server/stopXrayService","method":"post"},{"path":"/panel/api/server/restartXrayService","method":"post"},{"path":"/panel/api/server/installXray/{version}","method":"post"},{"path":"/panel/api/server/updatePanel","method":"post"},{"path":"/panel/api/server/setUpdateChannel","method":"post"},{"path":"/panel/api/server/updateGeofile","method":"post"},{"path":"/panel/api/server/updateGeofile/{fileName}","method":"post"},{"path":"/panel/api/server/logs/{count}","method":"post"},{"path":"/panel/api/server/xraylogs/{count}","method":"post"},{"path":"/panel/api/server/importDB","method":"post"},{"path":"/panel/api/server/getNewEchCert","method":"post"},{"path":"/panel/api/server/getCertHash","method":"post"},{"path":"/panel/api/server/getRemoteCertHash","method":"post"},{"path":"/panel/api/server/clientIps","method":"get"},{"path":"/panel/api/server/clientIps","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
---
|
||||
title: Settings
|
||||
description: >-
|
||||
Panel configuration and user credentials. All endpoints live under
|
||||
/panel/api/setting and require a logged-in session or Bearer token.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return every panel setting: web server, Telegram bot, subscription,
|
||||
security, LDAP. The full JSON blob that the Settings page edits.
|
||||
url: >-
|
||||
#return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the computed default settings based on the request host. Useful
|
||||
to preview what a fresh install would use.
|
||||
url: >-
|
||||
#return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
|
||||
- depth: 2
|
||||
title: >-
|
||||
Persist every setting at once. The body mirrors the shape returned by
|
||||
/all. Invalid values (bad ports, missing cert pairs, etc.) are rejected
|
||||
before write.
|
||||
url: >-
|
||||
#persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write
|
||||
- depth: 2
|
||||
title: >-
|
||||
Change the panel admin username and password. Requires the current
|
||||
credentials for verification. The session is refreshed with the new
|
||||
values on success.
|
||||
url: >-
|
||||
#change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
|
||||
- depth: 2
|
||||
title: >-
|
||||
Restart the entire 3x-ui process after a 3-second grace period. The
|
||||
connection drops immediately; the panel comes back online ~5-10 seconds
|
||||
later.
|
||||
url: >-
|
||||
#restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test SMTP connection with stage-by-stage reporting (connect, auth,
|
||||
send). Returns structured result with stage and message.
|
||||
url: >-
|
||||
#test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test Telegram bot connection by sending a test message to the configured
|
||||
chat.
|
||||
url: >-
|
||||
#test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the built-in default Xray JSON config template that ships with
|
||||
this panel version.
|
||||
url: >-
|
||||
#return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Return every panel setting: web server, Telegram bot, subscription,
|
||||
security, LDAP. The full JSON blob that the Settings page edits.
|
||||
id: >-
|
||||
return-every-panel-setting-web-server-telegram-bot-subscription-security-ldap-the-full-json-blob-that-the-settings-page-edits
|
||||
- content: >-
|
||||
Return the computed default settings based on the request host. Useful
|
||||
to preview what a fresh install would use.
|
||||
id: >-
|
||||
return-the-computed-default-settings-based-on-the-request-host-useful-to-preview-what-a-fresh-install-would-use
|
||||
- content: >-
|
||||
Persist every setting at once. The body mirrors the shape returned by
|
||||
/all. Invalid values (bad ports, missing cert pairs, etc.) are
|
||||
rejected before write.
|
||||
id: >-
|
||||
persist-every-setting-at-once-the-body-mirrors-the-shape-returned-by-all-invalid-values-bad-ports-missing-cert-pairs-etc-are-rejected-before-write
|
||||
- content: >-
|
||||
Change the panel admin username and password. Requires the current
|
||||
credentials for verification. The session is refreshed with the new
|
||||
values on success.
|
||||
id: >-
|
||||
change-the-panel-admin-username-and-password-requires-the-current-credentials-for-verification-the-session-is-refreshed-with-the-new-values-on-success
|
||||
- content: >-
|
||||
Restart the entire 3x-ui process after a 3-second grace period. The
|
||||
connection drops immediately; the panel comes back online ~5-10
|
||||
seconds later.
|
||||
id: >-
|
||||
restart-the-entire-3x-ui-process-after-a-3-second-grace-period-the-connection-drops-immediately-the-panel-comes-back-online-5-10-seconds-later
|
||||
- content: >-
|
||||
Test SMTP connection with stage-by-stage reporting (connect, auth,
|
||||
send). Returns structured result with stage and message.
|
||||
id: >-
|
||||
test-smtp-connection-with-stage-by-stage-reporting-connect-auth-send-returns-structured-result-with-stage-and-message
|
||||
- content: >-
|
||||
Test Telegram bot connection by sending a test message to the
|
||||
configured chat.
|
||||
id: >-
|
||||
test-telegram-bot-connection-by-sending-a-test-message-to-the-configured-chat
|
||||
- content: >-
|
||||
Return the built-in default Xray JSON config template that ships with
|
||||
this panel version.
|
||||
id: >-
|
||||
return-the-built-in-default-xray-json-config-template-that-ships-with-this-panel-version
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/setting/all","method":"post"},{"path":"/panel/api/setting/defaultSettings","method":"post"},{"path":"/panel/api/setting/update","method":"post"},{"path":"/panel/api/setting/updateUser","method":"post"},{"path":"/panel/api/setting/restartPanel","method":"post"},{"path":"/panel/api/setting/testSmtp","method":"post"},{"path":"/panel/api/setting/testTgBot","method":"post"},{"path":"/panel/api/setting/getDefaultJsonConfig","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
---
|
||||
title: Subscription Server
|
||||
description: >-
|
||||
A separate HTTP/HTTPS server that serves proxy subscription links (standard,
|
||||
JSON, and Clash) to clients. The server listens on its own port (default
|
||||
10882) and is configured in Settings → Subscription. Paths are configurable;
|
||||
defaults are shown below. All subscription endpoints set response headers for
|
||||
client apps to read traffic/expiry info.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return base64-encoded subscription links for all enabled clients
|
||||
matching the subscription ID. When the request has an Accept: text/html
|
||||
header or ?html=1, renders a styled info page instead. Default path:
|
||||
/sub/:subid.
|
||||
url: >-
|
||||
#return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return subscription as a JSON array of proxy configs (one per enabled
|
||||
client). Only when JSON subscription is enabled in settings. Default
|
||||
path: /json/:subid.
|
||||
url: >-
|
||||
#return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return subscription as a Clash/Mihomo-compatible YAML config, including
|
||||
configured global Clash routing rules. Only when Clash subscription is
|
||||
enabled in settings. Default path: /clash/:subid.
|
||||
url: >-
|
||||
#return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Return base64-encoded subscription links for all enabled clients
|
||||
matching the subscription ID. When the request has an Accept:
|
||||
text/html header or ?html=1, renders a styled info page instead.
|
||||
Default path: /sub/:subid.
|
||||
id: >-
|
||||
return-base64-encoded-subscription-links-for-all-enabled-clients-matching-the-subscription-id-when-the-request-has-an-accept-texthtml-header-or-html1-renders-a-styled-info-page-instead-default-path-subsubid
|
||||
- content: >-
|
||||
Return subscription as a JSON array of proxy configs (one per enabled
|
||||
client). Only when JSON subscription is enabled in settings. Default
|
||||
path: /json/:subid.
|
||||
id: >-
|
||||
return-subscription-as-a-json-array-of-proxy-configs-one-per-enabled-client-only-when-json-subscription-is-enabled-in-settings-default-path-jsonsubid
|
||||
- content: >-
|
||||
Return subscription as a Clash/Mihomo-compatible YAML config,
|
||||
including configured global Clash routing rules. Only when Clash
|
||||
subscription is enabled in settings. Default path: /clash/:subid.
|
||||
id: >-
|
||||
return-subscription-as-a-clashmihomo-compatible-yaml-config-including-configured-global-clash-routing-rules-only-when-clash-subscription-is-enabled-in-settings-default-path-clashsubid
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/{subPath}{subid}","method":"get"},{"path":"/{jsonPath}{subid}","method":"get"},{"path":"/{clashPath}{subid}","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: WebSocket
|
||||
description: >-
|
||||
Real-time status updates via WebSocket. Connect once at
|
||||
<code>ws://<panel>/ws</code> to receive a stream of JSON messages without
|
||||
polling. Requires an authenticated session cookie (Bearer token auth is not
|
||||
supported). Each message has a <code>type</code> field that identifies the
|
||||
payload shape.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||
session cookie (Bearer token auth is not supported here). Returns 101
|
||||
Switching Protocols on success. The server then pushes JSON messages
|
||||
described below.
|
||||
url: >-
|
||||
#upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Upgrade an HTTP connection to a WebSocket. Requires an authenticated
|
||||
session cookie (Bearer token auth is not supported here). Returns 101
|
||||
Switching Protocols on success. The server then pushes JSON messages
|
||||
described below.
|
||||
id: >-
|
||||
upgrade-an-http-connection-to-a-websocket-requires-an-authenticated-session-cookie-bearer-token-auth-is-not-supported-here-returns-101-switching-protocols-on-success-the-server-then-pushes-json-messages-described-below
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/ws","method":"get"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
---
|
||||
title: Xray Settings
|
||||
description: >-
|
||||
Xray configuration template, outbound management, Warp/Nord integration, and
|
||||
config testing. All endpoints under /panel/api/xray.
|
||||
full: true
|
||||
_openapi:
|
||||
preload:
|
||||
- ./public/openapi.json
|
||||
toc:
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the Xray config template (JSON string), available inbound tags,
|
||||
client reverse tags, and the configured outbound test URL in one
|
||||
response.
|
||||
url: >-
|
||||
#return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the built-in default Xray config shipped with the panel
|
||||
(identical to /panel/api/setting/getDefaultJsonConfig).
|
||||
url: >-
|
||||
#return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return traffic statistics for every outbound. Each outbound shows
|
||||
up/down/total counters.
|
||||
url: >-
|
||||
#return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
||||
- depth: 2
|
||||
title: >-
|
||||
Return the most recent Xray process stdout/stderr output. Useful to
|
||||
check for startup errors or runtime warnings.
|
||||
url: >-
|
||||
#return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
|
||||
- depth: 2
|
||||
title: >-
|
||||
Save the Xray JSON config template and optionally the outbound test URL.
|
||||
Both are sent as form fields.
|
||||
url: >-
|
||||
#save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
|
||||
- depth: 2
|
||||
title: >-
|
||||
Manage Cloudflare Warp integration. The action parameter selects the
|
||||
operation.
|
||||
url: >-
|
||||
#manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
|
||||
- depth: 2
|
||||
title: Manage NordVPN integration. The action parameter selects the operation.
|
||||
url: '#manage-nordvpn-integration-the-action-parameter-selects-the-operation'
|
||||
- depth: 2
|
||||
title: Reset traffic counters for a specific outbound by tag.
|
||||
url: '#reset-traffic-counters-for-a-specific-outbound-by-tag'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test an outbound configuration. Sends the outbound JSON (required),
|
||||
optionally all outbounds (to resolve sockopt.dialerProxy dependencies),
|
||||
and a mode flag.
|
||||
url: >-
|
||||
#test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
|
||||
- depth: 2
|
||||
title: >-
|
||||
Test a batch of outbounds (max 50) through one shared temp xray
|
||||
instance. Returns an array of results in input order, each with the
|
||||
outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
|
||||
breakdown.
|
||||
url: >-
|
||||
#test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown
|
||||
- depth: 2
|
||||
title: >-
|
||||
Live state of routing balancers in the running core
|
||||
(RoutingService.GetBalancerInfo): current override and the targets the
|
||||
strategy prefers. Returns a map keyed by balancer tag.
|
||||
url: >-
|
||||
#live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag
|
||||
- depth: 2
|
||||
title: >-
|
||||
Force a balancer in the running core to always pick one outbound
|
||||
(RoutingService.OverrideBalancerTarget). Applied live without a restart;
|
||||
cleared automatically when Xray restarts.
|
||||
url: >-
|
||||
#force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
|
||||
- depth: 2
|
||||
title: >-
|
||||
Ask the running core which outbound its router would pick for a
|
||||
synthetic connection (RoutingService.TestRoute). No traffic is sent.
|
||||
url: >-
|
||||
#ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
|
||||
- depth: 2
|
||||
title: >-
|
||||
List all outbound subscriptions (remote URLs that supply additional
|
||||
outbounds), newest first.
|
||||
url: >-
|
||||
#list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
||||
- depth: 2
|
||||
title: >-
|
||||
Create an outbound subscription. The URL is fetched, parsed into
|
||||
outbounds with stable tags, and merged additively into the running Xray
|
||||
config.
|
||||
url: >-
|
||||
#create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
|
||||
- depth: 2
|
||||
title: >-
|
||||
Update an existing outbound subscription by id. Accepts the same form
|
||||
fields as create.
|
||||
url: >-
|
||||
#update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
||||
- depth: 2
|
||||
title: Delete an outbound subscription by id.
|
||||
url: '#delete-an-outbound-subscription-by-id'
|
||||
- depth: 2
|
||||
title: >-
|
||||
Delete an outbound subscription by id (POST alias of DELETE for
|
||||
axios-friendly clients).
|
||||
url: >-
|
||||
#delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
|
||||
- depth: 2
|
||||
title: >-
|
||||
Force an immediate re-fetch of the subscription and return the parsed
|
||||
outbounds. Signals Xray to reload.
|
||||
url: >-
|
||||
#force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
|
||||
- depth: 2
|
||||
title: >-
|
||||
Reorder a subscription one step up or down in priority (controls its
|
||||
position in the merged outbounds).
|
||||
url: >-
|
||||
#reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
|
||||
- depth: 2
|
||||
title: >-
|
||||
Preview a subscription URL: fetch and parse it into outbounds without
|
||||
persisting anything.
|
||||
url: >-
|
||||
#preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
|
||||
structuredData:
|
||||
headings:
|
||||
- content: >-
|
||||
Return the Xray config template (JSON string), available inbound tags,
|
||||
client reverse tags, and the configured outbound test URL in one
|
||||
response.
|
||||
id: >-
|
||||
return-the-xray-config-template-json-string-available-inbound-tags-client-reverse-tags-and-the-configured-outbound-test-url-in-one-response
|
||||
- content: >-
|
||||
Return the built-in default Xray config shipped with the panel
|
||||
(identical to /panel/api/setting/getDefaultJsonConfig).
|
||||
id: >-
|
||||
return-the-built-in-default-xray-config-shipped-with-the-panel-identical-to-panelapisettinggetdefaultjsonconfig
|
||||
- content: >-
|
||||
Return traffic statistics for every outbound. Each outbound shows
|
||||
up/down/total counters.
|
||||
id: >-
|
||||
return-traffic-statistics-for-every-outbound-each-outbound-shows-updowntotal-counters
|
||||
- content: >-
|
||||
Return the most recent Xray process stdout/stderr output. Useful to
|
||||
check for startup errors or runtime warnings.
|
||||
id: >-
|
||||
return-the-most-recent-xray-process-stdoutstderr-output-useful-to-check-for-startup-errors-or-runtime-warnings
|
||||
- content: >-
|
||||
Save the Xray JSON config template and optionally the outbound test
|
||||
URL. Both are sent as form fields.
|
||||
id: >-
|
||||
save-the-xray-json-config-template-and-optionally-the-outbound-test-url-both-are-sent-as-form-fields
|
||||
- content: >-
|
||||
Manage Cloudflare Warp integration. The action parameter selects the
|
||||
operation.
|
||||
id: >-
|
||||
manage-cloudflare-warp-integration-the-action-parameter-selects-the-operation
|
||||
- content: >-
|
||||
Manage NordVPN integration. The action parameter selects the
|
||||
operation.
|
||||
id: manage-nordvpn-integration-the-action-parameter-selects-the-operation
|
||||
- content: Reset traffic counters for a specific outbound by tag.
|
||||
id: reset-traffic-counters-for-a-specific-outbound-by-tag
|
||||
- content: >-
|
||||
Test an outbound configuration. Sends the outbound JSON (required),
|
||||
optionally all outbounds (to resolve sockopt.dialerProxy
|
||||
dependencies), and a mode flag.
|
||||
id: >-
|
||||
test-an-outbound-configuration-sends-the-outbound-json-required-optionally-all-outbounds-to-resolve-sockoptdialerproxy-dependencies-and-a-mode-flag
|
||||
- content: >-
|
||||
Test a batch of outbounds (max 50) through one shared temp xray
|
||||
instance. Returns an array of results in input order, each with the
|
||||
outbound tag, delay, HTTP status and a connect/TLS/TTFB timing
|
||||
breakdown.
|
||||
id: >-
|
||||
test-a-batch-of-outbounds-max-50-through-one-shared-temp-xray-instance-returns-an-array-of-results-in-input-order-each-with-the-outbound-tag-delay-http-status-and-a-connecttlsttfb-timing-breakdown
|
||||
- content: >-
|
||||
Live state of routing balancers in the running core
|
||||
(RoutingService.GetBalancerInfo): current override and the targets the
|
||||
strategy prefers. Returns a map keyed by balancer tag.
|
||||
id: >-
|
||||
live-state-of-routing-balancers-in-the-running-core-routingservicegetbalancerinfo-current-override-and-the-targets-the-strategy-prefers-returns-a-map-keyed-by-balancer-tag
|
||||
- content: >-
|
||||
Force a balancer in the running core to always pick one outbound
|
||||
(RoutingService.OverrideBalancerTarget). Applied live without a
|
||||
restart; cleared automatically when Xray restarts.
|
||||
id: >-
|
||||
force-a-balancer-in-the-running-core-to-always-pick-one-outbound-routingserviceoverridebalancertarget-applied-live-without-a-restart-cleared-automatically-when-xray-restarts
|
||||
- content: >-
|
||||
Ask the running core which outbound its router would pick for a
|
||||
synthetic connection (RoutingService.TestRoute). No traffic is sent.
|
||||
id: >-
|
||||
ask-the-running-core-which-outbound-its-router-would-pick-for-a-synthetic-connection-routingservicetestroute-no-traffic-is-sent
|
||||
- content: >-
|
||||
List all outbound subscriptions (remote URLs that supply additional
|
||||
outbounds), newest first.
|
||||
id: >-
|
||||
list-all-outbound-subscriptions-remote-urls-that-supply-additional-outbounds-newest-first
|
||||
- content: >-
|
||||
Create an outbound subscription. The URL is fetched, parsed into
|
||||
outbounds with stable tags, and merged additively into the running
|
||||
Xray config.
|
||||
id: >-
|
||||
create-an-outbound-subscription-the-url-is-fetched-parsed-into-outbounds-with-stable-tags-and-merged-additively-into-the-running-xray-config
|
||||
- content: >-
|
||||
Update an existing outbound subscription by id. Accepts the same form
|
||||
fields as create.
|
||||
id: >-
|
||||
update-an-existing-outbound-subscription-by-id-accepts-the-same-form-fields-as-create
|
||||
- content: Delete an outbound subscription by id.
|
||||
id: delete-an-outbound-subscription-by-id
|
||||
- content: >-
|
||||
Delete an outbound subscription by id (POST alias of DELETE for
|
||||
axios-friendly clients).
|
||||
id: >-
|
||||
delete-an-outbound-subscription-by-id-post-alias-of-delete-for-axios-friendly-clients
|
||||
- content: >-
|
||||
Force an immediate re-fetch of the subscription and return the parsed
|
||||
outbounds. Signals Xray to reload.
|
||||
id: >-
|
||||
force-an-immediate-re-fetch-of-the-subscription-and-return-the-parsed-outbounds-signals-xray-to-reload
|
||||
- content: >-
|
||||
Reorder a subscription one step up or down in priority (controls its
|
||||
position in the merged outbounds).
|
||||
id: >-
|
||||
reorder-a-subscription-one-step-up-or-down-in-priority-controls-its-position-in-the-merged-outbounds
|
||||
- content: >-
|
||||
Preview a subscription URL: fetch and parse it into outbounds without
|
||||
persisting anything.
|
||||
id: >-
|
||||
preview-a-subscription-url-fetch-and-parse-it-into-outbounds-without-persisting-anything
|
||||
contents: []
|
||||
---
|
||||
|
||||
{/* This file was generated by Fumadocs. Do not edit this file directly. Any changes should be made by running the generation command again. */}
|
||||
|
||||
export default function Layout(props) {
|
||||
const { APIPage, OpenAPIPage } = props.components ?? {};
|
||||
// "APIPage" is the old name from v10, this allows both for backward compatibility
|
||||
const Comp = OpenAPIPage ?? APIPage;
|
||||
return (
|
||||
<>
|
||||
{props.children}
|
||||
<Comp document="./public/openapi.json" webhooks={[]} operations={[{"path":"/panel/api/xray/","method":"post"},{"path":"/panel/api/xray/getDefaultJsonConfig","method":"get"},{"path":"/panel/api/xray/getOutboundsTraffic","method":"get"},{"path":"/panel/api/xray/getXrayResult","method":"get"},{"path":"/panel/api/xray/update","method":"post"},{"path":"/panel/api/xray/warp/{action}","method":"post"},{"path":"/panel/api/xray/nord/{action}","method":"post"},{"path":"/panel/api/xray/resetOutboundsTraffic","method":"post"},{"path":"/panel/api/xray/testOutbound","method":"post"},{"path":"/panel/api/xray/testOutbounds","method":"post"},{"path":"/panel/api/xray/balancerStatus","method":"post"},{"path":"/panel/api/xray/balancerOverride","method":"post"},{"path":"/panel/api/xray/routeTest","method":"post"},{"path":"/panel/api/xray/outbound-subs","method":"get"},{"path":"/panel/api/xray/outbound-subs","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}","method":"delete"},{"path":"/panel/api/xray/outbound-subs/{id}/del","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/refresh","method":"post"},{"path":"/panel/api/xray/outbound-subs/{id}/move","method":"post"},{"path":"/panel/api/xray/outbound-subs/parse","method":"post"}]} showTitle />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
title: Database
|
||||
description: 3x-ui storage backends — SQLite (default) and PostgreSQL — the database path, connection pool, and SQLite-to-PostgreSQL migration.
|
||||
icon: Database
|
||||
---
|
||||
|
||||
3x-ui stores everything — inbounds, clients, settings — in a database. You choose
|
||||
the backend at install time; both are first-class.
|
||||
|
||||
## SQLite (default)
|
||||
|
||||
A single file at `/etc/x-ui/x-ui.db`. Zero setup, ideal for small and medium
|
||||
deployments. The folder is configurable with
|
||||
[`XUI_DB_FOLDER`](/docs/reference/env-vars#database) (on Windows it defaults next
|
||||
to the binary).
|
||||
|
||||
## PostgreSQL
|
||||
|
||||
Recommended for high client counts or multi-node setups. The installer can
|
||||
install PostgreSQL locally for you, or accept a DSN to an existing server. At
|
||||
runtime the backend is selected via environment variables, which the installer
|
||||
writes to `/etc/default/x-ui`:
|
||||
|
||||
```bash title="/etc/default/x-ui"
|
||||
XUI_DB_TYPE=postgres
|
||||
XUI_DB_DSN=postgres://xui:password@127.0.0.1:5432/xui?sslmode=disable
|
||||
```
|
||||
|
||||
Tune the connection pool with `XUI_DB_MAX_OPEN_CONNS` and
|
||||
`XUI_DB_MAX_IDLE_CONNS`.
|
||||
|
||||
### Docker
|
||||
|
||||
`docker compose up -d` keeps using SQLite. To run with the bundled PostgreSQL
|
||||
service, uncomment the two `XUI_DB_*` lines in `docker-compose.yml` and start
|
||||
with the profile:
|
||||
|
||||
```bash
|
||||
docker compose --profile postgres up -d
|
||||
```
|
||||
|
||||
## Migrate SQLite → PostgreSQL
|
||||
|
||||
Move an existing SQLite install to PostgreSQL with the built-in command:
|
||||
|
||||
```bash
|
||||
x-ui migrate-db --dsn "postgres://xui:password@127.0.0.1:5432/xui?sslmode=disable"
|
||||
```
|
||||
|
||||
Then set `XUI_DB_TYPE` and `XUI_DB_DSN` in `/etc/default/x-ui` and restart:
|
||||
|
||||
```bash
|
||||
systemctl restart x-ui
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The source SQLite file is left untouched — remove it manually only after you've
|
||||
verified the new backend works.
|
||||
</Callout>
|
||||
|
||||
## Backups
|
||||
|
||||
Whichever backend you use, back it up regularly — see
|
||||
[Backup & restore](/docs/operations/backup-restore).
|
||||
@@ -0,0 +1,84 @@
|
||||
---
|
||||
title: Environment Variables
|
||||
description: Complete reference for 3x-ui's XUI_* environment variables — database, panel, logging, memory, and the tunnel health monitor.
|
||||
icon: Variable
|
||||
---
|
||||
|
||||
3x-ui reads its runtime configuration from `XUI_*` environment variables. On a
|
||||
script install the installer writes them to the service environment file
|
||||
(`/etc/default/x-ui`, or `/etc/conf.d/x-ui` / `/etc/sysconfig/x-ui` depending on
|
||||
the distro); for Docker you set them in `docker-compose.yml` or `docker run -e`.
|
||||
Defaults are sensible — set only what you need to change, then restart:
|
||||
`systemctl restart x-ui`.
|
||||
|
||||
## Database
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ----------- | -------------------------------------------------------------------- |
|
||||
| `XUI_DB_TYPE` | `sqlite` | Backend: `sqlite`, or `postgres` (also accepts `postgresql` / `pg`). |
|
||||
| `XUI_DB_FOLDER` | `/etc/x-ui` | Folder for the SQLite database file (`x-ui.db`). |
|
||||
| `XUI_DB_DSN` | — | PostgreSQL connection string (used when `XUI_DB_TYPE=postgres`). |
|
||||
| `XUI_DB_MAX_OPEN_CONNS` | — | Max open connections in the PostgreSQL pool. |
|
||||
| `XUI_DB_MAX_IDLE_CONNS` | — | Max idle connections in the PostgreSQL pool. |
|
||||
|
||||
The default SQLite database path is `/etc/x-ui/x-ui.db`. See
|
||||
[Database](/docs/reference/database) for the SQLite ↔ PostgreSQL details.
|
||||
|
||||
## Panel
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | ------------------------------------------------------------------------ |
|
||||
| `XUI_PORT` | — | Override the panel port (1–65535). Takes precedence over the stored setting. |
|
||||
| `XUI_INIT_WEB_BASE_PATH` | `/` | Initial web base path on **first** launch (e.g. `/panel`). |
|
||||
| `XUI_ENABLE_FAIL2BAN` | `true` | Enable Fail2ban-based IP-limit enforcement. |
|
||||
| `XUI_SKIP_HSTS` | `false` | Skip the HSTS header — set `true` when TLS is terminated by a reverse proxy. |
|
||||
|
||||
## Logging & binaries
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ---------------- | ---------------- | ----------------------------------------------------------- |
|
||||
| `XUI_LOG_LEVEL` | `info` | `debug`, `info`, `notice`, `warning`, or `error`. |
|
||||
| `XUI_DEBUG` | `false` | Debug mode (forces log level to `debug`). |
|
||||
| `XUI_LOG_FOLDER` | `/var/log/x-ui` | Log output directory. |
|
||||
| `XUI_BIN_FOLDER` | `bin` | Folder for the Xray-core binary and geosite/geoip files. |
|
||||
|
||||
## Memory & profiling
|
||||
|
||||
The panel keeps memory low via `GOGC` and periodic releases. These are advanced
|
||||
knobs — leave them unset unless you're tuning a constrained host.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------------------- | ------- | ----------------------------------------------------------------- |
|
||||
| `XUI_GOGC` | — | Go GC target percentage; lower = less RAM, slightly more CPU. |
|
||||
| `XUI_MEMORY_RELEASE_INTERVAL` | — | Minutes between `FreeOSMemory` calls; `0` disables. |
|
||||
| `XUI_MEMORY_LIMIT` | — | Go soft memory limit in **MiB**. |
|
||||
| `GOMEMLIMIT` | — | Go-syntax soft limit (e.g. `400MiB`); takes precedence over above.|
|
||||
| `XUI_PPROF` | `false` | Expose pprof profiling on `127.0.0.1:6060`. |
|
||||
|
||||
## Xray
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------ | ------- | --------------------- |
|
||||
| `XRAY_VMESS_AEAD_FORCED` | `false` | Force VMess AEAD. |
|
||||
|
||||
## Tunnel health monitor
|
||||
|
||||
Optional watchdog: it probes a URL (optionally **through** a local Xray inbound)
|
||||
and restarts Xray after repeated failures. A restart drops all connected
|
||||
clients, so enable it deliberately.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------------------- | -------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| `XUI_TUNNEL_HEALTH_MONITOR` | `false` | Enable the monitor. |
|
||||
| `XUI_TUNNEL_HEALTH_PROXY` | — | Proxy to send the probe through, e.g. `socks5://127.0.0.1:1080`. Empty = only checks host connectivity. |
|
||||
| `XUI_TUNNEL_HEALTH_URL` | `https://www.cloudflare.com/cdn-cgi/trace` | URL to probe. |
|
||||
| `XUI_TUNNEL_HEALTH_INTERVAL` | `30s` | Interval between probes. |
|
||||
| `XUI_TUNNEL_HEALTH_TIMEOUT` | `10s` | Per-probe timeout. |
|
||||
| `XUI_TUNNEL_HEALTH_FAILURES` | `3` | Consecutive failures before a restart. |
|
||||
| `XUI_TUNNEL_HEALTH_COOLDOWN` | `5m` | Minimum delay between restarts. |
|
||||
|
||||
## Unattended install
|
||||
|
||||
| Variable | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------- |
|
||||
| `XUI_NONINTERACTIVE` | Set to `1` (or run with no TTY) to install with zero prompts; generated credentials are written to `/etc/x-ui/install-result.env`. See [Installation](/docs/guide/installation#unattended--cloud-init). |
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Reference",
|
||||
"icon": "BookMarked",
|
||||
"pages": ["env-vars", "database", "ports-firewall", "api"]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
---
|
||||
title: Ports & Firewall
|
||||
description: The ports 3x-ui uses by default and ready-made ufw / nftables rules to open them.
|
||||
icon: Network
|
||||
---
|
||||
|
||||
Open only the ports you actually use. Below are the common ones and a generator
|
||||
for `ufw` and `nftables` rules.
|
||||
|
||||
## Common ports
|
||||
|
||||
| Port (default) | Purpose |
|
||||
| -------------- | ----------------------------------------- |
|
||||
| `22` | SSH (keep this open!). |
|
||||
| `2053` | Panel (configurable). |
|
||||
| `2096` | Subscription server (if separate). |
|
||||
| `443` | A common inbound port (TLS / REALITY). |
|
||||
| `80` / `443` | Reverse proxy (if you run one). |
|
||||
|
||||
Your actual inbound ports depend on the inbounds you create.
|
||||
|
||||
## Generate firewall rules
|
||||
|
||||
<FirewallRulesGenerator />
|
||||
|
||||
<Callout type="warn">
|
||||
Always keep SSH allowed before enabling a default-deny policy, and test from a
|
||||
second session so you don't lock yourself out.
|
||||
</Callout>
|
||||
|
||||
## Related
|
||||
|
||||
<Cards>
|
||||
<Card title="Security" href="/docs/operations/security" description="Fail2ban, IP limits, and hardening." />
|
||||
<Card title="Environment variables" href="/docs/reference/env-vars" description="XUI_PORT and friends." />
|
||||
</Cards>
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: کلاینتها
|
||||
description: مدیریت کلاینتهای 3x-ui — اعتبارنامهها، محدودیت ترافیک و انقضا، محدودیت IP، گروهها، اقدامات گروهی، لینکهای خارجی و وضعیت آنلاین.
|
||||
icon: Users
|
||||
---
|
||||
|
||||
یک **کلاینت** یک کاربر منفرد است که با یک **ایمیل** یکتا شناسایی میشود. در پنل
|
||||
فعلی، کلاینتها رکوردهای درجهیک هستند که میتوانند همزمان به **چندین ورودی**
|
||||
متصل شوند و حسابداری ترافیک بهصورت جداگانه برای هر کلاینت انجام میشود.
|
||||
|
||||
## فیلدهای کلاینت
|
||||
|
||||
| فیلد | اعمال بر | معنی |
|
||||
| -------------- | --------------------- | ------------------------------------------------------------------ |
|
||||
| **Email** | همه | شناسهی یکتا که برای حسابداری و جستوجوها استفاده میشود. |
|
||||
| **ID (UUID)** | VLESS, VMess | اعتبارنامهی کلاینت. |
|
||||
| **Password** | Trojan, Shadowsocks | اعتبارنامهی کلاینت. |
|
||||
| **Auth** | Hysteria2 | اعتبارنامهی کلاینت. |
|
||||
| **Flow** | VLESS | جریان XTLS، برای مثال `xtls-rprx-vision`. |
|
||||
| **Limit IP** | همه | بیشینهی تعداد IPهای مبدأ همزمان (با Fail2ban اعمال میشود). |
|
||||
| **Total (GB)** | همه | سهمیهی ترافیک؛ هنگام اتمام، کلاینت غیرفعال میشود. |
|
||||
| **Expiry** | همه | تاریخی که پس از آن کلاینت از کار میافتد. |
|
||||
| **Reset** | همه | دورهی تمدید خودکار به **روز** (سهمیه را از نو میچرخاند). |
|
||||
| **Telegram ID**| همه | کلاینت را به یک کاربر Telegram برای سلفسرویس/اعلانها پیوند میدهد.|
|
||||
| **Sub ID** | همه | شناسهی اشتراک که لینکهای این کلاینت را گروهبندی میکند. |
|
||||
| **Group** | همه | گروه اختیاری کلاینت برای سازماندهی و فیلترکردن گروهی. |
|
||||
| **Comment** | همه | یادداشت متنی آزاد. |
|
||||
|
||||
<Callout type="info">
|
||||
رسیدن به محدودیت **ترافیک** یا **انقضا** کلاینت را غیرفعال میکند؛ پنل میتواند
|
||||
هنگام غیرفعالشدن خودکار کلاینتها، Xray را بهصورت خودکار راهاندازی مجدد کند
|
||||
(`restartXrayOnClientDisable`، بهصورت پیشفرض فعال).
|
||||
</Callout>
|
||||
|
||||
## محدودیتها و کنترل IP
|
||||
|
||||
- سقفهای **ترافیک / انقضا** هنگام رسیدن، کلاینت را غیرفعال میکنند؛ یک دورهی
|
||||
**Reset** سهمیه را بهصورت خودکار تمدید میکند.
|
||||
- **Limit IP** تعداد IPهای مبدأ همزمان را محدود میکند. اعمال آن به Fail2ban متکی
|
||||
است — به [امنیت](/docs/operations/security) مراجعه کنید. میتوانید IPهای اخیر یک
|
||||
کلاینت را مشاهده کرده و آنها را از بخش اقدامات کلاینت پاک کنید.
|
||||
- **وضعیت آنلاین** و زمانهای **آخرینبار آنلاین** برای هر کلاینت (و در پیکربندیهای
|
||||
چندنودی برای هر نود) ثبت میشوند.
|
||||
|
||||
## لینکهای اشتراکگذاری و لینکهای خارجی
|
||||
|
||||
هر کلاینت برای ورودیهایش لینکهای اشتراکگذاری و یک کد QR دارد، بهعلاوهی یک
|
||||
[اشتراک](/docs/config/subscription) ترکیبی. همچنین میتوانید **لینکهای خارجی** به
|
||||
یک کلاینت متصل کنید — لینکهای اضافی `vless://`، `vmess://`، `trojan://`، `ss://`،
|
||||
`hysteria2://` یا `wireguard://`، یا یک URL اشتراک از راه دور — تا در کنار
|
||||
لینکهای تولیدشده توسط پنل، در اشتراک کلاینت نمایش داده شوند.
|
||||
|
||||
برای بررسی دقیق محتویات یک لینک، آن را در
|
||||
[بازرس لینک اشتراکگذاری](/docs/config/share-links) جایگذاری کنید.
|
||||
|
||||
## اقدامات گروهی
|
||||
|
||||
برای مدیریت همزمان تعداد زیادی کلاینت، پنل از اقدامات گروهی **ساخت، فعالسازی،
|
||||
غیرفعالسازی، حذف، اتصال/قطع اتصال** (به ورودیها)، **بازنشانی ترافیک** و
|
||||
**تنظیم** (افزودن روز / افزودن بایت / تعیین flow) پشتیبانی میکند. اقدامات
|
||||
نگهداری همچنین به شما امکان میدهند کلاینتهای **تمامشده** (سهمیه/انقضای پایانیافته)
|
||||
و کلاینتهای **یتیم** (متصلنشده به هیچ ورودی) را حذف کنید.
|
||||
|
||||
<Callout type="warn">
|
||||
لینک اشتراکگذاری یک کلاینت حاوی اعتبارنامهی آن است. با لینکها و کدهای QR مانند
|
||||
رمز عبور رفتار کنید و در صورت نشت یکی از آنها، اعتبارنامه را تعویض کنید.
|
||||
</Callout>
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
title: ورودیها و پروتکلها
|
||||
description: ساخت ورودیها در 3x-ui — پروتکلها، انتقالها، بازنشانی و انقضای ترافیک، و fallbackهایی که چند پروتکل را روی یک پورت سرویس میدهند.
|
||||
icon: ArrowDownToLine
|
||||
---
|
||||
|
||||
یک **ورودی (inbound)** شنوندهای است که اتصالهای کلاینت را روی یک پورت با
|
||||
پروتکل و انتقال مشخصی میپذیرد. بیشتر کارهای روزمره شما ساخت و مدیریت ورودیها و
|
||||
کلاینتهای درون آنهاست.
|
||||
|
||||
## ساخت یک ورودی
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### افزودن یک ورودی
|
||||
|
||||
به **Inbounds → Add** بروید، یک توضیح (remark) برای آن بگذارید، یک **پروتکل**
|
||||
انتخاب کنید، و یک **پورت** و آدرس شنود انتخاب کنید.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### انتخاب انتقال و امنیت
|
||||
|
||||
انتقال (TCP، WebSocket، gRPC، HTTPUpgrade، XHTTP، …) و لایه امنیتی (هیچکدام،
|
||||
TLS یا REALITY) را انتخاب کنید. به [انتقالها](/docs/config/transports) و
|
||||
[REALITY](/docs/config/reality) مراجعه کنید.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### افزودن کلاینتها
|
||||
|
||||
یک یا چند کلاینت اضافه کنید که هر کدام اعتبارنامه، محدودیتها و لینک اشتراک
|
||||
خودش را دارد. به [کلاینتها](/docs/config/clients) مراجعه کنید.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### تنظیم محدودیت ترافیک، انقضا و بازنشانی
|
||||
|
||||
بهصورت اختیاری میتوانید کل ترافیک را محدود کنید و یک تاریخ انقضا برای ورودی
|
||||
تعیین کنید، و یک زمانبندی **بازنشانی ترافیک** دورهای انتخاب کنید: `never`
|
||||
(پیشفرض)، `hourly`، `daily`، `weekly` یا `monthly`.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## پروتکلهای پشتیبانیشده
|
||||
|
||||
ویرایشگر ورودی این پروتکلها را میپذیرد:
|
||||
|
||||
| پروتکل | توضیحات |
|
||||
| ---------------------- | ------------------------------------------------------------------------ |
|
||||
| **VLESS** | سبک؛ پایهی REALITY + XTLS-Vision. توصیهشده. |
|
||||
| **VMess** | قدیمیتر اما با پشتیبانی بسیار گسترده در کلاینتها. |
|
||||
| **Trojan** | مبتنی بر TLS؛ از XTLS و fallback پشتیبانی میکند. |
|
||||
| **Shadowsocks** | شامل رمزهای Shadowsocks-2022 (`2022-blake3-*`). |
|
||||
| **WireGuard** | تونل مدرن. |
|
||||
| **Hysteria2** | با عنوان `hysteria` انتخاب میشود؛ پنل لینکهای `hysteria2://` تولید میکند. |
|
||||
| **HTTP** | پراکسی HTTP. |
|
||||
| **Mixed (SOCKS/HTTP)** | یک شنونده ترکیبی SOCKS + HTTP. |
|
||||
| **Dokodemo-door / Tunnel** | فورواردینگ پورت / هدایت ترافیک. |
|
||||
| **MTProto** | پراکسی MTProto تلگرام که توسط یک فرایند همراه `mtg` سرویس میشود (نه Xray). |
|
||||
|
||||
<Callout type="info">
|
||||
Hysteria2 در سطح داخلی یک پروتکل جداگانه نیست — همان پروتکل `hysteria` است که
|
||||
نسخه انتقال آن روی ۲ تنظیم شده، و پنل برای آن لینکهای اشتراک `hysteria2://`
|
||||
تولید میکند.
|
||||
</Callout>
|
||||
|
||||
## Fallbackها — چند پروتکل روی یک پورت
|
||||
|
||||
Fallbackها به یک پورت TLS واحد (مثلاً `443`) اجازه میدهند بیش از یک پروتکل را
|
||||
سرویس دهد — برای مثال VLESS **و** Trojan — با هدایت دستدادنهای (handshake)
|
||||
ناهماهنگ به یک ورودی فرزند. در 3x-ui، fallbackها در پنل مدیریت میشوند (لیست
|
||||
**Fallbacks** یک ورودی اصلی) بهجای آنکه بهصورت دستی در JSON نوشته شوند.
|
||||
|
||||
Fallbackها تنها زمانی در دسترساند که ورودی اصلی اینگونه باشد:
|
||||
|
||||
- **VLESS** یا **Trojan**،
|
||||
- روی انتقال خام **TCP**،
|
||||
- با امنیت **TLS** یا **REALITY**.
|
||||
|
||||
هر قاعده fallback یک ورودی فرزند را هدف قرار میدهد و میتواند بر اساس `path`،
|
||||
`alpn` و `dest` تطبیق یابد. لینکهای اشتراک کلاینت برای یک فرزند fallback
|
||||
بهصورت خودکار بازنویسی میشوند تا آدرس، پورت و TLS ورودی اصلی را اعلام کنند.
|
||||
|
||||
## مطمئن نیستید کدام را انتخاب کنید؟
|
||||
|
||||
از این جادوگر استفاده کنید تا بر اساس اهداف و کلاینتهایتان یک پیشنهاد دریافت کنید:
|
||||
|
||||
<ProtocolWizard />
|
||||
|
||||
<Callout type="info">
|
||||
برای مقاومت در برابر سانسور با کلاینتهای مدرن، **VLESS + REALITY +
|
||||
XTLS-Vision** معمولاً بهترین انتخاب است — به
|
||||
[REALITY](/docs/config/reality) ادامه دهید.
|
||||
</Callout>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"title": "پیکربندی",
|
||||
"icon": "Settings",
|
||||
"pages": [
|
||||
"panel",
|
||||
"ssl-certificates",
|
||||
"inbounds",
|
||||
"reality",
|
||||
"transports",
|
||||
"clients",
|
||||
"subscription",
|
||||
"share-links"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: تنظیمات پنل
|
||||
description: تمام تنظیمات پنل 3x-ui — وبسرور، TLS، نمایش، امنیت و اعلانها — همراه با مقادیر پیشفرض برگرفته از سورس.
|
||||
icon: SlidersHorizontal
|
||||
---
|
||||
|
||||
**تنظیمات پنل** نحوهی ارائه و ایمنسازی خودِ پنل را کنترل میکند (جدا از ورودیها
|
||||
و کلاینتهای شما). تنظیمات بهصورت جفتهای کلید/مقدار ذخیره میشوند؛ مقادیر
|
||||
پیشفرض زیر مستقیماً از سورس پنل گرفته شدهاند. اطلاعات محرمانه (توکنها، گذرواژهها)
|
||||
فقط با نشانگرِ «تنظیمشده / تنظیمنشده» نمایش داده میشوند و هرگز بهصورت کامل به
|
||||
مرورگر بازگردانده نمیشوند.
|
||||
|
||||
## وبسرور
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| ------------------- | ----------------------- | ----------------------------------------------------------------------- |
|
||||
| `webPort` | `2053` | پورت پنل (۱ تا ۶۵۵۳۵). متغیر محیطی `XUI_PORT` در زمان اجرا آن را بازنویسی میکند. |
|
||||
| `webListen` | _(همهی واسطها)_ | پنل را به یک IP مشخص مقید کنید. |
|
||||
| `webBasePath` | `/` | مسیر URLی که پنل زیر آن ارائه میشود (همیشه به شکل `/…/` نرمالسازی میشود). |
|
||||
| `webCertFile` / `webKeyFile` | _(هیچکدام)_ | گواهی + کلید TLS. وقتی هر دو تنظیم شوند، پنل با **HTTPS** ارائه میشود. |
|
||||
| `sessionMaxAge` | `360` | طول عمر نشست بر حسب **دقیقه** (پیشفرض ۶ ساعت). |
|
||||
| `trustedProxyCIDRs` | `127.0.0.1/32,::1/128` | IPها/CIDRهایی که هدرهای فورواردشدهشان (IP واقعی کلاینت) مورد اعتماد است. |
|
||||
| `panelOutbound` | _(هیچکدام)_ | مسیریابی خروجیِ خود پنل (بررسی بهروزرسانیها، Telegram، واکشی geo/sub) از طریق یک خروجی Xray با نام مشخص. |
|
||||
|
||||
پس از تغییر پورت یا مسیر پایه، آدرس پنل بهصورت
|
||||
`http(s)://<server>:<port><web-base-path>` درمیآید. میتوانید مسیر پایه را در
|
||||
نخستین اجرا با [`XUI_INIT_WEB_BASE_PATH`](/docs/reference/env-vars) از پیش تعیین کنید.
|
||||
|
||||
### TLS
|
||||
|
||||
ارائهی پنل روی HTTPS از اطلاعات احراز هویت شما در حین انتقال محافظت میکند. یا
|
||||
`webCertFile` + `webKeyFile` را تنظیم کنید — [منوی SSL در `x-ui`](/docs/config/ssl-certificates)
|
||||
میتواند یک گواهی Let's Encrypt برای شما تهیه کند — یا TLS را روی یک
|
||||
[پروکسی معکوس](/docs/operations/reverse-proxy) خاتمه دهید.
|
||||
|
||||
<Callout type="warn">
|
||||
هرگز پنل را روی اینترنت عمومی با HTTP ساده در دسترس قرار ندهید. از TLS، یک پورت
|
||||
غیرپیشفرض، و یک مسیر پایهی وب تصادفیِ بلند استفاده کنید.
|
||||
</Callout>
|
||||
|
||||
## نمایش
|
||||
|
||||
| Setting | Default | Meaning |
|
||||
| ---------------- | ------------------------------------------------ | ------------------------------------------------------------- |
|
||||
| `pageSize` | `25` | تعداد ردیف در هر صفحه از فهرستها (`0` صفحهبندی را غیرفعال میکند). |
|
||||
| `expireDiff` | `0` | تعداد روز پیش از انقضا برای شروع هشدار. |
|
||||
| `trafficDiff` | `0` | درصد باقیمانده از سهمیه که در آن هشدار آغاز میشود. |
|
||||
| `remarkTemplate` | `{{INBOUND}}-{{EMAIL}}\|📊{{TRAFFIC_LEFT}}\|⏳{{DAYS_LEFT}}D` | الگوی پیشفرض یادداشت کلاینت (نگاه کنید به [لینکهای اشتراکگذاری](/docs/config/share-links#remark-template-variables)). |
|
||||
| `timeLocation` | `Local` | منطقهی زمانی برای آمار و انقضا. |
|
||||
| `datepicker` | `gregorian` | تقویم برای ورودیهای تاریخ (میلادی یا جلالی/شمسی). |
|
||||
|
||||
## امنیت و احراز هویت
|
||||
|
||||
اطلاعات احراز هویت، احراز هویت دومرحلهای، محدودکنندهی حملات جستجوی فراگیر،
|
||||
نشستها و LDAP در [نخستین ورود](/docs/guide/first-login) و
|
||||
[امنیت](/docs/operations/security) پوشش داده شدهاند. بهطور خلاصه:
|
||||
|
||||
- گذرواژهها بهصورت هشهای **bcrypt** ذخیره میشوند؛ تغییر آنها همهی نشستها را از سیستم خارج میکند.
|
||||
- **۲FA (TOTP)** میتواند در زمان ورود الزامی شود.
|
||||
- یک سازوکار جایگزین **LDAP** میتواند زمانی که بررسی گذرواژهی محلی ناموفق باشد، کاربران را احراز هویت کند.
|
||||
- دسترسی API از **توکنهای API** استفاده میکند که زیر تنظیمات پنل مدیریت میشوند (نگاه کنید به
|
||||
[مرجع API](/docs/reference/api/api-tokens)).
|
||||
|
||||
## اعلانها و اشتراک
|
||||
|
||||
این موارد گروههای تنظیمات و صفحههای مخصوص خود را دارند:
|
||||
|
||||
<Cards>
|
||||
<Card title="ربات Telegram" href="/docs/operations/telegram-bot" description="توکن، شناسههای چت، هشدارها و گزارشها." />
|
||||
<Card title="اشتراک" href="/docs/config/subscription" description="سرور اشتراک، قالبها و مسیرها." />
|
||||
<Card title="امنیت" href="/docs/operations/security" description="۲FA، محدودیتهای IP و سختسازی." />
|
||||
</Cards>
|
||||
|
||||
<Callout type="info">
|
||||
اعلانهای ایمیل (SMTP) نیز قابل پیکربندیاند (میزبان، پورت، رمزنگاری،
|
||||
گیرندگان) با همان انواع رویدادهای ربات Telegram.
|
||||
</Callout>
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
title: REALITY
|
||||
description: راهاندازی یک ورودی VLESS + REALITY همراه با XTLS-Vision در 3x-ui — کلیدها، short IDها، SNI، اثرانگشتها و اشتباهات رایج.
|
||||
icon: ShieldCheck
|
||||
---
|
||||
|
||||
**REALITY** یک لایهی امنیتی انتقال در Xray است که پروکسی شما را بهشکل ترافیک
|
||||
عادی به یک وبسایت واقعی و پرطرفدار جلوه میدهد. برخلاف TLS کلاسیک، سرور شما به
|
||||
**هیچ گواهی اختصاصی** نیاز ندارد — دستدهی (handshake) TLS سایت مقصد (`dest`) را
|
||||
قرض میگیرد. در ترکیب با جریان **XTLS-Vision**، سریع است و در برابر بازرسی عمیق
|
||||
بستهها (DPI) مقاومت میکند.
|
||||
|
||||
REALITY همراه با **VLESS** (و Trojan) بهکار میرود. جریان توصیهشده
|
||||
`xtls-rprx-vision` است.
|
||||
|
||||
## تنظیمات کلیدی
|
||||
|
||||
وقتی **REALITY** را بهعنوان حالت امنیتی روی یک ورودی VLESS انتخاب میکنید، 3x-ui
|
||||
این فیلدها را نمایش میدهد:
|
||||
|
||||
| فیلد | چیست |
|
||||
| ------------------------ | ------------------------------------------------------------------ |
|
||||
| **Dest (target)** | یک سایت TLS واقعی برای جعل هویت، برای مثال `www.microsoft.com:443`. |
|
||||
| **SNI / Server Names** | نام میزبانی که کلاینتها ارسال میکنند؛ باید با گواهی مقصد بخواند. |
|
||||
| **Public / Private key** | یک جفتکلید **x25519**. کلید خصوصی روی سرور باقی میماند. |
|
||||
| **Short IDs** | رشتههای Hex برای احراز هویت کلاینتها (میتوانید چندتا داشته باشید).|
|
||||
| **Flow** | روی `xtls-rprx-vision` تنظیم شود. |
|
||||
| **Fingerprint (uTLS)** | اثرانگشت TLS کلاینت برای تقلید، برای مثال `chrome`. |
|
||||
|
||||
کلید خصوصی با ابزار `x25519` از Xray تولید میشود (پنل میتواند این جفتکلید را
|
||||
برای شما بسازد):
|
||||
|
||||
```bash title="generate an x25519 keypair"
|
||||
xray x25519
|
||||
```
|
||||
|
||||
## راهاندازی در پنل
|
||||
|
||||
<Steps>
|
||||
|
||||
<Step>
|
||||
### ساخت یک ورودی VLESS
|
||||
|
||||
یک ورودی جدید اضافه کنید، پروتکل **VLESS** را انتخاب کنید و **Security** را روی
|
||||
**reality** تنظیم کنید.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### انتخاب مقصد (dest) و SNI
|
||||
|
||||
سایتی معتبر که از TLS 1.3 و HTTP/2 پشتیبانی میکند و هم از سرور و هم از کلاینتهای
|
||||
شما در دسترس است انتخاب کنید (برای مثال `www.microsoft.com:443`). نام سرورها / SNI
|
||||
را طوری تنظیم کنید که با گواهی آن سایت بخواند.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### تولید کلیدها و short IDها
|
||||
|
||||
جفتکلید x25519 و یک یا چند short ID تولید کنید. **کلید خصوصی** را محرمانه نگه
|
||||
دارید؛ کلاینتها فقط **کلید عمومی** را دریافت میکنند.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### تنظیم جریان و اثرانگشت
|
||||
|
||||
از جریان `xtls-rprx-vision` و یک اثرانگشت رایج uTLS مانند `chrome` استفاده کنید.
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### افزودن کلاینت و اشتراکگذاری لینک
|
||||
|
||||
یک کلاینت بسازید، سپس از لینک اشتراکگذاری یا کد QR آن در یک برنامهی سازگار
|
||||
(v2rayNG، Hiddify، Mihomo و دیگران) استفاده کنید.
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
|
||||
## پیکربندی چه شکلی است
|
||||
|
||||
روی سرور، `streamSettings` یک ورودی REALITY تقریباً به این شکل است:
|
||||
|
||||
```json title="server inbound (excerpt)"
|
||||
{
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"dest": "www.microsoft.com:443",
|
||||
"serverNames": ["www.microsoft.com"],
|
||||
"privateKey": "<x25519 private key>",
|
||||
"shortIds": ["<hex short id>"],
|
||||
"fingerprint": "chrome"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
لینک اشتراکگذاری متناظر کلاینت، پارامترهای **عمومی** را حمل میکند:
|
||||
|
||||
```text title="vless:// (excerpt)"
|
||||
vless://<uuid>@<server>:443?security=reality&pbk=<public-key>&sid=<short-id>&sni=www.microsoft.com&fp=chrome&spx=%2F&flow=xtls-rprx-vision#my-reality
|
||||
```
|
||||
|
||||
- `pbk` — کلید **عمومی** REALITY
|
||||
- `sid` — short ID (با یکی از موارد روی سرور میخواند)
|
||||
- `sni` — نام سرور (با گواهی مقصد میخواند)
|
||||
- `fp` — اثرانگشت کلاینت
|
||||
- `spx` — مسیر spiderX
|
||||
- `flow` — `xtls-rprx-vision`
|
||||
|
||||
## اشتباهات رایج
|
||||
|
||||
<Callout type="warn">
|
||||
|
||||
- **مقصد نامناسب.** مقدار `dest` باید یک سایت واقعی باشد که از **TLS 1.3** و
|
||||
**HTTP/2** پشتیبانی میکند، در دسترس است و در منطقهی شما مسدود نیست. سایتی را
|
||||
انتخاب کنید که مالک آن نیستید و ترافیک زیادی دارد.
|
||||
- **عدم تطابق SNI.** مقدار SNI / نام سرورها باید با گواهی واقعی مقصد بخواند، وگرنه
|
||||
دستدهی، استتار را لو میدهد.
|
||||
- **نشت کلید خصوصی.** فقط و فقط **کلید عمومی** را میان کلاینتها توزیع کنید.
|
||||
- **جریان نادرست.** REALITY + XTLS-Vision به `flow = xtls-rprx-vision` هم در ورودیِ
|
||||
مدخل کلاینت و هم در لینک اشتراکگذاری نیاز دارد.
|
||||
|
||||
</Callout>
|
||||
|
||||
## تولید یک پیکربندی
|
||||
|
||||
از تولیدکنندهی زیر برای ساخت یک جفتکلید تازهی X25519، UUID و short ID استفاده
|
||||
کنید، سپس JSON ورودی سرور و لینک اشتراکگذاری کلاینت را کپی کنید. همهچیز **در
|
||||
مرورگر شما** محاسبه میشود — هیچ کلید یا لینکی به جایی ارسال نمیشود.
|
||||
|
||||
<RealityConfigGenerator />
|
||||
|
||||
<Callout type="info">
|
||||
**کلید خصوصی** فقط به سرور شما تعلق دارد. لینک تولیدشدهی `vless://` (که حاوی
|
||||
**کلید عمومی** است) را با کلاینتها به اشتراک بگذارید.
|
||||
</Callout>
|
||||
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: لینکهای اشتراکگذاری
|
||||
description: قالبهای لینک اشتراکگذاری 3x-ui (vless، vmess، trojan، ss، hysteria2، mtproto)، متغیرهای قالب توضیحات، و بازرس لینک درونمرورگری.
|
||||
icon: Link
|
||||
---
|
||||
|
||||
3x-ui برای هر کلاینت یک **لینک اشتراکگذاری** (و کد QR) تولید میکند. برنامههای کلاینت
|
||||
مانند v2rayNG، Hiddify و Mihomo این لینکها را برای پیکربندی خودشان وارد میکنند.
|
||||
|
||||
## قالبهای لینک
|
||||
|
||||
| Scheme | ساختار |
|
||||
| -------------- | --------------------------------------------------------------- |
|
||||
| `vless://` | `vless://<uuid>@<host>:<port>?<params>#<remark>` |
|
||||
| `vmess://` | `vmess://<base64-json>` (یک شیء JSON کدشده با base64) |
|
||||
| `trojan://` | `trojan://<password>@<host>:<port>?<params>#<remark>` |
|
||||
| `ss://` | `ss://<userinfo>@<host>:<port>?<params>#<remark>` (SIP002؛ Shadowsocks-2022 از userinfo کدگذاریشده با درصد استفاده میکند) |
|
||||
| `hysteria2://` | `hysteria2://<auth>@<host>:<port>?<params>#<remark>` |
|
||||
| `tg://proxy` | `tg://proxy?server=…&port=…&secret=…` (MTProto) |
|
||||
|
||||
پارامترهای کوئری تنظیمات انتقال و امنیت را حمل میکنند — `security`،
|
||||
`sni`، `fp`، `pbk`، `sid`، `spx`، `flow`، `type`، `path`، `host`، `alpn` و
|
||||
بیشتر.
|
||||
|
||||
## بازرسی یک لینک
|
||||
|
||||
هر لینک اشتراکگذاری را بچسبانید تا تکتک فیلدها رمزگشایی شوند. تجزیه **بهطور کامل در
|
||||
مرورگر شما** انجام میشود — لینک هرگز روی شبکه ارسال نمیشود.
|
||||
|
||||
<ShareLinkInspector />
|
||||
|
||||
<Callout type="warn">
|
||||
لینکهای اشتراکگذاری همهی آنچه را برای اتصال بهعنوان یک کلاینت لازم است در بر دارند، از جمله
|
||||
اعتبارنامهی کلاینت. با آنها مانند رمز عبور رفتار کنید.
|
||||
</Callout>
|
||||
|
||||
## متغیرهای قالب توضیحات
|
||||
|
||||
متنی که در هر لینک پس از `#` میآید (**توضیحات** یا remark) از یک قالب تولید میشود
|
||||
که شما در تنظیمات پنل کنترل میکنید (`remarkTemplate`). مقدار پیشفرض این است:
|
||||
|
||||
```text
|
||||
{{INBOUND}}-{{EMAIL}}|📊{{TRAFFIC_LEFT}}|⏳{{DAYS_LEFT}}D
|
||||
```
|
||||
|
||||
توکنها از نحو `{{UPPER_CASE}}` استفاده میکنند. قالب بر اساس `|` به بخشهایی تقسیم میشود؛
|
||||
بخشی که تنها مقدارش نشانگر نامحدود `∞` باشد (برای `TRAFFIC_LEFT`،
|
||||
`TRAFFIC_TOTAL`، `DAYS_LEFT` یا `TIME_LEFT`) حذف میشود، تا کلاینتهای نامحدود
|
||||
تزئینات خالی نشان ندهند.
|
||||
|
||||
### توکنهای در دسترس
|
||||
|
||||
| توکن | مقدار |
|
||||
| ----- | ----- |
|
||||
| `{{EMAIL}}` / `{{USERNAME}}` | ایمیل کلاینت (شناسه) |
|
||||
| `{{INBOUND}}` | توضیحات اینباند |
|
||||
| `{{HOST}}` | توضیحات ردیف هاست (هاستهای مدیریتشده) |
|
||||
| `{{ID}}` / `{{SHORT_ID}}` | UUID کلاینت / ۸ کاراکتر نخست آن |
|
||||
| `{{TELEGRAM_ID}}` · `{{SUB_ID}}` · `{{COMMENT}}` | شناسه Telegram، شناسه اشتراک، توضیح |
|
||||
| `{{STATUS}}` / `{{STATUS_EMOJI}}` | `active`/`expired`/`depleted`/`disabled` (یا ✅⏳🚫) |
|
||||
| `{{DAYS_LEFT}}` / `{{TIME_LEFT}}` | روزهای باقیمانده، یا `Xd Xh Xm` (`∞` در صورت نامحدود بودن) |
|
||||
| `{{EXPIRE_DATE}}` / `{{JALALI_EXPIRE_DATE}}` / `{{EXPIRE_UNIX}}` | تاریخ انقضا بهصورت میلادی / شمسی / ثانیههای Unix |
|
||||
| `{{CREATED_UNIX}}` | زمان ایجاد (ثانیههای Unix) |
|
||||
| `{{TRAFFIC_USED}}` / `{{TRAFFIC_LEFT}}` / `{{TRAFFIC_TOTAL}}` | مصرف خوانا برای انسان (`∞` در صورت نامحدود بودن) |
|
||||
| `{{TRAFFIC_USED_BYTES}}` / `{{TRAFFIC_LEFT_BYTES}}` / `{{TRAFFIC_TOTAL_BYTES}}` | همان، بر حسب بایت |
|
||||
| `{{UP}}` / `{{DOWN}}` | آپلود / دانلود (خوانا برای انسان) |
|
||||
| `{{RESET_DAYS}}` · `{{USAGE_PERCENTAGE}}` | دوره بازنشانی (روز) · درصد مصرفشده |
|
||||
| `{{PROTOCOL}}` / `{{TRANSPORT}}` / `{{SECURITY}}` | مثلاً `VLESS` / `ws` / `REALITY` |
|
||||
|
||||
<Callout type="info">
|
||||
توکنهای مصرف (ترافیک، روزها، وضعیت) در **بدنه**ی اشتراک ظاهر میشوند اما
|
||||
از نمای نمایش/QR حذف میگردند، تا یک QR اشتراکگذاریشده سهمیهی باقیماندهی یک کلاینت
|
||||
را افشا نکند. توکنهای تاریخ از تنظیم `datepicker` پیروی میکنند (میلادی یا شمسی).
|
||||
</Callout>
|
||||
|
||||
## مرتبط
|
||||
|
||||
<Cards>
|
||||
<Card title="REALITY" href="/docs/config/reality" description="یک پیکربندی و لینک VLESS + REALITY بسازید." />
|
||||
<Card title="اشتراک" href="/docs/config/subscription" description="همهی لینکهای یک کلاینت را از یک URL ارائه دهید." />
|
||||
</Cards>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user