commit 955e819677a3bf792fe8e7f58f0c654b8d0bff69 Author: SamNet-dev Date: Mon Feb 23 23:46:21 2026 -0600 feat: migrate paqctl to self-hosted Gitea (git.samnet.dev) All repository URLs updated from GitHub (SamNet-dev/paqctl) to Gitea (git.samnet.dev/SamNet-dev/paqctl). Third-party references (hanselime/paqet, Xray, microsocks) remain on GitHub. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2af3729 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.pyc +*.pyo +.claude/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..901405d --- /dev/null +++ b/LICENSE @@ -0,0 +1,203 @@ +GNU AFFERO GENERAL PUBLIC LICENSE +Version 3, 19 November 2007 + +Copyright (C) 2026 SamNet Technologies, LLC +Copyright (C) 2007 Free Software Foundation, Inc. + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are 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. + +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. + +Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. + +A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. + +The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. + +An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. + +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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. +Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. + +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 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 work with which it is combined will remain governed by version 3 of the GNU General Public License. + +14. Revised Versions of this License. +The Free Software Foundation may publish revised and/or new versions of the GNU Affero 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 Affero 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 Affero 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 Affero 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. + +paqctl - Paqet Management Tool +Copyright (C) 2026 SamNet Technologies, LLC + +This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. + +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 AGPL, see . diff --git a/README.md b/README.md new file mode 100644 index 0000000..fd0e26a --- /dev/null +++ b/README.md @@ -0,0 +1,2152 @@ +``` + _ _ + _ __ __ _ __ _ ___| |_| | +| '_ \ / _` |/ _` |/ __| __| | +| |_) | (_| | (_| | (__| |_| | +| .__/ \__,_|\__, |\___|\__|_| +|_| |_| +``` + +[![Version](https://img.shields.io/badge/version-1.0.0-blue.svg)](https://git.samnet.dev/SamNet-dev/paqctl/releases) +[![License](https://img.shields.io/badge/license-AGPL--3.0-green.svg)](LICENSE) +[![Server](https://img.shields.io/badge/server-Linux-lightgrey.svg)](https://git.samnet.dev/SamNet-dev/paqctl) +[![Client](https://img.shields.io/badge/client-Windows%20%7C%20macOS%20%7C%20Linux-green.svg)](https://git.samnet.dev/SamNet-dev/paqctl) + +**Bypass firewall restrictions and access the free internet** + +[نسخه فارسی](#نسخه-فارسی) + +--- + +## What is this? + +paqctl is a unified management tool for bypass proxies. It helps you connect to a server outside restricted networks (like behind the Great Firewall) and access the internet freely. You run the **server** component on a VPS, and the **client** on your Windows/Mac/Linux machine. + +--- + +## Two Methods + +This tool supports **two different bypass methods**. Choose based on your situation: + +| | **Paqet** | **GFW-Knocker (GFK)** | +|---|---|---| +| **Difficulty** | Easy ⭐ | Advanced ⭐⭐⭐ | +| **Best for** | Most situations | Heavy censorship (GFW) | +| **Your proxy** | `127.0.0.1:1080` | `127.0.0.1:14000` | +| **Technology** | KCP over raw sockets | Violated TCP + QUIC tunnel | +| **Server needs** | Just paqet | GFK + Xray | + +### Which should I use? + +``` +START HERE + | + v ++----------------------------------+ +| Is your network heavily censored | +| (like Iran or China's GFW)? | ++----------------------------------+ + | | + YES NO + | | + v v ++-----------+ +-----------+ +| Try GFK | | Use Paqet | +| first | | | ++-----------+ +-----------+ +``` + +> **Tip:** You can install BOTH and have a backup! They use different ports. + +--- + +## How It Works + +### Paqet (Simple) + +``` +YOUR COMPUTER YOUR VPS INTERNET ++--------------+ +--------------+ +----------+ +| Browser | | Paqet | | Google | +| | | | Server | | YouTube | +| v | ---KCP--> | | | -------> | etc. | +| Paqet | (random | v | | | +| Client | UDP) | SOCKS5 | | | ++--------------+ +--------------+ +----------+ + 127.0.0.1:1080 your.vps.ip +``` + +**How Paqet bypasses firewalls:** +1. Uses KCP protocol over raw TCP packets with custom TCP flags +2. Sends packets via raw sockets, making them hard to fingerprint +3. DPI systems can't easily identify it as proxy traffic + +--- + +### GFW-Knocker (Advanced) + +``` +YOUR COMPUTER YOUR VPS INTERNET ++--------------+ +--------------+ +----------+ +| Browser | | GFK Server | | Google | +| | | "Violated | | | | YouTube | +| v | TCP" | v | | etc. | +| GFK Client | ---------> | QUIC Tunnel | -------> | | +| (VIO+QUIC) | (malformed | | | | | +| | | +QUIC) | v | | | +| Port 14000 | | Xray | | | ++--------------+ +--------------+ +----------+ + 127.0.0.1:14000 your.vps.ip +``` + +**How GFK bypasses firewalls:** +1. **Violated TCP**: Sends TCP packets that are intentionally "broken" - they have wrong flags, no proper handshake. Firewalls expect normal TCP and often pass these through. +2. **QUIC Tunnel**: Inside these violated packets, there's a QUIC connection carrying your actual data. +3. **Xray Backend**: On the server, Xray provides the actual SOCKS5 proxy service. + +--- + +
+Click here if you want to set up GFK alongside an Xray panel (3x-ui, Marzban, etc.) — includes server-to-server bridge setup + +If your foreign server already has an Xray panel (3x-ui, Marzban, etc.), paqctl detects it and works alongside it. Your panel stays untouched — paqctl only adds what's needed. + +**What paqctl does when it detects Xray:** + +| Scenario | What paqctl does | +|---|---| +| **No Xray installed** | Installs Xray with SOCKS5 proxy automatically (nothing to configure) | +| **Xray panel running** | Keeps your panel, adds a SOCKS5 inbound on a free port (e.g. 10443), appends an extra port mapping automatically | +| **Xray installed but not running** | Installs its own SOCKS5 (same as fresh install) | + +When a panel is detected, paqctl gives you **two connections** automatically: +- **Panel mapping** (`14000:443`) — for server-to-server panel traffic (vmess/vless) +- **SOCKS5 mapping** (`14001:10443`) — for direct proxy use from Windows/Mac (no v2rayN needed) + +--- + +### Setup A: Server-to-Server (Iran panel to Foreign panel) + +This is for when you have a panel on **both** servers (Iran + foreign) and want to route the Iran panel's outbound through the GFK tunnel instead of a direct connection. + +**1. Install paqctl on the foreign server (server role):** +```bash +curl -fsSL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash +``` +- Choose **server** role +- Set port mapping: `14000:443` (where `443` is your panel's inbound port) +- paqctl detects Xray and adds SOCKS5 alongside your panel (e.g. `14001:10443`) + +**2. Install paqctl on the Iran server (client role):** +```bash +curl -fsSL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash +``` +- Choose **client** role +- Use the **exact same** port mappings shown in the server output (e.g. `14000:443,14001:10443`) +- Use the same auth code from the server setup + +**3. Update your Iran panel outbound to route through GFK:** + +In your Iran panel (3x-ui, Marzban, etc.), change the outbound that connects to the foreign server: + +**Before** (direct connection — blocked by DPI): +```json +{ + "tag": "vmess_out", + "protocol": "vmess", + "settings": { + "vnext": [{ + "address": "FOREIGN_SERVER_IP", + "port": 443, + "users": [{"id": "your-uuid", "security": "auto"}] + }] + } +} +``` + +**After** (routed through GFK tunnel): +```json +{ + "tag": "vmess_out", + "protocol": "vmess", + "settings": { + "vnext": [{ + "address": "127.0.0.1", + "port": 14000, + "users": [{"id": "your-uuid", "security": "auto"}] + }] + } +} +``` + +In 3x-ui: go to **Xray Configs → Outbounds → Add Outbound** (or edit existing), and fill in: +- **Address**: `127.0.0.1` +- **Port**: `14000` (the VIO port, NOT the original server port) +- **Protocol/ID/encryption**: keep the same as before (from your foreign panel's inbound) +- **Security**: None (traffic is already encrypted inside the GFK tunnel) + +> **Where do I get the UUID?** From your foreign server's panel — go to **Inbounds**, find the inbound you're connecting to, and copy its UUID/ID. If you already had a working outbound before, just change the address and port — everything else stays the same. + +**Traffic flow:** +``` +End user --> Iran panel inbound --> Iran panel outbound (127.0.0.1:14000) + --> GFK client (VIO port) --> QUIC tunnel over violated TCP + --> Foreign GFK server --> 127.0.0.1:443 (foreign panel inbound) --> Internet +``` + +--- + +### Setup B: Direct Client (Windows/Mac to Foreign server) + +This is for when you **don't have an Iran server** — you connect directly from your Windows or Mac to the foreign server through GFK. paqctl auto-adds a SOCKS5 proxy so you can use it as a simple browser proxy. + +**1. Install paqctl on the foreign server** (same as above) + +**2. On your Windows/Mac**, install the GFK client and use the SOCKS5 mapping: +- The server output will show something like: `Mappings: 14000:443,14001:10443` +- Use `14001` as your proxy port — this is the direct SOCKS5 (no panel/v2rayN needed) +- Configure your browser or system proxy to `SOCKS5 127.0.0.1:14001` + +**Traffic flow:** +``` +Browser (SOCKS5 127.0.0.1:14001) --> GFK client + --> QUIC tunnel over violated TCP + --> Foreign GFK server --> 127.0.0.1:10443 (SOCKS5 proxy) --> Internet +``` + +--- + +**Multiple ports:** If your panel uses multiple ports, map them all: +``` +14000:443,14001:8080,14002:2020 +``` +paqctl will add SOCKS5 on the next available port and append it automatically. + +> **Note:** The "Firewall: VIO port blocked" status message (shown in green) is **normal and correct**. It means the firewall is properly configured for GFK's raw socket to work. + +
+ +--- + +## Quick Start + +### 1. Server Setup (Linux VPS) + +Run this on your VPS (requires root): + +```bash +curl -fsSL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash +``` + +> The installer automatically downloads the latest paqet release from GitHub. + +Then open the interactive menu: + +```bash +sudo paqctl menu +``` + +After setup, get your connection info: + +```bash +sudo paqctl info +``` + +This will show you the **Server IP**, **Port**, and **Key/Auth Code** you need for the client. + +--- + +### 2. Client Setup + +
+

🪟 Windows Client Setup (Click to expand)

+ +## Windows Client - Complete Guide + +### Prerequisites + +- Windows 10 or 11 +- Administrator access +- Your server's connection info (from `paqctl info` on server) + +--- + +## 🚀 Easy Method (Recommended) - Using .bat Files + +The simplest way to get started - just download, double-click, and connect! + +### Step 1: Download + +1. Go to: https://git.samnet.dev/SamNet-dev/paqctl +2. Click the green **"Code"** button → **"Download ZIP"** +3. Extract the ZIP file anywhere (e.g., Desktop) +4. Open the `windows` folder inside + +### Step 2: Install Protocol + +You'll see two `.bat` files: +- `Paqet-Client.bat` - For Paqet protocol (simple, recommended) +- `GFK-Client.bat` - For GFW-knocker protocol (advanced) + +**Right-click** your chosen `.bat` file → **"Run as administrator"** + +First run will install Npcap (required for raw sockets). Follow the installer prompts. + +### Step 3: Configure & Connect + +After installation, the script will ask for your server info: +- **Paqet:** Server address (e.g., `1.2.3.4:8443`) and encryption key +- **GFK:** Server IP and auth code + +Enter the values from your server (shown after server setup or via `paqctl info`). + +Once configured, press **Connect** and you're done! + +### Step 4: Use the Proxy + +Configure your browser to use SOCKS5 proxy: +- **Paqet:** `127.0.0.1:1080` +- **GFK:** `127.0.0.1:14000` + +To disconnect, press `Ctrl+C` in the window. + +--- + +## 💻 Advanced Method - PowerShell Script + +For more control, use the interactive PowerShell menu. + +### Step 1: Open PowerShell as Administrator + +1. Press `Win + S`, type `PowerShell` +2. Right-click "Windows PowerShell" → **"Run as administrator"** +3. Click "Yes" on the UAC prompt + +### Step 2: Run the Script + +**Option A: One-liner (downloads and runs automatically)** +```powershell +irm https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/windows/paqet-client.ps1 | iex +``` + +**Option B: Download first, then run** +```powershell +git clone https://git.samnet.dev/SamNet-dev/paqctl.git +cd paqctl\windows +.\paqet-client.ps1 +``` + +### Step 3: Use the Menu + +The interactive menu lets you: +1. Install paqet or GFK +2. Configure connection +3. Start/stop client +4. Check status + +--- + +### Step 4: Allow Script Execution + +Windows blocks scripts by default. Run this once: + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` + +Type `Y` and press Enter when prompted. + +--- + +### Step 5: Run the Client + +**Option 1: Double-click (Easiest)** +- Double-click `Paqet-Client.bat` +- It will automatically run as Administrator + +**Option 2: From PowerShell** +```powershell +.\paqet-client.ps1 +``` + +You'll see an interactive menu: + +``` +=============================================== + PAQET/GFK CLIENT MANAGER +=============================================== + + No backend installed yet + + 1. Install paqet (simple, all-in-one SOCKS5) + 2. Install GFW-knocker (advanced, for heavy DPI) + 3. Configure connection + 4. Start client + 5. Stop client + 6. Show status + 7. About (how it works) + 0. Exit + + Select option: +``` + +--- + +### Step 6: Install Your Chosen Backend + +> **Tip:** For a smoother experience, download and install [Npcap](https://npcap.com/#download) separately first. + +#### For Paqet (Recommended for most users): + +1. Press `1` and Enter +2. The script will: + - Download and install **Npcap** (network capture driver) + - Download the **paqet binary** +3. When Npcap installer opens: + - Click "I Agree" + - Keep default options checked + - Click "Install" + - Click "Finish" + +#### For GFK (If Paqet is blocked): + +1. Press `2` and Enter +2. The script will: + - Install **Npcap** + - Install **Python 3.10+** (if not present) + - Install Python packages: `scapy`, `aioquic` + - Copy GFK client scripts + +--- + +### Step 7: Configure Connection + +1. Press `3` and Enter +2. Enter the info from your server: + +**For Paqet:** +``` +Server address (e.g., 1.2.3.4:8443): +Encryption key (16+ chars): +``` + +**For GFK:** +``` +Server IP (e.g., 1.2.3.4): +Auth code (from server setup): +``` + +--- + +### Step 8: Start the Client + +1. Press `4` and Enter +2. The client will start and show logs +3. Keep this window open while using the proxy + +--- + +### Step 9: Configure Your Browser + +Now you need to tell your browser to use the proxy. + +**Your proxy address is:** +- **Paqet:** `127.0.0.1:1080` (SOCKS5) +- **GFK:** `127.0.0.1:14000` (SOCKS5) + +#### Firefox (Recommended): +1. Open Firefox +2. Go to Settings → General → Network Settings → Settings... +3. Select "Manual proxy configuration" +4. In "SOCKS Host": `127.0.0.1` +5. Port: `1080` (for Paqet) or `14000` (for GFK) +6. Select "SOCKS v5" +7. Check "Proxy DNS when using SOCKS v5" ← **Important!** +8. Click OK + +#### Chrome (via extension): +Chrome uses Windows proxy settings. Use a browser extension instead: +1. Install "SwitchyOmega" extension +2. Create a new profile +3. Set SOCKS5 proxy: `127.0.0.1:1080` or `127.0.0.1:14000` +4. Activate the profile + +--- + +### Step 10: Test Your Connection + +1. Open your browser (with proxy configured) +2. Go to: https://whatismyipaddress.com +3. Your IP should show your **VPS IP**, not your real IP +4. Try accessing blocked sites + +--- + +### Stopping the Client + +- Press `Ctrl+C` in the PowerShell window, OR +- Run the script again and choose option `5` (Stop client) + +--- + +### Troubleshooting Windows + +
+"Running scripts is disabled" error + +Run this command first: +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` +
+ +
+"Administrator privileges required" + +You must run PowerShell as Administrator. Right-click PowerShell and select "Run as administrator". +
+ +
+Npcap installation fails + +1. Download manually from https://npcap.com +2. Run the installer as Administrator +3. Make sure "WinPcap API-compatible Mode" is checked +4. Restart your computer after installation +
+ +
+Connection times out + +1. Make sure your server is running (`paqctl status` on server) +2. Check if your VPS firewall allows the port (8443 for Paqet, 45000 for GFK) +3. Try the other method (if Paqet fails, try GFK) +
+ +
+GFK: "Gateway MAC not found" + +The script couldn't detect your router's MAC address. You'll need to enter it manually: + +1. Open Command Prompt +2. Run: `arp -a` +3. Find your gateway IP (usually 192.168.1.1 or 192.168.0.1) +4. Copy the MAC address next to it (format: aa-bb-cc-dd-ee-ff) +5. Enter it when the script asks +
+ +
+ +--- + +
+

🍎 macOS Client Setup (Click to expand)

+ +## macOS Client - Complete Guide + +macOS requires manual setup since there's no automated script yet. + +### Prerequisites + +- macOS 10.15 (Catalina) or newer +- Administrator access (for sudo) +- Homebrew (recommended) +- Your server's connection info + +--- + +### Option A: Paqet on macOS + +#### Step 1: Install Homebrew (if not installed) + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +#### Step 2: Download Paqet Binary + +```bash +# Create directory +mkdir -p ~/paqet && cd ~/paqet + +# Download latest release (Intel Mac) +curl -LO https://github.com/hanselime/paqet/releases/download/v1.0.0-alpha.17/paqet-darwin-amd64-v1.0.0-alpha.17.tar.gz +tar -xzf paqet-darwin-amd64-v1.0.0-alpha.17.tar.gz + +# For Apple Silicon (M1/M2/M3): +# curl -LO https://github.com/hanselime/paqet/releases/download/v1.0.0-alpha.17/paqet-darwin-arm64-v1.0.0-alpha.17.tar.gz +# tar -xzf paqet-darwin-arm64-v1.0.0-alpha.17.tar.gz + +# Make executable +chmod +x paqet_darwin_amd64 +``` + +#### Step 3: Create Config File + +```bash +cat > ~/paqet/config.yaml << 'EOF' +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "en0" # Your network interface (en0 for macOS, eth0 for Linux) + ipv4: + addr: "YOUR_LOCAL_IP:0" # Your local IP, e.g., 192.168.1.100:0 + router_mac: "YOUR_ROUTER_MAC" # Gateway MAC, e.g., aa:bb:cc:dd:ee:ff + +server: + addr: "YOUR_SERVER_IP:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "YOUR_SECRET_KEY" +EOF +``` + +Replace the placeholders: +- `YOUR_LOCAL_IP`: Run `ifconfig en0 | grep inet` to find your IP +- `YOUR_ROUTER_MAC`: Run `arp -n | grep gateway` or check your router +- `YOUR_SERVER_IP` and `YOUR_SECRET_KEY`: Get from your server admin + +> **Tip:** Use `paqctl` for automatic configuration - it detects these values for you. + +#### Step 4: Run Paqet + +```bash +# Requires sudo for raw socket access +sudo ~/paqet/paqet_darwin_amd64 run -c ~/paqet/config.yaml +``` + +For Apple Silicon: +```bash +sudo ~/paqet/paqet_darwin_arm64 run -c ~/paqet/config.yaml +``` + +Your SOCKS5 proxy is now at `127.0.0.1:1080` + +--- + +### Option B: GFK on macOS + +GFK requires Python and some setup: + +#### Step 1: Install Python 3.10+ + +```bash +brew install python@3.11 +``` + +#### Step 2: Clone the Repository + +```bash +git clone https://git.samnet.dev/SamNet-dev/paqctl.git +cd paqctl/gfk/client +``` + +#### Step 3: Install Python Dependencies + +```bash +pip3 install scapy aioquic +``` + +#### Step 4: Create parameters.py + +```bash +cat > parameters.py << 'EOF' +# GFW-knocker client configuration +from scapy.all import conf + +# Server settings +vps_ip = "YOUR_SERVER_IP" +xray_server_ip = "127.0.0.1" + +# Port mappings (local_port: remote_port) +tcp_port_mapping = {14000: 443} +udp_port_mapping = {} + +# VIO (raw socket) ports +vio_tcp_server_port = 45000 +vio_tcp_client_port = 40000 +vio_udp_server_port = 35000 +vio_udp_client_port = 30000 + +# QUIC tunnel ports +quic_server_port = 25000 +quic_client_port = 20000 +quic_local_ip = "127.0.0.1" + +# QUIC settings +quic_verify_cert = False +quic_idle_timeout = 86400 +udp_timeout = 300 +quic_mtu = 1420 +quic_max_data = 1073741824 +quic_max_stream_data = 1073741824 +quic_auth_code = "YOUR_AUTH_CODE" +quic_certificate = "cert.pem" +quic_private_key = "key.pem" + +# SOCKS proxy +socks_port = 14000 +EOF +``` + +Replace `YOUR_SERVER_IP` and `YOUR_AUTH_CODE` with your actual values. + +#### Step 5: Run GFK Client + +```bash +# Requires sudo for raw socket access +sudo python3 mainclient.py +``` + +Your SOCKS5 proxy is now at `127.0.0.1:14000` + +--- + +### Configure macOS to Use Proxy + +#### System-wide (all apps): + +1. Open **System Preferences** → **Network** +2. Select your connection (Wi-Fi or Ethernet) +3. Click **Advanced** → **Proxies** +4. Check **SOCKS Proxy** +5. Server: `127.0.0.1` +6. Port: `1080` (Paqet) or `14000` (GFK) +7. Click **OK** → **Apply** + +#### Firefox only: + +Same as Windows - go to Firefox Settings → Network Settings → Manual proxy. + +--- + +### Troubleshooting macOS + +
+"Operation not permitted" error + +macOS requires special permissions for raw sockets: + +1. Run with `sudo` +2. If still failing, you may need to disable SIP (not recommended) or use a different method +
+ +
+Python package installation fails + +Try using a virtual environment: + +```bash +python3 -m venv ~/paqet-venv +source ~/paqet-venv/bin/activate +pip install scapy aioquic +``` + +Then run GFK from within the venv. +
+ +
+ +--- + +
+

🐧 Linux Client Setup (Click to expand)

+ +## Linux Client - Complete Guide + +### Option A: Paqet + +```bash +# Download paqet +mkdir -p ~/paqet && cd ~/paqet +curl -LO https://github.com/hanselime/paqet/releases/download/v1.0.0-alpha.17/paqet-linux-amd64-v1.0.0-alpha.17.tar.gz +tar -xzf paqet-linux-amd64-v1.0.0-alpha.17.tar.gz +chmod +x paqet_linux_amd64 + +# Create config +cat > config.yaml << 'EOF' +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "eth0" # Your network interface (ip link show) + ipv4: + addr: "YOUR_LOCAL_IP:0" # Your local IP, e.g., 192.168.1.100:0 + router_mac: "YOUR_ROUTER_MAC" # Gateway MAC (ip neigh | grep default) + +server: + addr: "YOUR_SERVER_IP:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "YOUR_SECRET_KEY" +EOF + +# Run (requires root for raw sockets) +sudo ./paqet_linux_amd64 run -c config.yaml +``` + +> **Tip:** Use `paqctl` for automatic configuration - it detects network values for you. + +### Option B: GFK + +```bash +# Install dependencies +sudo apt install python3 python3-pip # Debian/Ubuntu +# or: sudo dnf install python3 python3-pip # Fedora + +pip3 install scapy aioquic + +# Clone and configure +git clone https://git.samnet.dev/SamNet-dev/paqctl.git +cd paqctl/gfk/client + +# Create parameters.py (same as macOS section above) +# Then run: +sudo python3 mainclient.py +``` + +### Configure Browser + +Firefox: Settings → Network Settings → Manual proxy → SOCKS5 `127.0.0.1:1080` or `127.0.0.1:14000` + +Or use system-wide proxy via environment variables: + +```bash +export ALL_PROXY=socks5://127.0.0.1:1080 +``` + +
+ +--- + +
+

📦 Offline/Manual Installation - If GitHub is Blocked (Click to expand)

+ +## Offline/Manual Installation + +Can't download from GitHub? (e.g., behind DPI/firewall in Iran, China, etc.) + +No problem! Paqet is just **one small file** (~8MB). Download it somewhere else and copy it over. + +--- + +### Step 1: Get your server info first + +On your **server** (VPS), run: +```bash +sudo paqctl info +``` + +Write down these 3 things: +``` +Server IP: _______________ (e.g., 185.1.2.3) +Port: _______________ (e.g., 8443) +Key: _______________ (e.g., mySecretKey123) +``` + +--- + +### Step 2: Download paqet binary + +Do this on a machine that CAN access GitHub (your VPS, a friend's computer, VPN, etc.) + +**Go to:** https://github.com/hanselime/paqet/releases + +> **Note:** Check for the latest version. Examples below use v1.0.0-alpha.17 - use newer if available. + +**Click to download the right file for your CLIENT machine:** + +| Your Client OS | Download this file | +|----------------|-------------------| +| Windows | `paqet-windows-amd64-v1.0.0-alpha.17.zip` | +| Linux (most computers) | `paqet-linux-amd64-v1.0.0-alpha.17.tar.gz` | +| Linux (Raspberry Pi 3/4/5, ARM 64-bit) | `paqet-linux-arm64-v1.0.0-alpha.17.tar.gz` | +| Linux (Raspberry Pi 2, ARM 32-bit) | `paqet-linux-arm32-v1.0.0-alpha.17.tar.gz` | +| macOS (Intel) | `paqet-darwin-amd64-v1.0.0-alpha.17.tar.gz` | +| macOS (M1/M2/M3) | `paqet-darwin-arm64-v1.0.0-alpha.17.tar.gz` | + +--- + +### Step 3: Extract the binary + +**On Linux/macOS:** +```bash +tar -xzf paqet-linux-amd64-v1.0.0-alpha.17.tar.gz +mv paqet_linux_amd64 paqet +chmod +x paqet +``` + +**On Windows:** +- Right-click the ZIP file → "Extract All" +- You'll get `paqet.exe` + +--- + +### Step 4: Transfer to your client machine + +Pick ONE method: + +**Method A - SCP (if you downloaded on your VPS):** +```bash +# Run this FROM your VPS +scp paqet user@CLIENT_IP:/home/user/paqet +``` + +**Method B - USB Drive:** +1. Copy `paqet` (or `paqet.exe`) to USB +2. Plug USB into client machine +3. Copy file to a folder (e.g., `C:\paqet\` on Windows or `~/paqet/` on Linux) + +**Method C - SFTP/FileZilla:** +1. Connect to your client machine +2. Upload the `paqet` file + +--- + +### Step 5: Create config file + +On your **client machine**, create a file called `config.yaml` in the same folder as paqet. + +**First, find your network info:** + +| OS | Find Local IP | Find Router MAC | +|----|---------------|-----------------| +| Linux | `ip addr` or `hostname -I` | `ip neigh \| grep default` | +| macOS | `ifconfig en0 \| grep inet` | `arp -a \| grep gateway` | +| Windows | `ipconfig` | `arp -a` (look for your gateway IP) | + +**Copy this and fill in your values:** + +```yaml +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "eth0" # Linux: eth0/wlan0, macOS: en0, Windows: see note below + ipv4: + addr: "YOUR_LOCAL_IP:0" # e.g., 192.168.1.100:0 + router_mac: "YOUR_ROUTER_MAC" # e.g., aa:bb:cc:dd:ee:ff + +server: + addr: "YOUR_SERVER_IP:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "YOUR_SECRET_KEY" +``` + +> **Windows note:** Leave `interface: ""` empty - paqet will auto-detect. Or find your interface name in Network Connections. + +**Example with real values:** +```yaml +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "eth0" + ipv4: + addr: "192.168.1.100:0" + router_mac: "aa:bb:cc:dd:ee:ff" + +server: + addr: "185.1.2.3:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "mySecretKey123" +``` + +--- + +### Step 6: Run paqet + +**Linux/macOS:** +```bash +cd ~/paqet # Go to the folder with paqet +sudo ./paqet run -c config.yaml +``` + +**Windows (must run as Administrator):** +1. Open Command Prompt as Administrator +2. Run: +```cmd +cd C:\paqet +paqet.exe run -c config.yaml +``` + +You should see: +``` +[INFO] Starting paqet client... +[INFO] Connecting to server... +[INFO] SOCKS5 proxy listening on 127.0.0.1:1080 +``` + +--- + +### Step 7: Configure your browser + +**Firefox:** +1. Settings → Network Settings → Settings... +2. Select "Manual proxy configuration" +3. SOCKS Host: `127.0.0.1` Port: `1080` +4. Select "SOCKS v5" +5. Check "Proxy DNS when using SOCKS v5" +6. Click OK + +**Chrome (use system proxy or extension like SwitchyOmega)** + +--- + +### Step 8: Test it! + +1. Go to https://whatismyipaddress.com +2. Your IP should show your **VPS IP**, not your real IP +3. Try accessing blocked sites + +--- + +### Troubleshooting + +**"Connection refused" or timeout:** +- Check server is running: `sudo paqctl status` on VPS +- Check IP/port/key are correct in config.yaml +- Check firewall allows the port on VPS + +**"Permission denied":** +- Linux/macOS: Must run with `sudo` +- Windows: Must run as Administrator + +**To stop paqet:** +- Press `Ctrl+C` in the terminal + +### Notes + +- You don't need `paqctl` script for basic usage - paqet runs standalone +- Server and client versions should match +- For GFK, the process is more complex (needs Python) - use paqet if possible + +
+ +--- + +## Server Management + +After installing on your VPS, use these commands: + +```bash +# Show interactive menu +sudo paqctl menu + +# Quick commands +sudo paqctl status # Check if running +sudo paqctl start # Start the service +sudo paqctl stop # Stop the service +sudo paqctl restart # Restart the service +sudo paqctl info # Show connection info for clients +sudo paqctl logs # View recent logs +``` + +--- + +## Security Notes + +- **Change default keys/auth codes** - Never use example values in production +- **Keep your VPS IP private** - Don't share it publicly +- **Use strong encryption keys** - At least 16 characters for Paqet +- **Keep software updated** - Run `sudo paqctl update` periodically + +--- + +## FAQ + +
+Can I run both Paqet and GFK at the same time? + +**Yes!** They use different ports: +- Paqet: `127.0.0.1:1080` +- GFK: `127.0.0.1:14000` + +This is useful as a backup - if one method gets blocked, switch to the other. +
+ +
+Which VPS provider should I use? + +Any VPS outside your restricted region works. Popular choices: +- DigitalOcean +- Vultr +- Linode +- AWS Lightsail +- Hetzner + +Choose a location close to you for better speed (but outside the firewall). +
+ +
+Is this legal? + +This tool is for legitimate privacy and access needs. Laws vary by country. Use responsibly and check your local regulations. +
+ +
+My connection is slow. How can I improve it? + +1. Choose a VPS closer to your location +2. Try the other method (Paqet vs GFK) +3. Check your VPS isn't overloaded +4. Make sure your local network is stable +
+ +
+The server keeps disconnecting + +1. Check server logs: `sudo paqctl logs` +2. Make sure your VPS has enough resources +3. Check if the port is blocked by your ISP +4. Try switching between Paqet and GFK +
+ +--- + +## Contributing + +Issues and pull requests are welcome at: +https://git.samnet.dev/SamNet-dev/paqctl + +--- + +## License + +AGPL-3.0 License - See [LICENSE](LICENSE) file. + +--- + +## Acknowledgments + +- [paqet](https://github.com/hanselime/paqet) - KCP over raw TCP packets with custom flags (original source) +- [paqetNG](https://github.com/AliRezaBeigy/paqetNG) - Android client for paqet +- [GFW-knocker](https://github.com/GFW-knocker/gfw_resist_tcp_proxy) - Violated TCP technique +- [aioquic](https://github.com/aiortc/aioquic) - QUIC protocol implementation +- [scapy](https://scapy.net/) - Packet manipulation library +- [kcptun](https://github.com/xtaci/kcptun) - KCP protocol inspiration + +--- + +--- + +# نسخه فارسی + +## این چیست؟ + +پاکت‌کنترل یک ابزار مدیریت پروکسی برای دور زدن فایروال است. این ابزار به شما کمک می‌کند تا به سروری خارج از شبکه‌های محدود (مثل پشت فایروال بزرگ) متصل شوید و آزادانه به اینترنت دسترسی داشته باشید. + +شما کامپوننت **سرور** را روی VPS و **کلاینت** را روی ویندوز/مک/لینوکس خود اجرا می‌کنید. + +--- + +## دو روش + +این ابزار از **دو روش مختلف** پشتیبانی می‌کند: + +| | **Paqet** | **GFW-Knocker (GFK)** | +|---|---|---| +| **سختی** | آسان ⭐ | پیشرفته ⭐⭐⭐ | +| **مناسب برای** | اکثر شرایط | سانسور سنگین (GFW) | +| **پروکسی شما** | `127.0.0.1:1080` | `127.0.0.1:14000` | +| **تکنولوژی** | KCP روی raw socket | TCP نقض‌شده + تونل QUIC | +| **نیاز سرور** | فقط paqet | GFK + Xray | + +### کدام را استفاده کنم؟ + +- اگر شبکه شما سانسور سنگین دارد (مثل ایران یا GFW چین): **ابتدا GFK را امتحان کنید** +- در غیر این صورت: **از Paqet استفاده کنید** + +> **نکته:** می‌توانید هر دو را نصب کنید و یک بکاپ داشته باشید! از پورت‌های مختلف استفاده می‌کنند. + +--- + +## نحوه کار + +### Paqet (ساده) + +``` +[Browser] --> [Paqet Client] --KCP/Raw TCP--> [Paqet Server] --SOCKS5--> [Internet] + 127.0.0.1:1080 your.vps.ip +``` + +**نحوه دور زدن فایروال:** +1. از پروتکل KCP روی پکت‌های TCP خام با فلگ‌های سفارشی استفاده می‌کند +2. بسته‌ها را از طریق raw socket ارسال می‌کند که شناسایی آن‌ها سخت است +3. سیستم‌های DPI نمی‌توانند به راحتی آن را شناسایی کنند + +### GFW-Knocker (پیشرفته) + +``` +[Browser] --> [GFK Client] --Violated TCP--> [GFK Server] --> [Xray] --> [Internet] + (VIO+QUIC) (QUIC Tunnel) (SOCKS5) + 127.0.0.1:14000 your.vps.ip +``` + +**نحوه دور زدن فایروال:** +1. **TCP نقض‌شده**: بسته‌های TCP ارسال می‌کند که عمداً "خراب" هستند +2. **تونل QUIC**: درون این بسته‌ها، یک اتصال QUIC داده‌های واقعی را حمل می‌کند +3. **بکند Xray**: روی سرور، Xray سرویس SOCKS5 را ارائه می‌دهد + +--- + +
+اینجا کلیک کنید اگر می‌خواهید GFK را در کنار پنل Xray نصب کنید (3x-ui، Marzban و غیره) — شامل راه‌اندازی bridge سرور به سرور + +> **آموزش ویدیویی (فارسی):** [آموزش نصب GFK سرور به سرور با تنظیم outbound در پنل Xray — توسط متین](https://www.youtube.com/watch?v=BrONeIH8WPM) + +اگر سرور خارج شما از قبل پنل Xray دارد (3x-ui، Marzban و غیره)، paqctl آن را تشخیص می‌دهد و در کنار آن کار می‌کند. پنل شما دست نخورده می‌ماند — paqctl فقط چیزهای لازم را اضافه می‌کند. + +**رفتار paqctl هنگام تشخیص Xray:** + +| سناریو | عملکرد paqctl | +|---|---| +| **Xray نصب نیست** | Xray با پروکسی SOCKS5 به صورت خودکار نصب می‌شود (نیازی به تنظیم نیست) | +| **پنل Xray در حال اجراست** | پنل را نگه می‌دارد، یک اینباند SOCKS5 روی پورت آزاد اضافه می‌کند (مثلاً 10443)، و یک مپینگ اضافی اضافه می‌شود | +| **Xray نصب شده ولی اجرا نمی‌شود** | SOCKS5 خودش را نصب می‌کند (مثل نصب جدید) | + +وقتی پنل تشخیص داده می‌شود، paqctl **دو اتصال** به صورت خودکار می‌دهد: +- **مپینگ پنل** (`14000:443`) — برای ترافیک سرور به سرور (vmess/vless) +- **مپینگ SOCKS5** (`14001:10443`) — برای استفاده مستقیم از ویندوز/مک (بدون نیاز به v2rayN) + +--- + +### روش A: سرور به سرور (پنل ایران به پنل خارج) + +این روش برای وقتی است که روی **هر دو سرور** (ایران + خارج) پنل دارید و می‌خواهید اوتباند پنل ایران از تونل GFK عبور کند. + +**۱. نصب paqctl روی سرور خارج (نقش server):** +```bash +curl -fsSL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash +``` +- نقش **server** را انتخاب کنید +- مپینگ پورت: `14000:443` (که `443` پورت اینباند پنل شماست) +- paqctl تشخیص می‌دهد Xray در حال اجراست و SOCKS5 را در کنار پنل اضافه می‌کند (مثلاً `14001:10443`) + +**۲. نصب paqctl روی سرور ایران (نقش client):** +```bash +curl -fsSL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash +``` +- نقش **client** را انتخاب کنید +- **دقیقاً همان** مپینگ‌هایی که در خروجی سرور نمایش داده شد را استفاده کنید (مثلاً `14000:443,14001:10443`) +- همان کد احراز هویت سرور را استفاده کنید + +**۳. اوتباند پنل ایران را تغییر دهید:** + +در پنل ایران (3x-ui، Marzban و غیره)، اوتباندی که به سرور خارج متصل می‌شود را تغییر دهید: + +**قبل** (اتصال مستقیم — توسط DPI مسدود می‌شود): +```json +{ + "tag": "vmess_out", + "protocol": "vmess", + "settings": { + "vnext": [{ + "address": "IP_SERVER_KHAREJ", + "port": 443, + "users": [{"id": "your-uuid", "security": "auto"}] + }] + } +} +``` + +**بعد** (از طریق تونل GFK): +```json +{ + "tag": "vmess_out", + "protocol": "vmess", + "settings": { + "vnext": [{ + "address": "127.0.0.1", + "port": 14000, + "users": [{"id": "your-uuid", "security": "auto"}] + }] + } +} +``` + +در 3x-ui: به **Xray Configs → Outbounds → Add Outbound** بروید (یا اوتباند موجود را ویرایش کنید): +- **Address**: `127.0.0.1` +- **Port**: `14000` (پورت VIO، نه پورت اصلی سرور) +- **Protocol/ID/encryption**: همان تنظیمات قبلی (از اینباند پنل خارج شما) +- **Security**: None (ترافیک قبلاً درون تونل GFK رمزگذاری شده) + +> **UUID از کجا بیاورم؟** از پنل سرور خارج — به **Inbounds** بروید، اینباندی که می‌خواهید به آن متصل شوید را پیدا کنید و UUID/ID آن را کپی کنید. اگر قبلاً اوتباند کار می‌کرد، فقط address و port را تغییر دهید — بقیه تنظیمات همان می‌ماند. + +**مسیر ترافیک:** +``` +کاربر --> اینباند پنل ایران --> اوتباند پنل ایران (127.0.0.1:14000) + --> GFK client (پورت VIO) --> تونل QUIC روی TCP نقض‌شده + --> GFK server خارج --> 127.0.0.1:443 (اینباند پنل خارج) --> اینترنت +``` + +--- + +### روش B: کلاینت مستقیم (ویندوز/مک به سرور خارج) + +این روش برای وقتی است که **سرور ایران ندارید** — مستقیماً از ویندوز یا مک خود به سرور خارج از طریق GFK متصل می‌شوید. paqctl به صورت خودکار یک پروکسی SOCKS5 اضافه می‌کند تا بتوانید به عنوان پروکسی مرورگر استفاده کنید. + +**۱. نصب paqctl روی سرور خارج** (مثل بالا) + +**۲. روی ویندوز/مک خود** کلاینت GFK را نصب کنید و از مپینگ SOCKS5 استفاده کنید: +- خروجی سرور چیزی شبیه این نشان می‌دهد: `Mappings: 14000:443,14001:10443` +- از `14001` به عنوان پورت پروکسی استفاده کنید — این SOCKS5 مستقیم است (نیازی به پنل/v2rayN نیست) +- پروکسی مرورگر یا سیستم را روی `SOCKS5 127.0.0.1:14001` تنظیم کنید + +**مسیر ترافیک:** +``` +مرورگر (SOCKS5 127.0.0.1:14001) --> GFK client + --> تونل QUIC روی TCP نقض‌شده + --> GFK server خارج --> 127.0.0.1:10443 (پروکسی SOCKS5) --> اینترنت +``` + +--- + +**چند پورت:** اگر پنل شما از چند پورت استفاده می‌کند، همه را مپ کنید: +``` +14000:443,14001:8080,14002:2020 +``` +paqctl به صورت خودکار SOCKS5 را روی پورت آزاد بعدی اضافه و مپ می‌کند. + +> **توجه:** پیام وضعیت "Firewall: VIO port blocked" (که با رنگ سبز نمایش داده می‌شود) **عادی و صحیح** است. این به معنای آن است که فایروال به درستی برای کار raw socket در GFK تنظیم شده است. + +
+ +--- + +## شروع سریع + +### ۱. راه‌اندازی سرور (VPS لینوکس) + +این دستور را روی VPS خود اجرا کنید (نیاز به root دارد): + +```bash +curl -fsSL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash +``` + +سپس منوی تعاملی را باز کنید: + +```bash +sudo paqctl menu +``` + +بعد از راه‌اندازی، اطلاعات اتصال را دریافت کنید: + +```bash +sudo paqctl info +``` + +این دستور **آی‌پی سرور**، **پورت** و **کلید/کد احراز هویت** را نشان می‌دهد. + +--- + +### ۲. راه‌اندازی کلاینت + +
+

🪟 راه‌اندازی کلاینت ویندوز (کلیک کنید)

+ +## راهنمای کامل کلاینت ویندوز + +### پیش‌نیازها + +- ویندوز ۱۰ یا ۱۱ +- دسترسی Administrator +- اطلاعات اتصال سرور (از دستور `paqctl info` روی سرور) + +--- + +## 🚀 روش آسان (پیشنهادی) - استفاده از فایل‌های .bat + +ساده‌ترین روش - فقط دانلود کنید، دوبار کلیک کنید و وصل شوید! + +### مرحله ۱: دانلود + +1. بروید به: https://git.samnet.dev/SamNet-dev/paqctl +2. روی دکمه سبز **"Code"** کلیک کنید → **"Download ZIP"** +3. فایل ZIP را در هر جایی استخراج کنید (مثلاً دسکتاپ) +4. وارد پوشه `windows` شوید + +### مرحله ۲: نصب پروتکل + +دو تا فایل `.bat` می‌بینید: +- `Paqet-Client.bat` - برای پروتکل Paqet (ساده، پیشنهادی) +- `GFK-Client.bat` - برای پروتکل GFW-knocker (پیشرفته) + +روی فایل `.bat` مورد نظر **راست‌کلیک** کنید → **"Run as administrator"** + +اجرای اول Npcap را نصب می‌کند (برای raw socket لازم است). مراحل نصب را دنبال کنید. + +### مرحله ۳: پیکربندی و اتصال + +بعد از نصب، اسکریپت اطلاعات سرور را می‌خواهد: +- **Paqet:** آدرس سرور (مثلاً `1.2.3.4:8443`) و کلید رمزنگاری +- **GFK:** آی‌پی سرور و کد احراز هویت + +مقادیر را از سرور وارد کنید (بعد از نصب سرور نشان داده می‌شود یا با `paqctl info`). + +وقتی تنظیم شد، **Connect** را بزنید و تمام! + +### مرحله ۴: استفاده از پروکسی + +مرورگر را روی پروکسی SOCKS5 تنظیم کنید: +- **Paqet:** `127.0.0.1:1080` +- **GFK:** `127.0.0.1:14000` + +برای قطع اتصال، `Ctrl+C` را در پنجره فشار دهید. + +--- + +## 💻 روش پیشرفته - اسکریپت PowerShell + +برای کنترل بیشتر، از منوی تعاملی PowerShell استفاده کنید. + +### مرحله ۱: باز کردن PowerShell با دسترسی Administrator + +1. کلید `Win + S` را فشار دهید، تایپ کنید `PowerShell` +2. روی "Windows PowerShell" راست‌کلیک → **"Run as administrator"** +3. روی "Yes" در پنجره UAC کلیک کنید + +### مرحله ۲: اجرای اسکریپت + +**گزینه A: یک خطی (خودکار دانلود و اجرا می‌کند)** +```powershell +irm https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/windows/paqet-client.ps1 | iex +``` + +**گزینه B: اول دانلود، بعد اجرا** +```powershell +git clone https://git.samnet.dev/SamNet-dev/paqctl.git +cd paqctl\windows +.\paqet-client.ps1 +``` + +### مرحله ۳: استفاده از منو + +منوی تعاملی امکان این کارها را می‌دهد: +1. نصب paqet یا GFK +2. پیکربندی اتصال +3. شروع/توقف کلاینت +4. بررسی وضعیت + +> **نکته:** اگر خطای "Running scripts is disabled" دیدید، این را یک بار اجرا کنید: +> ```powershell +> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +> ``` + +--- + +### مرحله ۵: اجرای کلاینت + +**روش ۱: دوبار کلیک (آسان‌تر)** +- روی فایل `Paqet-Client.bat` دوبار کلیک کنید +- به صورت خودکار با دسترسی Administrator اجرا می‌شود + +**روش ۲: از PowerShell** +```powershell +.\paqet-client.ps1 +``` + +یک منوی تعاملی خواهید دید: + +``` +=============================================== + PAQET/GFK CLIENT MANAGER +=============================================== + + 1. Install paqet (ساده، SOCKS5 همه‌کاره) + 2. Install GFW-knocker (پیشرفته، برای DPI سنگین) + 3. Configure connection + 4. Start client + 5. Stop client + 6. Show status + 0. Exit + + Select option: +``` + +--- + +### مرحله ۶: نصب بکند انتخابی + +> **نکته:** برای تجربه روان‌تر، ابتدا [Npcap](https://npcap.com/#download) را جداگانه دانلود و نصب کنید. + +#### برای Paqet (توصیه‌شده): + +1. کلید `1` را بزنید و Enter +2. اسکریپت موارد زیر را انجام می‌دهد: + - دانلود و نصب **Npcap** + - دانلود **باینری paqet** +3. وقتی نصب‌کننده Npcap باز شد: + - روی "I Agree" کلیک کنید + - روی "Install" کلیک کنید + - روی "Finish" کلیک کنید + +#### برای GFK (اگر Paqet مسدود است): + +1. کلید `2` را بزنید و Enter +2. اسکریپت موارد زیر را انجام می‌دهد: + - نصب **Npcap** + - نصب **Python 3.10+** + - نصب پکیج‌های Python + +--- + +### مرحله ۷: پیکربندی اتصال + +1. کلید `3` را بزنید و Enter +2. اطلاعات سرور خود را وارد کنید: + +**برای Paqet:** +``` +Server address: <آی‌پی:پورت سرور> +Encryption key: <کلید از سرور> +``` + +**برای GFK:** +``` +Server IP: <آی‌پی سرور> +Auth code: <کد احراز هویت از سرور> +``` + +--- + +### مرحله ۸: شروع کلاینت + +1. کلید `4` را بزنید و Enter +2. کلاینت شروع به کار می‌کند +3. این پنجره را باز نگه دارید + +--- + +### مرحله ۹: پیکربندی مرورگر + +**آدرس پروکسی شما:** +- **Paqet:** `127.0.0.1:1080` (SOCKS5) +- **GFK:** `127.0.0.1:14000` (SOCKS5) + +#### Firefox (توصیه‌شده): +1. Firefox را باز کنید +2. بروید به Settings → General → Network Settings → Settings... +3. "Manual proxy configuration" را انتخاب کنید +4. در "SOCKS Host": `127.0.0.1` +5. Port: `1080` (برای Paqet) یا `14000` (برای GFK) +6. "SOCKS v5" را انتخاب کنید +7. "Proxy DNS when using SOCKS v5" را تیک بزنید ← **مهم!** +8. روی OK کلیک کنید + +#### Chrome: +1. افزونه "SwitchyOmega" را نصب کنید +2. یک پروفایل جدید بسازید +3. پروکسی SOCKS5 را تنظیم کنید: `127.0.0.1:1080` یا `127.0.0.1:14000` +4. پروفایل را فعال کنید + +--- + +### مرحله ۱۰: تست اتصال + +1. مرورگر خود را باز کنید +2. بروید به: https://whatismyipaddress.com +3. آی‌پی شما باید **آی‌پی VPS** را نشان دهد +4. سایت‌های مسدود را امتحان کنید + +--- + +### متوقف کردن کلاینت + +- در پنجره PowerShell کلید `Ctrl+C` را بزنید، یا +- اسکریپت را دوباره اجرا کنید و گزینه `5` را انتخاب کنید + +--- + +### رفع مشکلات + +
+خطای "اجرای اسکریپت غیرفعال است" + +ابتدا این دستور را اجرا کنید: +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +``` +
+ +
+"نیاز به دسترسی Administrator" + +باید PowerShell را به عنوان Administrator اجرا کنید. روی PowerShell راست‌کلیک کنید و "Run as administrator" را انتخاب کنید. +
+ +
+نصب Npcap ناموفق است + +1. به صورت دستی از https://npcap.com دانلود کنید +2. نصب‌کننده را به عنوان Administrator اجرا کنید +3. مطمئن شوید "WinPcap API-compatible Mode" تیک خورده است +4. کامپیوتر را ریستارت کنید +
+ +
+اتصال timeout می‌شود + +1. مطمئن شوید سرور در حال اجرا است +2. بررسی کنید که فایروال VPS پورت را اجازه می‌دهد +3. روش دیگر را امتحان کنید +
+ +
+GFK: "MAC گیت‌وی پیدا نشد" + +1. Command Prompt را باز کنید +2. اجرا کنید: `arp -a` +3. آی‌پی گیت‌وی خود را پیدا کنید (معمولاً 192.168.1.1) +4. آدرس MAC کنار آن را کپی کنید +5. وقتی اسکریپت پرسید آن را وارد کنید +
+ +
+ +--- + +
+

🍎 راه‌اندازی کلاینت مک (کلیک کنید)

+ +## راهنمای کامل کلاینت macOS + +macOS نیاز به راه‌اندازی دستی دارد. + +### پیش‌نیازها + +- macOS 10.15 یا جدیدتر +- دسترسی Administrator (برای sudo) +- Homebrew (توصیه‌شده) +- اطلاعات اتصال سرور + +--- + +### گزینه A: Paqet روی macOS + +#### مرحله ۱: نصب Homebrew + +```bash +/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" +``` + +#### مرحله ۲: دانلود باینری Paqet + +```bash +mkdir -p ~/paqet && cd ~/paqet + +# برای Intel Mac: +curl -LO https://github.com/hanselime/paqet/releases/download/v1.0.0-alpha.17/paqet-darwin-amd64-v1.0.0-alpha.17.tar.gz +tar -xzf paqet-darwin-amd64-v1.0.0-alpha.17.tar.gz + +# برای Apple Silicon (M1/M2/M3): +# curl -LO https://github.com/hanselime/paqet/releases/download/v1.0.0-alpha.17/paqet-darwin-arm64-v1.0.0-alpha.17.tar.gz +# tar -xzf paqet-darwin-arm64-v1.0.0-alpha.17.tar.gz + +chmod +x paqet_darwin_amd64 +``` + +#### مرحله ۳: ایجاد فایل پیکربندی + +```bash +cat > ~/paqet/config.yaml << 'EOF' +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "en0" # اینترفیس شبکه (ifconfig برای پیدا کردن) + ipv4: + addr: "YOUR_LOCAL_IP:0" # IP محلی شما، مثلا 192.168.1.100:0 + router_mac: "YOUR_ROUTER_MAC" # MAC روتر (arp -a | grep gateway) + +server: + addr: "YOUR_SERVER_IP:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "YOUR_SECRET_KEY" +EOF +``` + +مقادیر زیر را جایگزین کنید: +- `YOUR_LOCAL_IP`: با `ifconfig en0 | grep inet` پیدا کنید +- `YOUR_ROUTER_MAC`: با `arp -a | grep gateway` پیدا کنید +- `YOUR_SERVER_IP` و `YOUR_SECRET_KEY`: از ادمین سرور بگیرید + +> **نکته:** از `paqctl` برای تنظیم خودکار استفاده کنید - مقادیر شبکه را خودش تشخیص می‌دهد. + +#### مرحله ۴: اجرای Paqet + +```bash +sudo ~/paqet/paqet_darwin_amd64 run -c ~/paqet/config.yaml +# یا برای Apple Silicon: +sudo ~/paqet/paqet_darwin_arm64 run -c ~/paqet/config.yaml +``` + +پروکسی SOCKS5 شما اکنون در `127.0.0.1:1080` است. + +--- + +### گزینه B: GFK روی macOS + +#### مرحله ۱: نصب Python + +```bash +brew install python@3.11 +``` + +#### مرحله ۲: کلون مخزن + +```bash +git clone https://git.samnet.dev/SamNet-dev/paqctl.git +cd paqctl/gfk/client +``` + +#### مرحله ۳: نصب وابستگی‌ها + +```bash +pip3 install scapy aioquic +``` + +#### مرحله ۴: ایجاد parameters.py + +فایل `parameters.py` را با اطلاعات سرور خود بسازید (مشابه بخش انگلیسی بالا). + +#### مرحله ۵: اجرا + +```bash +sudo python3 mainclient.py +``` + +پروکسی در `127.0.0.1:14000` است. + +--- + +### پیکربندی macOS برای استفاده از پروکسی + +1. **System Preferences** → **Network** را باز کنید +2. اتصال خود را انتخاب کنید +3. **Advanced** → **Proxies** را کلیک کنید +4. **SOCKS Proxy** را تیک بزنید +5. Server: `127.0.0.1` +6. Port: `1080` یا `14000` +7. **OK** → **Apply** + +
+ +--- + +
+

🐧 راه‌اندازی کلاینت لینوکس (کلیک کنید)

+ +## راهنمای کامل کلاینت لینوکس + +### گزینه A: Paqet + +```bash +# دانلود paqet +mkdir -p ~/paqet && cd ~/paqet +curl -LO https://github.com/hanselime/paqet/releases/download/v1.0.0-alpha.17/paqet-linux-amd64-v1.0.0-alpha.17.tar.gz +tar -xzf paqet-linux-amd64-v1.0.0-alpha.17.tar.gz +chmod +x paqet_linux_amd64 + +# ایجاد config +cat > config.yaml << 'EOF' +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "eth0" # اینترفیس شبکه (ip link show) + ipv4: + addr: "YOUR_LOCAL_IP:0" # IP محلی شما، مثلا 192.168.1.100:0 + router_mac: "YOUR_ROUTER_MAC" # MAC روتر (ip neigh | grep default) + +server: + addr: "YOUR_SERVER_IP:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "YOUR_SECRET_KEY" +EOF + +# اجرا (نیاز به root) +sudo ./paqet_linux_amd64 run -c config.yaml +``` + +> **نکته:** از `paqctl` برای تنظیم خودکار استفاده کنید - مقادیر شبکه را خودش تشخیص می‌دهد. + +### گزینه B: GFK + +```bash +# نصب وابستگی‌ها +sudo apt install python3 python3-pip # Debian/Ubuntu +pip3 install scapy aioquic + +# کلون و پیکربندی +git clone https://git.samnet.dev/SamNet-dev/paqctl.git +cd paqctl/gfk/client + +# ایجاد parameters.py (مشابه بخش macOS) +# سپس اجرا: +sudo python3 mainclient.py +``` + +### پیکربندی مرورگر + +Firefox: Settings → Network Settings → Manual proxy → SOCKS5 `127.0.0.1:1080` یا `127.0.0.1:14000` + +
+ +--- + +
+

📦 نصب آفلاین/دستی - اگر GitHub مسدود است (کلیک کنید)

+ +## نصب آفلاین/دستی + +نمی‌توانید از GitHub دانلود کنید؟ (مثلاً پشت فایروال در ایران، چین و غیره) + +مشکلی نیست! Paqet فقط **یک فایل کوچک** (~۸ مگابایت) است. از جای دیگر دانلود کنید و کپی کنید. + +--- + +### مرحله ۱: اول اطلاعات سرور را بگیرید + +روی **سرور** (VPS)، این دستور را بزنید: +```bash +sudo paqctl info +``` + +این ۳ چیز را یادداشت کنید: +``` +آی‌پی سرور: _______________ (مثلاً 185.1.2.3) +پورت: _______________ (مثلاً 8443) +کلید: _______________ (مثلاً mySecretKey123) +``` + +--- + +### مرحله ۲: دانلود باینری paqet + +این کار را روی دستگاهی انجام دهید که به GitHub دسترسی دارد (VPS شما، کامپیوتر دوست، VPN و غیره) + +**بروید به:** https://github.com/hanselime/paqet/releases + +> **نکته:** آخرین نسخه را چک کنید. مثال‌های زیر از v1.0.0-alpha.17 استفاده می‌کنند - اگر جدیدتر موجود است آن را بگیرید. + +**فایل مناسب سیستم کلاینت خود را دانلود کنید:** + +| سیستم کلاینت شما | این فایل را دانلود کنید | +|-----------------|----------------------| +| ویندوز | `paqet-windows-amd64-v1.0.0-alpha.17.zip` | +| لینوکس (اکثر کامپیوترها) | `paqet-linux-amd64-v1.0.0-alpha.17.tar.gz` | +| لینوکس (Raspberry Pi 3/4/5, ARM 64-bit) | `paqet-linux-arm64-v1.0.0-alpha.17.tar.gz` | +| لینوکس (Raspberry Pi 2, ARM 32-bit) | `paqet-linux-arm32-v1.0.0-alpha.17.tar.gz` | +| مک (Intel) | `paqet-darwin-amd64-v1.0.0-alpha.17.tar.gz` | +| مک (M1/M2/M3) | `paqet-darwin-arm64-v1.0.0-alpha.17.tar.gz` | + +--- + +### مرحله ۳: استخراج باینری + +**در لینوکس/مک:** +```bash +tar -xzf paqet-linux-amd64-v1.0.0-alpha.17.tar.gz +mv paqet_linux_amd64 paqet +chmod +x paqet +``` + +**در ویندوز:** +- روی فایل ZIP راست‌کلیک کنید ← "Extract All" +- فایل `paqet.exe` را خواهید داشت + +--- + +### مرحله ۴: انتقال به دستگاه کلاینت + +یک روش را انتخاب کنید: + +**روش A - SCP (اگر روی VPS دانلود کردید):** +```bash +# این را روی VPS خود اجرا کنید +scp paqet user@CLIENT_IP:/home/user/paqet +``` + +**روش B - فلش USB:** +1. فایل `paqet` (یا `paqet.exe`) را به USB کپی کنید +2. USB را به دستگاه کلاینت وصل کنید +3. فایل را به یک پوشه کپی کنید (مثلاً `C:\paqet\` در ویندوز یا `~/paqet/` در لینوکس) + +**روش C - SFTP/FileZilla:** +1. به دستگاه کلاینت متصل شوید +2. فایل `paqet` را آپلود کنید + +--- + +### مرحله ۵: ساخت فایل کانفیگ + +روی **دستگاه کلاینت**، یک فایل به نام `config.yaml` در همان پوشه‌ای که paqet است بسازید. + +**اول اطلاعات شبکه خود را پیدا کنید:** + +| سیستم‌عامل | پیدا کردن IP محلی | پیدا کردن MAC روتر | +|-----------|------------------|-------------------| +| لینوکس | `ip addr` یا `hostname -I` | `ip neigh \| grep default` | +| مک | `ifconfig en0 \| grep inet` | `arp -a \| grep gateway` | +| ویندوز | `ipconfig` | `arp -a` (دنبال IP گیت‌وی بگردید) | + +**این را کپی کنید و مقادیر خود را بگذارید:** + +```yaml +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "eth0" # لینوکس: eth0/wlan0، مک: en0، ویندوز: نکته پایین را ببینید + ipv4: + addr: "YOUR_LOCAL_IP:0" # مثلاً 192.168.1.100:0 + router_mac: "YOUR_ROUTER_MAC" # مثلاً aa:bb:cc:dd:ee:ff + +server: + addr: "YOUR_SERVER_IP:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "YOUR_SECRET_KEY" +``` + +> **نکته ویندوز:** مقدار `interface: ""` را خالی بگذارید - paqet خودش تشخیص می‌دهد. یا نام اینترفیس را در Network Connections پیدا کنید. + +**مثال با مقادیر واقعی:** +```yaml +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "eth0" + ipv4: + addr: "192.168.1.100:0" + router_mac: "aa:bb:cc:dd:ee:ff" + +server: + addr: "185.1.2.3:8443" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "mySecretKey123" +``` + +--- + +### مرحله ۶: اجرای paqet + +**لینوکس/مک:** +```bash +cd ~/paqet # به پوشه paqet بروید +sudo ./paqet run -c config.yaml +``` + +**ویندوز (باید به عنوان Administrator اجرا شود):** +1. Command Prompt را به عنوان Administrator باز کنید +2. اجرا کنید: +```cmd +cd C:\paqet +paqet.exe run -c config.yaml +``` + +باید این را ببینید: +``` +[INFO] Starting paqet client... +[INFO] Connecting to server... +[INFO] SOCKS5 proxy listening on 127.0.0.1:1080 +``` + +--- + +### مرحله ۷: پیکربندی مرورگر + +**فایرفاکس:** +1. Settings ← Network Settings ← Settings... +2. "Manual proxy configuration" را انتخاب کنید +3. SOCKS Host: `127.0.0.1` Port: `1080` +4. "SOCKS v5" را انتخاب کنید +5. تیک "Proxy DNS when using SOCKS v5" را بزنید +6. OK کنید + +**کروم (از system proxy یا افزونه SwitchyOmega استفاده کنید)** + +--- + +### مرحله ۸: تست کنید! + +1. بروید به https://whatismyipaddress.com +2. آی‌پی شما باید **آی‌پی VPS** باشد، نه آی‌پی واقعی شما +3. سایت‌های مسدود را امتحان کنید + +--- + +### عیب‌یابی + +**"Connection refused" یا تایم‌اوت:** +- چک کنید سرور اجرا باشد: `sudo paqctl status` روی VPS +- چک کنید IP/پورت/کلید در config.yaml درست باشد +- چک کنید فایروال VPS پورت را اجازه دهد + +**"Permission denied":** +- لینوکس/مک: باید با `sudo` اجرا شود +- ویندوز: باید به عنوان Administrator اجرا شود + +**برای توقف paqet:** +- در ترمینال `Ctrl+C` بزنید + +
+ +--- + +## مدیریت سرور + +بعد از نصب روی VPS: + +```bash +sudo paqctl menu # منوی تعاملی +sudo paqctl status # بررسی وضعیت +sudo paqctl start # شروع سرویس +sudo paqctl stop # توقف سرویس +sudo paqctl restart # ریستارت +sudo paqctl info # اطلاعات اتصال +sudo paqctl logs # مشاهده لاگ‌ها +``` + +--- + +## نکات امنیتی + +- **کلیدها را تغییر دهید** - هرگز از مقادیر نمونه استفاده نکنید +- **آی‌پی VPS را خصوصی نگه دارید** +- **از کلیدهای قوی استفاده کنید** - حداقل ۱۶ کاراکتر +- **به‌روز نگه دارید** - `sudo paqctl update` + +--- + +## سوالات متداول + +
+آیا می‌توانم Paqet و GFK را همزمان اجرا کنم؟ + +**بله!** از پورت‌های مختلف استفاده می‌کنند: +- Paqet: `127.0.0.1:1080` +- GFK: `127.0.0.1:14000` + +اگر یکی مسدود شد، به دیگری سوییچ کنید. +
+ +
+از کدام VPS استفاده کنم؟ + +هر VPS خارج از منطقه محدود: +- DigitalOcean +- Vultr +- Linode +- AWS Lightsail +- Hetzner + +مکانی نزدیک انتخاب کنید (اما خارج از فایروال). +
+ +
+اتصال کند است + +1. VPS نزدیک‌تر انتخاب کنید +2. روش دیگر را امتحان کنید +3. VPS را بررسی کنید +4. شبکه محلی را بررسی کنید +
+ +
+سرور مدام قطع می‌شود + +1. لاگ‌ها را بررسی کنید: `sudo paqctl logs` +2. منابع VPS را بررسی کنید +3. پورت توسط ISP مسدود نشده باشد +4. بین Paqet و GFK سوییچ کنید +
+ +--- + +## مشارکت + +مشکلات و pull request در گیتی: +https://git.samnet.dev/SamNet-dev/paqctl + +--- + +## قدردانی + +- [paqet](https://github.com/hanselime/paqet) - پروکسی مبتنی بر KCP با SOCKS5 داخلی (سورس اصلی) +- [GFW-knocker](https://github.com/GFW-knocker/gfw_resist_tcp_proxy) - تکنیک TCP نقض‌شده +- [aioquic](https://github.com/aiortc/aioquic) - پیاده‌سازی QUIC +- [scapy](https://scapy.net/) - کتابخانه دستکاری بسته +- [kcptun](https://github.com/xtaci/kcptun) - الهام‌بخش پروتکل KCP diff --git a/gfk/client/mainclient.py b/gfk/client/mainclient.py new file mode 100644 index 0000000..80fd3ee --- /dev/null +++ b/gfk/client/mainclient.py @@ -0,0 +1,84 @@ +import subprocess +import os +import time +import sys +import signal +import platform + +IS_WINDOWS = platform.system() == 'Windows' + +scripts = ['quic_client.py', 'vio_client.py'] + + +def kill_existing_script(script_name): + """Kill any existing instance of the script (cross-platform)""" + if IS_WINDOWS: + # On Windows, use taskkill to find and kill python processes + try: + result = subprocess.run( + ['wmic', 'process', 'where', + f"commandline like '%{script_name}%' and name like '%python%'", + 'get', 'processid'], + capture_output=True, text=True, stderr=subprocess.DEVNULL + ) + for line in result.stdout.split('\n'): + line = line.strip() + if line.isdigit(): + subprocess.run(['taskkill', '/F', '/PID', line], + stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL) + except Exception: + pass + else: + subprocess.run(['pkill', '-f', script_name], stderr=subprocess.DEVNULL) + + +def run_script(script_name): + """Start a script, killing any existing instance first""" + kill_existing_script(script_name) + time.sleep(0.5) + + if IS_WINDOWS: + p = subprocess.Popen([sys.executable, script_name], + creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) + else: + p = subprocess.Popen([sys.executable, script_name]) + return p + + +processes = [] + + +def signal_handler(sig, frame): + print('\nShutting down GFK client...') + for p in processes: + try: + p.terminate() + p.wait(timeout=3) + except Exception: + try: + p.kill() + except Exception: + pass + sys.exit(0) + + +if __name__ == "__main__": + signal.signal(signal.SIGINT, signal_handler) + if not IS_WINDOWS: + signal.signal(signal.SIGTERM, signal_handler) + + print("Starting GFK client...") + p1 = run_script(scripts[0]) + time.sleep(1) + p2 = run_script(scripts[1]) + processes.extend([p1, p2]) + + print("GFK running. Press Ctrl+C to stop.\n") + + try: + p1.wait() + p2.wait() + print("All subprocesses have completed.") + except KeyboardInterrupt: + signal_handler(None, None) + diff --git a/gfk/client/quic_client.py b/gfk/client/quic_client.py new file mode 100644 index 0000000..a0d3109 --- /dev/null +++ b/gfk/client/quic_client.py @@ -0,0 +1,380 @@ +import asyncio +import logging +import sys +import time +import multiprocessing +from aioquic.asyncio import QuicConnectionProtocol, connect +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.events import ConnectionTerminated, StreamDataReceived, StreamReset +import parameters + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("QuicClient") + +active_protocols = [] +is_quic_established = False + + +class TunnelClientProtocol(QuicConnectionProtocol): + def __init__(self, *args, **kwargs): + global is_quic_established + is_quic_established = False + + super().__init__(*args, **kwargs) + self.loop = asyncio.get_event_loop() + self.tcp_connections = {} + self.tcp_syn_wait = {} + self.udp_addr_to_stream = {} + self.udp_stream_to_addr = {} + self.udp_stream_to_transport = {} + self.udp_last_activity = {} + active_protocols.append(self) + asyncio.create_task(self.cleanup_stale_udp_connections()) + asyncio.create_task(self.check_start_connectivity()) + + async def check_start_connectivity(self): + global is_quic_established + try: + await asyncio.sleep(7) + if is_quic_established: + logger.info(f"Quic Established!") + else: + logger.info(f"Quic FAILED to connect") + self.connection_lost("quic connectivity") + except SystemExit as e: + logger.info(f"connectivity SystemExit: {e}") + except Exception as e: + logger.info(f"connectivity err: {e}") + + def connection_lost(self, exc): + super().connection_lost(exc) + self.close_all_tcp_connections() + logger.info("QUIC connection lost. exit") + for protocol in active_protocols: + protocol.close_all_tcp_connections() + protocol.close_all_udp_connections() + protocol.close() + protocol = None + if self in active_protocols: + active_protocols.remove(self) + time.sleep(1) + sys.exit() + + def close_all_tcp_connections(self): + logger.info("close all tcp") + for stream_id, (reader, writer) in self.tcp_connections.items(): + logger.info(f"Closing TCP connection for stream {stream_id}...") + try: + writer.close() + except Exception as e: + logger.info(f"Error closing tcp socket: {e}") + for stream_id, (reader, writer) in self.tcp_syn_wait.items(): + logger.info(f"Closing TCP connection for stream {stream_id}...") + try: + writer.close() + except Exception as e: + logger.info(f"Error closing tcp socket: {e}") + self.tcp_connections.clear() + self.tcp_syn_wait.clear() + + def close_all_udp_connections(self): + logger.info("close all udp") + self.udp_addr_to_stream.clear() + self.udp_stream_to_addr.clear() + self.udp_last_activity.clear() + self.udp_stream_to_transport.clear() + + def close_this_stream(self, stream_id): + try: + logger.info(f"FIN to stream={stream_id} sent") + self._quic.send_stream_data(stream_id, b"", end_stream=True) + self.transmit() + except Exception as e: + logger.info(f"Error closing stream at client: {e}") + + try: + if stream_id in self.tcp_connections: + try: + writer = self.tcp_connections[stream_id][1] + writer.close() + del self.tcp_connections[stream_id] + except Exception as e: + logger.info(f"Error closing tcp estblsh at client: {e}") + if stream_id in self.tcp_syn_wait: + try: + writer = self.tcp_syn_wait[stream_id][1] + writer.close() + del self.tcp_syn_wait[stream_id] + except Exception as e: + logger.info(f"Error closing tcp syn at client: {e}") + if stream_id in self.udp_stream_to_addr: + try: + addr = self.udp_stream_to_addr.get(stream_id) + del self.udp_addr_to_stream[addr] + del self.udp_stream_to_addr[stream_id] + del self.udp_last_activity[stream_id] + del self.udp_stream_to_transport[stream_id] + except Exception as e: + logger.info(f"Error closing udp at client: {e}") + except Exception as e: + logger.info(f"Error closing socket at client: {e}") + + async def cleanup_stale_udp_connections(self): + logger.info("UDP cleanup task running!") + check_time = min(parameters.udp_timeout, 60) + while True: + await asyncio.sleep(check_time) + current_time = self.loop.time() + stale_streams = [ + stream_id for stream_id, last_time in self.udp_last_activity.items() + if current_time - last_time > parameters.udp_timeout + ] + for stream_id in stale_streams: + logger.info(f"idle UDP stream={stream_id} timeout reached") + self.close_this_stream(stream_id) + + async def forward_tcp_to_quic(self, stream_id): + logger.info(f"Task TCP to QUIC started") + try: + (reader, writer) = self.tcp_syn_wait[stream_id] + self.tcp_connections[stream_id] = (reader, writer) + del self.tcp_syn_wait[stream_id] + + while True: + data = await reader.read(4096) + if not data: + break + self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False) + self.transmit() + + except Exception as e: + logger.info(f"Error forwarding TCP to QUIC: {e}") + finally: + logger.info(f"Task TCP to QUIC Ended") + self.close_this_stream(stream_id) + + async def handle_tcp_connection(self, reader, writer, target_port): + stream_id = None + try: + stream_id = self._quic.get_next_available_stream_id() + self.tcp_syn_wait[stream_id] = (reader, writer) + + req_data = parameters.quic_auth_code + "connect,tcp," + str(target_port) + ",!###!" + self._quic.send_stream_data(stream_id=stream_id, data=req_data.encode("utf-8"), end_stream=False) + self.transmit() + + except Exception as e: + logger.info(f"Client Error handle tcp connection: {e}") + if stream_id is not None: + self.close_this_stream(stream_id) + + async def forward_udp_to_quic(self, udp_protocol): + logger.info("Task UDP to Quic started") + stream_id = None + try: + while True: + data, addr = await udp_protocol.queue.get() + + stream_id = self.udp_addr_to_stream.get(addr) + if stream_id is not None: + self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False) + self.transmit() + self.udp_last_activity[stream_id] = self.loop.time() + else: + stream_id = self.new_udp_stream(addr, udp_protocol) + if stream_id is not None: + await asyncio.sleep(0.1) + self.udp_last_activity[stream_id] = self.loop.time() + self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False) + self.transmit() + + except Exception as e: + logger.info(f"Error forwarding UDP to QUIC: {e}") + finally: + logger.info(f"Task UDP to QUIC Ended") + if stream_id is not None: + self.close_this_stream(stream_id) + + def new_udp_stream(self, addr, udp_protocol): + logger.info(f"new stream for UDP addr {addr} -> {udp_protocol.target_port}") + try: + stream_id = self._quic.get_next_available_stream_id() + self.udp_addr_to_stream[addr] = stream_id + self.udp_stream_to_addr[stream_id] = addr + self.udp_stream_to_transport[stream_id] = udp_protocol.transport + self.udp_last_activity[stream_id] = self.loop.time() + + req_data = parameters.quic_auth_code + "connect,udp," + str(udp_protocol.target_port) + ",!###!" + self._quic.send_stream_data(stream_id=stream_id, data=req_data.encode("utf-8"), end_stream=False) + self.transmit() + return stream_id + except Exception as e: + logger.info(f"Client Error creating new udp stream: {e}") + return None + + def quic_event_received(self, event): + if isinstance(event, StreamDataReceived): + try: + if event.end_stream: + logger.info(f"Stream={event.stream_id} closed by server.") + self.close_this_stream(event.stream_id) + + elif event.stream_id in self.tcp_connections: + writer = self.tcp_connections[event.stream_id][1] + writer.write(event.data) + asyncio.create_task(writer.drain()) + + elif event.stream_id in self.udp_stream_to_addr: + addr = self.udp_stream_to_addr[event.stream_id] + transport = self.udp_stream_to_transport[event.stream_id] + transport.sendto(event.data, addr) + + elif event.stream_id in self.tcp_syn_wait: + if event.data == (parameters.quic_auth_code + "i am ready,!###!").encode("utf-8"): + asyncio.create_task(self.forward_tcp_to_quic(event.stream_id)) + else: + logger.warning("unknown Data arrived to client") + + except Exception as e: + logger.info(f"Quic event client error: {e}") + + elif isinstance(event, StreamReset): + logger.info(f"Stream {event.stream_id} reset unexpectedly.") + self.close_this_stream(event.stream_id) + + elif isinstance(event, ConnectionTerminated): + logger.info(f"Connection lost: {event.reason_phrase}") + self.connection_lost(event.reason_phrase) + + +async def run_client(): + global is_quic_established + + configuration = QuicConfiguration(is_client=True) + configuration.verify_mode = parameters.quic_verify_cert + configuration.max_data = parameters.quic_max_data + configuration.max_stream_data = parameters.quic_max_stream_data + configuration.idle_timeout = parameters.quic_idle_timeout + configuration.max_datagram_size = parameters.quic_mtu + + try: + logger.warning("Attempting to connect to QUIC server...") + async with connect(parameters.quic_local_ip, + parameters.vio_udp_client_port, + configuration=configuration, + create_protocol=TunnelClientProtocol, + local_port=parameters.quic_client_port) as client: + + async def start_tcp_server(local_port, target_port): + logger.warning(f"client listen tcp:{local_port} -> to server tcp:{target_port}") + server = await asyncio.start_server( + lambda r, w: asyncio.create_task(handle_tcp_client(r, w, target_port)), + '0.0.0.0', local_port + ) + async with server: + await server.serve_forever() + logger.info("tcp server finished") + + async def handle_tcp_client(reader, writer, target_port): + while not active_protocols: + logger.info("Waiting for an active QUIC connection...") + await asyncio.sleep(1) + protocol = active_protocols[-1] + await protocol.handle_tcp_connection(reader, writer, target_port) + + async def start_udp_server(local_port, target_port): + while True: + try: + logger.warning(f"client listen udp:{local_port} -> to server udp:{target_port}") + loop = asyncio.get_event_loop() + transport, udp_protocol = await loop.create_datagram_endpoint( + lambda: UdpProtocol(client, target_port), + local_addr=('0.0.0.0', local_port) + ) + mytask = asyncio.create_task(handle_udp_client(udp_protocol)) + while True: + await asyncio.sleep(0.05) + if udp_protocol.has_error: + mytask.cancel() + await asyncio.sleep(1) + break + + logger.info(f"udp server finished") + except Exception as e: + logger.info(f"start_udp_server ERR: {e}") + + async def handle_udp_client(udp_protocol): + logger.info("creating udp task ....") + while not active_protocols: + logger.info("Waiting for an active QUIC connection...") + await asyncio.sleep(1) + protocol = active_protocols[-1] + await protocol.forward_udp_to_quic(udp_protocol) + + class UdpProtocol: + def __init__(self, client, target_port): + self.transport = None + self.client = client + self.target_port = target_port + self.has_error = False + self.queue = asyncio.Queue() + + def connection_made(self, transport): + logger.info("NEW DGRAM listen created") + logger.info(transport.get_extra_info('socket')) + self.transport = transport + + def datagram_received(self, data, addr): + self.queue.put_nowait((data, addr)) + + def error_received(self, exc): + logger.info(f"UDP error received: {exc}") + self.has_error = True + if self.transport: + self.transport.close() + logger.info("UDP transport closed") + + def connection_lost(self, exc): + logger.info(f"UDP lost. {exc}") + self.has_error = True + if self.transport: + self.transport.close() + logger.info("UDP transport closed") + + is_quic_established = True + + tcp_servers_list = [] + for lport, tport in parameters.tcp_port_mapping.items(): + tcp_servers_list.append(start_tcp_server(lport, tport)) + + udp_servers_list = [] + for lport, tport in parameters.udp_port_mapping.items(): + udp_servers_list.append(start_udp_server(lport, tport)) + + await asyncio.gather( + asyncio.Future(), + *tcp_servers_list, + *udp_servers_list + ) + except SystemExit as e: + logger.info(f"Caught SystemExit: {e}") + except asyncio.CancelledError as e: + logger.info(f"cancelling error: {e}. Retrying...") + except ConnectionError as e: + logger.info(f"Connection error: {e}. Retrying...") + except Exception as e: + logger.info(f"Generic error: {e}. Retrying...") + + +def Quic_client(): + asyncio.run(run_client()) + + +if __name__ == "__main__": + while True: + process = multiprocessing.Process(target=Quic_client) + process.start() + while process.is_alive(): + time.sleep(5) + logger.info("client is dead. restarting ...") + time.sleep(1) diff --git a/gfk/client/vio_client.py b/gfk/client/vio_client.py new file mode 100644 index 0000000..416a972 --- /dev/null +++ b/gfk/client/vio_client.py @@ -0,0 +1,202 @@ +from scapy.all import AsyncSniffer,IP,TCP,Raw,conf,Ether,get_if_hwaddr +import asyncio +import random +import parameters +import logging +import os + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("VioClient") + +vps_ip = parameters.vps_ip +vio_tcp_server_port = parameters.vio_tcp_server_port +vio_tcp_client_port = parameters.vio_tcp_client_port +vio_udp_client_port = parameters.vio_udp_client_port +quic_local_ip = parameters.quic_local_ip +quic_client_port = parameters.quic_client_port +tcp_flags = getattr(parameters, 'tcp_flags', 'AP') + +# Windows-specific: get local IP and gateway MAC for Ethernet frames +my_ip = getattr(parameters, 'my_ip', None) +gateway_mac = getattr(parameters, 'gateway_mac', None) +is_windows = os.name == 'nt' + +try: + local_mac = get_if_hwaddr(conf.iface) +except Exception: + local_mac = None + +tcp_options=[ + ('MSS', 1280), + ('WScale', 8), + ('SAckOK', ''), +] + + +async def async_sniff_realtime(qu1): + logger.info("sniffer started") + try: + def process_packet(packet): + # Check flags using 'in' to handle different flag orderings (AP vs PA) + flags = str(packet[TCP].flags) if packet.haslayer(TCP) else '' + if packet.haslayer(TCP) and packet[IP].src == vps_ip and packet[TCP].sport == vio_tcp_server_port and 'A' in flags and 'P' in flags: + data1 = packet[TCP].load + qu1.put_nowait(data1) + + async def start_sniffer(): + sniffer = AsyncSniffer(prn=process_packet, + filter=f"tcp and src host {vps_ip} and src port {vio_tcp_server_port}", + store=False) + sniffer.start() + return sniffer + + sniffer = await start_sniffer() + return sniffer + except Exception as e: + logger.info(f"sniff Generic error: {e}....") + raise # Re-raise so caller knows sniffer failed + + +async def forward_vio_to_quic(qu1, transport): + logger.info(f"Task vio to Quic started") + addr = (quic_local_ip, quic_client_port) + try: + while True: + data = await qu1.get() + if data == None: + break + transport.sendto(data, addr) + except Exception as e: + logger.info(f"Error forwarding vio to Quic: {e}") + finally: + logger.info(f"Task vio to Quic Ended.") + + +# Build base packet based on OS +if is_windows and gateway_mac and my_ip and local_mac: + logger.info(f"Windows mode: using Ethernet frames (gw_mac={gateway_mac}, my_ip={my_ip})") + basepkt = Ether(dst=gateway_mac, src=local_mac) / IP(src=my_ip, dst=vps_ip) / TCP(sport=vio_tcp_client_port, dport=vio_tcp_server_port, seq=0, flags=tcp_flags, ack=0, options=tcp_options) / Raw(load=b"") + skt = conf.L2socket(iface=conf.iface) +else: + logger.info(f"Linux mode: using L3 socket") + basepkt = IP(dst=vps_ip) / TCP(sport=vio_tcp_client_port, dport=vio_tcp_server_port, seq=0, flags=tcp_flags, ack=0, options=tcp_options) / Raw(load=b"") + skt = conf.L3socket() + + +def send_to_violated_TCP(binary_data): + new_pkt = basepkt.copy() + new_pkt[TCP].seq = random.randint(1024,1048576) + new_pkt[TCP].ack = random.randint(1024,1048576) + new_pkt[TCP].load = binary_data + skt.send(new_pkt) + + +async def forward_quic_to_vio(protocol): + logger.info(f"Task QUIC to vio started") + try: + while True: + data = await protocol.queue.get() + if data == None: + break + send_to_violated_TCP(data) + except Exception as e: + logger.info(f"Error forwarding QUIC to vio: {e}") + finally: + logger.info(f"Task QUIC to vio Ended.") + + +async def start_udp_server(qu1): + while True: + try: + logger.warning(f"listen quic:{vio_udp_client_port} -> violated tcp:{vio_tcp_server_port}") + loop = asyncio.get_event_loop() + transport, udp_protocol = await loop.create_datagram_endpoint( + lambda: UdpProtocol(), + local_addr=('0.0.0.0', vio_udp_client_port) + ) + task1 = asyncio.create_task(forward_quic_to_vio(udp_protocol)) + task2 = asyncio.create_task(forward_vio_to_quic(qu1, transport)) + + while True: + await asyncio.sleep(0.02) + if udp_protocol.has_error: + task1.cancel() + task2.cancel() + await asyncio.sleep(1) + logger.info(f"all task cancelled") + break + + except Exception as e: + logger.info(f"vioclient ERR: {e}") + finally: + transport.close() + await asyncio.sleep(0.5) + transport.abort() + logger.info("aborting transport ...") + await asyncio.sleep(1.5) + logger.info("vio inner finished") + + +class UdpProtocol: + def __init__(self): + self.transport = None + self.has_error = False + self.queue = asyncio.Queue() + + def connection_made(self, transport): + logger.info("NEW DGRAM listen created") + logger.info(transport.get_extra_info('socket')) + self.transport = transport + + def pause_writing(self): + pass + + def resume_writing(self): + pass + + def datagram_received(self, data, addr): + self.queue.put_nowait(data) + + def error_received(self, exc): + logger.info(f"UDP error received: {exc}") + self.has_error = True + if self.transport: + self.transport.close() + logger.info("UDP transport closed") + + def connection_lost(self, exc): + logger.info(f"UDP lost. {exc}") + self.has_error = True + if self.transport: + self.transport.close() + logger.info("UDP transport closed") + + +async def run_vio_client(): + sniffer = None + try: + qu1 = asyncio.Queue() + sniffer = await async_sniff_realtime(qu1) + + await asyncio.gather( + start_udp_server(qu1), + return_exceptions=True + ) + + logger.info("end ?") + except SystemExit as e: + logger.info(f"Caught SystemExit: {e}") + except asyncio.CancelledError as e: + logger.info(f"cancelling error: {e}") + except ConnectionError as e: + logger.info(f"Connection error: {e}") + except Exception as e: + logger.info(f"Generic error: {e}") + finally: + if sniffer is not None: + sniffer.stop() + logger.info("stop sniffer") + + +if __name__ == "__main__": + asyncio.run(run_vio_client()) diff --git a/gfk/server/mainserver.py b/gfk/server/mainserver.py new file mode 100644 index 0000000..3f7d5bb --- /dev/null +++ b/gfk/server/mainserver.py @@ -0,0 +1,36 @@ +import subprocess +import os +import time +import sys +import signal + + +scripts = ['quic_server.py', 'vio_server.py'] + + +def run_script(script_name): + # Use sys.executable to run with the same Python interpreter (venv) + os.system(f"pkill -f {script_name}") + time.sleep(0.5) + p = subprocess.Popen([sys.executable, script_name]) + return p + + +processes = [] +def signal_handler(sig, frame): + print('You pressed Ctrl+C!') + for p in processes: + print("terminated:",p) + p.terminate() + sys.exit(0) + + +if __name__ == "__main__": + p1 = run_script(scripts[0]) + time.sleep(1) + p2 = run_script(scripts[1]) + processes.extend([p1, p2]) # Modify global list, don't shadow it + signal.signal(signal.SIGINT, signal_handler) + p1.wait() + p2.wait() + print("All subprocesses have completed.") diff --git a/gfk/server/quic_server.py b/gfk/server/quic_server.py new file mode 100644 index 0000000..29bd845 --- /dev/null +++ b/gfk/server/quic_server.py @@ -0,0 +1,310 @@ +import asyncio +import logging +import signal +import sys +from aioquic.asyncio import QuicConnectionProtocol, serve +from aioquic.quic.configuration import QuicConfiguration +from aioquic.quic.events import ConnectionTerminated, StreamDataReceived, StreamReset +import parameters + +# Setup basic logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("QuicServer") + +# Global list to track active protocol instances +active_protocols = [] + +class TunnelServerProtocol(QuicConnectionProtocol): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.loop = asyncio.get_event_loop() + self.tcp_connections = {} # Map TCP connections to QUIC streams + self.udp_connections = {} # Map UDP connections to QUIC streams + self.udp_last_activity = {} # Track last activity time for UDP connections + active_protocols.append(self) # Add this protocol instance to the list + try: + asyncio.create_task(self.cleanup_stale_udp_connections()) + except Exception as e: + logger.info(f"Error in cleanup_stale_udp task: {e}") + + def connection_lost(self, exc): + logger.info("Quic channel lost") + if self in active_protocols: + active_protocols.remove(self) + super().connection_lost(exc) + self.close_all_tcp_connections() + self.close_all_udp_connections() + + def close_all_tcp_connections(self): + logger.info("Closing all TCP connections from server...") + for stream_id, (reader, writer) in self.tcp_connections.items(): + logger.info(f"Closing TCP connection for stream {stream_id}...") + writer.close() + self.tcp_connections.clear() + + def close_all_udp_connections(self): + logger.info("Closing all UDP connections from server...") + for stream_id, (transport, _) in self.udp_connections.items(): + logger.info(f"Closing UDP connection for stream {stream_id}...") + transport.close() + self.udp_connections.clear() + self.udp_last_activity.clear() + + + def close_this_stream(self, stream_id): + try: + logger.info(f"FIN to stream={stream_id} sent") + self._quic.send_stream_data(stream_id, b"", end_stream=True) # Send FIN flag + self.transmit() # Send the FIN flag over the network + except Exception as e: + logger.info(f"Error closing stream at server: {e}") + + try: + if stream_id in self.tcp_connections: + writer = self.tcp_connections[stream_id][1] + writer.close() + del self.tcp_connections[stream_id] + if stream_id in self.udp_connections: + transport, _ = self.udp_connections[stream_id] + transport.close() + del self.udp_connections[stream_id] + del self.udp_last_activity[stream_id] + except Exception as e: + logger.info(f"Error closing socket at server: {e}") + + + + + async def cleanup_stale_udp_connections(self): + logger.info("UDP cleanup task running!") + check_time = min(parameters.udp_timeout,60) + while True: + await asyncio.sleep(check_time) # Run cleanup every 60 seconds + current_time = self.loop.time() + stale_streams = [ + stream_id for stream_id, last_time in self.udp_last_activity.items() + if current_time - last_time > parameters.udp_timeout + ] + for stream_id in stale_streams: + logger.info(f"idle UDP stream={stream_id} timeout reached") + self.close_this_stream(stream_id) + + + + async def forward_tcp_to_quic(self, stream_id, reader): + logger.info(f"Task TCP to QUIC started") + try: + while True: + data = await reader.read(4096) # Read data from TCP socket + if not data: + break + # logger.info(f"Forwarding data from TCP to QUIC on stream {stream_id}") + self._quic.send_stream_data(stream_id=stream_id, data=data, end_stream=False) + self.transmit() # Flush + except Exception as e: + logger.info(f"Error forwarding TCP to QUIC: {e}") + finally: + logger.info(f"Task TCP to QUIC Ended") + self.close_this_stream(stream_id) + + + + async def connect_tcp(self, stream_id, target_port): + logger.info(f"Connecting to TCP:{target_port}...") + try: + reader, writer = await asyncio.open_connection(parameters.xray_server_ip_address, target_port) + logger.info(f"TCP connection established for stream {stream_id} to port {target_port}") + + # Start forwarding data from TCP to QUIC + asyncio.create_task(self.forward_tcp_to_quic(stream_id, reader)) + + resp_data = parameters.quic_auth_code + "i am ready,!###!" + self._quic.send_stream_data(stream_id=stream_id, data=resp_data.encode("utf-8"), end_stream=False) + self.transmit() # Flush + + self.tcp_connections[stream_id] = (reader, writer) + except Exception as e: + logger.info(f"Failed to establish TCP:{target_port} connection: {e}") + self.close_this_stream(stream_id) + + + + async def forward_udp_to_quic(self, stream_id, protocol): + logger.info(f"Task UDP to QUIC started") + try: + while True: + data, _ = await protocol.queue.get() # Wait for data from UDP + if(data == None): + break + # logger.info(f"Forwarding data from UDP to QUIC on stream {stream_id}") + self._quic.send_stream_data(stream_id, data) + self.transmit() # Flush + self.udp_last_activity[stream_id] = self.loop.time() + except Exception as e: + logger.info(f"Error forwarding UDP to QUIC: {e}") + finally: + logger.info(f"Task UDP to QUIC Ended") + self.close_this_stream(stream_id) + + + async def connect_udp(self, stream_id, target_port): + class UdpProtocol: + def __init__(self): + self.transport = None + self.queue = asyncio.Queue() + self.stream_id = stream_id + + def connection_made(self, transport): + self.transport = transport + + def datagram_received(self, data, addr): + logger.info(f"put this to queue data={data} addr={addr}") + self.queue.put_nowait((data, addr)) + + def error_received(self, exc): + logger.info(f"UDP error received: {exc}") + self.queue.put_nowait((None, None)) # to cancel task + if(self.transport): + self.transport.close() + logger.info("UDP transport closed") + + def connection_lost(self, exc): + logger.info("UDP connection lost.") + self.queue.put_nowait((None, None)) # to cancel task + if(self.transport): + self.transport.close() + logger.info("UDP transport closed") + + try: + # Create a UDP socket + logger.info(f"Connecting to UDP:{target_port}...") + loop = asyncio.get_event_loop() + transport, protocol = await loop.create_datagram_endpoint( + UdpProtocol, + remote_addr=(parameters.xray_server_ip_address, target_port) + ) + self.udp_connections[stream_id] = (transport, protocol) + self.udp_last_activity[stream_id] = self.loop.time() # Track last activity time + logger.info(f"UDP connection established for stream {stream_id} to port {target_port}") + + asyncio.create_task(self.forward_udp_to_quic(stream_id, protocol)) + except Exception as e: + logger.info(f"Failed to establish UDP connection: {e}") + + + + + def quic_event_received(self, event): + # print("EVENT",event) + if isinstance(event, StreamDataReceived): + try: + # logger.info(f"Server received from QUIC on stream {event.stream_id}") + # logger.info(f"Server TCP IDs -> {self.tcp_connections.keys()}") + # logger.info(f"Server UDP IDs -> {self.udp_connections.keys()}") + + if event.end_stream: + logger.info(f"Stream={event.stream_id} closed by client.") + self.close_this_stream(event.stream_id) + + # Forward data to the corresponding TCP connection + elif event.stream_id in self.tcp_connections: + writer = self.tcp_connections[event.stream_id][1] + writer.write(event.data) # Send data over TCP + try: + asyncio.create_task(writer.drain()) + except ConnectionResetError as e42: + logger.info(f"ERR in writer drain task : {e42}") + except Exception as e43: + logger.info(f"ERR in writer drain task : {e43}") + + # Forward data to the corresponding UDP connection + elif event.stream_id in self.udp_connections: + transport, _ = self.udp_connections[event.stream_id] + transport.sendto(event.data) # Send data over UDP + self.udp_last_activity[event.stream_id] = self.loop.time() # Update last activity time + + else: + socket_type = None + socket_port = 0 + + # Assume req is like => auth+"connect,udp,443,!###!" + new_req = event.data.split(b",!###!", 1) + req_header = "" + try: + req_header = new_req[0].decode("utf-8") + except Exception as e47: + logger.info(f"ERR in req decoding : {e47}") + req_header="" + + logger.info("New req comes -> " + req_header) + + if req_header.startswith(parameters.quic_auth_code + "connect,"): + j = len(parameters.quic_auth_code) + 8 + if req_header[j:j + 3] == "tcp": + socket_type = "tcp" + elif req_header[j:j + 3] == "udp": + socket_type = "udp" + + try: + socket_port = int(req_header[j + 4:]) + except ValueError: + logger.info("Invalid port.") + + if socket_port > 0: + if socket_type == "tcp": + # New stream detected, create a TCP connection + asyncio.create_task(self.connect_tcp(event.stream_id, socket_port)) + elif socket_type == "udp": + # New stream detected, create a UDP connection + asyncio.create_task(self.connect_udp(event.stream_id, socket_port)) + else: + logger.info("Invalid Req: socket type unknown.") + else: + logger.info("Invalid Req: socket port unknown.") + else: + logger.info("Invalid Req header") + + except Exception as e: + logger.info(f"Quic event server error: {e}") + + elif isinstance(event, StreamReset): + # Handle stream reset (client closed the stream) + logger.info(f"Stream {event.stream_id} reset by client.") + self.close_this_stream(event.stream_id) + + elif isinstance(event, ConnectionTerminated): + logger.info(f"Connection lost: {event.reason_phrase}") + self.connection_lost(event.reason_phrase) + + +async def run_server(): + configuration = QuicConfiguration(is_client=False) + configuration.load_cert_chain(parameters.quic_cert_filepath[0], parameters.quic_cert_filepath[1]) + configuration.max_data = parameters.quic_max_data + configuration.max_stream_data = parameters.quic_max_stream_data + configuration.idle_timeout = parameters.quic_idle_timeout + configuration.max_datagram_size = parameters.quic_mtu + + # Start QUIC server + await serve("0.0.0.0", parameters.quic_server_port, configuration=configuration, create_protocol=TunnelServerProtocol) + logger.warning(f"Server listening for QUIC on port {parameters.quic_server_port}") + + # Keep the server running + await asyncio.Future() # Run forever + + +def handle_shutdown(signum, frame): + logger.info("Shutting down server gracefully...") + for protocol in active_protocols: + protocol.close_all_tcp_connections() + protocol.close_all_udp_connections() + protocol.close() + logger.info("Server shutdown complete.") + sys.exit(0) + + +if __name__ == "__main__": + signal.signal(signal.SIGTERM, handle_shutdown) + signal.signal(signal.SIGINT, handle_shutdown) + + asyncio.run(run_server()) diff --git a/gfk/server/vio_server.py b/gfk/server/vio_server.py new file mode 100644 index 0000000..5390c62 --- /dev/null +++ b/gfk/server/vio_server.py @@ -0,0 +1,231 @@ +from scapy.all import AsyncSniffer,IP,TCP,Raw,conf +import asyncio +import random +import parameters +import logging +import time + + + +# Setup basic logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger("VioServer") + + +vps_ip = parameters.vps_ip + +vio_tcp_server_port = parameters.vio_tcp_server_port +vio_udp_server_port = parameters.vio_udp_server_port +quic_local_ip = parameters.quic_local_ip +quic_server_port = parameters.quic_server_port +tcp_flags = getattr(parameters, 'tcp_flags', 'AP') + + + +global client_ip # obtained during sniffing +global client_port # obtained during sniffing + +client_ip = "1.1.1.1" +client_port = 443 + +tcp_options=[ + ('MSS', 1280), # Maximum Segment Size + ('WScale', 8), # Window Scale + ('SAckOK', ''), # Selective ACK Permitted + ] + + + + +async def async_sniff_realtime(qu1): + logger.info("sniffer started") + try: + def process_packet(packet): + # logger.info(f"sniffed before if at {time.time()}") + # Check flags using 'in' to handle different flag orderings (AP vs PA) + flags = str(packet[TCP].flags) if packet.haslayer(TCP) else '' + if packet.haslayer(TCP) and packet[TCP].dport == vio_tcp_server_port and 'A' in flags and 'P' in flags: + data1 = packet[TCP].load + client_ip = packet[IP].src + client_port = packet[TCP].sport + qu1.put_nowait( (data1,client_ip,client_port) ) + # logger.info(f"sniffed on tcp : {client_ip} {client_port} at {time.time()}") + + + async def start_sniffer(): + sniffer = AsyncSniffer(prn=process_packet, + filter=f"tcp and dst host {vps_ip} and dst port {vio_tcp_server_port}", + store=False) + sniffer.start() + return sniffer + + sniffer = await start_sniffer() + return sniffer + except Exception as e: + logger.info(f"sniff Generic error: {e}....") + + + + +async def forward_vio_to_quic(qu1, transport): + global client_ip + global client_port + logger.info(f"Task vio to Quic started") + addr = (quic_local_ip,quic_server_port) + # addr = ("192.168.1.140",quic_server_port) + try: + while True: + # update client_ip, client_port from the queue + data,client_ip,client_port = await qu1.get() + # logger.info(f"data qu1 fetched {data} at {time.time()}") + if(data == None): + break + transport.sendto(data , addr) + # logger.info(f"data sent to udp {data} -> {addr} at {time.time()}") + # qu1.task_done() + except Exception as e: + logger.info(f"Error forwarding vio to Quic: {e}") + finally: + logger.info(f"Task vio to Quic Ended.") + + + +basepkt = IP(src=vps_ip) / TCP(sport=vio_tcp_server_port, seq=1, flags=tcp_flags, ack=0, options=tcp_options) / Raw(load=b"") + +skt = conf.L3socket() + +def send_to_violated_TCP(binary_data,client_ip,client_port): + # logger.info(f"client ip = {client_ip}") + new_pkt = basepkt.copy() + new_pkt[IP].dst = client_ip + new_pkt[TCP].dport = client_port + new_pkt[TCP].seq = random.randint(1024,1048576) + new_pkt[TCP].ack = random.randint(1024,1048576) + new_pkt[TCP].load = binary_data + skt.send(new_pkt) + + + + +async def forward_quic_to_vio(protocol): + logger.info(f"Task QUIC to vio started") + global client_ip + global client_port + try: + while True: + data = await protocol.queue.get() # Wait for data from UDP + if(data == None): + break + send_to_violated_TCP(data,client_ip,client_port) + # logger.info(f"data send to tcp {data} at {time.time()}") + except Exception as e: + logger.info(f"Error forwarding QUIC to vio: {e}") + finally: + logger.info(f"Task QUIC to vio Ended.") + + + + +async def start_udp_server(qu1): + while True: + try: + logger.warning(f"violated tcp:{vio_tcp_server_port} -> quic {quic_local_ip}:{quic_server_port} -> ") + loop = asyncio.get_event_loop() + transport, udp_protocol = await loop.create_datagram_endpoint( + lambda: UdpProtocol(), + local_addr=("0.0.0.0", vio_udp_server_port), + remote_addr=(quic_local_ip, quic_server_port) + # remote_addr=("192.168.1.140", quic_server_port) + ) + + task1 = asyncio.create_task(forward_quic_to_vio(udp_protocol)) + task2 = asyncio.create_task(forward_vio_to_quic(qu1,transport)) + + while True: + await asyncio.sleep(0.02) # this make async loop to switch better between process + if(udp_protocol.has_error): + task1.cancel() + task2.cancel() + await asyncio.sleep(1) + logger.info(f"all task cancelled") + break + + except Exception as e: + logger.info(f"vioServer ERR: {e}") + finally: + transport.close() + await asyncio.sleep(0.5) + transport.abort() + logger.info("aborting transport ...") + await asyncio.sleep(1.5) + logger.info("vio inner finished") + + + + +class UdpProtocol: + def __init__(self): + self.transport = None + self.has_error = False + self.queue = asyncio.Queue() + + def connection_made(self, transport): + logger.info("NEW DGRAM socket created") + logger.info(transport.get_extra_info('socket')) + self.transport = transport + + def pause_writing(self): + pass # UDP doesn't need flow control, but we had to implement + + def resume_writing(self): + pass # UDP doesn't need flow control, but we had to implement + + def datagram_received(self, data, addr): + self.queue.put_nowait(data) + # logger.info(f"data received from udp {data} at {time.time()}") + + def error_received(self, exc): + logger.info(f"UDP error received: {exc}") + self.has_error = True + if(self.transport): + self.transport.close() + logger.info("UDP transport closed") + + def connection_lost(self, exc): + logger.info(f"UDP lost. {exc}") + self.has_error = True + if(self.transport): + self.transport.close() + logger.info("UDP transport closed") + + + + +async def run_vio_server(): + sniffer = None + try: + qu1 = asyncio.Queue() + sniffer = await async_sniff_realtime(qu1) + + await asyncio.gather( + start_udp_server(qu1), + return_exceptions=True + ) + + logger.info("end ?") + except SystemExit as e: + logger.info(f"Caught SystemExit: {e}") + except asyncio.CancelledError as e: + logger.info(f"cancelling error: {e}") + except ConnectionError as e: + logger.info(f"Connection error: {e}") + except Exception as e: + logger.info(f"Generic error: {e}") + finally: + if sniffer is not None: + sniffer.stop() + logger.info("stop sniffer") + + +if __name__ == "__main__": + asyncio.run(run_vio_server()) diff --git a/paqctl.sh b/paqctl.sh new file mode 100644 index 0000000..7187fe5 --- /dev/null +++ b/paqctl.sh @@ -0,0 +1,7626 @@ +#!/bin/bash +# +# ╔═══════════════════════════════════════════════════════════════════╗ +# ║ PAQCTL - Paqet Manager v1.0.0 ║ +# ║ ║ +# ║ One-click setup for Paqet raw-socket proxy ║ +# ║ ║ +# ║ * Installs paqet binary + libpcap ║ +# ║ * Auto-detects network config ║ +# ║ * Configures server or client mode ║ +# ║ * Manages iptables rules ║ +# ║ * Auto-start on boot via systemd/OpenRC/SysVinit ║ +# ║ * Easy management via CLI or interactive menu ║ +# ║ ║ +# ║ Paqet: https://git.samnet.dev/SamNet-dev/paqctl ║ +# ╚═══════════════════════════════════════════════════════════════════╝ +# +# Usage: +# curl -sL https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh | sudo bash +# +# Or: wget paqctl.sh && sudo bash paqctl.sh +# + +set -eo pipefail + +# Require bash +if [ -z "$BASH_VERSION" ]; then + echo "Error: This script requires bash. Please run with: bash $0" + exit 1 +fi + +VERSION="1.0.0" + +# Pinned versions for stability (update these after testing new releases) +PAQET_VERSION_PINNED="v1.0.0-alpha.17" +XRAY_VERSION_PINNED="v26.2.4" +GFK_VERSION_PINNED="v1.0.0" + +PAQET_REPO="hanselime/paqet" +PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest" +INSTALL_DIR="${INSTALL_DIR:-/opt/paqctl}" +BACKUP_DIR="$INSTALL_DIR/backups" +GFK_REPO="SamNet-dev/paqctl" +GFK_BRANCH="main" +GFK_RAW_URL="https://git.samnet.dev/${GFK_REPO}/raw/branch/${GFK_BRANCH}/gfk" +GFK_DIR="$INSTALL_DIR/gfk" +MICROSOCKS_REPO="rofl0r/microsocks" +BACKEND="${BACKEND:-paqet}" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +#═══════════════════════════════════════════════════════════════════════ +# Utility Functions +#═══════════════════════════════════════════════════════════════════════ + +print_header() { + echo -e "${CYAN}" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ PAQCTL - Paqet Manager v${VERSION} ║" + echo "║ Raw-socket encrypted proxy - bypass firewalls ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[✓]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[!]${NC} $1" +} + +log_error() { + echo -e "${RED}[✗]${NC} $1" +} + +check_root() { + if [ "$EUID" -ne 0 ]; then + log_error "This script must be run as root (use sudo)" + exit 1 + fi +} + +detect_os() { + OS="unknown" + OS_VERSION="unknown" + OS_FAMILY="unknown" + HAS_SYSTEMD=false + PKG_MANAGER="unknown" + + if [ -f /etc/os-release ]; then + . /etc/os-release + OS="$ID" + OS_VERSION="${VERSION_ID:-unknown}" + elif [ -f /etc/redhat-release ]; then + OS="rhel" + elif [ -f /etc/debian_version ]; then + OS="debian" + elif [ -f /etc/alpine-release ]; then + OS="alpine" + elif [ -f /etc/arch-release ]; then + OS="arch" + elif [ -f /etc/SuSE-release ] || [ -f /etc/SUSE-brand ]; then + OS="opensuse" + else + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + fi + + case "$OS" in + ubuntu|debian|linuxmint|pop|elementary|zorin|kali|raspbian) + OS_FAMILY="debian" + PKG_MANAGER="apt" + ;; + rhel|centos|fedora|rocky|almalinux|oracle|amazon|amzn) + OS_FAMILY="rhel" + if command -v dnf &>/dev/null; then + PKG_MANAGER="dnf" + else + PKG_MANAGER="yum" + fi + ;; + arch|manjaro|endeavouros|garuda) + OS_FAMILY="arch" + PKG_MANAGER="pacman" + ;; + opensuse|opensuse-leap|opensuse-tumbleweed|sles) + OS_FAMILY="suse" + PKG_MANAGER="zypper" + ;; + alpine) + OS_FAMILY="alpine" + PKG_MANAGER="apk" + ;; + *) + OS_FAMILY="unknown" + PKG_MANAGER="unknown" + ;; + esac + + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + HAS_SYSTEMD=true + fi + + log_info "Detected: $OS ($OS_FAMILY family), Package manager: $PKG_MANAGER" +} + +install_package() { + local package="$1" + log_info "Installing $package..." + + case "$PKG_MANAGER" in + apt) + apt-get update -q 2>/dev/null || log_warn "apt-get update failed, attempting install anyway..." + if apt-get install -y -q "$package"; then + log_success "$package installed successfully" + else + log_error "Failed to install $package" + return 1 + fi + ;; + dnf) + if dnf install -y -q "$package"; then + log_success "$package installed successfully" + else + log_error "Failed to install $package" + return 1 + fi + ;; + yum) + if yum install -y -q "$package"; then + log_success "$package installed successfully" + else + log_error "Failed to install $package" + return 1 + fi + ;; + pacman) + if pacman -Sy --noconfirm "$package"; then + log_success "$package installed successfully" + else + log_error "Failed to install $package" + return 1 + fi + ;; + zypper) + if zypper install -y -n "$package"; then + log_success "$package installed successfully" + else + log_error "Failed to install $package" + return 1 + fi + ;; + apk) + if apk add --no-cache "$package"; then + log_success "$package installed successfully" + else + log_error "Failed to install $package" + return 1 + fi + ;; + *) + log_warn "Unknown package manager. Please install $package manually." + return 1 + ;; + esac +} + +check_dependencies() { + if [ "$OS_FAMILY" = "alpine" ]; then + if ! command -v bash &>/dev/null; then + apk add --no-cache bash 2>/dev/null + fi + fi + + if ! command -v curl &>/dev/null; then + install_package curl || log_warn "Could not install curl automatically" + fi + + if ! command -v tar &>/dev/null; then + install_package tar || log_warn "Could not install tar automatically" + fi + + if ! command -v ip &>/dev/null; then + case "$PKG_MANAGER" in + apt) install_package iproute2 || log_warn "Could not install iproute2" ;; + dnf|yum) install_package iproute || log_warn "Could not install iproute" ;; + pacman) install_package iproute2 || log_warn "Could not install iproute2" ;; + zypper) install_package iproute2 || log_warn "Could not install iproute2" ;; + apk) install_package iproute2 || log_warn "Could not install iproute2" ;; + esac + fi + + if ! command -v tput &>/dev/null; then + case "$PKG_MANAGER" in + apt) install_package ncurses-bin || log_warn "Could not install ncurses-bin" ;; + apk) install_package ncurses || log_warn "Could not install ncurses" ;; + *) install_package ncurses || log_warn "Could not install ncurses" ;; + esac + fi + + # Firewall rules: use firewalld if active, otherwise iptables + if _is_firewalld_active; then + log_info "firewalld detected — will use firewall-cmd for rules" + elif ! command -v iptables &>/dev/null; then + log_info "Installing iptables..." + case "$PKG_MANAGER" in + apt) install_package iptables || log_warn "Could not install iptables - firewall rules may not work" ;; + dnf|yum) install_package iptables || log_warn "Could not install iptables" ;; + pacman) install_package iptables || log_warn "Could not install iptables" ;; + zypper) install_package iptables || log_warn "Could not install iptables" ;; + apk) install_package iptables || log_warn "Could not install iptables" ;; + *) log_warn "Please install iptables manually for firewall rules to work" ;; + esac + fi + + # openssl is required for GFK certificate generation + if ! command -v openssl &>/dev/null; then + install_package openssl || log_warn "Could not install openssl" + fi + + # libpcap is required by paqet + install_libpcap +} + +install_libpcap() { + log_info "Checking for libpcap..." + + # Check if already available + if ldconfig -p 2>/dev/null | grep -q libpcap; then + log_success "libpcap already installed" + return 0 + fi + + case "$PKG_MANAGER" in + apt) install_package libpcap-dev ;; + dnf|yum) install_package libpcap-devel ;; + pacman) install_package libpcap ;; + zypper) install_package libpcap-devel ;; + apk) install_package libpcap-dev ;; + *) log_warn "Please install libpcap manually for your distribution"; return 1 ;; + esac + + # Fedora/RHEL: ensure libpcap.so.1 symlink exists (package may only install versioned .so) + if [ "$PKG_MANAGER" = "dnf" ] || [ "$PKG_MANAGER" = "yum" ]; then + if ! ldconfig -p 2>/dev/null | grep -q 'libpcap\.so\.1 '; then + local _pcap_lib + _pcap_lib=$(find /usr/lib64 /usr/lib /lib64 /lib -name 'libpcap.so.*' -type f 2>/dev/null | head -1) + if [ -n "$_pcap_lib" ]; then + local _libdir + _libdir=$(dirname "$_pcap_lib") + if [ ! -e "${_libdir}/libpcap.so.1" ]; then + log_info "Creating libpcap.so.1 symlink for Fedora/RHEL compatibility" + ln -sf "$_pcap_lib" "${_libdir}/libpcap.so.1" + fi + ldconfig 2>/dev/null || true + fi + fi + fi +} + +detect_arch() { + local arch + arch=$(uname -m) + case "$arch" in + x86_64|amd64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + armv7l|armv7|armhf) echo "arm32" ;; + mips64el|mips64le) echo "mips64le" ;; + mips64) echo "mips64" ;; + mipsel|mipsle) echo "mipsle" ;; + mips) echo "mips" ;; + *) + log_error "Unsupported architecture: $arch" + log_error "Paqet supports amd64, arm64, arm32, and MIPS variants" + exit 1 + ;; + esac +} + +#═══════════════════════════════════════════════════════════════════════ +# Input Validation Functions +#═══════════════════════════════════════════════════════════════════════ + +_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; } + +_validate_ip() { + [[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1 + local IFS='.'; set -- $1 + [ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ] +} + +_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; } + +_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; } + +_validate_version_tag() { + [[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]] +} + +#═══════════════════════════════════════════════════════════════════════ +# Binary Download & Install +#═══════════════════════════════════════════════════════════════════════ + +# Retry helper with exponential backoff for API requests +_curl_with_retry() { + local url="$1" + local max_attempts="${2:-3}" + local attempt=1 + local delay=2 + local response="" + while [ $attempt -le $max_attempts ]; do + response=$(curl -s --max-time 15 "$url" 2>/dev/null) + if [ -n "$response" ]; then + # Check for rate limit response + if echo "$response" | grep -q '"message".*rate limit'; then + log_warn "GitHub API rate limited, waiting ${delay}s (attempt $attempt/$max_attempts)" + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + continue + fi + echo "$response" + return 0 + fi + [ $attempt -lt $max_attempts ] && sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + done + return 1 +} + +get_latest_version() { + local response + response=$(_curl_with_retry "$PAQET_API_URL" 3) + if [ -z "$response" ]; then + log_error "Failed to query GitHub API after retries" + return 1 + fi + local tag + tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"') + if [ -z "$tag" ]; then + log_error "Could not determine latest paqet version" + return 1 + fi + if ! _validate_version_tag "$tag"; then + log_error "Invalid version tag format: $tag" + return 1 + fi + echo "$tag" +} + +download_paqet() { + local version="$1" + local arch + arch=$(detect_arch) + local os_name="linux" + local ext="tar.gz" + local filename="paqet-${os_name}-${arch}-${version}.${ext}" + local url="https://github.com/${PAQET_REPO}/releases/download/${version}/${filename}" + + log_info "Downloading paqet ${version} for ${os_name}/${arch}..." + + if ! mkdir -p "$INSTALL_DIR/bin"; then + log_error "Failed to create directory $INSTALL_DIR/bin" + return 1 + fi + local tmp_file + tmp_file=$(mktemp "/tmp/paqet-download-XXXXXXXX.${ext}") || { log_error "Failed to create temp file"; return 1; } + + # Try curl first, fallback to wget + local download_ok=false + if curl -sL --max-time 180 --retry 3 --retry-delay 5 --fail -o "$tmp_file" "$url" 2>/dev/null; then + download_ok=true + elif command -v wget &>/dev/null; then + log_info "curl failed, trying wget..." + rm -f "$tmp_file" + if wget -q --timeout=180 --tries=3 -O "$tmp_file" "$url" 2>/dev/null; then + download_ok=true + fi + fi + + if [ "$download_ok" != "true" ]; then + log_error "Failed to download: $url" + log_error "Try manual download: wget '$url' and place binary in $INSTALL_DIR/bin/" + rm -f "$tmp_file" + return 1 + fi + + # Validate download + local fsize + fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0) + if [ "$fsize" -lt 1000 ]; then + log_error "Downloaded file is too small ($fsize bytes). Download may have failed." + rm -f "$tmp_file" + return 1 + fi + + # Extract + log_info "Extracting..." + local tmp_extract + tmp_extract=$(mktemp -d "/tmp/paqet-extract-XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then + log_error "Failed to extract archive" + rm -f "$tmp_file" + rm -rf "$tmp_extract" + return 1 + fi + + # Find the binary in extracted files + local binary_name="paqet_${os_name}_${arch}" + local found_binary="" + found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1) + if [ -z "$found_binary" ]; then + # Try alternate name patterns + found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1) + fi + if [ -z "$found_binary" ]; then + found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1) + fi + + if [ -z "$found_binary" ]; then + log_error "Could not find paqet binary in archive" + rm -f "$tmp_file" + rm -rf "$tmp_extract" + return 1 + fi + + # Stop paqet if running to avoid "Text file busy" error + if pgrep -f "$INSTALL_DIR/bin/paqet" &>/dev/null; then + log_info "Stopping paqet to update binary..." + pkill -f "$INSTALL_DIR/bin/paqet" 2>/dev/null || true + sleep 1 + fi + + if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then + log_error "Failed to copy paqet binary to $INSTALL_DIR/bin/" + rm -f "$tmp_file" + rm -rf "$tmp_extract" + return 1 + fi + if ! chmod +x "$INSTALL_DIR/bin/paqet"; then + log_error "Failed to make paqet binary executable" + return 1 + fi + + # Copy example configs if they exist + find "$tmp_extract" -name "*.yaml.example" -exec cp {} "$INSTALL_DIR/" \; 2>/dev/null || true + + rm -f "$tmp_file" + rm -rf "$tmp_extract" + + # Verify binary runs + if "$INSTALL_DIR/bin/paqet" version &>/dev/null; then + log_success "paqet ${version} installed successfully" + else + log_warn "paqet binary installed but version check failed (may need libpcap)" + fi +} + +#═══════════════════════════════════════════════════════════════════════ +# Network Auto-Detection +#═══════════════════════════════════════════════════════════════════════ + +detect_network() { + log_info "Auto-detecting network configuration..." + + # Default interface - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats + # Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0 + # OpenVZ: "default dev venet0 scope link" -> $3 = venet0 + local _route_line + _route_line=$(ip route show default 2>/dev/null | head -1) + if [[ "$_route_line" == *" via "* ]]; then + # Standard format with gateway + DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}') + elif [[ "$_route_line" == *" dev "* ]]; then + # OpenVZ/direct format without gateway + DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}') + fi + + # Validate detected interface exists + if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then + DETECTED_IFACE="" + fi + + if [ -z "$DETECTED_IFACE" ]; then + # Skip loopback, docker, veth, bridge, and other virtual interfaces + # Note: grep -v returns exit 1 if no matches, so we add || true for pipefail + DETECTED_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '{gsub(/ /,"",$2); print $2}' | { grep -vE '^(lo|docker[0-9]|br-|veth|virbr|tun|tap|wg)' || true; } | head -1) + fi + + # Local IP - wrap entire pipeline to prevent pipefail exit + if [ -n "$DETECTED_IFACE" ]; then + # Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP + DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true ) + fi + if [ -z "$DETECTED_IP" ]; then + DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}') + [ -z "$DETECTED_IP" ] && DETECTED_IP=$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}') + fi + + # Gateway IP - only present in standard "via X" format, not in OpenVZ + if [[ "$_route_line" == *" via "* ]]; then + DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}') + else + DETECTED_GATEWAY="" + fi + + # Gateway MAC + DETECTED_GW_MAC="" + if [ -n "$DETECTED_GATEWAY" ]; then + # Try ip neigh first (most reliable on Linux) + DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}') + if [ -z "$DETECTED_GW_MAC" ]; then + # Trigger ARP resolution + ping -c 1 -W 2 "$DETECTED_GATEWAY" &>/dev/null || true + sleep 1 + DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}') + fi + if [ -z "$DETECTED_GW_MAC" ] && command -v arp &>/dev/null; then + # Fallback: parse arp output looking for MAC pattern + # Note: grep returns exit 1 if no matches, so we add || true for pipefail + DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | head -1) + fi + fi + + log_info "Interface: ${DETECTED_IFACE:-unknown}" + log_info "Local IP: ${DETECTED_IP:-unknown}" + log_info "Gateway: ${DETECTED_GATEWAY:-unknown}" + log_info "GW MAC: ${DETECTED_GW_MAC:-unknown}" +} + +#═══════════════════════════════════════════════════════════════════════ +# Configuration Wizard +#═══════════════════════════════════════════════════════════════════════ + +run_config_wizard() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} PAQCTL CONFIGURATION WIZARD${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + # Backend selection + echo -e "${BOLD}Select backend:${NC}" + echo " 1. paqet (Go/KCP, built-in SOCKS5, single binary)" + echo " 2. gfw-knocker (Python/QUIC, port forwarding + SOCKS5)" + echo "" + local backend_choice + read -p " Enter choice [1/2]: " backend_choice < /dev/tty || true + case "$backend_choice" in + 2) BACKEND="gfw-knocker" ;; + *) BACKEND="paqet" ;; + esac + echo "" + log_info "Selected backend: $BACKEND" + echo "" + + # Role selection + echo -e "${BOLD}Select role:${NC}" + echo " 1. Server (accept connections from clients)" + echo " 2. Client (connect to a server, provides SOCKS5 proxy)" + echo "" + local role_choice + read -p " Enter choice [1/2]: " role_choice < /dev/tty || true + case "$role_choice" in + 1) ROLE="server" ;; + 2) ROLE="client" ;; + *) + log_warn "Invalid choice. Defaulting to server." + ROLE="server" + ;; + esac + echo "" + log_info "Selected role: $ROLE" + + if [ "$BACKEND" = "paqet" ]; then + _wizard_paqet + else + _wizard_gfk + fi + + # Save settings + save_settings +} + +_wizard_paqet() { + # Auto-detect network + detect_network + echo "" + + # Confirm/override interface + echo -e "${BOLD}Network interface${NC} [${DETECTED_IFACE:-eth0}]:" + read -p " Interface: " input < /dev/tty || true + IFACE="${input:-$DETECTED_IFACE}" + IFACE="${IFACE:-eth0}" + if ! _validate_iface "$IFACE"; then + log_warn "Invalid interface name. Using eth0." + IFACE="eth0" + fi + + # Confirm/override local IP + echo -e "${BOLD}Local IP${NC} [${DETECTED_IP:-auto}]:" + read -p " IP: " input < /dev/tty || true + LOCAL_IP="${input:-$DETECTED_IP}" + if [ -n "$LOCAL_IP" ] && ! _validate_ip "$LOCAL_IP"; then + log_warn "Invalid IP format. Using detected IP." + LOCAL_IP="$DETECTED_IP" + fi + + # Confirm/override gateway MAC + echo -e "${BOLD}Gateway MAC address${NC} [${DETECTED_GW_MAC:-auto}]:" + read -p " MAC: " input < /dev/tty || true + GW_MAC="${input:-$DETECTED_GW_MAC}" + + if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then + if [ -n "$GW_MAC" ]; then + log_warn "Invalid MAC format detected." + else + log_error "Could not detect gateway MAC address." + fi + log_error "Please enter it manually (format: aa:bb:cc:dd:ee:ff)" + read -p " Gateway MAC: " GW_MAC < /dev/tty || true + if [ -z "$GW_MAC" ] || ! _validate_mac "$GW_MAC"; then + log_error "Valid gateway MAC is required for paqet to function." + exit 1 + fi + fi + + if [ "$ROLE" = "server" ]; then + echo "" + echo -e "${BOLD}Listen port${NC} [8443]:" + read -p " Port: " input < /dev/tty || true + LISTEN_PORT="${input:-8443}" + if ! [[ "$LISTEN_PORT" =~ ^[0-9]+$ ]] || [ "$LISTEN_PORT" -lt 1 ] || [ "$LISTEN_PORT" -gt 65535 ]; then + log_warn "Invalid port. Using default 8443." + LISTEN_PORT=8443 + fi + + echo "" + log_info "Generating encryption key..." + ENCRYPTION_KEY=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true) + if [ -z "$ENCRYPTION_KEY" ]; then + log_warn "Could not auto-generate key. Using openssl fallback..." + ENCRYPTION_KEY=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true) + fi + if [ -z "$ENCRYPTION_KEY" ] || [ "${#ENCRYPTION_KEY}" -lt 16 ]; then + log_error "Failed to generate a valid encryption key" + return 1 + fi + echo "" + echo -e "${GREEN}${BOLD} Encryption Key: ${ENCRYPTION_KEY}${NC}" + echo "" + echo -e "${YELLOW} IMPORTANT: Save this key! Clients need it to connect.${NC}" + echo "" + SOCKS_PORT="" + else + echo "" + echo -e "${BOLD}Remote server address${NC} (IP:PORT):" + read -p " Server: " REMOTE_SERVER < /dev/tty || true + if [ -z "$REMOTE_SERVER" ]; then + log_error "Remote server address is required." + exit 1 + fi + + echo "" + echo -e "${BOLD}Encryption key${NC} (from server setup):" + read -p " Key: " ENCRYPTION_KEY < /dev/tty || true + if [ -z "$ENCRYPTION_KEY" ]; then + log_error "Encryption key is required." + exit 1 + fi + + echo "" + echo -e "${BOLD}SOCKS5 listen port${NC} [1080]:" + read -p " SOCKS port: " input < /dev/tty || true + SOCKS_PORT="${input:-1080}" + if ! [[ "$SOCKS_PORT" =~ ^[0-9]+$ ]] || [ "$SOCKS_PORT" -lt 1 ] || [ "$SOCKS_PORT" -gt 65535 ]; then + log_warn "Invalid port. Using default 1080." + SOCKS_PORT=1080 + fi + LISTEN_PORT="" + fi + + # Generate YAML config + generate_config +} + +_wizard_gfk() { + if [ "$ROLE" = "server" ]; then + # Server IP (this machine's public IP) + detect_network + echo "" + echo -e "${BOLD}This server's public IP${NC} [${DETECTED_IP:-}]:" + read -p " IP: " input < /dev/tty || true + GFK_SERVER_IP="${input:-$DETECTED_IP}" + if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then + log_error "Valid server IP is required." + exit 1 + fi + + # VIO TCP port (must be closed to OS, raw socket handles it) + echo "" + echo -e "${BOLD}VIO TCP port${NC} [45000] (raw socket port, must be blocked by firewall):" + read -p " Port: " input < /dev/tty || true + GFK_VIO_PORT="${input:-45000}" + if ! _validate_port "$GFK_VIO_PORT"; then + log_warn "Invalid port. Using default 45000." + GFK_VIO_PORT=45000 + fi + + # QUIC port + echo "" + echo -e "${BOLD}QUIC tunnel port${NC} [25000]:" + read -p " Port: " input < /dev/tty || true + GFK_QUIC_PORT="${input:-25000}" + if ! _validate_port "$GFK_QUIC_PORT"; then + log_warn "Invalid port. Using default 25000." + GFK_QUIC_PORT=25000 + fi + + # Auth code + echo "" + local auto_auth + auto_auth=$(openssl rand -base64 16 2>/dev/null | tr -d '=+/' | head -c 16) + echo -e "${BOLD}QUIC auth code${NC} [auto-generated]:" + read -p " Auth code: " input < /dev/tty || true + GFK_AUTH_CODE="${input:-$auto_auth}" + echo "" + echo -e "${GREEN}${BOLD} Auth Code: ${GFK_AUTH_CODE}${NC}" + echo "" + echo -e "${YELLOW} IMPORTANT: Save this auth code! Clients need it to connect.${NC}" + echo "" + + # Port mappings + echo -e "${BOLD}TCP port mappings${NC} (local:remote, comma-separated) [14000:443]:" + echo -e " ${DIM}Example: 14000:443,15000:2096,16000:10809${NC}" + read -p " Mappings: " input < /dev/tty || true + GFK_PORT_MAPPINGS="${input:-14000:443}" + MICROSOCKS_PORT="" + + else + # Client: server IP + echo "" + echo -e "${BOLD}Remote server IP${NC} (server's public IP):" + read -p " Server IP: " GFK_SERVER_IP < /dev/tty || true + if [ -z "$GFK_SERVER_IP" ] || ! _validate_ip "$GFK_SERVER_IP"; then + log_error "Valid server IP is required." + exit 1 + fi + + # Server's VIO TCP port (what port the server is listening on) + echo "" + echo -e "${BOLD}Server's VIO TCP port${NC} [45000] (must match server config):" + read -p " Port: " input < /dev/tty || true + GFK_VIO_PORT="${input:-45000}" + if ! _validate_port "$GFK_VIO_PORT"; then + log_warn "Invalid port. Using default 45000." + GFK_VIO_PORT=45000 + fi + + # Local VIO client port (client's local binding) + echo "" + echo -e "${BOLD}Local VIO client port${NC} [40000]:" + read -p " Port: " input < /dev/tty || true + GFK_VIO_CLIENT_PORT="${input:-40000}" + if ! _validate_port "$GFK_VIO_CLIENT_PORT"; then + log_warn "Invalid port. Using default 40000." + GFK_VIO_CLIENT_PORT=40000 + fi + + # Server's QUIC port + echo "" + echo -e "${BOLD}Server's QUIC port${NC} [25000] (must match server config):" + read -p " Port: " input < /dev/tty || true + GFK_QUIC_PORT="${input:-25000}" + if ! _validate_port "$GFK_QUIC_PORT"; then + log_warn "Invalid port. Using default 25000." + GFK_QUIC_PORT=25000 + fi + + # Local QUIC client port + echo "" + echo -e "${BOLD}Local QUIC client port${NC} [20000]:" + read -p " Port: " input < /dev/tty || true + GFK_QUIC_CLIENT_PORT="${input:-20000}" + if ! _validate_port "$GFK_QUIC_CLIENT_PORT"; then + log_warn "Invalid port. Using default 20000." + GFK_QUIC_CLIENT_PORT=20000 + fi + + # Auth code (from server) + echo "" + echo -e "${BOLD}QUIC auth code${NC} (from server setup):" + read -p " Auth code: " GFK_AUTH_CODE < /dev/tty || true + if [ -z "$GFK_AUTH_CODE" ]; then + log_error "Auth code is required." + exit 1 + fi + + # Port mappings (must match server) + echo "" + echo -e "${BOLD}TCP port mappings${NC} (must match server) [14000:443]:" + read -p " Mappings: " input < /dev/tty || true + GFK_PORT_MAPPINGS="${input:-14000:443}" + fi + + # Generate GFK config + generate_gfk_config +} + +generate_config() { + log_info "Generating paqet configuration..." + + # Validate required fields + if [ -z "$IFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GW_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then + log_error "Missing required configuration fields (interface, ip, gateway_mac, or secret)" + return 1 + fi + if [ "$ROLE" = "server" ]; then + if [ -z "$LISTEN_PORT" ]; then log_error "Missing listen port"; return 1; fi + else + if [ -z "$REMOTE_SERVER" ] || [ -z "$SOCKS_PORT" ]; then + log_error "Missing server address or SOCKS port" + return 1 + fi + local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}" + if ! _validate_ip "$_rs_ip" || ! _validate_port "$_rs_port"; then + log_error "Server address must be valid IP:PORT (e.g. 1.2.3.4:8443)" + return 1 + fi + fi + + # Escape YAML special characters to prevent injection + _escape_yaml() { + local s="$1" + # If value contains special chars, quote it + if [[ "$s" =~ [:\#\[\]{}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then + s="${s//\\/\\\\}" # Escape backslashes + s="${s//\"/\\\"}" # Escape double quotes + printf '"%s"' "$s" + else + printf '%s' "$s" + fi + } + + # Ensure install directory exists + mkdir -p "$INSTALL_DIR" || { log_error "Failed to create $INSTALL_DIR"; return 1; } + + local tmp_conf + tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + # Set permissions on temp file before writing (fixes race condition) + chmod 600 "$tmp_conf" 2>/dev/null + + ( + umask 077 + local _y_iface _y_ip _y_mac _y_key _y_server _y_port + _y_iface=$(_escape_yaml "$IFACE") + _y_ip=$(_escape_yaml "$LOCAL_IP") + _y_mac=$(_escape_yaml "$GW_MAC") + _y_key=$(_escape_yaml "$ENCRYPTION_KEY") + # Build TCP flags YAML array (default: ["PA"]) + local _tcp_local_flags _tcp_remote_flags + _tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/') + _tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/') + + if [ "$ROLE" = "server" ]; then + cat > "$tmp_conf" << EOF +role: "server" + +log: + level: "info" + +listen: + addr: ":${LISTEN_PORT}" + +network: + interface: "${_y_iface}" + ipv4: + addr: "${_y_ip}:${LISTEN_PORT}" + router_mac: "${_y_mac}" + tcp: + local_flag: ${_tcp_local_flags} + remote_flag: ${_tcp_remote_flags} + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_y_key}" +EOF + else + local _rs_ip="${REMOTE_SERVER%:*}" _rs_port="${REMOTE_SERVER##*:}" + _y_server=$(_escape_yaml "$REMOTE_SERVER") + cat > "$tmp_conf" << EOF +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:${SOCKS_PORT}" + +network: + interface: "${_y_iface}" + ipv4: + addr: "${_y_ip}:0" + router_mac: "${_y_mac}" + tcp: + local_flag: ${_tcp_local_flags} + remote_flag: ${_tcp_remote_flags} + +server: + addr: "${_y_server}" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_y_key}" +EOF + fi + ) + if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then + log_error "Failed to save configuration file" + rm -f "$tmp_conf" + return 1 + fi + # Ensure final permissions (mv preserves source permissions on most systems) + chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null + log_success "Configuration saved to $INSTALL_DIR/config.yaml" +} + +save_settings() { + # Preserve existing Telegram settings if present + local _tg_token="" _tg_chat="" _tg_interval=6 _tg_enabled=false + local _tg_alerts=true _tg_daily=true _tg_weekly=true _tg_label="" _tg_start_hour=0 + if [ -f "$INSTALL_DIR/settings.conf" ]; then + # Safe settings loading without eval + while IFS='=' read -r key value; do + [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue + # Remove surrounding quotes and sanitize value + value="${value#\"}"; value="${value%\"}" + # Validate value doesn't contain dangerous characters + if [[ "$value" =~ [\`\$\(] ]]; then + continue # Skip potentially dangerous values + fi + case "$key" in + TELEGRAM_BOT_TOKEN) _tg_token="$value" ;; + TELEGRAM_CHAT_ID) _tg_chat="$value" ;; + TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && _tg_interval="$value" ;; + TELEGRAM_ENABLED) _tg_enabled="$value" ;; + TELEGRAM_ALERTS_ENABLED) _tg_alerts="$value" ;; + TELEGRAM_DAILY_SUMMARY) _tg_daily="$value" ;; + TELEGRAM_WEEKLY_SUMMARY) _tg_weekly="$value" ;; + TELEGRAM_SERVER_LABEL) _tg_label="$value" ;; + TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && _tg_start_hour="$value" ;; + esac + done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf") + fi + + # Sanitize sensitive values - remove shell metacharacters and control chars + _sanitize_value() { + printf '%s' "$1" | tr -d '"$`\\'\''(){}[]<>|;&!\n\r\t' + } + local _safe_key; _safe_key=$(_sanitize_value "${ENCRYPTION_KEY:-}") + local _safe_auth; _safe_auth=$(_sanitize_value "${GFK_AUTH_CODE:-}") + local _tmp + _tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + ( + umask 077 + cat > "$_tmp" << EOF +BACKEND="${BACKEND:-paqet}" +ROLE="${ROLE}" +PAQET_VERSION="${PAQET_VERSION:-unknown}" +PAQCTL_VERSION="${VERSION}" +LISTEN_PORT="${LISTEN_PORT:-}" +SOCKS_PORT="${SOCKS_PORT:-}" +INTERFACE="${IFACE:-}" +LOCAL_IP="${LOCAL_IP:-}" +GATEWAY_MAC="${GW_MAC:-}" +ENCRYPTION_KEY="${_safe_key}" +PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}" +PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}" +REMOTE_SERVER="${REMOTE_SERVER:-}" +GFK_VIO_PORT="${GFK_VIO_PORT:-}" +GFK_VIO_CLIENT_PORT="${GFK_VIO_CLIENT_PORT:-}" +GFK_QUIC_PORT="${GFK_QUIC_PORT:-}" +GFK_QUIC_CLIENT_PORT="${GFK_QUIC_CLIENT_PORT:-}" +GFK_AUTH_CODE="${_safe_auth}" +GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}" +GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}" +GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}" +XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}" +MICROSOCKS_PORT="${MICROSOCKS_PORT:-}" +GFK_SERVER_IP="${GFK_SERVER_IP:-}" +GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}" +TELEGRAM_BOT_TOKEN="${_tg_token}" +TELEGRAM_CHAT_ID="${_tg_chat}" +TELEGRAM_INTERVAL=${_tg_interval} +TELEGRAM_ENABLED=${_tg_enabled} +TELEGRAM_ALERTS_ENABLED=${_tg_alerts} +TELEGRAM_DAILY_SUMMARY=${_tg_daily} +TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly} +TELEGRAM_SERVER_LABEL="${_tg_label}" +TELEGRAM_START_HOUR=${_tg_start_hour} +EOF + ) + if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then + log_error "Failed to save settings" + rm -f "$_tmp" + return 1 + fi + chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null + log_success "Settings saved" +} + +#═══════════════════════════════════════════════════════════════════════ +# Firewall Management +#═══════════════════════════════════════════════════════════════════════ + +_is_firewalld_active() { + command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running +} + +apply_iptables_rules() { + local port="$1" + if [ -z "$port" ]; then + log_error "No port specified for iptables rules" + return 1 + fi + + log_info "Applying firewall rules for port $port..." + + # firewalld path (Fedora/RHEL) + if _is_firewalld_active; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + { log_error "Failed to add PREROUTING NOTRACK rule via firewalld"; return 1; } + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + { log_error "Failed to add OUTPUT NOTRACK rule via firewalld"; return 1; } + firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + { log_error "Failed to add RST DROP rule via firewalld"; return 1; } + log_success "IPv4 firewalld rules applied" + # IPv6 + firewall-cmd --direct --add-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + persist_iptables_rules + return 0 + fi + + # iptables path (Debian/Ubuntu/Arch/etc.) + modprobe iptable_raw 2>/dev/null || true + modprobe iptable_mangle 2>/dev/null || true + + if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then + log_warn "ufw is active — ensure port $port/tcp is allowed: sudo ufw allow $port/tcp" + fi + + local TAG="paqctl" + + if iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \ + iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null && \ + iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null; then + log_info "iptables rules already in place" + else + iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || { + log_error "Failed to add PREROUTING NOTRACK rule" + return 1 + } + iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || { + log_error "Failed to add OUTPUT NOTRACK rule" + return 1 + } + iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \ + iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || { + log_error "Failed to add RST DROP rule" + return 1 + } + log_success "IPv4 iptables rules applied" + fi + + if command -v ip6tables &>/dev/null; then + local _ipv6_ok=true + ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false + ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || _ipv6_ok=false + ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \ + ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || _ipv6_ok=false + if [ "$_ipv6_ok" = true ]; then + log_success "IPv6 iptables rules applied" + else + log_warn "Some IPv6 iptables rules failed (IPv6 may not be available)" + fi + fi + + # Persist rules + persist_iptables_rules +} + +remove_iptables_rules() { + local port="$1" + if [ -z "$port" ]; then return 0; fi + + log_info "Removing firewall rules for port $port..." + + # firewalld path + if _is_firewalld_active; then + firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 filter INPUT 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 filter OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + # IPv6 + firewall-cmd --direct --remove-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 filter INPUT 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 filter OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + log_success "firewalld rules removed" + return 0 + fi + + # iptables path + local TAG="paqctl" + iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true + iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true + iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true + iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true + if command -v ip6tables &>/dev/null; then + ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true + ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true + ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true + ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true + fi + log_success "iptables rules removed" +} + +persist_iptables_rules() { + if _is_firewalld_active; then + firewall-cmd --runtime-to-permanent 2>/dev/null || true + return 0 + fi + if command -v netfilter-persistent &>/dev/null; then + netfilter-persistent save 2>/dev/null || true + elif command -v iptables-save &>/dev/null; then + if [ -d /etc/iptables ]; then + iptables-save > /etc/iptables/rules.v4 2>/dev/null || true + command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true + elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then + mkdir -p /etc/iptables + iptables-save > /etc/iptables/rules.v4 2>/dev/null || true + command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true + elif [ -d /etc/sysconfig ]; then + iptables-save > /etc/sysconfig/iptables 2>/dev/null || true + command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true + fi + fi +} + +check_iptables_rules() { + local port="$1" + if [ -z "$port" ]; then return 1; fi + + local ok=true + + if _is_firewalld_active; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || ok=false + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || ok=false + firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || ok=false + else + local TAG="paqctl" + iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false + iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || ok=false + iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || ok=false + fi + + if [ "$ok" = true ]; then + return 0 + else + return 1 + fi +} + +#═══════════════════════════════════════════════════════════════════════ +# GFW-knocker Backend Functions +#═══════════════════════════════════════════════════════════════════════ + +install_python_deps() { + log_info "Installing Python dependencies for GFW-knocker..." + if ! command -v python3 &>/dev/null; then + install_package python3 + fi + # Ensure python3 version >= 3.10 + local pyver + pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "0.0") + local pymajor pyminor + pymajor=$(echo "$pyver" | cut -d. -f1) + pyminor=$(echo "$pyver" | cut -d. -f2) + if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then + log_error "Python 3.10+ required, found $pyver" + return 1 + fi + + # Install venv support (varies by distro) + # - Debian/Ubuntu: needs python3-venv or python3.X-venv package + # - Fedora/RHEL/Arch/openSUSE: venv included with python3, just need pip + # - Alpine: needs py3-pip + case "$PKG_MANAGER" in + apt) + # Debian/Ubuntu needs python3-venv package (version-specific) + local pyver_full + pyver_full=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null) + if [ -n "$pyver_full" ]; then + install_package "python${pyver_full}-venv" || install_package "python3-venv" + else + install_package "python3-venv" + fi + ;; + dnf) + # Fedora/RHEL 8+: venv is included with python3, just ensure pip + install_package "python3-pip" || true + ;; + yum) + # Older RHEL/CentOS 7 + install_package "python3-pip" || true + ;; + pacman) + # Arch Linux: venv included with python, pip is separate + install_package "python-pip" || true + ;; + zypper) + # openSUSE: venv included with python3 + install_package "python3-pip" || true + ;; + apk) + # Alpine + install_package "py3-pip" || true + ;; + *) + # Try generic python3-venv, ignore if fails (venv may be built-in) + install_package "python3-venv" 2>/dev/null || true + ;; + esac + + # Create virtual environment + local VENV_DIR="$INSTALL_DIR/venv" + # Check if venv exists AND is complete (has pip) + if [ ! -x "$VENV_DIR/bin/pip" ]; then + # Remove broken/incomplete venv if exists + [ -d "$VENV_DIR" ] && rm -rf "$VENV_DIR" + log_info "Creating Python virtual environment..." + python3 -m venv "$VENV_DIR" || { + log_error "Failed to create virtual environment (is python3-venv installed?)" + return 1 + } + fi + + # Verify pip exists after venv creation + if [ ! -x "$VENV_DIR/bin/pip" ]; then + log_error "venv created but pip missing (install python3-venv package)" + return 1 + fi + + # Install packages in venv + log_info "Installing scapy and aioquic in venv..." + "$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null || true + "$VENV_DIR/bin/pip" install --quiet scapy aioquic 2>/dev/null || { + # Try with --break-system-packages as fallback (shouldn't be needed in venv) + "$VENV_DIR/bin/pip" install scapy aioquic || { + log_error "Failed to install Python packages (scapy, aioquic)" + return 1 + } + } + + # Verify + if "$VENV_DIR/bin/python" -c "import scapy; import aioquic" 2>/dev/null; then + log_success "Python dependencies installed (scapy, aioquic)" + else + log_error "Python package verification failed" + return 1 + fi +} + +install_microsocks() { + log_info "Installing microsocks for SOCKS5 proxy..." + if [ -x "$INSTALL_DIR/bin/microsocks" ]; then + log_success "microsocks already installed" + return 0 + fi + # Build dependencies + command -v gcc &>/dev/null || install_package gcc + command -v make &>/dev/null || install_package make + local tmp_dir + tmp_dir=$(mktemp -d) + if ! curl -sL "https://github.com/${MICROSOCKS_REPO}/archive/refs/heads/master.tar.gz" -o "$tmp_dir/microsocks.tar.gz"; then + log_error "Failed to download microsocks" + rm -rf "$tmp_dir" + return 1 + fi + tar -xzf "$tmp_dir/microsocks.tar.gz" -C "$tmp_dir" 2>/dev/null || { + log_error "Failed to extract microsocks" + rm -rf "$tmp_dir" + return 1 + } + local src_dir + src_dir=$(find "$tmp_dir" -maxdepth 1 -type d -name "microsocks*" | head -1) + if [ -z "$src_dir" ]; then + log_error "microsocks source directory not found" + rm -rf "$tmp_dir" + return 1 + fi + if ! make -C "$src_dir" -j"$(nproc 2>/dev/null || echo 1)" 2>/dev/null; then + log_error "Failed to compile microsocks" + rm -rf "$tmp_dir" + return 1 + fi + mkdir -p "$INSTALL_DIR/bin" + cp "$src_dir/microsocks" "$INSTALL_DIR/bin/microsocks" + chmod 755 "$INSTALL_DIR/bin/microsocks" + rm -rf "$tmp_dir" + log_success "microsocks installed" +} + +#─────────────────────────────────────────────────────────────────────── +# Xray Installation (for GFK server - provides SOCKS5 on port 443) +#─────────────────────────────────────────────────────────────────────── + +XRAY_CONFIG_DIR="/usr/local/etc/xray" +XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json" + +check_xray_installed() { + command -v xray &>/dev/null && return 0 + [ -x /usr/local/bin/xray ] && return 0 + [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && return 0 + return 1 +} + +install_xray() { + if check_xray_installed; then + log_info "Xray is already installed" + return 0 + fi + + log_info "Installing Xray ${XRAY_VERSION_PINNED}..." + + # Use official Xray install script with pinned version for stability + local tmp_script + tmp_script=$(mktemp) + if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then + log_error "Failed to download Xray installer" + rm -f "$tmp_script" + return 1 + fi + + # Install specific version (not latest) for stability + if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then + log_error "Failed to install Xray" + rm -f "$tmp_script" + return 1 + fi + rm -f "$tmp_script" + + log_success "Xray ${XRAY_VERSION_PINNED} installed" +} + +configure_xray_socks() { + local listen_port="${1:-443}" + + log_info "Configuring Xray SOCKS5 proxy on port $listen_port..." + + mkdir -p "$XRAY_CONFIG_DIR" + + # Create simple SOCKS5 inbound config + cat > "$XRAY_CONFIG_FILE" << EOF +{ + "log": { + "loglevel": "warning" + }, + "inbounds": [ + { + "tag": "socks-in", + "port": ${listen_port}, + "listen": "127.0.0.1", + "protocol": "socks", + "settings": { + "auth": "noauth", + "udp": true + }, + "sniffing": { + "enabled": true, + "destOverride": ["http", "tls"] + } + } + ], + "outbounds": [ + { + "tag": "direct", + "protocol": "freedom", + "settings": {} + } + ] +} +EOF + chmod 644 "$XRAY_CONFIG_FILE" # Xray service runs as 'nobody', needs read access + log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)" +} + +# Check if running xray is paqctl's own standalone install (not a real panel) +# Returns 0 if standalone (all inbounds are socks on 127.0.0.1), 1 if panel +_is_paqctl_standalone_xray() { + [ -f "$XRAY_CONFIG_FILE" ] || return 1 + command -v python3 &>/dev/null || return 1 + python3 -c " +import json, sys +try: + with open(sys.argv[1]) as f: + cfg = json.load(f) + inbounds = cfg.get('inbounds', []) + if not inbounds: + sys.exit(1) + for i in inbounds: + if i.get('protocol') != 'socks' or i.get('listen', '0.0.0.0') != '127.0.0.1': + sys.exit(1) + sys.exit(0) +except: + sys.exit(1) +" "$XRAY_CONFIG_FILE" 2>/dev/null +} + +# Add a SOCKS5 inbound to an existing xray config (panel) without touching other inbounds +_add_xray_gfk_socks() { + local port="$1" + python3 -c " +import json, sys +port = int(sys.argv[1]) +config_path = sys.argv[2] +try: + with open(config_path, 'r') as f: + cfg = json.load(f) +except: + cfg = {'inbounds': [], 'outbounds': [{'tag': 'direct', 'protocol': 'freedom', 'settings': {}}]} +cfg.setdefault('inbounds', []) +cfg['inbounds'] = [i for i in cfg['inbounds'] if i.get('tag') != 'gfk-socks'] +cfg['inbounds'].append({ + 'tag': 'gfk-socks', + 'port': port, + 'listen': '127.0.0.1', + 'protocol': 'socks', + 'settings': {'auth': 'noauth', 'udp': True}, + 'sniffing': {'enabled': True, 'destOverride': ['http', 'tls']} +}) +if not any(o.get('protocol') == 'freedom' for o in cfg.get('outbounds', [])): + cfg.setdefault('outbounds', []).append({'tag': 'direct', 'protocol': 'freedom', 'settings': {}}) +with open(config_path, 'w') as f: + json.dump(cfg, f, indent=2) +" "$port" "$XRAY_CONFIG_FILE" 2>/dev/null + if [ $? -ne 0 ]; then + log_error "Failed to add SOCKS5 inbound to existing Xray config" + return 1 + fi + log_success "Added GFK SOCKS5 inbound on 127.0.0.1:$port" +} + +start_xray() { + log_info "Starting Xray service..." + + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + # Stop first, reload daemon, then start - with retry + systemctl stop xray 2>/dev/null || true + sleep 1 + systemctl daemon-reload 2>/dev/null || true + systemctl enable xray 2>/dev/null || true + + # Try up to 3 times + local attempt + for attempt in 1 2 3; do + systemctl start xray 2>/dev/null + sleep 2 + if systemctl is-active --quiet xray; then + log_success "Xray started" + return 0 + fi + [ "$attempt" -lt 3 ] && sleep 1 + done + log_error "Failed to start Xray after 3 attempts" + return 1 + else + # Direct start for non-systemd + local _xray_bin="" + [ -x /usr/local/bin/xray ] && _xray_bin="/usr/local/bin/xray" + [ -z "$_xray_bin" ] && [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && _xray_bin="/usr/local/x-ui/bin/xray-linux-amd64" + if [ -n "$_xray_bin" ]; then + pkill -x xray 2>/dev/null || true + sleep 1 + nohup "$_xray_bin" run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 & + sleep 2 + if pgrep -f "xray" &>/dev/null; then + log_success "Xray started" + return 0 + fi + fi + log_error "Failed to start Xray" + return 1 + fi +} + +stop_xray() { + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl stop xray 2>/dev/null || true + else + pkill -x xray 2>/dev/null || true + fi +} + +setup_xray_for_gfk() { + local target_port + target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1) + + if pgrep -x xray &>/dev/null || pgrep -f "xray-linux" &>/dev/null; then + # Check if this is paqctl's own standalone Xray (not a real panel) + if _is_paqctl_standalone_xray; then + log_info "Existing Xray is paqctl's standalone install — reconfiguring..." + stop_xray + sleep 1 + # Fall through to standalone install path below + else + XRAY_PANEL_DETECTED=true + log_info "Existing Xray detected — adding SOCKS5 alongside panel..." + + # Clean up any leftover standalone GFK xray from prior installs + pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true + rm -f "${XRAY_CONFIG_DIR}/gfk-socks.json" 2>/dev/null + + # Check all existing target ports from mappings + local mapping pairs + IFS=',' read -ra pairs <<< "${GFK_PORT_MAPPINGS:-14000:443}" + for mapping in "${pairs[@]}"; do + local vio_port="${mapping%%:*}" + local tp="${mapping##*:}" + if ss -tln 2>/dev/null | grep -q ":${tp} "; then + log_success "Port $tp is listening — GFK will forward VIO port $vio_port to this port" + else + log_warn "Port $tp is NOT listening — make sure your panel inbound is on port $tp" + fi + done + + # Find free port for SOCKS5 (starting at 10443) + local socks_port=10443 + while ss -tln 2>/dev/null | grep -q ":${socks_port} "; do + socks_port=$((socks_port + 1)) + if [ "$socks_port" -gt 65000 ]; then + log_warn "Could not find free port for SOCKS5 — panel-only mode" + echo "" + local first_vio + first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1) + log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}" + return 0 + fi + done + + # Add SOCKS5 inbound to existing xray config + _add_xray_gfk_socks "$socks_port" || { + log_warn "Could not add SOCKS5 to panel config — panel-only mode" + return 0 + } + + # Restart xray to load new config + systemctl restart xray 2>/dev/null || pkill -SIGHUP xray 2>/dev/null || true + sleep 2 + + # Find next VIO port (highest existing + 1) and append SOCKS5 mapping + local max_vio=0 + for mapping in "${pairs[@]}"; do + local v="${mapping%%:*}" + [ "$v" -gt "$max_vio" ] && max_vio="$v" + done + local socks_vio=$((max_vio + 1)) + GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS},${socks_vio}:${socks_port}" + GFK_SOCKS_PORT="$socks_port" + GFK_SOCKS_VIO_PORT="$socks_vio" + + log_success "SOCKS5 proxy added on port $socks_port (VIO port $socks_vio)" + echo "" + log_info "Port mappings updated: ${GFK_PORT_MAPPINGS}" + log_warn "Use these SAME mappings on the client side" + echo "" + local first_vio + first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1) + log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}" + log_warn "For direct SOCKS5: use 127.0.0.1:${socks_vio} as your proxy on client" + return 0 + fi + fi + + install_xray || return 1 + configure_xray_socks "$target_port" || return 1 + start_xray || return 1 +} + +download_gfk() { + log_info "Downloading GFW-knocker scripts..." + if ! mkdir -p "$GFK_DIR"; then + log_error "Failed to create $GFK_DIR" + return 1 + fi + # Note: parameters.py is generated by generate_gfk_config(), don't download it + # Download server scripts from gfk/server/ + local server_files="mainserver.py quic_server.py vio_server.py" + local f + for f in $server_files; do + if ! curl -sL "$GFK_RAW_URL/server/$f" -o "$GFK_DIR/$f"; then + log_error "Failed to download $f" + return 1 + fi + done + # Download client scripts from gfk/client/ + local client_files="mainclient.py quic_client.py vio_client.py" + for f in $client_files; do + if ! curl -sL "$GFK_RAW_URL/client/$f" -o "$GFK_DIR/$f"; then + log_error "Failed to download $f" + return 1 + fi + done + chmod 600 "$GFK_DIR"/*.py + # Patch mainserver.py to use venv python for subprocesses + if [ -f "$GFK_DIR/mainserver.py" ]; then + sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py" + fi + log_success "GFW-knocker scripts downloaded to $GFK_DIR" +} + +generate_gfk_certs() { + if [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ]; then + log_info "GFW-knocker certificates already exist" + return 0 + fi + if ! command -v openssl &>/dev/null; then + log_info "Installing openssl..." + install_package openssl || { log_error "Failed to install openssl"; return 1; } + fi + log_info "Generating QUIC TLS certificates..." + if ! openssl req -x509 -newkey rsa:2048 -keyout "$GFK_DIR/key.pem" \ + -out "$GFK_DIR/cert.pem" -days 3650 -nodes -subj "/CN=gfk" 2>/dev/null; then + log_error "Failed to generate certificates" + return 1 + fi + chmod 600 "$GFK_DIR/key.pem" "$GFK_DIR/cert.pem" + log_success "QUIC certificates generated" +} + +generate_gfk_config() { + log_info "Generating GFW-knocker configuration..." + # Ensure GFK directory exists + mkdir -p "$GFK_DIR" || { log_error "Failed to create $GFK_DIR"; return 1; } + local _tmp + _tmp=$(mktemp "$GFK_DIR/parameters.py.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + + # Determine port values based on role - validate they are numeric + local vio_tcp_server_port="${GFK_VIO_PORT:-45000}" + local vio_tcp_client_port="${GFK_VIO_CLIENT_PORT:-40000}" + local vio_udp_server_port="${GFK_VIO_UDP_SERVER:-35000}" + local vio_udp_client_port="${GFK_VIO_UDP_CLIENT:-30000}" + local quic_server_port="${GFK_QUIC_PORT:-25000}" + local quic_client_port="${GFK_QUIC_CLIENT_PORT:-20000}" + + # Validate all ports are numeric + for _p in "$vio_tcp_server_port" "$vio_tcp_client_port" "$vio_udp_server_port" \ + "$vio_udp_client_port" "$quic_server_port" "$quic_client_port"; do + if ! [[ "$_p" =~ ^[0-9]+$ ]]; then + log_error "Invalid port number: $_p" + rm -f "$_tmp" + return 1 + fi + done + + # Escape Python string - prevents code injection + _escape_py_string() { + local s="$1" + s="${s//\\/\\\\}" # Escape backslashes first + s="${s//\"/\\\"}" # Escape double quotes + s="${s//\'/\\\'}" # Escape single quotes + s="${s//$'\n'/\\n}" # Escape newlines + s="${s//$'\r'/\\r}" # Escape carriage returns + printf '%s' "$s" + } + + # Validate and escape server IP + local safe_server_ip + safe_server_ip=$(_escape_py_string "${GFK_SERVER_IP:-}") + if ! _validate_ip "${GFK_SERVER_IP:-}"; then + log_error "Invalid server IP: ${GFK_SERVER_IP:-}" + rm -f "$_tmp" + return 1 + fi + + # Validate and escape auth code + local safe_auth_code + safe_auth_code=$(_escape_py_string "${GFK_AUTH_CODE:-}") + + # Build port mapping dict string with validation + local tcp_mapping="${GFK_PORT_MAPPINGS:-14000:443}" + local mapping_str="{" + local first=true + local pair + for pair in $(echo "$tcp_mapping" | tr ',' ' '); do + local lport rport + lport=$(echo "$pair" | cut -d: -f1) + rport=$(echo "$pair" | cut -d: -f2) + # Validate both ports are numeric + if ! [[ "$lport" =~ ^[0-9]+$ ]] || ! [[ "$rport" =~ ^[0-9]+$ ]]; then + log_error "Invalid port mapping: $pair (must be numeric:numeric)" + rm -f "$_tmp" + return 1 + fi + if [ "$first" = true ]; then + mapping_str="${mapping_str}${lport}: ${rport}" + first=false + else + mapping_str="${mapping_str}, ${lport}: ${rport}" + fi + done + mapping_str="${mapping_str}}" + + # Escape GFK_DIR for Python string + local safe_gfk_dir + safe_gfk_dir=$(_escape_py_string "${GFK_DIR}") + + ( + umask 077 + cat > "$_tmp" << PYEOF +# GFW-knocker parameters - auto-generated by paqctl +# Do not edit manually + +vps_ip = "${safe_server_ip}" +xray_server_ip_address = "127.0.0.1" + +tcp_port_mapping = ${mapping_str} +udp_port_mapping = {} + +vio_tcp_server_port = ${vio_tcp_server_port} +vio_tcp_client_port = ${vio_tcp_client_port} +vio_udp_server_port = ${vio_udp_server_port} +vio_udp_client_port = ${vio_udp_client_port} + +quic_server_port = ${quic_server_port} +quic_client_port = ${quic_client_port} +quic_local_ip = "127.0.0.1" + +quic_idle_timeout = 86400 +udp_timeout = 300 +quic_mtu = 1420 +quic_verify_cert = False +quic_max_data = 1073741824 +quic_max_stream_data = 1073741824 + +quic_auth_code = "${safe_auth_code}" + +quic_cert_filepath = ("${safe_gfk_dir}/cert.pem", "${safe_gfk_dir}/key.pem") + +tcp_flags = "${GFK_TCP_FLAGS:-AP}" +PYEOF + ) + if ! mv "$_tmp" "$GFK_DIR/parameters.py"; then + log_error "Failed to save GFW-knocker configuration" + rm -f "$_tmp" + return 1 + fi + chmod 600 "$GFK_DIR/parameters.py" + log_success "GFW-knocker configuration saved" +} + +create_gfk_client_wrapper() { + log_info "Creating GFW-knocker client wrapper..." + local wrapper="$INSTALL_DIR/bin/gfk-client.sh" + mkdir -p "$INSTALL_DIR/bin" + cat > "$wrapper" << 'WRAPEOF' +#!/bin/bash +set -e +GFK_DIR="REPLACE_ME_GFK_DIR" +INSTALL_DIR="REPLACE_ME_INSTALL_DIR" + +cd "$GFK_DIR" +"$INSTALL_DIR/venv/bin/python" mainclient.py & +PID1=$! +trap "kill $PID1 2>/dev/null; wait" EXIT INT TERM +wait +WRAPEOF + sed "s#REPLACE_ME_GFK_DIR#${GFK_DIR}#g; s#REPLACE_ME_INSTALL_DIR#${INSTALL_DIR}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper" + chmod 755 "$wrapper" + log_success "Client wrapper created at $wrapper" +} + +#═══════════════════════════════════════════════════════════════════════ +# Service Management +#═══════════════════════════════════════════════════════════════════════ + +setup_service() { + log_info "Setting up auto-start on boot..." + + # Check which backends are installed + local paqet_installed=false gfk_installed=false + [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true + fi + + # If both backends are installed, create a combined service + local _both_installed=false + [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ] && _both_installed=true + + # Compute ExecStart based on backend + local _exec_start _working_dir _svc_desc _svc_type="simple" + if [ "$_both_installed" = true ]; then + _svc_desc="Paqet Combined Proxy Service (Paqet + GFK)" + _working_dir="${INSTALL_DIR}" + _svc_type="forking" + # Create a wrapper script that starts both backends + cat > "${INSTALL_DIR}/bin/start-both.sh" << BOTH_SCRIPT +#!/bin/bash +INSTALL_DIR="/opt/paqctl" +GFK_DIR="\${INSTALL_DIR}/gfk" +ROLE="${ROLE}" + +# Source config for ports +[ -f "\${INSTALL_DIR}/settings.conf" ] && . "\${INSTALL_DIR}/settings.conf" + +# Detect firewall backend +_use_firewalld=false +if command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running; then + _use_firewalld=true +fi + +# Apply firewall rules (server + client) +if [ "\$ROLE" = "server" ]; then + port="\${LISTEN_PORT:-8443}" + vio_port="\${GFK_VIO_PORT:-45000}" + TAG="paqctl" + if [ "\$_use_firewalld" = true ]; then + # Paqet rules via firewalld + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "\$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + # GFK rules via firewalld + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + # IPv6 GFK + firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "\$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --runtime-to-permanent 2>/dev/null || true + else + # Paqet rules via iptables + modprobe iptable_raw 2>/dev/null || true + modprobe iptable_mangle 2>/dev/null || true + iptables -t raw -C PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\ + iptables -t raw -A PREROUTING -p tcp --dport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null + iptables -t raw -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\ + iptables -t raw -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null + iptables -t mangle -C OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \\ + iptables -t mangle -A OUTPUT -p tcp --sport "\$port" -m comment --comment "\$TAG" --tcp-flags RST RST -j DROP 2>/dev/null + # GFK rules via iptables + modprobe iptable_raw 2>/dev/null || true + iptables -t raw -C PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\ + iptables -t raw -A PREROUTING -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null + iptables -t raw -C OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\ + iptables -t raw -A OUTPUT -p tcp --sport "\$vio_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null + iptables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + iptables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null + iptables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + iptables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null + if command -v ip6tables &>/dev/null; then + ip6tables -C INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + ip6tables -A INPUT -p tcp --dport "\$vio_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || true + ip6tables -C OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + ip6tables -A OUTPUT -p tcp --sport "\$vio_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || true + fi + fi +else + # GFK client firewall rules + vio_client_port="\${GFK_VIO_CLIENT_PORT:-40000}" + TAG="paqctl" + if [ "\$_use_firewalld" = true ]; then + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "\$vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "\$vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --runtime-to-permanent 2>/dev/null || true + else + modprobe iptable_raw 2>/dev/null || true + iptables -t raw -C PREROUTING -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\ + iptables -t raw -A PREROUTING -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null + iptables -t raw -C OUTPUT -p tcp --sport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null || \\ + iptables -t raw -A OUTPUT -p tcp --sport "\$vio_client_port" -m comment --comment "\$TAG" -j NOTRACK 2>/dev/null + iptables -C INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + iptables -A INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null + iptables -C OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + iptables -A OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null + if command -v ip6tables &>/dev/null; then + ip6tables -C INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + ip6tables -A INPUT -p tcp --dport "\$vio_client_port" -m comment --comment "\$TAG" -j DROP 2>/dev/null || true + ip6tables -C OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || \\ + ip6tables -A OUTPUT -p tcp --sport "\$vio_client_port" --tcp-flags RST RST -m comment --comment "\$TAG" -j DROP 2>/dev/null || true + fi + fi +fi + +# Start paqet backend +(umask 077; touch /var/log/paqet-backend.log) +nohup "\${INSTALL_DIR}/bin/paqet" run -c "\${INSTALL_DIR}/config.yaml" > /var/log/paqet-backend.log 2>&1 & +echo \$! > /run/paqet-backend.pid + +# Start GFK backend +(umask 077; touch /var/log/gfk-backend.log) +if [ "\$ROLE" = "server" ]; then + # Start Xray if available + if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ] || [ -x /usr/local/x-ui/bin/xray-linux-amd64 ]; then + if ! pgrep -f "xray run" &>/dev/null; then + systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null & + sleep 2 + fi + fi + cd "\$GFK_DIR" + nohup "\${INSTALL_DIR}/venv/bin/python" "\${GFK_DIR}/mainserver.py" > /var/log/gfk-backend.log 2>&1 & +else + if [ -x "\${INSTALL_DIR}/bin/gfk-client.sh" ]; then + nohup "\${INSTALL_DIR}/bin/gfk-client.sh" > /var/log/gfk-backend.log 2>&1 & + else + cd "\$GFK_DIR" + nohup "\${INSTALL_DIR}/venv/bin/python" "\${GFK_DIR}/mainclient.py" > /var/log/gfk-backend.log 2>&1 & + fi +fi +echo \$! > /run/gfk-backend.pid + +sleep 1 +exit 0 +BOTH_SCRIPT + chmod +x "${INSTALL_DIR}/bin/start-both.sh" + _exec_start="${INSTALL_DIR}/bin/start-both.sh" + elif [ "$BACKEND" = "gfw-knocker" ]; then + _svc_desc="GFW-knocker Proxy Service" + _working_dir="${GFK_DIR}" + if [ "$ROLE" = "server" ]; then + _exec_start="${INSTALL_DIR}/venv/bin/python ${GFK_DIR}/mainserver.py" + else + _exec_start="${INSTALL_DIR}/bin/gfk-client.sh" + fi + else + _svc_desc="Paqet Proxy Service" + _working_dir="${INSTALL_DIR}" + _exec_start="${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" + fi + + if [ "$HAS_SYSTEMD" = "true" ]; then + if [ "$_both_installed" = true ]; then + # Combined service for both backends + cat > /etc/systemd/system/paqctl.service << EOF +[Unit] +Description=${_svc_desc} +After=network-online.target +Wants=network-online.target + +[Service] +Type=${_svc_type} +WorkingDirectory=${_working_dir} +ExecStart=${_exec_start} +ExecStop=/usr/local/bin/paqctl stop +ExecStopPost=/usr/local/bin/paqctl _remove-firewall +RemainAfterExit=yes +KillMode=mixed +KillSignal=SIGTERM +TimeoutStopSec=30 +LimitNOFILE=65535 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=paqctl + +[Install] +WantedBy=multi-user.target +EOF + else + # Single backend service + cat > /etc/systemd/system/paqctl.service << EOF +[Unit] +Description=${_svc_desc} +After=network-online.target +Wants=network-online.target + +[Service] +Type=${_svc_type} +WorkingDirectory=${_working_dir} +ExecStartPre=/usr/local/bin/paqctl _apply-firewall +ExecStart=${_exec_start} +ExecStopPost=/usr/local/bin/paqctl _remove-firewall +Restart=on-failure +RestartSec=5 +KillMode=mixed +KillSignal=SIGTERM +TimeoutStopSec=30 +LimitNOFILE=65535 +StandardOutput=journal +StandardError=journal +SyslogIdentifier=paqctl + +[Install] +WantedBy=multi-user.target +EOF + fi + + systemctl daemon-reload 2>/dev/null || true + systemctl enable paqctl.service 2>/dev/null || true + log_success "Systemd service created and enabled" + + elif command -v rc-update &>/dev/null; then + local _openrc_run + _openrc_run=$(command -v openrc-run 2>/dev/null || echo "/sbin/openrc-run") + cat > /etc/init.d/paqctl << EOF +#!${_openrc_run} + +name="paqctl" +description="${_svc_desc}" +command="$(echo "${_exec_start}" | awk '{print $1}')" +command_args="$(echo "${_exec_start}" | cut -d' ' -f2-)" +if [ "\$command_args" = "\$command" ]; then command_args=""; fi +command_background=true +pidfile="/run/\${RC_SVCNAME}.pid" + +depend() { + need net + after firewall +} + +start_pre() { + /usr/local/bin/paqctl _apply-firewall +} + +stop_post() { + /usr/local/bin/paqctl _remove-firewall +} +EOF + if ! chmod +x /etc/init.d/paqctl; then + log_error "Failed to make init script executable" + return 1 + fi + rc-update add paqctl default 2>/dev/null || true + log_success "OpenRC service created and enabled" + + elif [ -d /etc/init.d ]; then + cat > /etc/init.d/paqctl << SYSV +#!/bin/bash +### BEGIN INIT INFO +# Provides: paqctl +# Required-Start: \$remote_fs \$network \$syslog +# Required-Stop: \$remote_fs \$network \$syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: ${_svc_desc} +### END INIT INFO + +case "\$1" in + start) + /usr/local/bin/paqctl _apply-firewall + ${_exec_start} & + _pid=\$! + sleep 1 + if kill -0 "\$_pid" 2>/dev/null; then + echo \$_pid > /run/paqctl.pid + else + echo "Failed to start paqet" + /usr/local/bin/paqctl _remove-firewall + exit 1 + fi + ;; + stop) + if [ -f /run/paqctl.pid ]; then + _pid=\$(cat /run/paqctl.pid) + kill "\$_pid" 2>/dev/null + _count=0 + while kill -0 "\$_pid" 2>/dev/null && [ \$_count -lt 10 ]; do + sleep 1 + _count=\$((_count + 1)) + done + kill -0 "\$_pid" 2>/dev/null && kill -9 "\$_pid" 2>/dev/null + rm -f /run/paqctl.pid + fi + /usr/local/bin/paqctl _remove-firewall + ;; + restart) + \$0 stop + sleep 1 + \$0 start + ;; + status) + [ -f /run/paqctl.pid ] && kill -0 "\$(cat /run/paqctl.pid)" 2>/dev/null && echo "Running" || echo "Stopped" + ;; + *) + echo "Usage: \$0 {start|stop|restart|status}" + exit 1 + ;; +esac +SYSV + if ! chmod +x /etc/init.d/paqctl; then + log_error "Failed to make init script executable" + return 1 + fi + if command -v update-rc.d &>/dev/null; then + update-rc.d paqctl defaults 2>/dev/null || true + elif command -v chkconfig &>/dev/null; then + chkconfig paqctl on 2>/dev/null || true + fi + log_success "SysVinit service created and enabled" + + else + log_warn "Could not set up auto-start. You can start paqet manually with: sudo paqctl start" + fi +} + +setup_logrotate() { + # Only set up if logrotate is available + command -v logrotate &>/dev/null || return 0 + + log_info "Setting up log rotation..." + + cat > /etc/logrotate.d/paqctl << 'LOGROTATE' +/var/log/paqctl.log +/var/log/paqet-backend.log +/var/log/gfk-backend.log +/var/log/xray.log +{ + daily + rotate 7 + compress + delaycompress + missingok + notifempty + create 0640 root root + sharedscripts + postrotate + # Signal processes to reopen logs if needed + systemctl reload paqctl.service 2>/dev/null || true + endscript +} +LOGROTATE + + log_success "Log rotation configured (7 days, compressed)" +} + +#═══════════════════════════════════════════════════════════════════════ +# Management Script (Embedded) +#═══════════════════════════════════════════════════════════════════════ + +create_management_script() { + local tmp_script + tmp_script=$(mktemp "$INSTALL_DIR/paqctl.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + cat > "$tmp_script" << 'MANAGEMENT' +#!/bin/bash +# +# paqctl - Paqet Manager +# https://git.samnet.dev/SamNet-dev/paqctl +# + +VERSION="1.0.0" + +# Pinned versions for stability (update these after testing new releases) +PAQET_VERSION_PINNED="v1.0.0-alpha.17" +XRAY_VERSION_PINNED="v26.2.4" +GFK_VERSION_PINNED="v1.0.0" + +INSTALL_DIR="REPLACE_ME_INSTALL_DIR" +BACKUP_DIR="$INSTALL_DIR/backups" +PAQET_REPO="hanselime/paqet" +PAQET_API_URL="https://api.github.com/repos/${PAQET_REPO}/releases/latest" +GFK_REPO="SamNet-dev/paqctl" +GFK_BRANCH="main" +GFK_RAW_URL="https://git.samnet.dev/${GFK_REPO}/raw/branch/${GFK_BRANCH}/gfk" +GFK_DIR="$INSTALL_DIR/gfk" +MICROSOCKS_REPO="rofl0r/microsocks" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +MAGENTA='\033[0;35m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +# Input validation helpers +_validate_port() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -ge 1 ] && [ "$1" -le 65535 ]; } +_validate_ip() { + [[ "$1" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]] || return 1 + local IFS='.'; set -- $1 + [ "$1" -le 255 ] && [ "$2" -le 255 ] && [ "$3" -le 255 ] && [ "$4" -le 255 ] +} +_validate_mac() { [[ "$1" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; } +_validate_iface() { [[ "$1" =~ ^[a-zA-Z0-9._-]+$ ]] && [ ${#1} -le 64 ]; } +# Safe string length check - prevents DoS via extremely long inputs +_check_length() { [ ${#1} -le "${2:-256}" ]; } + +# Network auto-detection +detect_network() { + log_info "Auto-detecting network configuration..." + + # Default interface - handle both standard "via X dev Y" and OpenVZ "dev Y scope link" formats + # Standard: "default via 192.168.1.1 dev eth0" -> $5 = eth0 + # OpenVZ: "default dev venet0 scope link" -> $3 = venet0 + local _route_line + _route_line=$(ip route show default 2>/dev/null | head -1) + if [[ "$_route_line" == *" via "* ]]; then + # Standard format with gateway + DETECTED_IFACE=$(echo "$_route_line" | awk '{print $5}') + elif [[ "$_route_line" == *" dev "* ]]; then + # OpenVZ/direct format without gateway + DETECTED_IFACE=$(echo "$_route_line" | awk '{print $3}') + fi + + # Validate detected interface exists + if [ -n "$DETECTED_IFACE" ] && ! ip link show "$DETECTED_IFACE" &>/dev/null; then + DETECTED_IFACE="" + fi + + if [ -z "$DETECTED_IFACE" ]; then + # Note: grep -v returns exit 1 if no matches, so we add || true for pipefail + DETECTED_IFACE=$(ip -o link show 2>/dev/null | awk -F': ' '{gsub(/ /,"",$2); print $2}' | { grep -vE '^(lo|docker[0-9]|br-|veth|virbr|tun|tap|wg)' || true; } | head -1) + fi + + # Local IP - wrap entire pipeline to prevent pipefail exit + if [ -n "$DETECTED_IFACE" ]; then + # Note: wrap in subshell with || true to handle cases where interface is invalid or has no IP + DETECTED_IP=$( (ip -4 addr show "$DETECTED_IFACE" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) || true ) + fi + if [ -z "$DETECTED_IP" ]; then + DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}') + [ -z "$DETECTED_IP" ] && DETECTED_IP=$(ip -4 addr show scope global 2>/dev/null | awk '/inet /{gsub(/\/.*/, "", $2); print $2; exit}') + fi + + # Gateway IP - handle OpenVZ format (may not have gateway) + if [[ "$_route_line" == *" via "* ]]; then + DETECTED_GATEWAY=$(echo "$_route_line" | awk '{print $3}') + else + DETECTED_GATEWAY="" + fi + + # Gateway MAC + DETECTED_GW_MAC="" + if [ -n "$DETECTED_GATEWAY" ]; then + DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}') + if [ -z "$DETECTED_GW_MAC" ]; then + ping -c 1 -W 2 "$DETECTED_GATEWAY" &>/dev/null || true + sleep 1 + DETECTED_GW_MAC=$(ip neigh show "$DETECTED_GATEWAY" 2>/dev/null | awk '/lladdr/{print $5; exit}') + fi + if [ -z "$DETECTED_GW_MAC" ] && command -v arp &>/dev/null; then + # Note: grep returns exit 1 if no matches, so we add || true for pipefail + DETECTED_GW_MAC=$(arp -n "$DETECTED_GATEWAY" 2>/dev/null | { grep -oE '([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}' || true; } | head -1) + fi + fi + + log_info "Interface: ${DETECTED_IFACE:-unknown}" + log_info "Local IP: ${DETECTED_IP:-unknown}" + log_info "Gateway: ${DETECTED_GATEWAY:-unknown}" + log_info "GW MAC: ${DETECTED_GW_MAC:-unknown}" +} + +_load_settings() { + [ -f "$INSTALL_DIR/settings.conf" ] || return 0 + # Safe settings loading without eval - uses case statement + while IFS='=' read -r key value; do + [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue + value="${value#\"}"; value="${value%\"}" + # Skip values with dangerous shell characters + [[ "$value" =~ [\`\$\(] ]] && continue + case "$key" in + BACKEND) BACKEND="$value" ;; + ROLE) ROLE="$value" ;; + PAQET_VERSION) PAQET_VERSION="$value" ;; + PAQCTL_VERSION) PAQCTL_VERSION="$value" ;; + LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;; + SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;; + INTERFACE) INTERFACE="$value" ;; + LOCAL_IP) LOCAL_IP="$value" ;; + GATEWAY_MAC) GATEWAY_MAC="$value" ;; + ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;; + PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;; + PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$value" ;; + REMOTE_SERVER) REMOTE_SERVER="$value" ;; + GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;; + GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;; + GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;; + GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;; + GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;; + GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;; + GFK_SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_PORT="$value" ;; + GFK_SOCKS_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_VIO_PORT="$value" ;; + XRAY_PANEL_DETECTED) XRAY_PANEL_DETECTED="$value" ;; + MICROSOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && MICROSOCKS_PORT="$value" ;; + GFK_SERVER_IP) GFK_SERVER_IP="$value" ;; + GFK_TCP_FLAGS) [[ "$value" =~ ^[FSRPAUEC]+$ ]] && GFK_TCP_FLAGS="$value" ;; + TELEGRAM_BOT_TOKEN) TELEGRAM_BOT_TOKEN="$value" ;; + TELEGRAM_CHAT_ID) TELEGRAM_CHAT_ID="$value" ;; + TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_INTERVAL="$value" ;; + TELEGRAM_ENABLED) TELEGRAM_ENABLED="$value" ;; + TELEGRAM_ALERTS_ENABLED) TELEGRAM_ALERTS_ENABLED="$value" ;; + TELEGRAM_DAILY_SUMMARY) TELEGRAM_DAILY_SUMMARY="$value" ;; + TELEGRAM_WEEKLY_SUMMARY) TELEGRAM_WEEKLY_SUMMARY="$value" ;; + TELEGRAM_SERVER_LABEL) TELEGRAM_SERVER_LABEL="$value" ;; + TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_START_HOUR="$value" ;; + esac + done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf") +} + +# Load settings +_load_settings +ROLE=${ROLE:-server} +PAQET_VERSION=${PAQET_VERSION:-unknown} +LISTEN_PORT=${LISTEN_PORT:-8443} +SOCKS_PORT=${SOCKS_PORT:-1080} +INTERFACE=${INTERFACE:-eth0} +LOCAL_IP=${LOCAL_IP:-} +GATEWAY_MAC=${GATEWAY_MAC:-} +ENCRYPTION_KEY=${ENCRYPTION_KEY:-} +REMOTE_SERVER=${REMOTE_SERVER:-} +TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} +TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} +TELEGRAM_INTERVAL=${TELEGRAM_INTERVAL:-6} +TELEGRAM_ENABLED=${TELEGRAM_ENABLED:-false} +TELEGRAM_ALERTS_ENABLED=${TELEGRAM_ALERTS_ENABLED:-true} +TELEGRAM_DAILY_SUMMARY=${TELEGRAM_DAILY_SUMMARY:-true} +TELEGRAM_WEEKLY_SUMMARY=${TELEGRAM_WEEKLY_SUMMARY:-true} +TELEGRAM_SERVER_LABEL=${TELEGRAM_SERVER_LABEL:-} +TELEGRAM_START_HOUR=${TELEGRAM_START_HOUR:-0} +BACKEND=${BACKEND:-paqet} +GFK_VIO_PORT=${GFK_VIO_PORT:-} +GFK_QUIC_PORT=${GFK_QUIC_PORT:-} +GFK_AUTH_CODE=${GFK_AUTH_CODE:-} +GFK_PORT_MAPPINGS=${GFK_PORT_MAPPINGS:-} +GFK_SOCKS_PORT=${GFK_SOCKS_PORT:-} +GFK_SOCKS_VIO_PORT=${GFK_SOCKS_VIO_PORT:-} +XRAY_PANEL_DETECTED=${XRAY_PANEL_DETECTED:-false} +MICROSOCKS_PORT=${MICROSOCKS_PORT:-} +GFK_SERVER_IP=${GFK_SERVER_IP:-} +GFK_TCP_FLAGS=${GFK_TCP_FLAGS:-AP} + +# Ensure root +if [ "$EUID" -ne 0 ]; then + echo -e "${RED}Error: This command must be run as root (use sudo paqctl)${NC}" + exit 1 +fi + +log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +log_success() { echo -e "${GREEN}[✓]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[!]${NC} $1"; } +log_error() { echo -e "${RED}[✗]${NC} $1"; } + +# Retry helper with exponential backoff for API requests +_curl_with_retry() { + local url="$1" + local max_attempts="${2:-3}" + local attempt=1 + local delay=2 + local response="" + while [ $attempt -le $max_attempts ]; do + response=$(curl -s --max-time 15 "$url" 2>/dev/null) + if [ -n "$response" ]; then + if echo "$response" | grep -q '"message".*rate limit'; then + log_warn "API rate limited, waiting ${delay}s..." + sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + continue + fi + echo "$response" + return 0 + fi + [ $attempt -lt $max_attempts ] && sleep $delay + delay=$((delay * 2)) + attempt=$((attempt + 1)) + done + return 1 +} + +_validate_version_tag() { + # Strict validation: only allow vX.Y.Z or X.Y.Z format with optional -suffix + [[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9._-]+)?$ ]] +} + +# Safe sed: escape replacement string to prevent metachar injection +_sed_escape() { printf '%s\n' "$1" | sed 's/[&/\]/\\&/g'; } +_safe_update_setting() { + local key="$1" value="$2" file="$3" + local escaped_value + escaped_value=$(_sed_escape "$value") + sed "s/^${key}=.*/${key}=\"${escaped_value}\"/" "$file" > "$file.tmp" 2>/dev/null && mv "$file.tmp" "$file" || true +} + +print_header() { + echo -e "${CYAN}" + echo "╔════════════════════════════════════════════════════════════════╗" + echo "║ PAQCTL - Paqet Manager v${VERSION} ║" + echo "║ Raw-socket encrypted proxy - bypass firewalls ║" + echo "╚════════════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +#═══════════════════════════════════════════════════════════════════════ +# Settings Save (management script) +#═══════════════════════════════════════════════════════════════════════ + +save_settings() { + local _tg_token="${TELEGRAM_BOT_TOKEN:-}" + local _tg_chat="${TELEGRAM_CHAT_ID:-}" + local _tg_interval="${TELEGRAM_INTERVAL:-6}" + local _tg_enabled="${TELEGRAM_ENABLED:-false}" + local _tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}" + local _tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}" + local _tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}" + local _tg_label="${TELEGRAM_SERVER_LABEL:-}" + local _tg_start_hour="${TELEGRAM_START_HOUR:-0}" + # Sanitize sensitive values - remove shell metacharacters and control chars + _sanitize_value() { + printf '%s' "$1" | tr -d '"$`\\'\''(){}[]<>|;&!\n\r\t' + } + local _safe_key; _safe_key=$(_sanitize_value "${ENCRYPTION_KEY:-}") + local _safe_auth; _safe_auth=$(_sanitize_value "${GFK_AUTH_CODE:-}") + local _tmp + _tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + (umask 077; cat > "$_tmp" << SEOF +BACKEND="${BACKEND:-paqet}" +ROLE="${ROLE}" +PAQET_VERSION="${PAQET_VERSION:-unknown}" +PAQCTL_VERSION="${VERSION}" +LISTEN_PORT="${LISTEN_PORT:-}" +SOCKS_PORT="${SOCKS_PORT:-}" +INTERFACE="${INTERFACE:-}" +LOCAL_IP="${LOCAL_IP:-}" +GATEWAY_MAC="${GATEWAY_MAC:-}" +ENCRYPTION_KEY="${_safe_key}" +PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}" +PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}" +REMOTE_SERVER="${REMOTE_SERVER:-}" +GFK_VIO_PORT="${GFK_VIO_PORT:-}" +GFK_VIO_CLIENT_PORT="${GFK_VIO_CLIENT_PORT:-}" +GFK_QUIC_PORT="${GFK_QUIC_PORT:-}" +GFK_QUIC_CLIENT_PORT="${GFK_QUIC_CLIENT_PORT:-}" +GFK_AUTH_CODE="${_safe_auth}" +GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}" +GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}" +GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}" +XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}" +MICROSOCKS_PORT="${MICROSOCKS_PORT:-}" +GFK_SERVER_IP="${GFK_SERVER_IP:-}" +GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}" +TELEGRAM_BOT_TOKEN="${_tg_token}" +TELEGRAM_CHAT_ID="${_tg_chat}" +TELEGRAM_INTERVAL=${_tg_interval} +TELEGRAM_ENABLED=${_tg_enabled} +TELEGRAM_ALERTS_ENABLED=${_tg_alerts} +TELEGRAM_DAILY_SUMMARY=${_tg_daily} +TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly} +TELEGRAM_SERVER_LABEL="${_tg_label}" +TELEGRAM_START_HOUR=${_tg_start_hour} +SEOF + ) + if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then + log_error "Failed to save settings" + rm -f "$_tmp" + return 1 + fi + chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null +} + +#═══════════════════════════════════════════════════════════════════════ +# Architecture Detection & Paqet Download (management script) +#═══════════════════════════════════════════════════════════════════════ + +detect_arch() { + local arch + arch=$(uname -m) + case "$arch" in + x86_64|amd64) echo "amd64" ;; + aarch64|arm64) echo "arm64" ;; + armv7l|armv7|armhf) echo "arm32" ;; + mips64el|mips64le) echo "mips64le" ;; + mips64) echo "mips64" ;; + mipsel|mipsle) echo "mipsle" ;; + mips) echo "mips" ;; + *) + log_error "Unsupported architecture: $arch" + return 1 + ;; + esac +} + +download_paqet() { + local version="$1" + local arch + arch=$(detect_arch) || return 1 + local os_name="linux" + local ext="tar.gz" + local filename="paqet-${os_name}-${arch}-${version}.${ext}" + local url="https://github.com/${PAQET_REPO}/releases/download/${version}/${filename}" + + log_info "Downloading paqet ${version} for ${os_name}/${arch}..." + + mkdir -p "$INSTALL_DIR/bin" || { log_error "Failed to create directory"; return 1; } + local tmp_file + tmp_file=$(mktemp "/tmp/paqet-download-XXXXXXXX.${ext}") || { log_error "Failed to create temp file"; return 1; } + + # Try curl first, fallback to wget + local download_ok=false + if curl -sL --max-time 180 --retry 3 --retry-delay 5 --fail -o "$tmp_file" "$url" 2>/dev/null; then + download_ok=true + elif command -v wget &>/dev/null; then + log_info "curl failed, trying wget..." + rm -f "$tmp_file" + if wget -q --timeout=180 --tries=3 -O "$tmp_file" "$url" 2>/dev/null; then + download_ok=true + fi + fi + + if [ "$download_ok" != "true" ]; then + log_error "Failed to download: $url" + log_error "Try manual download: wget '$url' and place binary in $INSTALL_DIR/bin/" + rm -f "$tmp_file" + return 1 + fi + + # Validate download + local fsize + fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0) + if [ "$fsize" -lt 1000 ]; then + log_error "Downloaded file is too small ($fsize bytes)" + rm -f "$tmp_file" + return 1 + fi + + # Extract + log_info "Extracting..." + local tmp_extract + tmp_extract=$(mktemp -d "/tmp/paqet-extract-XXXXXXXX") || { log_error "Failed to create temp dir"; return 1; } + if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then + log_error "Failed to extract archive" + rm -f "$tmp_file"; rm -rf "$tmp_extract" + return 1 + fi + + # Find the binary + local binary_name="paqet_${os_name}_${arch}" + local found_binary="" + found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1) + [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1) + [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1) + + if [ -z "$found_binary" ]; then + log_error "Could not find paqet binary in archive" + rm -f "$tmp_file"; rm -rf "$tmp_extract" + return 1 + fi + + # Stop paqet if running to avoid "Text file busy" error + pkill -f "$INSTALL_DIR/bin/paqet" 2>/dev/null || true + sleep 1 + + if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then + log_error "Failed to copy paqet binary" + rm -f "$tmp_file"; rm -rf "$tmp_extract" + return 1 + fi + chmod +x "$INSTALL_DIR/bin/paqet" || { log_error "Failed to make paqet executable"; return 1; } + + rm -f "$tmp_file"; rm -rf "$tmp_extract" + + if "$INSTALL_DIR/bin/paqet" version &>/dev/null; then + log_success "paqet ${version} installed successfully" + else + log_warn "paqet installed but version check failed (may need libpcap)" + fi +} + +#═══════════════════════════════════════════════════════════════════════ +# GFK Helper Functions (management script) +#═══════════════════════════════════════════════════════════════════════ + +install_python_deps() { + log_info "Installing Python dependencies..." + if ! command -v python3 &>/dev/null; then + if command -v apt-get &>/dev/null; then apt-get install -y python3 2>/dev/null + elif command -v dnf &>/dev/null; then dnf install -y python3 2>/dev/null + elif command -v yum &>/dev/null; then yum install -y python3 2>/dev/null + elif command -v apk &>/dev/null; then apk add python3 2>/dev/null + fi + fi + # Verify Python 3.10+ (required for GFK) + local pyver pymajor pyminor + pyver=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "0.0") + pymajor=$(echo "$pyver" | cut -d. -f1) + pyminor=$(echo "$pyver" | cut -d. -f2) + if [ "$pymajor" -lt 3 ] || { [ "$pymajor" -eq 3 ] && [ "$pyminor" -lt 10 ]; }; then + log_error "Python 3.10+ required, found $pyver" + return 1 + fi + # Install python3-venv (version-specific for apt, generic for others) + if command -v apt-get &>/dev/null; then + apt-get install -y "python${pyver}-venv" 2>/dev/null || apt-get install -y python3-venv 2>/dev/null + elif command -v dnf &>/dev/null; then + dnf install -y python3-pip 2>/dev/null # dnf includes venv in python3 + elif command -v yum &>/dev/null; then + yum install -y python3-pip 2>/dev/null + elif command -v apk &>/dev/null; then + apk add py3-pip 2>/dev/null + fi + # Use venv (recreate if broken/incomplete) + local VENV_DIR="$INSTALL_DIR/venv" + if [ ! -x "$VENV_DIR/bin/pip" ]; then + [ -d "$VENV_DIR" ] && rm -rf "$VENV_DIR" + python3 -m venv "$VENV_DIR" || { log_error "Failed to create venv (is python3-venv installed?)"; return 1; } + fi + # Verify pip exists after venv creation + if [ ! -x "$VENV_DIR/bin/pip" ]; then + log_error "venv created but pip missing (install python${pyver}-venv)" + return 1 + fi + "$VENV_DIR/bin/pip" install --quiet --upgrade pip 2>/dev/null || true + "$VENV_DIR/bin/pip" install --quiet scapy aioquic 2>/dev/null || { log_error "Failed to install Python packages"; return 1; } + "$VENV_DIR/bin/python" -c "import scapy; import aioquic" 2>/dev/null || { log_error "Python deps verification failed"; return 1; } + log_success "Python dependencies OK" +} + +install_microsocks() { + log_info "Installing microsocks..." + [ -x "$INSTALL_DIR/bin/microsocks" ] && { log_success "microsocks already installed"; return 0; } + command -v gcc &>/dev/null || { + if command -v apt-get &>/dev/null; then apt-get install -y gcc make 2>/dev/null + elif command -v yum &>/dev/null; then yum install -y gcc make 2>/dev/null + elif command -v apk &>/dev/null; then apk add gcc make musl-dev 2>/dev/null + fi + } + local tmp_dir; tmp_dir=$(mktemp -d) + curl -sL "https://github.com/${MICROSOCKS_REPO}/archive/refs/heads/master.tar.gz" -o "$tmp_dir/ms.tar.gz" || { rm -rf "$tmp_dir"; return 1; } + tar -xzf "$tmp_dir/ms.tar.gz" -C "$tmp_dir" 2>/dev/null || { rm -rf "$tmp_dir"; return 1; } + local src; src=$(find "$tmp_dir" -maxdepth 1 -type d -name "microsocks*" | head -1) + [ -z "$src" ] && { rm -rf "$tmp_dir"; return 1; } + make -C "$src" -j"$(nproc 2>/dev/null || echo 1)" 2>/dev/null || { rm -rf "$tmp_dir"; return 1; } + mkdir -p "$INSTALL_DIR/bin" + cp "$src/microsocks" "$INSTALL_DIR/bin/microsocks" + chmod 755 "$INSTALL_DIR/bin/microsocks" + rm -rf "$tmp_dir" + log_success "microsocks installed" +} + +download_gfk() { + log_info "Downloading GFW-knocker scripts..." + mkdir -p "$GFK_DIR" || return 1 + # Note: parameters.py is generated by generate_gfk_config(), don't download it + local f + # Download server scripts from gfk/server/ + for f in mainserver.py quic_server.py vio_server.py; do + curl -sL "$GFK_RAW_URL/server/$f" -o "$GFK_DIR/$f" || { log_error "Failed to download $f"; return 1; } + done + # Download client scripts from gfk/client/ + for f in mainclient.py quic_client.py vio_client.py; do + curl -sL "$GFK_RAW_URL/client/$f" -o "$GFK_DIR/$f" || { log_error "Failed to download $f"; return 1; } + done + chmod 600 "$GFK_DIR"/*.py + # Patch mainserver.py to use venv python for subprocesses + [ -f "$GFK_DIR/mainserver.py" ] && sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py" + log_success "GFW-knocker scripts downloaded" +} + +generate_gfk_certs() { + [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ] && return 0 + if ! command -v openssl &>/dev/null; then + log_info "Installing openssl..." + if command -v apt-get &>/dev/null; then apt-get install -y openssl 2>/dev/null + elif command -v dnf &>/dev/null; then dnf install -y openssl 2>/dev/null + elif command -v yum &>/dev/null; then yum install -y openssl 2>/dev/null + elif command -v apk &>/dev/null; then apk add openssl 2>/dev/null + elif command -v pacman &>/dev/null; then pacman -S --noconfirm openssl 2>/dev/null + fi + command -v openssl &>/dev/null || { log_error "Failed to install openssl"; return 1; } + fi + log_info "Generating QUIC certificates..." + openssl req -x509 -newkey rsa:2048 -keyout "$GFK_DIR/key.pem" \ + -out "$GFK_DIR/cert.pem" -days 3650 -nodes -subj "/CN=gfk" 2>/dev/null || return 1 + chmod 600 "$GFK_DIR/key.pem" "$GFK_DIR/cert.pem" + log_success "QUIC certificates generated" +} + +generate_gfk_config() { + log_info "Generating GFW-knocker config..." + local _tmp; _tmp=$(mktemp "$GFK_DIR/parameters.py.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + local vio_tcp_server_port="${GFK_VIO_PORT:-45000}" + local vio_tcp_client_port="${GFK_VIO_CLIENT_PORT:-40000}" + local vio_udp_server_port="${GFK_VIO_UDP_SERVER:-35000}" + local vio_udp_client_port="${GFK_VIO_UDP_CLIENT:-30000}" + local quic_server_port="${GFK_QUIC_PORT:-25000}" + local quic_client_port="${GFK_QUIC_CLIENT_PORT:-20000}" + # Validate all ports are numeric + local _p; for _p in "$vio_tcp_server_port" "$vio_tcp_client_port" "$vio_udp_server_port" \ + "$vio_udp_client_port" "$quic_server_port" "$quic_client_port"; do + [[ "$_p" =~ ^[0-9]+$ ]] || { log_error "Invalid port: $_p"; rm -f "$_tmp"; return 1; } + done + # Escape Python strings to prevent code injection + _esc_py() { local s="$1"; s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; s="${s//\'/\\\'}"; printf '%s' "$s"; } + local safe_ip; safe_ip=$(_esc_py "${GFK_SERVER_IP:-}") + local safe_auth; safe_auth=$(_esc_py "${GFK_AUTH_CODE:-}") + local safe_dir; safe_dir=$(_esc_py "${GFK_DIR}") + # Validate and build port mapping + local tcp_mapping="${GFK_PORT_MAPPINGS:-14000:443}" + local mapping_str="{" first=true pair lport rport + for pair in $(echo "$tcp_mapping" | tr ',' ' '); do + lport=$(echo "$pair" | cut -d: -f1); rport=$(echo "$pair" | cut -d: -f2) + [[ "$lport" =~ ^[0-9]+$ ]] && [[ "$rport" =~ ^[0-9]+$ ]] || { log_error "Invalid mapping: $pair"; rm -f "$_tmp"; return 1; } + [ "$first" = true ] && { mapping_str="${mapping_str}${lport}: ${rport}"; first=false; } || mapping_str="${mapping_str}, ${lport}: ${rport}" + done + mapping_str="${mapping_str}}" + (umask 077; cat > "$_tmp" << PYEOF +vps_ip = "${safe_ip}" +xray_server_ip_address = "127.0.0.1" +tcp_port_mapping = ${mapping_str} +udp_port_mapping = {} +vio_tcp_server_port = ${vio_tcp_server_port} +vio_tcp_client_port = ${vio_tcp_client_port} +vio_udp_server_port = ${vio_udp_server_port} +vio_udp_client_port = ${vio_udp_client_port} +quic_server_port = ${quic_server_port} +quic_client_port = ${quic_client_port} +quic_local_ip = "127.0.0.1" +quic_idle_timeout = 86400 +udp_timeout = 300 +quic_mtu = 1420 +quic_verify_cert = False +quic_max_data = 1073741824 +quic_max_stream_data = 1073741824 +quic_auth_code = "${safe_auth}" +quic_cert_filepath = ("${safe_dir}/cert.pem", "${safe_dir}/key.pem") +tcp_flags = "${GFK_TCP_FLAGS:-AP}" +PYEOF + ) + mv "$_tmp" "$GFK_DIR/parameters.py" || { rm -f "$_tmp"; return 1; } + chmod 600 "$GFK_DIR/parameters.py" + log_success "GFW-knocker config saved" +} + +create_gfk_client_wrapper() { + local wrapper="$INSTALL_DIR/bin/gfk-client.sh" + mkdir -p "$INSTALL_DIR/bin" + cat > "$wrapper" << 'WEOF' +#!/bin/bash +set -e +GFK_DIR="REPLACE_GFK" +INSTALL_DIR="REPLACE_INST" +cd "$GFK_DIR" +"$INSTALL_DIR/venv/bin/python" mainclient.py & +PID1=$! +trap "kill $PID1 2>/dev/null; wait" EXIT INT TERM +wait +WEOF + sed "s#REPLACE_GFK#${GFK_DIR}#g; s#REPLACE_INST#${INSTALL_DIR}#g" "$wrapper" > "$wrapper.sed" && mv "$wrapper.sed" "$wrapper" + chmod 755 "$wrapper" +} + +#═══════════════════════════════════════════════════════════════════════ +# Service Control +#═══════════════════════════════════════════════════════════════════════ + +is_running() { + # Check which backends are installed + local paqet_installed=false gfk_installed=false + [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true + fi + + # If both backends installed, return true if EITHER is running + if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then + is_paqet_running && return 0 + is_gfk_running && return 0 + return 1 + fi + + # Single backend mode - original logic + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl is-active paqctl.service &>/dev/null && return 0 + elif [ -f /run/paqctl.pid ]; then + local _pid + _pid=$(cat /run/paqctl.pid 2>/dev/null) + # Validate PID is numeric and process exists + [[ "$_pid" =~ ^[0-9]+$ ]] && kill -0 "$_pid" 2>/dev/null && return 0 + fi + # Also check for the process directly with more specific patterns + if [ "$BACKEND" = "gfw-knocker" ]; then + # Use full path matching to avoid false positives + pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0 + pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0 + pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0 + else + # Match specific config file path + pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0 + fi + return 1 +} + +# Check if paqet backend specifically is running +is_paqet_running() { + pgrep -f "${INSTALL_DIR}/bin/paqet run -c ${INSTALL_DIR}/config.yaml" &>/dev/null && return 0 + return 1 +} + +# Check if GFK backend specifically is running +is_gfk_running() { + if [ "$ROLE" = "server" ]; then + pgrep -f "${GFK_DIR}/mainserver.py" &>/dev/null && return 0 + else + pgrep -f "${GFK_DIR}/mainclient.py" &>/dev/null && return 0 + pgrep -f "${INSTALL_DIR}/bin/gfk-client.sh" &>/dev/null && return 0 + fi + return 1 +} + +# Start paqet backend only +start_paqet_backend() { + if is_paqet_running; then + log_warn "paqet is already running" + return 0 + fi + + if [ ! -f "$INSTALL_DIR/bin/paqet" ]; then + log_error "paqet binary not installed. Use 'Install additional backend' first." + return 1 + fi + + # Generate config.yaml if missing - prompt for values + if [ ! -f "$INSTALL_DIR/config.yaml" ]; then + echo "" + echo -e "${YELLOW}config.yaml not found. Let's configure paqet:${NC}" + echo "" + + detect_network + local _det_iface="$DETECTED_IFACE" + local _det_ip="$DETECTED_IP" + local _det_mac="$DETECTED_GW_MAC" + + echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:" + read -p " Interface: " input < /dev/tty || true + local _iface="${input:-${_det_iface:-eth0}}" + + echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:" + read -p " IP: " input < /dev/tty || true + local _local_ip="${input:-$_det_ip}" + + echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:" + read -p " MAC: " input < /dev/tty || true + local _gw_mac="${input:-$_det_mac}" + + local _key + _key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true) + if [ -z "$_key" ]; then + _key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true) + fi + + if [ "$ROLE" = "server" ]; then + echo -e "${BOLD}Listen Port${NC} [8443]:" + read -p " Port: " input < /dev/tty || true + local _port="${input:-8443}" + + echo "" + echo -e "${GREEN}${BOLD} Generated Key: ${_key}${NC}" + echo -e "${BOLD}Encryption Key${NC} (Enter to use generated):" + read -p " Key: " input < /dev/tty || true + [ -n "$input" ] && _key="$input" + + LISTEN_PORT="$_port" + ENCRYPTION_KEY="$_key" + + cat > "$INSTALL_DIR/config.yaml" << EOFCFG +role: "server" +log: + level: "info" +listen: + addr: ":${_port}" +network: + interface: "${_iface}" + ipv4: + addr: "${_local_ip}:${_port}" + router_mac: "${_gw_mac}" +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_key}" +EOFCFG + else + echo -e "${BOLD}Remote Server${NC} (IP:PORT):" + read -p " Server: " input < /dev/tty || true + local _server="${input:-${REMOTE_SERVER:-}}" + + echo -e "${BOLD}Encryption Key${NC} (from server):" + read -p " Key: " input < /dev/tty || true + [ -n "$input" ] && _key="$input" + + echo -e "${BOLD}SOCKS5 Port${NC} [1080]:" + read -p " Port: " input < /dev/tty || true + local _socks="${input:-1080}" + + REMOTE_SERVER="$_server" + SOCKS_PORT="$_socks" + ENCRYPTION_KEY="$_key" + + cat > "$INSTALL_DIR/config.yaml" << EOFCFG +role: "client" +log: + level: "info" +socks5: + - listen: "127.0.0.1:${_socks}" +network: + interface: "${_iface}" + ipv4: + addr: "${_local_ip}:0" + router_mac: "${_gw_mac}" +server: + addr: "${_server}" +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_key}" +EOFCFG + fi + + if [ ! -f "$INSTALL_DIR/config.yaml" ]; then + log_error "Failed to write config.yaml" + return 1 + fi + chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null + INTERFACE="$_iface" + LOCAL_IP="$_local_ip" + GATEWAY_MAC="$_gw_mac" + save_settings 2>/dev/null || true + log_success "Configuration saved" + echo "" + fi + + log_info "Starting paqet backend..." + + # Apply paqet firewall rules + local _saved_backend="$BACKEND" + BACKEND="paqet" + _apply_firewall + BACKEND="$_saved_backend" + + (umask 077; touch /var/log/paqet-backend.log) + nohup "$INSTALL_DIR/bin/paqet" run -c "$INSTALL_DIR/config.yaml" > /var/log/paqet-backend.log 2>&1 & + echo $! > /run/paqet-backend.pid + + sleep 2 + if is_paqet_running; then + log_success "paqet backend started" + else + log_error "paqet failed to start. Check: tail /var/log/paqet-backend.log" + return 1 + fi +} + +# Stop paqet backend only +stop_paqet_backend() { + if ! is_paqet_running; then + log_warn "paqet is not running" + return 0 + fi + + log_info "Stopping paqet backend..." + + if [ -f /run/paqet-backend.pid ]; then + local _pid + _pid=$(cat /run/paqet-backend.pid 2>/dev/null) + if [ -n "$_pid" ] && [[ "$_pid" =~ ^[0-9]+$ ]]; then + kill "$_pid" 2>/dev/null + sleep 1 + kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null + fi + rm -f /run/paqet-backend.pid + fi + + pkill -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true + + # Remove paqet firewall rules + local _saved_backend="$BACKEND" + BACKEND="paqet" + _remove_firewall + BACKEND="$_saved_backend" + + sleep 1 + if ! is_paqet_running; then + log_success "paqet backend stopped" + else + pkill -9 -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true + log_success "paqet backend stopped (forced)" + fi +} + +# Start GFK backend only +start_gfk_backend() { + if is_gfk_running; then + log_warn "gfw-knocker is already running" + return 0 + fi + + if [ ! -d "$GFK_DIR" ] || [ ! -f "$GFK_DIR/quic_server.py" ]; then + log_error "gfw-knocker not installed. Use 'Install additional backend' first." + return 1 + fi + + log_info "Starting gfw-knocker backend..." + + # Apply GFK firewall rules + local _saved_backend="$BACKEND" + BACKEND="gfw-knocker" + _apply_firewall + BACKEND="$_saved_backend" + + (umask 077; touch /var/log/gfk-backend.log) + + if [ "$ROLE" = "server" ]; then + # Start Xray if not running + if command -v xray &>/dev/null || [ -x /usr/local/bin/xray ] || [ -x /usr/local/x-ui/bin/xray-linux-amd64 ]; then + if ! pgrep -f "xray run" &>/dev/null; then + systemctl start xray 2>/dev/null || xray run -c /usr/local/etc/xray/config.json &>/dev/null & + sleep 2 + fi + fi + # Run from GFK_DIR so relative script paths work + pushd "$GFK_DIR" >/dev/null + nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainserver.py" > /var/log/gfk-backend.log 2>&1 & + popd >/dev/null + else + if [ -x "$INSTALL_DIR/bin/gfk-client.sh" ]; then + nohup "$INSTALL_DIR/bin/gfk-client.sh" > /var/log/gfk-backend.log 2>&1 & + else + # Run from GFK_DIR so relative script paths work + pushd "$GFK_DIR" >/dev/null + nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainclient.py" > /var/log/gfk-backend.log 2>&1 & + popd >/dev/null + fi + fi + echo $! > /run/gfk-backend.pid + + sleep 2 + if is_gfk_running; then + log_success "gfw-knocker backend started" + else + log_error "gfw-knocker failed to start. Check: tail /var/log/gfk-backend.log" + return 1 + fi +} + +# Stop GFK backend only +stop_gfk_backend() { + if ! is_gfk_running; then + log_warn "gfw-knocker is not running" + return 0 + fi + + log_info "Stopping gfw-knocker backend..." + + if [ -f /run/gfk-backend.pid ]; then + local _pid + _pid=$(cat /run/gfk-backend.pid 2>/dev/null) + if [ -n "$_pid" ] && [[ "$_pid" =~ ^[0-9]+$ ]]; then + kill "$_pid" 2>/dev/null + sleep 1 + kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null + fi + rm -f /run/gfk-backend.pid + fi + + pkill -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true + pkill -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true + pkill -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true + pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true + + # Remove GFK firewall rules + local _saved_backend="$BACKEND" + BACKEND="gfw-knocker" + _remove_firewall + BACKEND="$_saved_backend" + + sleep 1 + if ! is_gfk_running; then + log_success "gfw-knocker backend stopped" + else + pkill -9 -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true + pkill -9 -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true + pkill -9 -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true + log_success "gfw-knocker backend stopped (forced)" + fi +} + +start_paqet() { + # Check which backends are installed + local paqet_installed=false gfk_installed=false + [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true + fi + + # If both backends installed, start both + if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then + local started_something=false + if ! is_paqet_running; then + start_paqet_backend && started_something=true + else + log_warn "paqet is already running" + fi + if ! is_gfk_running; then + start_gfk_backend && started_something=true + else + log_warn "gfw-knocker is already running" + fi + [ "$started_something" = true ] && return 0 + return 0 + fi + + # Single backend mode - original logic + if is_running; then + log_warn "${BACKEND} is already running" + return 0 + fi + + log_info "Starting paqet..." + local _direct_start=false + + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl start paqctl.service 2>/dev/null + elif command -v rc-service &>/dev/null; then + rc-service paqctl start 2>/dev/null + elif [ -x /etc/init.d/paqctl ]; then + /etc/init.d/paqctl start 2>/dev/null + else + # Direct start - track for cleanup on failure + _direct_start=true + _apply_firewall + (umask 077; touch /var/log/paqctl.log) + if [ "$BACKEND" = "gfw-knocker" ]; then + if [ "$ROLE" = "client" ] && [ -x "$INSTALL_DIR/bin/gfk-client.sh" ]; then + nohup "$INSTALL_DIR/bin/gfk-client.sh" > /var/log/paqctl.log 2>&1 & + else + nohup "$INSTALL_DIR/venv/bin/python" "$GFK_DIR/mainserver.py" > /var/log/paqctl.log 2>&1 & + fi + else + nohup "$INSTALL_DIR/bin/paqet" run -c "$INSTALL_DIR/config.yaml" > /var/log/paqctl.log 2>&1 & + fi + echo $! > /run/paqctl.pid + fi + + sleep 2 + if is_running; then + log_success "${BACKEND} started successfully" + else + log_error "${BACKEND} failed to start. Check logs: sudo paqctl logs" + # Clean up firewall rules on failure (only for direct start) + if [ "$_direct_start" = true ]; then + _remove_firewall + fi + return 1 + fi +} + +stop_paqet() { + # Check which backends are installed + local paqet_installed=false gfk_installed=false + [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true + fi + + # If both backends installed, stop both + if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then + local stopped_something=false + if is_paqet_running; then + stop_paqet_backend && stopped_something=true + fi + if is_gfk_running; then + stop_gfk_backend && stopped_something=true + fi + if [ "$stopped_something" = false ]; then + log_warn "No backends are running" + fi + return 0 + fi + + # Single backend mode - original logic + if ! is_running; then + log_warn "paqet is not running" + return 0 + fi + + log_info "Stopping paqet..." + + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl stop paqctl.service 2>/dev/null + elif command -v rc-service &>/dev/null; then + rc-service paqctl stop 2>/dev/null + elif [ -x /etc/init.d/paqctl ]; then + /etc/init.d/paqctl stop 2>/dev/null + else + if [ -f /run/paqctl.pid ]; then + local _pid + _pid=$(cat /run/paqctl.pid 2>/dev/null) + if [ -n "$_pid" ]; then + kill "$_pid" 2>/dev/null + local _count=0 + while kill -0 "$_pid" 2>/dev/null && [ $_count -lt 10 ]; do + sleep 1 + _count=$((_count + 1)) + done + kill -0 "$_pid" 2>/dev/null && kill -9 "$_pid" 2>/dev/null + fi + rm -f /run/paqctl.pid + fi + # Use specific paths to avoid killing unrelated processes + if [ "$BACKEND" = "gfw-knocker" ]; then + pkill -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true + pkill -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true + pkill -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true + pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true + else + pkill -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true + fi + _remove_firewall + fi + + sleep 1 + if ! is_running; then + log_success "${BACKEND} stopped" + else + log_warn "${BACKEND} may still be running, force killing..." + if [ "$BACKEND" = "gfw-knocker" ]; then + pkill -9 -f "${GFK_DIR}/mainserver.py" 2>/dev/null || true + pkill -9 -f "${GFK_DIR}/mainclient.py" 2>/dev/null || true + pkill -9 -f "${INSTALL_DIR}/bin/gfk-client.sh" 2>/dev/null || true + pkill -9 -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true + else + pkill -9 -f "${INSTALL_DIR}/bin/paqet run -c" 2>/dev/null || true + fi + sleep 1 + log_success "${BACKEND} stopped" + fi +} + +restart_paqet() { + stop_paqet + sleep 1 + start_paqet +} + +#═══════════════════════════════════════════════════════════════════════ +# Firewall (internal commands) +#═══════════════════════════════════════════════════════════════════════ + +_is_firewalld_active() { + command -v firewall-cmd &>/dev/null && firewall-cmd --state 2>/dev/null | grep -q running +} + +_apply_firewall() { + if ! _is_firewalld_active && ! command -v iptables &>/dev/null; then + echo -e "${YELLOW}[!]${NC} No firewall backend found (iptables or firewalld)." >&2 + return 1 + fi + + if [ "$BACKEND" = "gfw-knocker" ]; then + local vio_port + if [ "$ROLE" = "server" ]; then + vio_port="${GFK_VIO_PORT:-45000}" + else + vio_port="${GFK_VIO_CLIENT_PORT:-40000}" + fi + + if _is_firewalld_active; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add VIO port DROP rule via firewalld" >&2 + firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule via firewalld" >&2 + firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + else + local TAG="paqctl" + modprobe iptable_raw 2>/dev/null || true + iptables -t raw -C PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -t raw -C OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \ + iptables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add VIO port DROP rule" >&2 + iptables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \ + iptables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2 + if command -v ip6tables &>/dev/null; then + ip6tables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || \ + ip6tables -A INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true + ip6tables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || \ + ip6tables -A OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true + fi + fi + return 0 + fi + + [ "$ROLE" != "server" ] && return 0 + local port="${LISTEN_PORT:-8443}" + + if _is_firewalld_active; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add PREROUTING NOTRACK rule via firewalld" >&2 + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add OUTPUT NOTRACK rule via firewalld" >&2 + firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule via firewalld" >&2 + firewall-cmd --direct --query-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + else + local TAG="paqctl" + modprobe iptable_raw 2>/dev/null || true + modprobe iptable_mangle 2>/dev/null || true + iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add PREROUTING NOTRACK rule" >&2 + iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add OUTPUT NOTRACK rule" >&2 + iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \ + iptables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \ + echo -e "${YELLOW}[!]${NC} Failed to add RST DROP rule" >&2 + if command -v ip6tables &>/dev/null; then + ip6tables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + ip6tables -t raw -A PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + ip6tables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || \ + ip6tables -t raw -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + ip6tables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || \ + ip6tables -t mangle -A OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true + fi + fi +} + +_remove_firewall() { + if ! _is_firewalld_active && ! command -v iptables &>/dev/null; then + return 0 + fi + + if [ "$BACKEND" = "gfw-knocker" ]; then + local vio_port + if [ "$ROLE" = "server" ]; then + vio_port="${GFK_VIO_PORT:-45000}" + else + vio_port="${GFK_VIO_CLIENT_PORT:-40000}" + fi + + if _is_firewalld_active; then + firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + else + local TAG="paqctl" + iptables -t raw -D PREROUTING -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -t raw -D PREROUTING -p tcp --dport "$vio_port" -j NOTRACK 2>/dev/null || true + iptables -t raw -D OUTPUT -p tcp --sport "$vio_port" -j NOTRACK 2>/dev/null || true + iptables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true + iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true + iptables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true + iptables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true + if command -v ip6tables &>/dev/null; then + ip6tables -D INPUT -p tcp --dport "$vio_port" -m comment --comment "$TAG" -j DROP 2>/dev/null || true + ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "$TAG" -j DROP 2>/dev/null || true + ip6tables -D INPUT -p tcp --dport "$vio_port" -j DROP 2>/dev/null || true + ip6tables -D OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -j DROP 2>/dev/null || true + fi + fi + return 0 + fi + + [ "$ROLE" != "server" ] && return 0 + local port="${LISTEN_PORT:-8443}" + + if _is_firewalld_active; then + firewall-cmd --direct --remove-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --remove-rule ipv6 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + else + local TAG="paqctl" + iptables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + iptables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true + iptables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true + iptables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true + iptables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true + if command -v ip6tables &>/dev/null; then + ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" -j NOTRACK 2>/dev/null || true + ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" -m comment --comment "$TAG" --tcp-flags RST RST -j DROP 2>/dev/null || true + ip6tables -t raw -D PREROUTING -p tcp --dport "$port" -j NOTRACK 2>/dev/null || true + ip6tables -t raw -D OUTPUT -p tcp --sport "$port" -j NOTRACK 2>/dev/null || true + ip6tables -t mangle -D OUTPUT -p tcp --sport "$port" --tcp-flags RST RST -j DROP 2>/dev/null || true + fi + fi +} + +# Remove ALL paqctl-tagged firewall rules (for complete uninstall) +_remove_all_paqctl_firewall_rules() { + # firewalld: remove paqctl-tagged direct rules + if _is_firewalld_active; then + local _rules + _rules=$(firewall-cmd --direct --get-all-rules 2>/dev/null) || true + if [ -n "$_rules" ]; then + echo "$_rules" | grep "paqctl" | while IFS= read -r _rule; do + firewall-cmd --direct --remove-rule $_rule 2>/dev/null || true + firewall-cmd --permanent --direct --remove-rule $_rule 2>/dev/null || true + done + fi + return 0 + fi + + command -v iptables &>/dev/null || return 0 + local TAG="paqctl" + + # Remove all rules with "paqctl" comment from all tables + # Loop to remove multiple rules if port was changed + local i + for i in {1..10}; do + iptables -t raw -S 2>/dev/null | grep -q "paqctl" || break + iptables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do + # Convert -A to -D for deletion + local del_rule="${rule/-A /-D }" + eval "iptables -t raw $del_rule" 2>/dev/null || true + done + done + + for i in {1..10}; do + iptables -t mangle -S 2>/dev/null | grep -q "paqctl" || break + iptables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do + local del_rule="${rule/-A /-D }" + eval "iptables -t mangle $del_rule" 2>/dev/null || true + done + done + + for i in {1..10}; do + iptables -S 2>/dev/null | grep -q "paqctl" || break + iptables -S 2>/dev/null | grep "paqctl" | while read -r rule; do + local del_rule="${rule/-A /-D }" + eval "iptables $del_rule" 2>/dev/null || true + done + done + + # Same for IPv6 + if command -v ip6tables &>/dev/null; then + for i in {1..10}; do + ip6tables -t raw -S 2>/dev/null | grep -q "paqctl" || break + ip6tables -t raw -S 2>/dev/null | grep "paqctl" | while read -r rule; do + local del_rule="${rule/-A /-D }" + eval "ip6tables -t raw $del_rule" 2>/dev/null || true + done + done + + for i in {1..10}; do + ip6tables -t mangle -S 2>/dev/null | grep -q "paqctl" || break + ip6tables -t mangle -S 2>/dev/null | grep "paqctl" | while read -r rule; do + local del_rule="${rule/-A /-D }" + eval "ip6tables -t mangle $del_rule" 2>/dev/null || true + done + done + + for i in {1..10}; do + ip6tables -S 2>/dev/null | grep -q "paqctl" || break + ip6tables -S 2>/dev/null | grep "paqctl" | while read -r rule; do + local del_rule="${rule/-A /-D }" + eval "ip6tables $del_rule" 2>/dev/null || true + done + done + fi +} + +_persist_firewall() { + if _is_firewalld_active; then + firewall-cmd --runtime-to-permanent 2>/dev/null || true + return 0 + fi + if command -v netfilter-persistent &>/dev/null; then + netfilter-persistent save 2>/dev/null || true + elif command -v iptables-save &>/dev/null; then + if [ -d /etc/iptables ]; then + iptables-save > /etc/iptables/rules.v4 2>/dev/null || true + command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true + elif [ -f /etc/debian_version ] && [ ! -d /etc/iptables ]; then + mkdir -p /etc/iptables + iptables-save > /etc/iptables/rules.v4 2>/dev/null || true + command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/iptables/rules.v6 2>/dev/null || true + elif [ -d /etc/sysconfig ]; then + iptables-save > /etc/sysconfig/iptables 2>/dev/null || true + command -v ip6tables-save &>/dev/null && ip6tables-save > /etc/sysconfig/ip6tables 2>/dev/null || true + fi + fi +} + +#═══════════════════════════════════════════════════════════════════════ +# Status & Info +#═══════════════════════════════════════════════════════════════════════ + +show_status() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} PAQCTL STATUS (${BACKEND})${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + # Running status + if is_running; then + echo -e " Status: ${GREEN}● Running${NC}" + # Uptime + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + local started + started=$(systemctl show paqctl.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2) + if [ -n "$started" ]; then + local started_ts + started_ts=$(date -d "$started" +%s 2>/dev/null || echo 0) + if [ "$started_ts" -gt 0 ] 2>/dev/null; then + local now=$(date +%s) + local up=$((now - started_ts)) + local days=$((up / 86400)) + local hours=$(( (up % 86400) / 3600 )) + local mins=$(( (up % 3600) / 60 )) + if [ "$days" -gt 0 ]; then + echo -e " Uptime: ${days}d ${hours}h ${mins}m" + else + echo -e " Uptime: ${hours}h ${mins}m" + fi + fi + fi + fi + # PID + local pid + if [ "$BACKEND" = "gfw-knocker" ]; then + pid=$(pgrep -f "mainserver.py|mainclient.py" 2>/dev/null | head -1) + else + pid=$(pgrep -f "paqet run -c" 2>/dev/null | head -1) + fi + [ -n "$pid" ] && echo -e " PID: $pid" + + # CPU/RAM of process + if [ -n "$pid" ]; then + local cpu_mem + cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1) + if [ -n "$cpu_mem" ]; then + local cpu=$(echo "$cpu_mem" | awk '{print $1}') + local mem=$(echo "$cpu_mem" | awk '{print $2}') + echo -e " CPU: ${cpu}%" + echo -e " Memory: ${mem}%" + fi + fi + else + echo -e " Status: ${RED}● Stopped${NC}" + fi + + echo "" + echo -e " ${DIM}── Configuration ──${NC}" + echo -e " Backend: ${BOLD}${BACKEND}${NC}" + echo -e " Role: ${BOLD}${ROLE}${NC}" + echo -e " Version: ${PAQET_VERSION}" + + if [ "$BACKEND" = "gfw-knocker" ]; then + echo -e " Server IP: ${GFK_SERVER_IP}" + echo -e " VIO port: ${GFK_VIO_PORT}" + echo -e " QUIC port: ${GFK_QUIC_PORT}" + if [ "$ROLE" = "server" ]; then + if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ] && [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + local _md="" + IFS=',' read -ra _pairs <<< "${GFK_PORT_MAPPINGS}" + for _p in "${_pairs[@]}"; do + if [ "${_p%%:*}" = "${GFK_SOCKS_VIO_PORT}" ]; then + _md="${_md:+${_md}, }${_p} (SOCKS5)" + else + _md="${_md:+${_md}, }${_p} (panel)" + fi + done + echo -e " Mappings: ${_md}" + echo -e " SOCKS5: ${GREEN}127.0.0.1:${GFK_SOCKS_PORT}${NC} (server-side)" + echo -e " Client use: ${GREEN}127.0.0.1:${GFK_SOCKS_VIO_PORT}${NC} (set as proxy on client)" + elif [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then + echo -e " Mappings: ${GFK_PORT_MAPPINGS}" + echo -e " SOCKS5: ${YELLOW}not configured${NC}" + else + echo -e " Mappings: ${GFK_PORT_MAPPINGS}" + local _srv_port _cli_port + _srv_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f2) + _cli_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + echo -e " SOCKS5: ${GREEN}127.0.0.1:${_srv_port}${NC} (server-side)" + echo -e " Client use: ${GREEN}127.0.0.1:${_cli_port}${NC} (set as proxy on client)" + fi + echo -e " Auth code: ${GFK_AUTH_CODE:0:8}..." + local _vio_port="${GFK_VIO_PORT:-45000}" + local _input_ok=false _rst_ok=false + if iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -C INPUT -p tcp --dport "$_vio_port" -j DROP 2>/dev/null; then + _input_ok=true + fi + if iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -j DROP 2>/dev/null; then + _rst_ok=true + fi + if [ "$_input_ok" = true ] && [ "$_rst_ok" = true ]; then + echo -e " Firewall: ${GREEN}VIO port blocked${NC}" + elif [ "$_input_ok" = true ]; then + echo -e " Firewall: ${YELLOW}Partial (RST DROP missing)${NC}" + else + echo -e " Firewall: ${RED}VIO port NOT blocked${NC}" + fi + else + echo -e " Mappings: ${GFK_PORT_MAPPINGS}" + local _fv + if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + _fv="$GFK_SOCKS_VIO_PORT" + else + _fv=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + fi + echo -e " Proxy: ${GREEN}SOCKS5 127.0.0.1:${_fv}${NC} (set as browser proxy)" + fi + else + echo -e " Interface: ${INTERFACE}" + echo -e " Local IP: ${LOCAL_IP}" + if [ "$ROLE" = "server" ]; then + echo -e " Port: ${LISTEN_PORT}" + echo -e " Key: ${ENCRYPTION_KEY:0:8}..." + if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then + echo -e " Firewall: ${GREEN}Rules active${NC}" + else + echo -e " Firewall: ${RED}Rules missing${NC}" + fi + else + echo -e " Server: ${REMOTE_SERVER}" + echo -e " SOCKS port: ${SOCKS_PORT}" + echo -e " Key: ${ENCRYPTION_KEY:0:8}..." + fi + fi + + # Telegram + if [ "$TELEGRAM_ENABLED" = "true" ]; then + echo -e " Telegram: ${GREEN}Enabled${NC}" + else + echo -e " Telegram: ${DIM}Disabled${NC}" + fi + + echo "" +} + +#═══════════════════════════════════════════════════════════════════════ +# Logs +#═══════════════════════════════════════════════════════════════════════ + +show_logs() { + echo "" + log_info "Showing paqet logs (Ctrl+C to return to menu)..." + echo "" + + # Trap Ctrl+C to return to menu instead of exiting + trap 'echo ""; log_info "Returning to menu..."; return 0' INT + + if command -v journalctl &>/dev/null && [ -d /run/systemd/system ]; then + journalctl -u paqctl.service -f --no-pager -n 50 + elif [ -f /var/log/paqctl.log ]; then + tail -f -n 50 /var/log/paqctl.log + else + log_warn "No logs found. Is paqet running?" + fi + + # Restore default trap + trap - INT +} + +#═══════════════════════════════════════════════════════════════════════ +# Health Check +#═══════════════════════════════════════════════════════════════════════ + +health_check() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} HEALTH CHECK${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + local issues=0 + + if [ "$BACKEND" = "gfw-knocker" ]; then + # 1. Python scripts exist + if [ -f "$GFK_DIR/mainserver.py" ] && [ -f "$GFK_DIR/mainclient.py" ]; then + echo -e " ${GREEN}✓${NC} GFW-knocker scripts found" + else + echo -e " ${RED}✗${NC} GFW-knocker scripts missing from $GFK_DIR" + issues=$((issues + 1)) + fi + + # 2. Python + deps (check venv) + if [ -x "$INSTALL_DIR/venv/bin/python" ] && "$INSTALL_DIR/venv/bin/python" -c "import scapy; import aioquic" 2>/dev/null; then + echo -e " ${GREEN}✓${NC} Python dependencies OK (scapy, aioquic)" + else + echo -e " ${RED}✗${NC} Python dependencies missing (venv not setup)" + issues=$((issues + 1)) + fi + + # 3. Config + if [ -f "$GFK_DIR/parameters.py" ]; then + echo -e " ${GREEN}✓${NC} GFK configuration found" + else + echo -e " ${RED}✗${NC} GFK configuration missing" + issues=$((issues + 1)) + fi + + # 4. Certificates + if [ -f "$GFK_DIR/cert.pem" ] && [ -f "$GFK_DIR/key.pem" ]; then + echo -e " ${GREEN}✓${NC} QUIC certificates found" + else + echo -e " ${RED}✗${NC} QUIC certificates missing" + issues=$((issues + 1)) + fi + + # 5. Service running + if is_running; then + echo -e " ${GREEN}✓${NC} GFW-knocker is running" + else + echo -e " ${RED}✗${NC} GFW-knocker is not running" + issues=$((issues + 1)) + fi + + # 6. Firewall (server) + if [ "$ROLE" = "server" ]; then + # Check both tagged and untagged rules (tagged added by _apply_firewall, untagged by install wizard) + local _vio_port="${GFK_VIO_PORT:-45000}" + if iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -C INPUT -p tcp --dport "$_vio_port" -j DROP 2>/dev/null; then + echo -e " ${GREEN}✓${NC} VIO port ${_vio_port} INPUT blocked" + else + echo -e " ${RED}✗${NC} VIO port ${_vio_port} INPUT NOT blocked" + issues=$((issues + 1)) + fi + # Check RST DROP rule (prevents kernel from sending RST packets) + if iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -j DROP 2>/dev/null; then + echo -e " ${GREEN}✓${NC} VIO port ${_vio_port} RST DROP in place" + else + echo -e " ${RED}✗${NC} VIO port ${_vio_port} RST DROP missing" + issues=$((issues + 1)) + fi + fi + + # 7. SOCKS5 port (client) + if [ "$ROLE" = "client" ]; then + local _socks_vio + if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + _socks_vio="$GFK_SOCKS_VIO_PORT" + else + _socks_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + fi + if is_running && ss -tlnp 2>/dev/null | grep -q ":${_socks_vio} "; then + echo -e " ${GREEN}✓${NC} SOCKS5 port ${_socks_vio} is listening" + elif is_running; then + echo -e " ${RED}✗${NC} SOCKS5 port ${_socks_vio} not listening" + issues=$((issues + 1)) + fi + fi + else + # 1. Binary exists + if [ -x "$INSTALL_DIR/bin/paqet" ]; then + echo -e " ${GREEN}✓${NC} paqet binary found" + else + echo -e " ${RED}✗${NC} paqet binary not found at $INSTALL_DIR/bin/paqet" + issues=$((issues + 1)) + fi + + # 2. Config exists + if [ -f "$INSTALL_DIR/config.yaml" ]; then + echo -e " ${GREEN}✓${NC} Configuration file found" + else + echo -e " ${RED}✗${NC} Configuration file missing" + issues=$((issues + 1)) + fi + + # 3. Service running + if is_running; then + echo -e " ${GREEN}✓${NC} paqet is running" + else + echo -e " ${RED}✗${NC} paqet is not running" + issues=$((issues + 1)) + fi + + # 4. libpcap + if ldconfig -p 2>/dev/null | grep -q libpcap; then + echo -e " ${GREEN}✓${NC} libpcap is available" + else + echo -e " ${YELLOW}!${NC} libpcap not found in ldconfig (may still work)" + fi + + # 5. iptables (server only) + if [ "$ROLE" = "server" ]; then + if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then + echo -e " ${GREEN}✓${NC} iptables NOTRACK rules in place (port $LISTEN_PORT)" + else + echo -e " ${RED}✗${NC} iptables NOTRACK rules missing for port $LISTEN_PORT" + issues=$((issues + 1)) + fi + + if iptables -t mangle -C OUTPUT -p tcp --sport "$LISTEN_PORT" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null; then + echo -e " ${GREEN}✓${NC} iptables RST DROP rule in place" + else + echo -e " ${RED}✗${NC} iptables RST DROP rule missing" + issues=$((issues + 1)) + fi + fi + + # 6. Port listening (server) or connectivity (client) + if [ "$ROLE" = "server" ] && is_running; then + if ss -tlnp 2>/dev/null | grep -q ":${LISTEN_PORT}"; then + echo -e " ${GREEN}✓${NC} Port $LISTEN_PORT is listening" + else + echo -e " ${YELLOW}!${NC} Port $LISTEN_PORT not shown in ss (paqet uses raw sockets)" + fi + fi + + if [ "$ROLE" = "client" ] && is_running; then + if ss -tlnp 2>/dev/null | grep -q ":${SOCKS_PORT}"; then + echo -e " ${GREEN}✓${NC} SOCKS5 port $SOCKS_PORT is listening" + else + echo -e " ${RED}✗${NC} SOCKS5 port $SOCKS_PORT is not listening" + issues=$((issues + 1)) + fi + fi + + # 7. Paqet ping test + if is_running && [ -x "$INSTALL_DIR/bin/paqet" ]; then + echo -e " ${DIM}Running paqet ping test...${NC}" + local ping_result + ping_result=$(timeout 10 "$INSTALL_DIR/bin/paqet" ping -c "$INSTALL_DIR/config.yaml" 2>&1 || true) + if echo "$ping_result" | grep -qi "success\|pong\|ok\|alive\|rtt"; then + echo -e " ${GREEN}✓${NC} Paqet ping: OK" + elif [ -n "$ping_result" ]; then + echo -e " ${YELLOW}!${NC} Paqet ping: $(echo "$ping_result" | head -1)" + else + echo -e " ${YELLOW}!${NC} Paqet ping: no response (may not be supported)" + fi + fi + fi + + # 8. Network connectivity + if curl -s --max-time 5 https://api.github.com &>/dev/null; then + echo -e " ${GREEN}✓${NC} Internet connectivity: OK" + else + echo -e " ${YELLOW}!${NC} Cannot reach GitHub API (may be firewall/network)" + fi + + # 9. Systemd service + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + if systemctl is-enabled paqctl.service &>/dev/null; then + echo -e " ${GREEN}✓${NC} Auto-start on boot: enabled" + else + echo -e " ${YELLOW}!${NC} Auto-start on boot: disabled" + fi + fi + + echo "" + if [ "$issues" -eq 0 ]; then + echo -e " ${GREEN}${BOLD}All checks passed!${NC}" + else + echo -e " ${RED}${BOLD}$issues issue(s) found${NC}" + fi + echo "" +} + +#═══════════════════════════════════════════════════════════════════════ +# Update +#═══════════════════════════════════════════════════════════════════════ + +update_gfk() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} UPDATE GFW-KNOCKER${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + log_info "Downloading latest GFW-knocker scripts..." + local tmp_dir + tmp_dir=$(mktemp -d) + local server_files="mainserver.py quic_server.py vio_server.py" + local client_files="mainclient.py quic_client.py vio_client.py" + local f changed=false + # Download server scripts + for f in $server_files; do + if ! curl -sL "$GFK_RAW_URL/server/$f" -o "$tmp_dir/$f"; then + log_error "Failed to download $f" + rm -rf "$tmp_dir" + return 1 + fi + if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then + changed=true + fi + done + # Download client scripts + for f in $client_files; do + if ! curl -sL "$GFK_RAW_URL/client/$f" -o "$tmp_dir/$f"; then + log_error "Failed to download $f" + rm -rf "$tmp_dir" + return 1 + fi + if ! diff -q "$tmp_dir/$f" "$GFK_DIR/$f" &>/dev/null; then + changed=true + fi + done + + if [ "$changed" = true ]; then + local was_running=false + is_running && was_running=true + [ "$was_running" = true ] && stop_paqet + + # Backup old scripts + mkdir -p "$BACKUP_DIR" + local all_files="$server_files $client_files" + for f in $all_files; do + [ -f "$GFK_DIR/$f" ] && cp "$GFK_DIR/$f" "$BACKUP_DIR/${f}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true + done + + for f in $all_files; do + cp "$tmp_dir/$f" "$GFK_DIR/$f" + done + chmod 600 "$GFK_DIR"/*.py + # Patch mainserver.py to use venv python for subprocesses + [ -f "$GFK_DIR/mainserver.py" ] && sed -i "s|'python3'|'$INSTALL_DIR/venv/bin/python'|g" "$GFK_DIR/mainserver.py" + log_success "GFW-knocker scripts updated" + + # Also upgrade Python deps in venv + "$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true + + [ "$was_running" = true ] && start_paqet + else + log_success "GFW-knocker scripts are already up to date" + "$INSTALL_DIR/venv/bin/pip" install --quiet --upgrade scapy aioquic 2>/dev/null || true + fi + rm -rf "$tmp_dir" + + # Regenerate client wrapper (removes legacy microsocks startup) + if [ "$ROLE" = "client" ]; then + create_gfk_client_wrapper + pkill -f "${INSTALL_DIR}/bin/microsocks" 2>/dev/null || true + fi + + # Also check for management script updates + update_management_script + echo "" +} + +update_paqet() { + if [ "$BACKEND" = "gfw-knocker" ]; then + update_gfk + return + fi + + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} UPDATE PAQET${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + log_info "Querying GitHub for latest release..." + + # Get latest version from GitHub with retry + local response + response=$(_curl_with_retry "$PAQET_API_URL" 3) + if [ -z "$response" ]; then + log_error "Failed to query GitHub API after retries. Check your internet connection." + return 1 + fi + + local latest_tag + latest_tag=$(echo "$response" | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"') + if [ -z "$latest_tag" ] || ! _validate_version_tag "$latest_tag"; then + log_error "Could not determine valid version from GitHub" + return 1 + fi + + # Extract release date + local release_date + release_date=$(echo "$response" | grep -o '"published_at"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"' | cut -dT -f1) + + # Extract release notes (body field) + local release_notes="" + if command -v python3 &>/dev/null; then + release_notes=$(python3 -c " +import json,sys +try: + d=json.loads(sys.stdin.read()) + body=d.get('body','') + if body: + # Truncate to first 500 chars, strip markdown + body=body[:500].replace('**','').replace('##','').replace('# ','') + print(body) +except: pass +" <<< "$response" 2>/dev/null) + fi + + local current="${PAQET_VERSION:-unknown}" + local bin_ver + bin_ver=$("$INSTALL_DIR/bin/paqet" version 2>/dev/null || echo "unknown") + + echo "" + echo -e " ${DIM}── Version Info ──${NC}" + echo -e " Installed version: ${BOLD}${current}${NC}" + echo -e " Binary reports: ${BOLD}${bin_ver}${NC}" + echo -e " Latest release: ${BOLD}${latest_tag}${NC}" + [ -n "$release_date" ] && echo -e " Release date: ${release_date}" + + if [ "$current" = "$latest_tag" ]; then + echo "" + log_success "You are already on the latest version!" + echo "" + echo -e " ${DIM}Options:${NC}" + echo " 1. Force reinstall current version" + echo " 2. Rollback to previous version" + echo " 3. Update management script only" + echo " b. Back" + echo "" + read -p " Choice: " up_choice < /dev/tty || true + case "$up_choice" in + 1) + read -p " Force reinstall ${current}? [y/N]: " _fc < /dev/tty || true + [[ "$_fc" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; } + ;; + 2) rollback_paqet; return ;; + 3) update_management_script; return ;; + [bB]) return 0 ;; + *) return 0 ;; + esac + fi + + # Show release notes if available + if [ -n "$release_notes" ]; then + echo "" + echo -e " ${DIM}── Release Notes ──${NC}" + echo "$release_notes" | while IFS= read -r line; do + echo -e " ${DIM}${line}${NC}" + done + echo "" + fi + + echo "" + echo -e "${YELLOW}╔════════════════════════════════════════════════════════════════╗${NC}" + echo -e "${YELLOW}║ ${BOLD}⚠ WARNING: Updating may cause compatibility issues!${NC}${YELLOW} ║${NC}" + echo -e "${YELLOW}╠════════════════════════════════════════════════════════════════╣${NC}" + echo -e "${YELLOW}║${NC} paqctl was tested with: ${BOLD}${PAQET_VERSION_PINNED}${NC}" + echo -e "${YELLOW}║${NC} Newer versions may have breaking changes or bugs." + echo -e "${YELLOW}║${NC} You can rollback with: ${BOLD}sudo paqctl rollback${NC}" + echo -e "${YELLOW}╚════════════════════════════════════════════════════════════════╝${NC}" + echo "" + read -p " Update to ${latest_tag}? [y/N]: " confirm < /dev/tty || true + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + log_info "Update cancelled" + return 0 + fi + + # Download new binary + _download_and_install_binary "$latest_tag" || return 1 + + # Check for management script update + update_management_script +} + +_download_and_install_binary() { + local target_tag="$1" + local arch + arch=$(uname -m) + case "$arch" in + x86_64|amd64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + *) log_error "Unsupported architecture: $arch"; return 1 ;; + esac + + local filename="paqet-linux-${arch}-${target_tag}.tar.gz" + local url="https://github.com/${PAQET_REPO}/releases/download/${target_tag}/${filename}" + local tmp_file + tmp_file=$(mktemp "/tmp/paqet-update-XXXXXXXX.tar.gz") + + log_info "Downloading ${filename}..." + if ! curl -sL --max-time 120 --fail -o "$tmp_file" "$url"; then + log_error "Download failed: $url" + rm -f "$tmp_file" + return 1 + fi + + # Validate + local fsize + fsize=$(stat -c%s "$tmp_file" 2>/dev/null || stat -f%z "$tmp_file" 2>/dev/null || wc -c < "$tmp_file" 2>/dev/null || echo 0) + if [ "$fsize" -lt 1000 ]; then + log_error "Downloaded file too small ($fsize bytes). Aborting." + rm -f "$tmp_file" + return 1 + fi + + # Extract + local tmp_extract + tmp_extract=$(mktemp -d "/tmp/paqet-update-extract-XXXXXXXX") + if ! tar -xzf "$tmp_file" -C "$tmp_extract" 2>/dev/null; then + log_error "Failed to extract archive" + rm -f "$tmp_file" + rm -rf "$tmp_extract" + return 1 + fi + + local binary_name="paqet_linux_${arch}" + local found_binary + found_binary=$(find "$tmp_extract" -name "$binary_name" -type f 2>/dev/null | head -1) + [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f -executable 2>/dev/null | head -1) + [ -z "$found_binary" ] && found_binary=$(find "$tmp_extract" -name "paqet*" -type f 2>/dev/null | head -1) + + if [ -z "$found_binary" ]; then + log_error "Could not find paqet binary in archive" + rm -f "$tmp_file" + rm -rf "$tmp_extract" + return 1 + fi + + # Stop service, replace, start + local was_running=false + if is_running; then + was_running=true + stop_paqet + fi + + # Backup old binary with version tag for rollback + if ! mkdir -p "$BACKUP_DIR"; then + log_warn "Failed to create backup directory" + fi + local old_ver="${PAQET_VERSION:-unknown}" + cp "$INSTALL_DIR/bin/paqet" "$BACKUP_DIR/paqet.${old_ver}.$(date +%Y%m%d%H%M%S)" 2>/dev/null || true + + if ! cp "$found_binary" "$INSTALL_DIR/bin/paqet"; then + log_error "Failed to copy new binary" + rm -f "$tmp_file" + rm -rf "$tmp_extract" + # Restore from backup + local latest_backup + latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1) + [ -n "$latest_backup" ] && cp "$latest_backup" "$INSTALL_DIR/bin/paqet" && chmod +x "$INSTALL_DIR/bin/paqet" + [ "$was_running" = true ] && start_paqet + return 1 + fi + chmod +x "$INSTALL_DIR/bin/paqet" + + rm -f "$tmp_file" + rm -rf "$tmp_extract" + + # Verify the new binary works + if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then + log_warn "New binary failed verification. Restoring backup..." + local latest_backup + latest_backup=$(ls -t "$BACKUP_DIR"/paqet.* 2>/dev/null | head -1) + if [ -n "$latest_backup" ]; then + cp "$latest_backup" "$INSTALL_DIR/bin/paqet" + chmod +x "$INSTALL_DIR/bin/paqet" + log_error "Update failed — previous version restored" + return 1 + fi + log_error "Update failed and no backup available" + return 1 + fi + + # Update version in settings + PAQET_VERSION="$target_tag" + _safe_update_setting "PAQET_VERSION" "$target_tag" "$INSTALL_DIR/settings.conf" + + log_success "paqet updated to ${target_tag}" + + if [ "$was_running" = true ]; then + start_paqet + fi +} + +rollback_paqet() { + echo "" + if [ ! -d "$BACKUP_DIR" ]; then + log_warn "No backups found" + return 1 + fi + + local backups=() + local i=1 + echo -e " ${BOLD}Available binary backups:${NC}" + echo "" + for f in "$BACKUP_DIR"/paqet.*; do + [ -f "$f" ] || continue + backups+=("$f") + local bname=$(basename "$f") + local bsize=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f" 2>/dev/null || wc -c < "$f" 2>/dev/null || echo "?") + echo " $i. $bname (${bsize} bytes)" + i=$((i + 1)) + done + + if [ ${#backups[@]} -eq 0 ]; then + log_warn "No binary backups found in $BACKUP_DIR" + return 1 + fi + + echo "" + echo " 0. Cancel" + echo "" + read -p " Select backup to restore [0-${#backups[@]}]: " choice < /dev/tty || true + if [ "$choice" = "0" ]; then + log_info "Cancelled" + return 0 + fi + if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then + log_error "Invalid choice" + return 1 + fi + + local selected="${backups[$((choice-1))]}" + log_info "Rolling back to: $(basename "$selected")" + + local was_running=false + is_running && was_running=true + [ "$was_running" = true ] && stop_paqet + + if ! cp "$selected" "$INSTALL_DIR/bin/paqet"; then + log_error "Failed to restore backup" + [ "$was_running" = true ] && start_paqet + return 1 + fi + chmod +x "$INSTALL_DIR/bin/paqet" + + # Verify restored binary + if ! "$INSTALL_DIR/bin/paqet" version &>/dev/null; then + log_warn "Restored binary failed verification (may need libpcap)" + fi + + # Try to extract version from the filename (format: paqet.vX.Y.Z.TIMESTAMP) + local restored_ver="" + local _bname + _bname=$(basename "$selected") + # Extract version: remove 'paqet.' prefix and '.YYYYMMDDHHMMSS' timestamp suffix + restored_ver=$(echo "$_bname" | sed 's/^paqet\.//' | sed 's/\.[0-9]\{14\}$//') + # Validate extracted version looks reasonable + if [ -n "$restored_ver" ] && [ "$restored_ver" != "backup" ] && [ "$restored_ver" != "$_bname" ]; then + if _validate_version_tag "$restored_ver"; then + PAQET_VERSION="$restored_ver" + _safe_update_setting "PAQET_VERSION" "$restored_ver" "$INSTALL_DIR/settings.conf" + log_info "Restored version: $restored_ver" + else + log_warn "Could not determine version from backup filename, keeping current version setting" + fi + else + log_warn "Could not extract version from backup filename" + fi + + log_success "Rolled back successfully" + + [ "$was_running" = true ] && start_paqet +} + +update_management_script() { + local update_url="https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/paqctl.sh" + local tmp_script + tmp_script=$(mktemp "/tmp/paqctl-update-XXXXXXXX.sh") + + log_info "Checking for management script updates..." + if ! curl -sL --max-time 30 --max-filesize 2097152 -o "$tmp_script" "$update_url" 2>/dev/null; then + log_warn "Could not check for script updates" + rm -f "$tmp_script" + return 0 + fi + + # Validate: must contain our markers, be a bash script, and pass syntax check + if ! head -n 1 "$tmp_script" 2>/dev/null | grep -q "^#!.*bash"; then + log_warn "Downloaded file is not a bash script, skipping" + rm -f "$tmp_script" + return 0 + fi + if grep -q "PAQET_REPO=" "$tmp_script" && \ + grep -q "create_management_script" "$tmp_script" && \ + grep -q "PAQCTL_VERSION=" "$tmp_script" && \ + bash -n "$tmp_script" 2>/dev/null; then + local _update_output + if _update_output=$(bash "$tmp_script" --update-components 2>&1); then + log_success "Management script updated" + else + log_warn "Management script update execution failed: ${_update_output:-unknown error}" + fi + else + log_warn "Downloaded script failed validation, skipping" + fi + rm -f "$tmp_script" +} + +#═══════════════════════════════════════════════════════════════════════ +# Secret Key Generation +#═══════════════════════════════════════════════════════════════════════ + +generate_secret() { + echo "" + local key + key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true) + if [ -z "$key" ]; then + key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32) + fi + echo -e " ${GREEN}${BOLD}New encryption key: ${key}${NC}" + echo "" + echo -e " ${DIM}Share this key securely with client users.${NC}" + echo "" +} + +#═══════════════════════════════════════════════════════════════════════ +# Firewall Display +#═══════════════════════════════════════════════════════════════════════ + +show_firewall() { + if [ "$ROLE" != "server" ] && [ "$BACKEND" != "gfw-knocker" ]; then + echo "" + log_info "Firewall rules only apply in server mode or GFK client mode" + echo "" + return + fi + + local redraw=true + while true; do + if [ "$redraw" = true ]; then + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} FIREWALL RULES${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + local _fw_backend="iptables" + _is_firewalld_active && _fw_backend="firewalld" + echo -e " ${DIM}Backend: ${_fw_backend}${NC}" + echo "" + + if [ "$BACKEND" = "gfw-knocker" ]; then + local vio_port + if [ "$ROLE" = "server" ]; then + vio_port="${GFK_VIO_PORT:-45000}" + else + vio_port="${GFK_VIO_CLIENT_PORT:-40000}" + fi + echo -e " ${BOLD}Required rules for VIO port ${vio_port}:${NC}" + echo "" + if [ "$_fw_backend" = "firewalld" ]; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $vio_port)" \ + || echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $vio_port) ${DIM}MISSING${NC}" + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $vio_port)" \ + || echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $vio_port) ${DIM}MISSING${NC}" + firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} INPUT DROP (dport $vio_port)" \ + || echo -e " ${RED}✗${NC} INPUT DROP (dport $vio_port) ${DIM}MISSING${NC}" + firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} RST DROP (sport $vio_port)" \ + || echo -e " ${RED}✗${NC} RST DROP (sport $vio_port) ${DIM}MISSING${NC}" + else + iptables -t raw -C PREROUTING -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $vio_port)" \ + || echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $vio_port) ${DIM}MISSING${NC}" + iptables -t raw -C OUTPUT -p tcp --sport "$vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $vio_port)" \ + || echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $vio_port) ${DIM}MISSING${NC}" + iptables -C INPUT -p tcp --dport "$vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} INPUT DROP (dport $vio_port)" \ + || echo -e " ${RED}✗${NC} INPUT DROP (dport $vio_port) ${DIM}MISSING${NC}" + iptables -C OUTPUT -p tcp --sport "$vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} RST DROP (sport $vio_port)" \ + || echo -e " ${RED}✗${NC} RST DROP (sport $vio_port) ${DIM}MISSING${NC}" + fi + else + local port="${LISTEN_PORT:-8443}" + echo -e " ${BOLD}Required rules for port ${port}:${NC}" + echo "" + if [ "$_fw_backend" = "firewalld" ]; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)" \ + || echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $port) ${DIM}MISSING${NC}" + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)" \ + || echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $port) ${DIM}MISSING${NC}" + firewall-cmd --direct --query-rule ipv4 mangle OUTPUT 0 -p tcp --sport "$port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} RST DROP (sport $port)" \ + || echo -e " ${RED}✗${NC} RST DROP (sport $port) ${DIM}MISSING${NC}" + else + iptables -t raw -C PREROUTING -p tcp --dport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} PREROUTING NOTRACK (dport $port)" \ + || echo -e " ${RED}✗${NC} PREROUTING NOTRACK (dport $port) ${DIM}MISSING${NC}" + iptables -t raw -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} OUTPUT NOTRACK (sport $port)" \ + || echo -e " ${RED}✗${NC} OUTPUT NOTRACK (sport $port) ${DIM}MISSING${NC}" + iptables -t mangle -C OUTPUT -p tcp --sport "$port" -m comment --comment "paqctl" --tcp-flags RST RST -j DROP 2>/dev/null \ + && echo -e " ${GREEN}✓${NC} RST DROP (sport $port)" \ + || echo -e " ${RED}✗${NC} RST DROP (sport $port) ${DIM}MISSING${NC}" + fi + fi + + echo "" + echo -e " ${BOLD}Actions:${NC}" + echo " 1. Apply missing rules" + echo " 2. Remove all rules" + echo " b. Back" + echo "" + redraw=false + fi + + read -p " Choice: " fw_choice < /dev/tty || break + case "$fw_choice" in + 1) + _apply_firewall + _persist_firewall + log_success "Firewall rules applied and persisted" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 2) + _remove_firewall + _persist_firewall + log_success "Firewall rules removed" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + b|B) return ;; + "") ;; + *) echo -e " ${RED}Invalid choice${NC}" ;; + esac + done +} + +#═══════════════════════════════════════════════════════════════════════ +# Configuration +#═══════════════════════════════════════════════════════════════════════ + +_change_config_gfk() { + local was_running="$1" + echo "" + echo -e "${BOLD}Select role:${NC}" + echo " 1. Server" + echo " 2. Client" + echo "" + local role_choice + read -p " Enter choice [1/2]: " role_choice < /dev/tty || true + case "$role_choice" in + 1) ROLE="server" ;; + 2) ROLE="client" ;; + *) log_warn "Invalid. Keeping current role: $ROLE" ;; + esac + + if [ "$ROLE" = "server" ]; then + echo -e "${BOLD}Server public IP${NC} [${GFK_SERVER_IP}]:" + read -p " IP: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_ip "$input"; then + log_error "Invalid IP address"; return 1 + fi + [ -n "$input" ] && GFK_SERVER_IP="$input" + + echo -e "${BOLD}VIO TCP port${NC} [${GFK_VIO_PORT:-45000}]:" + read -p " Port: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_port "$input"; then + log_error "Invalid port number"; return 1 + fi + [ -n "$input" ] && GFK_VIO_PORT="$input" + + echo -e "${BOLD}QUIC port${NC} [${GFK_QUIC_PORT:-25000}]:" + read -p " Port: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_port "$input"; then + log_error "Invalid port number"; return 1 + fi + [ -n "$input" ] && GFK_QUIC_PORT="$input" + + echo -e "${BOLD}Auth code${NC} [keep current]:" + read -p " Code: " input < /dev/tty || true + [ -n "$input" ] && GFK_AUTH_CODE="$input" + + echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:" + read -p " Mappings: " input < /dev/tty || true + [ -n "$input" ] && GFK_PORT_MAPPINGS="$input" + + echo -e "${BOLD}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:" + echo -e " ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}" + echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}" + read -p " Flags: " input < /dev/tty || true + if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then + log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1 + fi + [ -n "$input" ] && GFK_TCP_FLAGS="$input" + else + echo -e "${BOLD}Server IP${NC} [${GFK_SERVER_IP}]:" + read -p " IP: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_ip "$input"; then + log_error "Invalid IP address"; return 1 + fi + [ -n "$input" ] && GFK_SERVER_IP="$input" + + echo -e "${BOLD}Server's VIO TCP port${NC} [${GFK_VIO_PORT:-45000}] (must match server):" + read -p " Port: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_port "$input"; then + log_error "Invalid port number"; return 1 + fi + [ -n "$input" ] && GFK_VIO_PORT="$input" + + echo -e "${BOLD}Local VIO client port${NC} [${GFK_VIO_CLIENT_PORT:-40000}]:" + read -p " Port: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_port "$input"; then + log_error "Invalid port number"; return 1 + fi + [ -n "$input" ] && GFK_VIO_CLIENT_PORT="$input" + + echo -e "${BOLD}Server's QUIC port${NC} [${GFK_QUIC_PORT:-25000}] (must match server):" + read -p " Port: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_port "$input"; then + log_error "Invalid port number"; return 1 + fi + [ -n "$input" ] && GFK_QUIC_PORT="$input" + + echo -e "${BOLD}Local QUIC client port${NC} [${GFK_QUIC_CLIENT_PORT:-20000}]:" + read -p " Port: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_port "$input"; then + log_error "Invalid port number"; return 1 + fi + [ -n "$input" ] && GFK_QUIC_CLIENT_PORT="$input" + + echo -e "${BOLD}Auth code${NC}:" + read -p " Code: " input < /dev/tty || true + [ -n "$input" ] && GFK_AUTH_CODE="$input" + + echo -e "${BOLD}Port mappings${NC} [${GFK_PORT_MAPPINGS:-14000:443}]:" + read -p " Mappings: " input < /dev/tty || true + [ -n "$input" ] && GFK_PORT_MAPPINGS="$input" + + echo -e "${BOLD}Outgoing TCP flags${NC} [${GFK_TCP_FLAGS:-AP}]:" + echo -e " ${DIM}Controls TCP flags on outgoing violated packets (default: AP)${NC}" + echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG)${NC}" + read -p " Flags: " input < /dev/tty || true + if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+$ ]]; then + log_error "Invalid flags. Use uppercase letters only: F, S, R, P, A, U, E, C"; return 1 + fi + [ -n "$input" ] && GFK_TCP_FLAGS="$input" + fi + + # Regenerate parameters.py + generate_gfk_config || { [ "$was_running" = true ] && start_paqet; return 1; } + + # Regenerate wrapper if client + if [ "$ROLE" = "client" ]; then + create_gfk_client_wrapper + fi + + # Save settings + local IFACE="" GW_MAC="" + save_settings + + # Re-apply firewall + _apply_firewall + + # Restart + [ "$was_running" = true ] && start_paqet + log_success "GFW-knocker configuration updated" +} + +change_config() { + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} CHANGE CONFIGURATION${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + local _warn_text="config" + [ "$BACKEND" = "gfw-knocker" ] && _warn_text="parameters.py" + echo -e " ${YELLOW}Warning: This will regenerate ${_warn_text} and restart ${BACKEND}.${NC}" + echo "" + read -p " Continue? [y/N]: " confirm < /dev/tty || true + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + return 0 + fi + + local was_running=false + is_running && was_running=true + [ "$was_running" = true ] && stop_paqet + + if [ "$BACKEND" = "gfw-knocker" ]; then + _remove_firewall + _change_config_gfk "$was_running" + return + fi + + # Remove old firewall rules (save old port before user changes it) + local _saved_port="$LISTEN_PORT" + if [ "$ROLE" = "server" ] && [ -n "$_saved_port" ]; then + _remove_firewall + fi + + # Re-run wizard (inline version) + echo "" + echo -e "${BOLD}Select role:${NC}" + echo " 1. Server" + echo " 2. Client" + echo "" + local role_choice + read -p " Enter choice [1/2]: " role_choice < /dev/tty || true + case "$role_choice" in + 1) ROLE="server" ;; + 2) ROLE="client" ;; + *) + log_warn "Invalid choice. Defaulting to server." + ROLE="server" + ;; + esac + + # Detect network + local _iface=$(ip route show default 2>/dev/null | awk '{print $5; exit}') + # Note: grep returns exit 1 if no matches, so we add || true for pipefail + local _ip=$(ip -4 addr show "$_iface" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1 | { grep -o '[0-9.]*' || true; } | head -1) + local _gw=$(ip route show default 2>/dev/null | awk '{print $3; exit}') + local _gw_mac="" + [ -n "$_gw" ] && _gw_mac=$(ip neigh show "$_gw" 2>/dev/null | awk '/lladdr/{print $5; exit}') + + echo "" + echo -e "${BOLD}Interface${NC} [${_iface:-$INTERFACE}]:" + read -p " Interface: " input < /dev/tty || true + INTERFACE="${input:-${_iface:-$INTERFACE}}" + + echo -e "${BOLD}Local IP${NC} [${_ip:-$LOCAL_IP}]:" + read -p " IP: " input < /dev/tty || true + LOCAL_IP="${input:-${_ip:-$LOCAL_IP}}" + + echo -e "${BOLD}Gateway MAC${NC} [${_gw_mac:-$GATEWAY_MAC}]:" + read -p " MAC: " input < /dev/tty || true + GATEWAY_MAC="${input:-${_gw_mac:-$GATEWAY_MAC}}" + if [ -n "$GATEWAY_MAC" ] && ! _validate_mac "$GATEWAY_MAC"; then + log_warn "Invalid MAC address format (expected: aa:bb:cc:dd:ee:ff)" + read -p " Enter valid MAC address: " input < /dev/tty || true + if [ -n "$input" ] && ! _validate_mac "$input"; then + log_warn "Invalid MAC format, keeping current value" + input="" + fi + [ -n "$input" ] && GATEWAY_MAC="$input" + fi + + if [ "$ROLE" = "server" ]; then + echo -e "${BOLD}Port${NC} [${LISTEN_PORT:-8443}]:" + read -p " Port: " input < /dev/tty || true + LISTEN_PORT="${input:-${LISTEN_PORT:-8443}}" + if ! _validate_port "$LISTEN_PORT"; then + log_warn "Invalid port. Using default 8443." + LISTEN_PORT=8443 + fi + + echo -e "${BOLD}Encryption key${NC} [keep current]:" + read -p " Key (enter to keep): " input < /dev/tty || true + [ -n "$input" ] && ENCRYPTION_KEY="$input" + REMOTE_SERVER="" + SOCKS_PORT="" + else + echo -e "${BOLD}Remote server${NC} (IP:PORT):" + read -p " Server: " input < /dev/tty || true + REMOTE_SERVER="${input:-$REMOTE_SERVER}" + + echo -e "${BOLD}Encryption key${NC}:" + read -p " Key: " input < /dev/tty || true + [ -n "$input" ] && ENCRYPTION_KEY="$input" + + echo -e "${BOLD}SOCKS5 port${NC} [${SOCKS_PORT:-1080}]:" + read -p " Port: " input < /dev/tty || true + SOCKS_PORT="${input:-${SOCKS_PORT:-1080}}" + LISTEN_PORT="" + fi + + # TCP flags (for both server and client) + echo -e "${BOLD}TCP local flag${NC} [${PAQET_TCP_LOCAL_FLAG:-PA}]:" + echo -e " ${DIM}Controls TCP flags on outgoing packets (default: PA = PSH+ACK)${NC}" + echo -e " ${DIM}Valid flags: S(SYN) A(ACK) P(PSH) R(RST) F(FIN) U(URG) E(ECE) C(CWR)${NC}" + echo -e " ${DIM}Multiple values: PA,A (tries PA first, then A)${NC}" + read -p " Flag: " input < /dev/tty || true + if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then + log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value." + input="" + fi + [ -n "$input" ] && PAQET_TCP_LOCAL_FLAG="$input" + + echo -e "${BOLD}TCP remote flag${NC} [${PAQET_TCP_REMOTE_FLAG:-PA}]:" + echo -e " ${DIM}Controls expected TCP flags on incoming packets (default: PA)${NC}" + echo -e " ${DIM}Should match the server/client counterpart's local flag${NC}" + read -p " Flag: " input < /dev/tty || true + if [ -n "$input" ] && ! [[ "$input" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]]; then + log_warn "Invalid flags. Use: FSRPAUEC (e.g., PA or PA,A). Keeping current value." + input="" + fi + [ -n "$input" ] && PAQET_TCP_REMOTE_FLAG="$input" + + # Save + local IFACE="$INTERFACE" + local GW_MAC="$GATEWAY_MAC" + # Regenerate YAML + local tmp_conf + tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") + # Validate required fields + if [ -z "$INTERFACE" ] || [ -z "$LOCAL_IP" ] || [ -z "$GATEWAY_MAC" ] || [ -z "$ENCRYPTION_KEY" ]; then + log_error "Missing required configuration fields" + rm -f "$tmp_conf" + [ "$was_running" = true ] && start_paqet + return 1 + fi + + # Escape YAML special characters to prevent injection + _escape_yaml() { + local s="$1" + if [[ "$s" =~ [:\#\[\]{}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then + s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s" + else + printf '%s' "$s" + fi + } + # Set permissions before writing + chmod 600 "$tmp_conf" 2>/dev/null + ( + umask 077 + local _y_iface _y_ip _y_mac _y_key _y_server _tcp_local_flags _tcp_remote_flags + _y_iface=$(_escape_yaml "$INTERFACE") + _y_ip=$(_escape_yaml "$LOCAL_IP") + _y_mac=$(_escape_yaml "$GATEWAY_MAC") + _y_key=$(_escape_yaml "$ENCRYPTION_KEY") + _tcp_local_flags=$(echo "${PAQET_TCP_LOCAL_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/') + _tcp_remote_flags=$(echo "${PAQET_TCP_REMOTE_FLAG:-PA}" | sed 's/,/", "/g; s/.*/["&"]/') + if [ "$ROLE" = "server" ]; then + cat > "$tmp_conf" << EOF +role: "server" + +log: + level: "info" + +listen: + addr: ":${LISTEN_PORT}" + +network: + interface: "${_y_iface}" + ipv4: + addr: "${_y_ip}:${LISTEN_PORT}" + router_mac: "${_y_mac}" + tcp: + local_flag: ${_tcp_local_flags} + remote_flag: ${_tcp_remote_flags} + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_y_key}" +EOF + else + _y_server=$(_escape_yaml "$REMOTE_SERVER") + cat > "$tmp_conf" << EOF +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:${SOCKS_PORT}" + +network: + interface: "${_y_iface}" + ipv4: + addr: "${_y_ip}:0" + router_mac: "${_y_mac}" + tcp: + local_flag: ${_tcp_local_flags} + remote_flag: ${_tcp_remote_flags} + +server: + addr: "${_y_server}" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_y_key}" +EOF + fi + ) + if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then + log_error "Failed to save configuration" + rm -f "$tmp_conf" + [ "$was_running" = true ] && start_paqet + return 1 + fi + chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null + + # Save settings + local _tmp + _tmp=$(mktemp "$INSTALL_DIR/settings.conf.XXXXXXXX") + # Read current telegram settings + local _tg_token="${TELEGRAM_BOT_TOKEN:-}" + local _tg_chat="${TELEGRAM_CHAT_ID:-}" + local _tg_interval="${TELEGRAM_INTERVAL:-6}" + local _tg_enabled="${TELEGRAM_ENABLED:-false}" + local _tg_alerts="${TELEGRAM_ALERTS_ENABLED:-true}" + local _tg_daily="${TELEGRAM_DAILY_SUMMARY:-true}" + local _tg_weekly="${TELEGRAM_WEEKLY_SUMMARY:-true}" + local _tg_label="${TELEGRAM_SERVER_LABEL:-}" + local _tg_start_hour="${TELEGRAM_START_HOUR:-0}" + ( + umask 077 + cat > "$_tmp" << EOF +BACKEND="${BACKEND:-paqet}" +ROLE="${ROLE}" +PAQET_VERSION="${PAQET_VERSION}" +PAQCTL_VERSION="${VERSION}" +LISTEN_PORT="${LISTEN_PORT:-}" +SOCKS_PORT="${SOCKS_PORT:-}" +INTERFACE="${INTERFACE}" +LOCAL_IP="${LOCAL_IP}" +GATEWAY_MAC="${GATEWAY_MAC}" +ENCRYPTION_KEY="${ENCRYPTION_KEY}" +REMOTE_SERVER="${REMOTE_SERVER:-}" +GFK_VIO_PORT="${GFK_VIO_PORT:-}" +GFK_QUIC_PORT="${GFK_QUIC_PORT:-}" +GFK_AUTH_CODE="${GFK_AUTH_CODE:-}" +GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS:-}" +GFK_SOCKS_PORT="${GFK_SOCKS_PORT:-}" +GFK_SOCKS_VIO_PORT="${GFK_SOCKS_VIO_PORT:-}" +XRAY_PANEL_DETECTED="${XRAY_PANEL_DETECTED:-false}" +MICROSOCKS_PORT="${MICROSOCKS_PORT:-}" +GFK_SERVER_IP="${GFK_SERVER_IP:-}" +GFK_TCP_FLAGS="${GFK_TCP_FLAGS:-AP}" +PAQET_TCP_LOCAL_FLAG="${PAQET_TCP_LOCAL_FLAG:-PA}" +PAQET_TCP_REMOTE_FLAG="${PAQET_TCP_REMOTE_FLAG:-PA}" +TELEGRAM_BOT_TOKEN="${_tg_token}" +TELEGRAM_CHAT_ID="${_tg_chat}" +TELEGRAM_INTERVAL=${_tg_interval} +TELEGRAM_ENABLED=${_tg_enabled} +TELEGRAM_ALERTS_ENABLED=${_tg_alerts} +TELEGRAM_DAILY_SUMMARY=${_tg_daily} +TELEGRAM_WEEKLY_SUMMARY=${_tg_weekly} +TELEGRAM_SERVER_LABEL="${_tg_label}" +TELEGRAM_START_HOUR=${_tg_start_hour} +EOF + ) + if ! mv "$_tmp" "$INSTALL_DIR/settings.conf"; then + log_error "Failed to save settings" + rm -f "$_tmp" + fi + chmod 600 "$INSTALL_DIR/settings.conf" 2>/dev/null + + log_success "Configuration updated" + + if [ "$was_running" = true ]; then + start_paqet + fi +} + +#═══════════════════════════════════════════════════════════════════════ +# Backup & Restore +#═══════════════════════════════════════════════════════════════════════ + +backup_config() { + (umask 077; mkdir -p "$BACKUP_DIR") + chmod 700 "$BACKUP_DIR" 2>/dev/null + local ts=$(date +%Y%m%d%H%M%S) + local backup_file="$BACKUP_DIR/paqctl-backup-${ts}.tar.gz" + + if ! (umask 077; tar -czf "$backup_file" \ + -C "$INSTALL_DIR" \ + config.yaml settings.conf 2>/dev/null); then + log_error "Failed to create backup archive" + rm -f "$backup_file" + return 1 + fi + echo "" + log_success "Backup saved to: $backup_file" + echo "" +} + +restore_config() { + echo "" + if [ ! -d "$BACKUP_DIR" ] || [ -z "$(ls -A "$BACKUP_DIR"/*.tar.gz 2>/dev/null)" ]; then + log_warn "No backups found in $BACKUP_DIR" + return 1 + fi + + echo -e "${BOLD}Available backups:${NC}" + echo "" + local i=1 + local backups=() + for f in "$BACKUP_DIR"/*.tar.gz; do + backups+=("$f") + echo " $i. $(basename "$f")" + i=$((i + 1)) + done + echo "" + echo " 0. Cancel" + echo "" + read -p " Select backup [0-${#backups[@]}]: " choice < /dev/tty || true + if [ "$choice" = "0" ]; then + log_info "Cancelled" + return 0 + fi + if ! [[ "$choice" =~ ^[0-9]+$ ]] || [ "$choice" -lt 1 ] || [ "$choice" -gt ${#backups[@]} ]; then + log_error "Invalid choice" + return 1 + fi + + local selected="${backups[$((choice-1))]}" + log_info "Restoring from: $(basename "$selected")" + + local was_running=false + is_running && was_running=true + [ "$was_running" = true ] && stop_paqet + + if ! (umask 077; tar -xzf "$selected" -C "$INSTALL_DIR" 2>/dev/null); then + log_error "Failed to extract backup archive" + [ "$was_running" = true ] && start_paqet + return 1 + fi + chmod 600 "$INSTALL_DIR/config.yaml" "$INSTALL_DIR/settings.conf" 2>/dev/null + chown root:root "$INSTALL_DIR/config.yaml" "$INSTALL_DIR/settings.conf" 2>/dev/null + + # Reload settings + _load_settings + + log_success "Configuration restored" + + [ "$was_running" = true ] && start_paqet +} + +#═══════════════════════════════════════════════════════════════════════ +# Telegram Integration +#═══════════════════════════════════════════════════════════════════════ + +# Secure Telegram API curl - writes token to temp file to avoid /proc exposure +_telegram_api_curl() { + local endpoint="$1" + shift + local _tg_tmp + _tg_tmp=$(mktemp "${INSTALL_DIR}/.tg_curl.XXXXXXXX") || return 1 + chmod 600 "$_tg_tmp" 2>/dev/null + printf 'url = "https://api.telegram.org/bot%s/%s"\n' "$TELEGRAM_BOT_TOKEN" "$endpoint" > "$_tg_tmp" + local _result + _result=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_tg_tmp" "$@" 2>/dev/null) + local _exit=$? + rm -f "$_tg_tmp" + [ $_exit -eq 0 ] && echo "$_result" + return $_exit +} + +escape_telegram_markdown() { + local text="$1" + text="${text//\\/\\\\}" + text="${text//\*/\\*}" + text="${text//_/\\_}" + text="${text//\`/\\\`}" + text="${text//\[/\\[}" + text="${text//\]/\\]}" + echo "$text" +} + +telegram_send_message() { + local message="$1" + { [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && return 1 + local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}" + label=$(escape_telegram_markdown "$label") + local _ip=$(curl -s --max-time 3 https://api.ipify.org 2>/dev/null || echo "") + if [ -n "$_ip" ]; then + message="[${label} | ${_ip}] ${message}" + else + message="[${label}] ${message}" + fi + local response + response=$(_telegram_api_curl "sendMessage" \ + -X POST \ + --data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \ + --data-urlencode "text=$message" \ + --data-urlencode "parse_mode=Markdown") + [ $? -ne 0 ] && return 1 + echo "$response" | grep -q '"ok":true' && return 0 + return 1 +} + +telegram_get_chat_id() { + local response + response=$(_telegram_api_curl "getUpdates") + [ -z "$response" ] && return 1 + echo "$response" | grep -q '"ok":true' || return 1 + local chat_id="" + if command -v python3 &>/dev/null; then + chat_id=$(python3 -c " +import json,sys +try: + d=json.loads(sys.stdin.read()) + msgs=d.get('result',[]) + if msgs: + print(msgs[-1]['message']['chat']['id']) +except: pass +" <<< "$response" 2>/dev/null) + fi + if [ -z "$chat_id" ]; then + chat_id=$(echo "$response" | grep -o '"chat"[[:space:]]*:[[:space:]]*{[[:space:]]*"id"[[:space:]]*:[[:space:]]*-\?[0-9]\+' | grep -o -- '-\?[0-9]\+$' | tail -1 2>/dev/null) + fi + if [ -n "$chat_id" ] && echo "$chat_id" | grep -qE '^-?[0-9]+$'; then + TELEGRAM_CHAT_ID="$chat_id" + return 0 + fi + return 1 +} + +telegram_build_report() { + local report="📊 *Paqet Status Report*" + report+=$'\n' + report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')" + report+=$'\n\n' + + if is_running; then + report+="✅ Status: Running" + else + report+="❌ Status: Stopped" + fi + report+=$'\n' + report+="📡 Role: ${ROLE}" + report+=$'\n' + report+="📦 Version: ${PAQET_VERSION}" + report+=$'\n' + + if [ "$ROLE" = "server" ]; then + report+="🔌 Port: ${LISTEN_PORT}" + report+=$'\n' + if iptables -t raw -C PREROUTING -p tcp --dport "$LISTEN_PORT" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null; then + report+="🛡 Firewall: Rules active" + else + report+="⚠️ Firewall: Rules missing" + fi + else + report+="🔗 Server: ${REMOTE_SERVER}" + report+=$'\n' + report+="🧦 SOCKS: port ${SOCKS_PORT}" + fi + report+=$'\n' + + # Uptime + if is_running && command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + local started + started=$(systemctl show paqctl.service --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2) + if [ -n "$started" ]; then + local started_ts + started_ts=$(date -d "$started" +%s 2>/dev/null || echo 0) + if [ "$started_ts" -gt 0 ] 2>/dev/null; then + local now=$(date +%s) + local up=$((now - started_ts)) + local days=$((up / 86400)) + local hours=$(( (up % 86400) / 3600 )) + local mins=$(( (up % 3600) / 60 )) + if [ "$days" -gt 0 ]; then + report+="⏱ Uptime: ${days}d ${hours}h ${mins}m" + else + report+="⏱ Uptime: ${hours}h ${mins}m" + fi + report+=$'\n' + fi + fi + fi + + # CPU/RAM + local pid + if [ "$BACKEND" = "gfw-knocker" ]; then + pid=$(pgrep -f "mainserver.py|mainclient.py" 2>/dev/null | head -1) + else + pid=$(pgrep -f "paqet run -c" 2>/dev/null | head -1) + fi + if [ -n "$pid" ]; then + local cpu_mem + cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1) + if [ -n "$cpu_mem" ]; then + local cpu=$(echo "$cpu_mem" | awk '{print $1}') + local mem=$(echo "$cpu_mem" | awk '{print $2}') + report+="💻 CPU: ${cpu}% | RAM: ${mem}%" + report+=$'\n' + fi + fi + + echo "$report" +} + +telegram_test_message() { + local interval_label="${TELEGRAM_INTERVAL:-6}" + local report=$(telegram_build_report) + local backend_name="${BACKEND:-paqet}" + + # Backend-specific description + local tech_desc="" + if [ "$BACKEND" = "gfw-knocker" ]; then + tech_desc="🔗 *What is GFW-Knocker?* +An advanced anti-censorship tool using 'violated TCP' packets + QUIC tunneling. +Designed for heavy DPI environments like the Great Firewall. +• Raw socket layer bypasses kernel TCP stack +• QUIC tunnel provides encrypted transport +• Requires Xray on server for SOCKS5 proxy" + else + tech_desc="🔗 *What is Paqet?* +A raw-socket encrypted proxy using KCP protocol. +Simple all-in-one solution with built-in SOCKS5 proxy. +• KCP over raw TCP packets with custom flags bypasses DPI +• Built-in SOCKS5 proxy (no extra software needed) +• Easy setup with just IP, port, and key" + fi + + local message="✅ *paqctl Connected!* + +📦 *About paqctl* +A unified management tool for bypass proxies. +Supports two backends for different network conditions: +• *paqet* — Simple KCP-based proxy (recommended) +• *gfw-knocker* — Advanced violated-TCP + QUIC tunnel + +━━━━━━━━━━━━━━━━━━━━ +${tech_desc} + +📬 *What this bot sends you every ${interval_label}h:* +• Service status & uptime +• CPU & RAM usage +• Configuration summary +• Firewall rule status + +⚠️ *Alerts:* +If the service goes down or is restarted, you will receive an immediate alert. + +━━━━━━━━━━━━━━━━━━━━ +🎮 *Available Commands:* +━━━━━━━━━━━━━━━━━━━━ +/status — Full status report +/health — Run health check +/restart — Restart ${backend_name} +/stop — Stop ${backend_name} +/start — Start ${backend_name} +/version — Show version info + +━━━━━━━━━━━━━━━━━━━━ +📊 *Your first report:* +━━━━━━━━━━━━━━━━━━━━ + +${report}" + telegram_send_message "$message" +} + +telegram_generate_notify_script() { + local script_path="$INSTALL_DIR/paqctl-telegram.sh" + local _tmp + _tmp=$(mktemp "${script_path}.XXXXXXXX") + cat > "$_tmp" << 'TGSCRIPT' +#!/bin/bash +# paqctl Telegram notification daemon + +INSTALL_DIR="REPLACE_ME_INSTALL_DIR" + +# Safe settings loader - parses key=value with validation +_load_settings() { + [ -f "$INSTALL_DIR/settings.conf" ] || return 0 + while IFS='=' read -r key value; do + [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue + value="${value#\"}"; value="${value%\"}" + # Skip values with dangerous shell characters + [[ "$value" =~ [\`\$\(] ]] && continue + case "$key" in + BACKEND|ROLE|PAQET_VERSION|PAQCTL_VERSION|INTERFACE|LOCAL_IP|GATEWAY_MAC|\ + ENCRYPTION_KEY|REMOTE_SERVER|GFK_AUTH_CODE|GFK_PORT_MAPPINGS|GFK_SERVER_IP|\ + XRAY_PANEL_DETECTED|\ + TELEGRAM_BOT_TOKEN|TELEGRAM_CHAT_ID|TELEGRAM_SERVER_LABEL|\ + TELEGRAM_ENABLED|TELEGRAM_ALERTS_ENABLED|TELEGRAM_DAILY_SUMMARY|TELEGRAM_WEEKLY_SUMMARY) + export "$key=$value" ;; + LISTEN_PORT|SOCKS_PORT|GFK_VIO_PORT|GFK_VIO_CLIENT_PORT|GFK_QUIC_PORT|GFK_QUIC_CLIENT_PORT|MICROSOCKS_PORT|\ + GFK_SOCKS_PORT|GFK_SOCKS_VIO_PORT|\ + TELEGRAM_INTERVAL|TELEGRAM_START_HOUR) + [[ "$value" =~ ^[0-9]*$ ]] && export "$key=$value" ;; + esac + done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf") +} +_load_settings + +TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-} +TELEGRAM_CHAT_ID=${TELEGRAM_CHAT_ID:-} +TELEGRAM_INTERVAL=${TELEGRAM_INTERVAL:-6} +TELEGRAM_ALERTS_ENABLED=${TELEGRAM_ALERTS_ENABLED:-true} +TELEGRAM_DAILY_SUMMARY=${TELEGRAM_DAILY_SUMMARY:-true} +TELEGRAM_WEEKLY_SUMMARY=${TELEGRAM_WEEKLY_SUMMARY:-true} +TELEGRAM_START_HOUR=${TELEGRAM_START_HOUR:-0} + +{ [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && exit 1 + +escape_telegram_markdown() { + local text="$1" + text="${text//\\/\\\\}" + text="${text//\*/\\*}" + text="${text//_/\\_}" + text="${text//\`/\\\`}" + text="${text//\[/\\[}" + text="${text//\]/\\]}" + echo "$text" +} + +# Secure Telegram API curl - writes token to temp file to avoid /proc exposure +_tg_api_curl() { + local endpoint="$1" + shift + local _tg_tmp + _tg_tmp=$(mktemp "${INSTALL_DIR}/.tg_curl.XXXXXXXX") || return 1 + chmod 600 "$_tg_tmp" 2>/dev/null + printf 'url = "https://api.telegram.org/bot%s/%s"\n' "$TELEGRAM_BOT_TOKEN" "$endpoint" > "$_tg_tmp" + local _result + _result=$(curl -s --max-time 10 --max-filesize 1048576 -K "$_tg_tmp" "$@" 2>/dev/null) + local _exit=$? + rm -f "$_tg_tmp" + [ $_exit -eq 0 ] && echo "$_result" + return $_exit +} + +send_message() { + local message="$1" + { [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; } && return 1 + local label="${TELEGRAM_SERVER_LABEL:-$(hostname 2>/dev/null || echo 'unknown')}" + label=$(escape_telegram_markdown "$label") + local _ip=$(curl -s --max-time 3 https://api.ipify.org 2>/dev/null || echo "") + [ -n "$_ip" ] && message="[${label} | ${_ip}] ${message}" || message="[${label}] ${message}" + local response + response=$(_tg_api_curl "sendMessage" \ + -X POST \ + --data-urlencode "chat_id=$TELEGRAM_CHAT_ID" \ + --data-urlencode "text=$message" \ + --data-urlencode "parse_mode=Markdown") + [ $? -ne 0 ] && return 1 + echo "$response" | grep -q '"ok":true' && return 0 + return 1 +} + +is_running() { + if [ "$BACKEND" = "gfw-knocker" ]; then + pgrep -f "mainserver.py|mainclient.py|gfk-client.sh" &>/dev/null + else + pgrep -f "paqet run -c" &>/dev/null + fi +} + +get_main_pid() { + if [ "$BACKEND" = "gfw-knocker" ]; then + pgrep -f "mainserver.py" 2>/dev/null | head -1 + else + pgrep -f "paqet run -c" 2>/dev/null | head -1 + fi +} + +build_report() { + local report="📊 *${BACKEND} Status Report*"$'\n' + report+="🕐 $(date '+%Y-%m-%d %H:%M %Z')"$'\n\n' + if is_running; then + report+="✅ Status: Running" + else + report+="❌ Status: Stopped" + fi + report+=$'\n'"📡 Role: ${ROLE:-unknown}"$'\n' + report+="📦 Version: ${PAQET_VERSION:-unknown}"$'\n' + local pid=$(get_main_pid) + if [ -n "$pid" ]; then + local cpu_mem=$(ps -p "$pid" -o %cpu=,%mem= 2>/dev/null | head -1) + if [ -n "$cpu_mem" ]; then + local cpu=$(echo "$cpu_mem" | awk '{print $1}') + local mem=$(echo "$cpu_mem" | awk '{print $2}') + report+="💻 CPU: ${cpu}% | RAM: ${mem}%"$'\n' + fi + fi + echo "$report" +} + +LAST_COMMAND_TIME=0 +COMMAND_COOLDOWN=5 + +check_commands() { + local response + response=$(_tg_api_curl "getUpdates" \ + -X POST \ + --data-urlencode "offset=${LAST_UPDATE_ID:-0}" \ + --data-urlencode "limit=10") + [ -z "$response" ] && return + echo "$response" | grep -q '"ok":true' || return + + if command -v python3 &>/dev/null; then + local cmds + local _safe_chat_id + _safe_chat_id=$(printf '%s' "$TELEGRAM_CHAT_ID" | tr -cd '0-9-') + [ -z "$_safe_chat_id" ] && return + cmds=$(python3 -c " +import json,sys +try: + d=json.loads(sys.stdin.read()) + chat_id=sys.argv[1] + if not chat_id: sys.exit(0) + for r in d.get('result',[]): + uid=r['update_id'] + txt=r.get('message',{}).get('text','').replace('|','') + cid=str(r.get('message',{}).get('chat',{}).get('id','')) + if cid==chat_id and txt.startswith('/'): + print(f'{uid}|{txt}') +except: pass +" "$_safe_chat_id" <<< "$response" 2>/dev/null) + + while IFS='|' read -r uid cmd; do + [ -z "$uid" ] && continue + # Validate uid is numeric + [[ "$uid" =~ ^[0-9]+$ ]] || continue + LAST_UPDATE_ID=$((uid + 1)) + cmd="${cmd%% *}" # strip arguments, match command only + + # Rate limiting + local _now + _now=$(date +%s) + if [ $((_now - LAST_COMMAND_TIME)) -lt $COMMAND_COOLDOWN ]; then + continue + fi + LAST_COMMAND_TIME=$_now + + case "$cmd" in + /status) send_message "$(build_report)" ;; + /health) send_message "$(/usr/local/bin/paqctl health 2>&1 | head -30)" ;; + /restart) /usr/local/bin/paqctl restart 2>&1; send_message "🔄 Service restarted" ;; + /stop) /usr/local/bin/paqctl stop 2>&1; send_message "⏹ Service stopped" ;; + /start) /usr/local/bin/paqctl start 2>&1; send_message "▶️ Service started" ;; + /version) send_message "📦 Version: ${PAQET_VERSION:-unknown} | paqctl: ${PAQCTL_VERSION:-unknown}" ;; + esac + done <<< "$cmds" + fi +} + +# Alert state +LAST_STATE="unknown" +LAST_REPORT=0 +LAST_DAILY=0 +LAST_WEEKLY=0 +LAST_UPDATE_ID=0 + +# Initialize update offset +init_response=$(_tg_api_curl "getUpdates" \ + -X POST \ + --data-urlencode "offset=-1") +if command -v python3 &>/dev/null; then + LAST_UPDATE_ID=$(python3 -c " +import json,sys +try: + d=json.loads(sys.stdin.read()) + r=d.get('result',[]) + if r: print(r[-1]['update_id']+1) + else: print(0) +except: print(0) +" <<< "$init_response" 2>/dev/null) +fi +LAST_UPDATE_ID=${LAST_UPDATE_ID:-0} + +# Send startup notification +send_message "🚀 *Telegram notifications started*"$'\n'"Reports every ${TELEGRAM_INTERVAL}h | Alerts: ${TELEGRAM_ALERTS_ENABLED}" + +while true; do + # Reload settings periodically (safe parser, no code execution) + _load_settings + + # Check commands from Telegram + check_commands + + # Service state alerts + current_state="stopped" + is_running && current_state="running" + + if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ]; then + if [ "$LAST_STATE" = "running" ] && [ "$current_state" = "stopped" ]; then + send_message "🚨 *ALERT:* ${BACKEND} service has stopped!" + elif [ "$LAST_STATE" = "stopped" ] && [ "$current_state" = "running" ]; then + send_message "✅ ${BACKEND} service is back up" + fi + + # High CPU alert + _pid=$(get_main_pid) + if [ -n "$_pid" ]; then + _cpu=$(ps -p "$_pid" -o %cpu= 2>/dev/null | awk '{printf "%.0f", $1}') + if [ "${_cpu:-0}" -gt 80 ] 2>/dev/null; then + send_message "⚠️ High CPU usage: ${_cpu}%" + fi + fi + fi + LAST_STATE="$current_state" + + # Periodic reports + _now=$(date +%s) + _interval_secs=$(( ${TELEGRAM_INTERVAL:-6} * 3600 )) + if [ $((_now - LAST_REPORT)) -ge "$_interval_secs" ]; then + send_message "$(build_report)" + LAST_REPORT=$_now + fi + + # Daily summary + _hour=$(date +%H) + _day_of_week=$(date +%u) + if [ "$TELEGRAM_DAILY_SUMMARY" = "true" ] && [ "$_hour" = "$(printf '%02d' ${TELEGRAM_START_HOUR:-0})" ]; then + if [ $((_now - LAST_DAILY)) -ge 86400 ]; then + send_message "📅 *Daily Summary*"$'\n'"$(build_report)" + LAST_DAILY=$_now + fi + fi + + # Weekly summary (Monday) + if [ "$TELEGRAM_WEEKLY_SUMMARY" = "true" ] && [ "$_day_of_week" = "1" ] && [ "$_hour" = "$(printf '%02d' ${TELEGRAM_START_HOUR:-0})" ]; then + if [ $((_now - LAST_WEEKLY)) -ge 604800 ]; then + send_message "📆 *Weekly Summary*"$'\n'"$(build_report)" + LAST_WEEKLY=$_now + fi + fi + + sleep 30 +done +TGSCRIPT + + sed "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$_tmp" > "$_tmp.sed" && mv "$_tmp.sed" "$_tmp" + if ! chmod +x "$_tmp"; then + log_error "Failed to make Telegram script executable" + rm -f "$_tmp" + return 1 + fi + if ! mv "$_tmp" "$script_path"; then + log_error "Failed to install Telegram script" + rm -f "$_tmp" + return 1 + fi +} + +setup_telegram_service() { + telegram_generate_notify_script + + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + cat > /etc/systemd/system/paqctl-telegram.service << EOF +[Unit] +Description=paqctl Telegram Notification Service +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +ExecStart=$(command -v bash) ${INSTALL_DIR}/paqctl-telegram.sh +Restart=on-failure +RestartSec=10 + +[Install] +WantedBy=multi-user.target +EOF + systemctl daemon-reload 2>/dev/null || true + systemctl enable paqctl-telegram.service 2>/dev/null || true + systemctl start paqctl-telegram.service 2>/dev/null || true + log_success "Telegram service started" + else + log_warn "Systemd not available. Run the Telegram daemon manually:" + log_info " nohup bash $INSTALL_DIR/paqctl-telegram.sh &" + fi +} + +stop_telegram_service() { + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl stop paqctl-telegram.service 2>/dev/null || true + systemctl disable paqctl-telegram.service 2>/dev/null || true + fi + pkill -f "paqctl-telegram.sh" 2>/dev/null || true + log_success "Telegram service stopped" +} + +show_telegram_menu() { + local redraw=true + while true; do + if [ "$redraw" = true ]; then + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} TELEGRAM NOTIFICATIONS${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + if [ "$TELEGRAM_ENABLED" = "true" ]; then + echo -e " Status: ${GREEN}Enabled${NC}" + if command -v systemctl &>/dev/null && systemctl is-active paqctl-telegram.service &>/dev/null; then + echo -e " Service: ${GREEN}Running${NC}" + else + echo -e " Service: ${RED}Stopped${NC}" + fi + else + echo -e " Status: ${DIM}Disabled${NC}" + fi + + echo "" + echo " 1. Setup / Change bot" + echo " 2. Test notification" + echo " 3. Enable & start service" + echo " 4. Disable & stop service" + echo " 5. Set check interval (currently: ${TELEGRAM_INTERVAL}h)" + echo " 6. Set server label (currently: ${TELEGRAM_SERVER_LABEL:-hostname})" + echo " 7. Toggle alerts (currently: ${TELEGRAM_ALERTS_ENABLED})" + echo " b. Back" + echo "" + redraw=false + fi + + read -p " Choice: " tg_choice < /dev/tty || break + case "$tg_choice" in + 1) + echo "" + echo -e "${BOLD}Telegram Bot Setup${NC}" + echo "" + echo " 1. Open Telegram and message @BotFather" + echo " 2. Send /newbot and follow the steps" + echo " 3. Copy the bot token" + echo "" + read -p " Enter bot token: " input < /dev/tty || true + if [ -n "$input" ]; then + TELEGRAM_BOT_TOKEN="$input" + echo "" + echo " Now send any message to your bot in Telegram..." + echo "" + for _i in $(seq 15 -1 1); do + printf "\r Waiting: %2ds " "$_i" + sleep 1 + done + printf "\r \r" + if telegram_get_chat_id; then + log_success "Chat ID detected: $TELEGRAM_CHAT_ID" + # Save + _safe_update_setting "TELEGRAM_BOT_TOKEN" "$TELEGRAM_BOT_TOKEN" "$INSTALL_DIR/settings.conf" + _safe_update_setting "TELEGRAM_CHAT_ID" "$TELEGRAM_CHAT_ID" "$INSTALL_DIR/settings.conf" + else + log_error "Could not detect chat ID. Make sure you sent a message to the bot." + echo "" + read -p " Enter chat ID manually (or press Enter to cancel): " input < /dev/tty || true + if [ -n "$input" ]; then + TELEGRAM_CHAT_ID="$input" + _safe_update_setting "TELEGRAM_BOT_TOKEN" "$TELEGRAM_BOT_TOKEN" "$INSTALL_DIR/settings.conf" + _safe_update_setting "TELEGRAM_CHAT_ID" "$TELEGRAM_CHAT_ID" "$INSTALL_DIR/settings.conf" + fi + fi + fi + redraw=true + ;; + 2) + if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then + log_error "Bot not configured. Run setup first." + else + if telegram_test_message; then + log_success "Test message sent!" + else + log_error "Failed to send. Check token and chat ID." + fi + fi + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 3) + if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then + log_error "Bot not configured. Run setup first." + else + TELEGRAM_ENABLED=true + _safe_update_setting "TELEGRAM_ENABLED" "true" "$INSTALL_DIR/settings.conf" + setup_telegram_service + fi + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 4) + TELEGRAM_ENABLED=false + _safe_update_setting "TELEGRAM_ENABLED" "false" "$INSTALL_DIR/settings.conf" + stop_telegram_service + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 5) + echo "" + read -p " Check interval in hours [1-24]: " input < /dev/tty || true + if [[ "$input" =~ ^[0-9]+$ ]] && [ "$input" -ge 1 ] && [ "$input" -le 24 ]; then + TELEGRAM_INTERVAL="$input" + _safe_update_setting "TELEGRAM_INTERVAL" "$input" "$INSTALL_DIR/settings.conf" + log_success "Interval set to ${input}h" + # Restart service if running + if command -v systemctl &>/dev/null && systemctl is-active paqctl-telegram.service &>/dev/null; then + telegram_generate_notify_script + systemctl restart paqctl-telegram.service 2>/dev/null || true + fi + else + log_warn "Invalid value" + fi + redraw=true + ;; + 6) + echo "" + read -p " Server label: " input < /dev/tty || true + if [ -n "$input" ]; then + TELEGRAM_SERVER_LABEL="$input" + _safe_update_setting "TELEGRAM_SERVER_LABEL" "$input" "$INSTALL_DIR/settings.conf" + log_success "Label set to: $input" + fi + redraw=true + ;; + 7) + if [ "$TELEGRAM_ALERTS_ENABLED" = "true" ]; then + TELEGRAM_ALERTS_ENABLED=false + else + TELEGRAM_ALERTS_ENABLED=true + fi + _safe_update_setting "TELEGRAM_ALERTS_ENABLED" "$TELEGRAM_ALERTS_ENABLED" "$INSTALL_DIR/settings.conf" + log_info "Alerts: $TELEGRAM_ALERTS_ENABLED" + redraw=true + ;; + b|B) return ;; + "") ;; + *) echo -e " ${RED}Invalid choice${NC}" ;; + esac + done +} + +#═══════════════════════════════════════════════════════════════════════ +# Switch Backend +#═══════════════════════════════════════════════════════════════════════ + +switch_backend() { + local current_backend="${BACKEND:-paqet}" + local new_backend + if [ "$current_backend" = "paqet" ]; then + new_backend="gfw-knocker" + else + new_backend="paqet" + fi + + # Check if the other backend is installed + local other_installed=false + if [ "$new_backend" = "gfw-knocker" ]; then + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && other_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && other_installed=true + fi + else + [ -f "$INSTALL_DIR/bin/paqet" ] && other_installed=true + fi + + if [ "$other_installed" = false ]; then + echo "" + echo -e "${YELLOW}${new_backend} is not installed.${NC}" + echo "" + echo " Use 'Install additional backend' option to install it first." + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + return 0 + fi + + echo "" + echo -e "${BOLD}Switch active backend from ${current_backend} to ${new_backend}?${NC}" + echo "" + echo " This will:" + echo " - Stop ${current_backend}" + echo " - Start ${new_backend}" + echo "" + read -p " Proceed? [y/N]: " confirm < /dev/tty || true + [[ "$confirm" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; } + + # Stop current + stop_paqet + _remove_firewall + + # Switch to new backend + BACKEND="$new_backend" + save_settings + + # Setup firewall and start new backend + _apply_firewall + start_paqet + + log_success "Switched to ${new_backend}" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true +} + +install_additional_backend() { + local current_backend="${BACKEND:-paqet}" + local new_backend + if [ "$current_backend" = "paqet" ]; then + new_backend="gfw-knocker" + else + new_backend="paqet" + fi + + # Check if already installed + local already_installed=false + if [ "$new_backend" = "gfw-knocker" ]; then + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && already_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && already_installed=true + fi + else + [ -f "$INSTALL_DIR/bin/paqet" ] && already_installed=true + fi + + if [ "$already_installed" = true ]; then + echo "" + echo -e "${GREEN}${new_backend} is already installed.${NC}" + echo "" + echo " Use 'Switch backend' to change the active backend." + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + return 0 + fi + + echo "" + echo -e "${BOLD}Install ${new_backend} alongside ${current_backend}?${NC}" + echo "" + echo " This will:" + echo " - Keep ${current_backend} running" + echo " - Install ${new_backend} as an additional option" + echo " - You can switch between them anytime" + echo "" + read -p " Proceed? [y/N]: " confirm < /dev/tty || true + [[ "$confirm" =~ ^[Yy]$ ]] || { log_info "Cancelled"; return 0; } + + echo "" + log_info "Installing ${new_backend}..." + + if [ "$new_backend" = "gfw-knocker" ]; then + # Collect GFK configuration for client role + if [ "$ROLE" = "client" ]; then + echo "" + echo -e "${BOLD}GFK Client Configuration${NC}" + echo -e "${DIM}(these must match your server settings)${NC}" + echo "" + + echo -e "${BOLD}Server IP${NC} (server's public IP):" + read -p " IP: " input < /dev/tty || true + if [ -z "$input" ] || ! _validate_ip "$input"; then + log_error "Valid server IP is required." + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + return 1 + fi + GFK_SERVER_IP="$input" + + echo -e "${BOLD}Server's VIO TCP port${NC} [45000] (must match server):" + read -p " Port: " input < /dev/tty || true + GFK_VIO_PORT="${input:-45000}" + if ! _validate_port "$GFK_VIO_PORT"; then + log_warn "Invalid port. Using default 45000." + GFK_VIO_PORT=45000 + fi + + echo -e "${BOLD}Local VIO client port${NC} [40000]:" + read -p " Port: " input < /dev/tty || true + GFK_VIO_CLIENT_PORT="${input:-40000}" + if ! _validate_port "$GFK_VIO_CLIENT_PORT"; then + log_warn "Invalid port. Using default 40000." + GFK_VIO_CLIENT_PORT=40000 + fi + + echo -e "${BOLD}Server's QUIC port${NC} [25000] (must match server):" + read -p " Port: " input < /dev/tty || true + GFK_QUIC_PORT="${input:-25000}" + if ! _validate_port "$GFK_QUIC_PORT"; then + log_warn "Invalid port. Using default 25000." + GFK_QUIC_PORT=25000 + fi + + echo -e "${BOLD}Local QUIC client port${NC} [20000]:" + read -p " Port: " input < /dev/tty || true + GFK_QUIC_CLIENT_PORT="${input:-20000}" + if ! _validate_port "$GFK_QUIC_CLIENT_PORT"; then + log_warn "Invalid port. Using default 20000." + GFK_QUIC_CLIENT_PORT=20000 + fi + + echo -e "${BOLD}QUIC auth code${NC} (from server setup):" + read -p " Auth code: " input < /dev/tty || true + if [ -z "$input" ]; then + log_error "Auth code is required." + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + return 1 + fi + GFK_AUTH_CODE="$input" + + echo -e "${BOLD}TCP port mappings${NC} (must match server) [14000:443]:" + read -p " Mappings: " input < /dev/tty || true + GFK_PORT_MAPPINGS="${input:-14000:443}" + echo "" + fi + + # Install GFK without changing current backend + if ! _install_gfk_components; then + log_error "Failed to install ${new_backend}" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + return 1 + fi + else + # Install paqet without changing current backend + if ! _install_paqet_components; then + log_error "Failed to install ${new_backend}" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + return 1 + fi + fi + + echo "" + log_success "${new_backend} installed successfully!" + echo "" + echo " Use 'Switch backend' to activate it." + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true +} + +_install_paqet_components() { + log_info "Downloading paqet binary..." + local _paqet_ver + _paqet_ver=$(curl -s --max-time 10 "$PAQET_API_URL" 2>/dev/null | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"') + if [ -z "$_paqet_ver" ] || ! _validate_version_tag "$_paqet_ver"; then + _paqet_ver="$PAQET_VERSION_PINNED" + fi + log_info "Using paqet ${_paqet_ver}" + if ! download_paqet "$_paqet_ver"; then + log_error "Failed to download paqet" + return 1 + fi + log_success "paqet binary installed" + + # Generate config.yaml if it doesn't exist + if [ ! -f "$INSTALL_DIR/config.yaml" ]; then + echo "" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} PAQET CONFIGURATION${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + # Detect network settings + detect_network + local _det_iface="$DETECTED_IFACE" + local _det_ip="$DETECTED_IP" + local _det_mac="$DETECTED_GW_MAC" + + # Prompt for interface + echo -e "${BOLD}Network Interface${NC} [${_det_iface:-eth0}]:" + read -p " Interface: " input < /dev/tty || true + local _iface="${input:-${_det_iface:-eth0}}" + + # Prompt for local IP + echo -e "${BOLD}Local IP${NC} [${_det_ip:-}]:" + read -p " IP: " input < /dev/tty || true + local _local_ip="${input:-$_det_ip}" + + # Prompt for gateway MAC + echo -e "${BOLD}Gateway MAC${NC} [${_det_mac:-}]:" + read -p " MAC: " input < /dev/tty || true + local _gw_mac="${input:-$_det_mac}" + + # Validate MAC if provided + if [ -n "$_gw_mac" ] && ! [[ "$_gw_mac" =~ ^([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}$ ]]; then + log_warn "Invalid MAC format. Expected: aa:bb:cc:dd:ee:ff" + read -p " Enter valid MAC: " input < /dev/tty || true + [ -n "$input" ] && _gw_mac="$input" + fi + + # Generate encryption key + local _key + _key=$("$INSTALL_DIR/bin/paqet" secret 2>/dev/null || true) + if [ -z "$_key" ]; then + _key=$(openssl rand -base64 32 2>/dev/null | tr -d '=+/' | head -c 32 || true) + fi + + if [ "$ROLE" = "server" ]; then + # Prompt for port + echo -e "${BOLD}Listen Port${NC} [8443]:" + read -p " Port: " input < /dev/tty || true + local _port="${input:-8443}" + + # Show generated key + echo "" + echo -e "${GREEN}${BOLD} Generated Encryption Key: ${_key}${NC}" + echo -e "${YELLOW} IMPORTANT: Save this key! Clients need it to connect.${NC}" + echo "" + echo -e "${BOLD}Encryption Key${NC} (press Enter to use generated key):" + read -p " Key: " input < /dev/tty || true + [ -n "$input" ] && _key="$input" + + LISTEN_PORT="$_port" + ENCRYPTION_KEY="$_key" + else + # Client prompts + echo -e "${BOLD}Remote Server${NC} (IP:PORT):" + read -p " Server: " input < /dev/tty || true + local _server="${input:-${REMOTE_SERVER:-}}" + if [ -z "$_server" ]; then + log_warn "No server specified. You must edit config.yaml later." + _server="SERVER_IP:8443" + fi + + echo -e "${BOLD}Encryption Key${NC} (from server):" + read -p " Key: " input < /dev/tty || true + [ -n "$input" ] && _key="$input" + + echo -e "${BOLD}SOCKS5 Port${NC} [1080]:" + read -p " Port: " input < /dev/tty || true + local _socks="${input:-1080}" + + REMOTE_SERVER="$_server" + SOCKS_PORT="$_socks" + ENCRYPTION_KEY="$_key" + fi + + # Validate required fields + if [ -z "$_iface" ] || [ -z "$_local_ip" ] || [ -z "$_gw_mac" ]; then + log_error "Missing required fields (interface, IP, or MAC)" + return 1 + fi + if [ -z "$_key" ] || [ "${#_key}" -lt 16 ]; then + log_error "Invalid encryption key" + return 1 + fi + + # Helper to escape YAML values + _escape_yaml_val() { + local s="$1" + if [[ "$s" =~ [:\#\[\]{}\"\'\|\>\<\&\*\!\%\@\`] ]] || [[ "$s" =~ ^[[:space:]] ]] || [[ "$s" =~ [[:space:]]$ ]]; then + s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '"%s"' "$s" + else + printf '%s' "$s" + fi + } + + local _y_iface _y_ip _y_mac _y_key + _y_iface=$(_escape_yaml_val "$_iface") + _y_ip=$(_escape_yaml_val "$_local_ip") + _y_mac=$(_escape_yaml_val "$_gw_mac") + _y_key=$(_escape_yaml_val "$_key") + + local tmp_conf + tmp_conf=$(mktemp "$INSTALL_DIR/config.yaml.XXXXXXXX") || { log_error "Failed to create temp file"; return 1; } + chmod 600 "$tmp_conf" 2>/dev/null + + if [ "$ROLE" = "server" ]; then + cat > "$tmp_conf" << EOF +role: "server" + +log: + level: "info" + +listen: + addr: ":${_port}" + +network: + interface: "${_y_iface}" + ipv4: + addr: "${_y_ip}:${_port}" + router_mac: "${_y_mac}" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_y_key}" +EOF + else + cat > "$tmp_conf" << EOF +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:${_socks}" + +network: + interface: "${_y_iface}" + ipv4: + addr: "${_y_ip}:0" + router_mac: "${_y_mac}" + +server: + addr: "${_server}" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "${_y_key}" +EOF + fi + + if ! mv "$tmp_conf" "$INSTALL_DIR/config.yaml"; then + log_error "Failed to save config.yaml" + rm -f "$tmp_conf" + return 1 + fi + chmod 600 "$INSTALL_DIR/config.yaml" 2>/dev/null + + # Update global vars for settings + INTERFACE="$_iface" + LOCAL_IP="$_local_ip" + GATEWAY_MAC="$_gw_mac" + + log_success "Configuration saved to $INSTALL_DIR/config.yaml" + + # Save to settings.conf for persistence + save_settings 2>/dev/null || true + fi +} + +check_xray_installed() { + command -v xray &>/dev/null && return 0 + [ -x /usr/local/bin/xray ] && return 0 + [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && return 0 + return 1 +} + +XRAY_CONFIG_DIR="/usr/local/etc/xray" +XRAY_CONFIG_FILE="$XRAY_CONFIG_DIR/config.json" + +install_xray() { + if check_xray_installed; then + log_info "Xray is already installed" + return 0 + fi + + log_info "Installing Xray ${XRAY_VERSION_PINNED}..." + + local tmp_script + tmp_script=$(mktemp) + if ! curl -sL https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o "$tmp_script"; then + log_error "Failed to download Xray installer" + rm -f "$tmp_script" + return 1 + fi + + if ! bash "$tmp_script" install --version "$XRAY_VERSION_PINNED" 2>/dev/null; then + log_error "Failed to install Xray" + rm -f "$tmp_script" + return 1 + fi + rm -f "$tmp_script" + + log_success "Xray ${XRAY_VERSION_PINNED} installed" +} + +configure_xray_socks() { + local listen_port="${1:-443}" + log_info "Configuring Xray SOCKS5 proxy on port $listen_port..." + mkdir -p "$XRAY_CONFIG_DIR" + cat > "$XRAY_CONFIG_FILE" << EOF +{ + "log": { "loglevel": "warning" }, + "inbounds": [{ + "tag": "socks-in", + "port": ${listen_port}, + "listen": "127.0.0.1", + "protocol": "socks", + "settings": { "auth": "noauth", "udp": true }, + "sniffing": { "enabled": true, "destOverride": ["http", "tls"] } + }], + "outbounds": [{ "tag": "direct", "protocol": "freedom", "settings": {} }] +} +EOF + chmod 644 "$XRAY_CONFIG_FILE" + log_success "Xray configured (SOCKS5 on 127.0.0.1:$listen_port)" +} + +_is_paqctl_standalone_xray() { + [ -f "$XRAY_CONFIG_FILE" ] || return 1 + command -v python3 &>/dev/null || return 1 + python3 -c " +import json, sys +try: + with open(sys.argv[1]) as f: + cfg = json.load(f) + inbounds = cfg.get('inbounds', []) + if not inbounds: + sys.exit(1) + for i in inbounds: + if i.get('protocol') != 'socks' or i.get('listen', '0.0.0.0') != '127.0.0.1': + sys.exit(1) + sys.exit(0) +except: + sys.exit(1) +" "$XRAY_CONFIG_FILE" 2>/dev/null +} + +_add_xray_gfk_socks() { + local port="$1" + python3 -c " +import json, sys +port = int(sys.argv[1]) +config_path = sys.argv[2] +try: + with open(config_path, 'r') as f: + cfg = json.load(f) +except: + cfg = {'inbounds': [], 'outbounds': [{'tag': 'direct', 'protocol': 'freedom', 'settings': {}}]} +cfg.setdefault('inbounds', []) +cfg['inbounds'] = [i for i in cfg['inbounds'] if i.get('tag') != 'gfk-socks'] +cfg['inbounds'].append({ + 'tag': 'gfk-socks', 'port': port, 'listen': '127.0.0.1', 'protocol': 'socks', + 'settings': {'auth': 'noauth', 'udp': True}, + 'sniffing': {'enabled': True, 'destOverride': ['http', 'tls']} +}) +if not any(o.get('protocol') == 'freedom' for o in cfg.get('outbounds', [])): + cfg.setdefault('outbounds', []).append({'tag': 'direct', 'protocol': 'freedom', 'settings': {}}) +with open(config_path, 'w') as f: + json.dump(cfg, f, indent=2) +" "$port" "$XRAY_CONFIG_FILE" 2>/dev/null + if [ $? -ne 0 ]; then + log_error "Failed to add SOCKS5 inbound to existing Xray config" + return 1 + fi + log_success "Added GFK SOCKS5 inbound on 127.0.0.1:$port" +} + +stop_xray() { + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl stop xray 2>/dev/null || true + else + pkill -x xray 2>/dev/null || true + fi +} + +start_xray() { + log_info "Starting Xray service..." + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl stop xray 2>/dev/null || true + sleep 1 + systemctl daemon-reload 2>/dev/null || true + systemctl enable xray 2>/dev/null || true + local attempt + for attempt in 1 2 3; do + systemctl start xray 2>/dev/null + sleep 2 + if systemctl is-active --quiet xray; then + log_success "Xray started" + return 0 + fi + [ "$attempt" -lt 3 ] && sleep 1 + done + log_error "Failed to start Xray after 3 attempts" + return 1 + else + local _xray_bin="" + [ -x /usr/local/bin/xray ] && _xray_bin="/usr/local/bin/xray" + [ -z "$_xray_bin" ] && [ -x /usr/local/x-ui/bin/xray-linux-amd64 ] && _xray_bin="/usr/local/x-ui/bin/xray-linux-amd64" + if [ -n "$_xray_bin" ]; then + pkill -x xray 2>/dev/null || true + sleep 1 + nohup "$_xray_bin" run -c "$XRAY_CONFIG_FILE" > /var/log/xray.log 2>&1 & + sleep 2 + if pgrep -f "xray" &>/dev/null; then + log_success "Xray started" + return 0 + fi + fi + log_error "Failed to start Xray" + return 1 + fi +} + +setup_xray_for_gfk() { + local target_port + target_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1) + + if pgrep -x xray &>/dev/null || pgrep -f "xray-linux" &>/dev/null; then + # Check if this is paqctl's own standalone Xray (not a real panel) + if _is_paqctl_standalone_xray; then + log_info "Existing Xray is paqctl's standalone install — reconfiguring..." + stop_xray + sleep 1 + # Fall through to standalone install path below + else + XRAY_PANEL_DETECTED=true + log_info "Existing Xray detected — adding SOCKS5 alongside panel..." + + # Clean up any leftover standalone GFK xray from prior installs + pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true + rm -f "${XRAY_CONFIG_DIR}/gfk-socks.json" 2>/dev/null + + # Check all existing target ports from mappings + local mapping pairs + IFS=',' read -ra pairs <<< "${GFK_PORT_MAPPINGS:-14000:443}" + for mapping in "${pairs[@]}"; do + local vio_port="${mapping%%:*}" + local tp="${mapping##*:}" + if ss -tln 2>/dev/null | grep -q ":${tp} "; then + log_success "Port $tp is listening — GFK will forward VIO port $vio_port to this port" + else + log_warn "Port $tp is NOT listening — make sure your panel inbound is on port $tp" + fi + done + + # Find free port for SOCKS5 (starting at 10443) + local socks_port=10443 + while ss -tln 2>/dev/null | grep -q ":${socks_port} "; do + socks_port=$((socks_port + 1)) + if [ "$socks_port" -gt 65000 ]; then + log_warn "Could not find free port for SOCKS5 — panel-only mode" + echo "" + local first_vio + first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1) + log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}" + return 0 + fi + done + + # Add SOCKS5 inbound to existing xray config + _add_xray_gfk_socks "$socks_port" || { + log_warn "Could not add SOCKS5 to panel config — panel-only mode" + return 0 + } + + # Restart xray to load new config + systemctl restart xray 2>/dev/null || pkill -SIGHUP xray 2>/dev/null || true + sleep 2 + + # Find next VIO port (highest existing + 1) and append SOCKS5 mapping + local max_vio=0 + for mapping in "${pairs[@]}"; do + local v="${mapping%%:*}" + [ "$v" -gt "$max_vio" ] && max_vio="$v" + done + local socks_vio=$((max_vio + 1)) + GFK_PORT_MAPPINGS="${GFK_PORT_MAPPINGS},${socks_vio}:${socks_port}" + GFK_SOCKS_PORT="$socks_port" + GFK_SOCKS_VIO_PORT="$socks_vio" + + log_success "SOCKS5 proxy added on port $socks_port (VIO port $socks_vio)" + echo "" + log_info "Port mappings updated: ${GFK_PORT_MAPPINGS}" + log_warn "Use these SAME mappings on the client side" + echo "" + local first_vio + first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1) + log_warn "For panel-to-panel: configure Iran panel outbound to 127.0.0.1:${first_vio}" + log_warn "For direct SOCKS5: use 127.0.0.1:${socks_vio} as your proxy on client" + return 0 + fi + fi + + install_xray || return 1 + configure_xray_socks "$target_port" || return 1 + start_xray || return 1 +} + +_install_gfk_components() { + log_info "Installing GFK components..." + + # Auto-detect server IP if not set (critical for server-side sniffer filter) + if [ -z "${GFK_SERVER_IP:-}" ] && [ "$ROLE" = "server" ]; then + GFK_SERVER_IP="${LOCAL_IP:-}" + [ -z "$GFK_SERVER_IP" ] && GFK_SERVER_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}') + [ -z "$GFK_SERVER_IP" ] && GFK_SERVER_IP=$(hostname -I 2>/dev/null | awk '{print $1}') + if [ -n "$GFK_SERVER_IP" ]; then + log_info "Auto-detected server IP: ${GFK_SERVER_IP}" + else + log_error "Could not detect server IP. Set GFK_SERVER_IP manually." + return 1 + fi + fi + + # Auto-generate auth code if not set + if [ -z "${GFK_AUTH_CODE:-}" ] || [ "$GFK_AUTH_CODE" = "not set" ]; then + GFK_AUTH_CODE=$(tr -dc 'a-zA-Z0-9' < /dev/urandom | head -c 16 2>/dev/null || openssl rand -hex 8) + log_info "Generated GFK auth code: ${GFK_AUTH_CODE}" + fi + + # Save settings with server IP and auth code + save_settings + + # Install Python dependencies (venv + scapy + aioquic) + install_python_deps || return 1 + + # Download GFK scripts (server and client) + download_gfk || return 1 + + # Generate TLS certificates for QUIC + generate_gfk_certs || return 1 + + # Setup Xray (server only — adds SOCKS5 alongside panel if detected) + if [ "$ROLE" = "server" ]; then + setup_xray_for_gfk || return 1 + elif [ "$ROLE" = "client" ]; then + create_gfk_client_wrapper + fi + + # Generate parameters.py config + generate_gfk_config || return 1 + + save_settings + + log_success "GFK components installed" +} + +#═══════════════════════════════════════════════════════════════════════ +# Uninstall +#═══════════════════════════════════════════════════════════════════════ + +uninstall_paqctl() { + echo "" + echo -e "${RED}${BOLD} UNINSTALL PAQCTL${NC}" + echo "" + echo -e " This will remove:" + if [ "$BACKEND" = "gfw-knocker" ]; then + echo " - GFW-knocker scripts and config" + else + echo " - paqet binary" + fi + echo " - All configuration files" + echo " - Systemd services" + echo " - Firewall rules" + echo " - Telegram service" + echo "" + read -p " Are you sure? [y/N]: " confirm < /dev/tty || true + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + log_info "Cancelled" + return 0 + fi + + # Stop services + stop_paqet + stop_telegram_service + + # Stop standalone GFK xray and clean up config + pkill -f "xray run -c.*gfk-socks.json" 2>/dev/null || true + rm -f /usr/local/etc/xray/gfk-socks.json 2>/dev/null + # If xray is paqctl's standalone install, stop and disable it entirely + if _is_paqctl_standalone_xray; then + log_info "Stopping paqctl's standalone Xray..." + systemctl stop xray 2>/dev/null || true + systemctl disable xray 2>/dev/null || true + elif [ -f "$XRAY_CONFIG_FILE" ] && command -v python3 &>/dev/null; then + # Remove gfk-socks inbound from panel's xray config if present + python3 -c " +import json, sys +try: + with open(sys.argv[1], 'r') as f: + cfg = json.load(f) + orig_len = len(cfg.get('inbounds', [])) + cfg['inbounds'] = [i for i in cfg.get('inbounds', []) if i.get('tag') != 'gfk-socks'] + if len(cfg['inbounds']) < orig_len: + with open(sys.argv[1], 'w') as f: + json.dump(cfg, f, indent=2) +except: pass +" "$XRAY_CONFIG_FILE" 2>/dev/null + systemctl restart xray 2>/dev/null || true + fi + + # Remove ALL paqctl firewall rules (tagged with "paqctl" comment) + log_info "Removing firewall rules..." + _remove_all_paqctl_firewall_rules + # Also try the port-specific removal for backwards compatibility + _remove_firewall + + # Remove systemd services + if command -v systemctl &>/dev/null; then + systemctl stop paqctl.service 2>/dev/null || true + systemctl disable paqctl.service 2>/dev/null || true + systemctl stop paqctl-telegram.service 2>/dev/null || true + systemctl disable paqctl-telegram.service 2>/dev/null || true + rm -f /etc/systemd/system/paqctl.service + rm -f /etc/systemd/system/paqctl-telegram.service + systemctl daemon-reload 2>/dev/null || true + fi + + # Remove OpenRC/SysVinit + rm -f /etc/init.d/paqctl 2>/dev/null + + # Remove symlink + rm -f /usr/local/bin/paqctl + + # Remove install directory + rm -rf "${INSTALL_DIR:?}" + + echo "" + log_success "paqctl has been completely uninstalled" + echo "" +} + +#═══════════════════════════════════════════════════════════════════════ +# Help +#═══════════════════════════════════════════════════════════════════════ + +show_help() { + echo "" + echo -e "${BOLD}paqctl${NC} - Paqet Manager v${VERSION}" + echo "" + echo -e "${BOLD}Usage:${NC} sudo paqctl " + echo "" + echo -e "${BOLD}Commands:${NC}" + echo " menu Interactive menu (default)" + echo " status Show service status and configuration" + echo "" + echo -e "${BOLD}Backend Control (individual):${NC}" + echo " start-paqet Start paqet backend only" + echo " stop-paqet Stop paqet backend only" + echo " start-gfk Start GFK backend only" + echo " stop-gfk Stop GFK backend only" + echo " start-all Start both backends" + echo " stop-all Stop both backends" + echo "" + echo -e "${BOLD}Legacy (uses active backend):${NC}" + echo " start Start active backend" + echo " stop Stop active backend" + echo " restart Restart active backend" + echo "" + echo -e "${BOLD}Other:${NC}" + echo " logs View logs (live)" + echo " health Run health check diagnostics" + echo " update Check for and install updates" + echo " config Change configuration" + echo " secret Generate a new encryption key" + echo " firewall Manage iptables rules" + echo " backup Backup configuration" + echo " restore Restore from backup" + echo " telegram Telegram notification settings" + echo " rollback Roll back to a previous paqet version" + echo " ping Test connectivity (paqet ping)" + echo " dump Capture packets for diagnostics (paqet dump)" + echo " uninstall Remove paqctl completely" + echo " version Show version info" + echo " help Show this help" + echo "" + echo -e "${BOLD}Paqet:${NC} https://git.samnet.dev/SamNet-dev/paqctl" + echo "" +} + +show_version() { + echo "" + echo -e " paqctl version: ${BOLD}${VERSION}${NC}" + if [ "$BACKEND" = "gfw-knocker" ]; then + echo -e " backend: ${BOLD}gfw-knocker${NC}" + local py_ver; py_ver=$(python3 --version 2>/dev/null || echo "unknown") + echo -e " python: ${BOLD}${py_ver}${NC}" + else + echo -e " paqet version: ${BOLD}${PAQET_VERSION}${NC}" + local bin_ver + bin_ver=$("$INSTALL_DIR/bin/paqet" version 2>/dev/null || echo "unknown") + echo -e " paqet binary: ${BOLD}${bin_ver}${NC}" + if echo "$PAQET_VERSION" | grep -qi "alpha\|beta\|rc"; then + echo "" + echo -e " ${YELLOW}Note: paqet is in alpha phase — expect breaking changes between versions.${NC}" + fi + fi + echo "" + echo -e " ${DIM}paqctl by sam: https://git.samnet.dev/SamNet-dev/paqctl${NC}" + echo "" +} + +#═══════════════════════════════════════════════════════════════════════ +# Paqet Diagnostic Tools (ping / dump) +#═══════════════════════════════════════════════════════════════════════ + +run_ping() { + echo "" + if [ "$BACKEND" = "gfw-knocker" ]; then + log_warn "ping diagnostic is only available for paqet backend" + return 0 + fi + if [ ! -x "$INSTALL_DIR/bin/paqet" ]; then + log_error "paqet binary not found" + return 1 + fi + if [ ! -f "$INSTALL_DIR/config.yaml" ]; then + log_error "config.yaml not found. Run: sudo paqctl config" + return 1 + fi + log_info "Running paqet ping (Ctrl+C to stop)..." + echo "" + "$INSTALL_DIR/bin/paqet" ping -c "$INSTALL_DIR/config.yaml" 2>&1 || true + echo "" +} + +run_dump() { + echo "" + if [ "$BACKEND" = "gfw-knocker" ]; then + log_warn "dump diagnostic is only available for paqet backend" + return 0 + fi + if [ ! -x "$INSTALL_DIR/bin/paqet" ]; then + log_error "paqet binary not found" + return 1 + fi + if [ ! -f "$INSTALL_DIR/config.yaml" ]; then + log_error "config.yaml not found. Run: sudo paqctl config" + return 1 + fi + log_info "Running paqet dump — packet capture diagnostic (Ctrl+C to stop)..." + echo -e "${DIM} This shows raw packets being sent and received by paqet.${NC}" + echo "" + "$INSTALL_DIR/bin/paqet" dump -c "$INSTALL_DIR/config.yaml" 2>&1 || true + echo "" +} + +#═══════════════════════════════════════════════════════════════════════ +# Settings Menu +#═══════════════════════════════════════════════════════════════════════ + +show_settings_menu() { + local redraw=true + while true; do + if [ "$redraw" = true ]; then + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} SETTINGS & TOOLS${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo " 1. Change configuration" + echo " 2. Manage firewall rules" + echo " 3. Generate encryption key" + echo " 4. Backup configuration" + echo " 5. Restore from backup" + echo " 6. Health check" + echo " 7. Telegram notifications" + echo " 8. Version info" + echo " 9. Rollback to previous version" + echo " p. Ping test (connectivity)" + echo " d. Packet dump (diagnostics)" + echo " a. Install additional backend" + echo " s. Switch backend (current: ${BACKEND})" + echo " u. Uninstall" + echo "" + echo " b. Back to main menu" + echo "" + redraw=false + fi + + read -p " Choice: " s_choice < /dev/tty || break + case "$s_choice" in + 1) change_config; redraw=true ;; + 2) show_firewall; redraw=true ;; + 3) generate_secret; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + 4) backup_config; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + 5) restore_config; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + 6) health_check; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + 7) show_telegram_menu; redraw=true ;; + 8) show_version; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + 9) rollback_paqet; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + p|P) run_ping; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + d|D) run_dump; read -n 1 -s -r -p " Press any key..." < /dev/tty || true; redraw=true ;; + a|A) install_additional_backend; redraw=true ;; + s|S) switch_backend; redraw=true ;; + u|U) uninstall_paqctl; exit 0 ;; + b|B) return ;; + "") ;; + *) echo -e " ${RED}Invalid choice${NC}" ;; + esac + done +} + +#═══════════════════════════════════════════════════════════════════════ +# Info Menu +#═══════════════════════════════════════════════════════════════════════ + +show_info_menu() { + local redraw=true + while true; do + if [ "$redraw" = true ]; then + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} INFO & HELP${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo " 1. How Paqet Works" + echo " 2. Server vs Client Mode" + echo " 3. Firewall Rules Explained" + echo " 4. Troubleshooting" + echo " 5. About" + echo "" + echo " b. Back" + echo "" + redraw=false + fi + + read -p " Choice: " i_choice < /dev/tty || break + case "$i_choice" in + 1) + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} HOW PAQET WORKS${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " ${BOLD}Overview:${NC}" + echo " Paqet is a bidirectional packet-level proxy written in Go." + echo " Unlike traditional proxies (Shadowsocks, V2Ray, etc.) that" + echo " operate at the application or transport layer, paqet works" + echo " at the raw socket level — below the OS network stack." + echo "" + echo -e " ${BOLD}How it works step by step:${NC}" + echo "" + echo " 1. PACKET CRAFTING" + echo " Paqet uses gopacket + libpcap to craft TCP packets" + echo " directly, bypassing the kernel's TCP/IP stack entirely." + echo " This means the OS doesn't even know there's a connection." + echo "" + echo " 2. KCP ENCRYPTED TRANSPORT" + echo " All traffic between client and server is encrypted using" + echo " the KCP protocol with AES symmetric key encryption." + echo " KCP provides reliable, ordered delivery over raw packets" + echo " with built-in error correction and retransmission." + echo "" + echo " 3. CONNECTION MULTIPLEXING" + echo " Multiple connections are multiplexed over a single KCP" + echo " session using smux, reducing overhead and improving" + echo " performance for concurrent requests." + echo "" + echo " 4. FIREWALL BYPASS" + echo " Because it operates below the OS network stack, paqet" + echo " bypasses traditional firewalls (ufw, firewalld) and" + echo " kernel-level connection tracking (conntrack). The OS" + echo " firewall never sees the traffic as a 'connection'." + echo "" + echo " 5. SOCKS5 PROXY (Client)" + echo " On the client side, paqet exposes a standard SOCKS5" + echo " proxy that any application can use. Traffic enters" + echo " the SOCKS5 port, gets encrypted and sent via raw" + echo " packets to the server, which forwards it to the" + echo " destination on the open internet." + echo "" + echo -e " ${DIM}Technical stack: Go, gopacket, libpcap, KCP, smux, AES${NC}" + echo -e " ${DIM}Project: https://git.samnet.dev/SamNet-dev/paqctl${NC}" + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 2) + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} SERVER VS CLIENT MODE${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " ${GREEN}${BOLD}SERVER MODE${NC}" + echo -e " ${DIM}─────────────────────────────────────────────${NC}" + echo " The server is the exit node. It receives encrypted raw" + echo " packets from clients, decrypts them, and forwards the" + echo " traffic to the open internet. Responses are encrypted" + echo " and sent back to the client." + echo "" + echo " Requirements:" + echo " - A server with a public IP address" + echo " - Root access (raw sockets need it)" + echo " - libpcap installed" + echo " - iptables NOTRACK + RST DROP rules (auto-managed)" + echo " - An open port (paqctl manages firewall rules, but you" + echo " may need to allow the port in your cloud provider's" + echo " security group / network firewall)" + echo "" + echo " After setup, share with your clients:" + echo " - Server IP and port (e.g. 1.2.3.4:8443)" + echo " - Encryption key (generated during setup)" + echo "" + echo -e " ${CYAN}${BOLD}CLIENT MODE${NC}" + echo -e " ${DIM}─────────────────────────────────────────────${NC}" + echo " The client connects to a paqet server and provides a" + echo " local SOCKS5 proxy. Applications on your machine connect" + echo " to the SOCKS5 port, and traffic is tunneled through" + echo " paqet's encrypted raw-socket connection to the server." + echo "" + echo " Requirements:" + echo " - Server IP:PORT and encryption key from the server admin" + echo " - Root access (raw sockets need it)" + echo " - libpcap installed" + echo "" + echo " Usage after setup:" + echo " Browser: Set SOCKS5 proxy to 127.0.0.1:1080" + echo " curl: curl --proxy socks5h://127.0.0.1:1080 URL" + echo " System: Configure system proxy to SOCKS5 127.0.0.1:1080" + echo "" + echo -e " ${BOLD}Data flow:${NC}" + echo " App -> SOCKS5(:1080) -> paqet client -> raw packets" + echo " -> internet -> paqet server -> destination website" + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 3) + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} FIREWALL RULES EXPLAINED${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo " Paqet requires specific iptables rules on the SERVER." + echo " These rules are needed because paqet crafts raw TCP" + echo " packets, and without them the kernel interferes." + echo "" + echo -e " ${BOLD}Rule 1: PREROUTING NOTRACK${NC}" + echo " iptables -t raw -A PREROUTING -p tcp --dport PORT -j NOTRACK" + echo "" + echo " WHY: Tells the kernel's connection tracker (conntrack) to" + echo " ignore incoming packets on the paqet port. Without this," + echo " conntrack tries to match packets to connections it doesn't" + echo " know about and may drop them." + echo "" + echo -e " ${BOLD}Rule 2: OUTPUT NOTRACK${NC}" + echo " iptables -t raw -A OUTPUT -p tcp --sport PORT -j NOTRACK" + echo "" + echo " WHY: Same as above but for outgoing packets. Prevents" + echo " conntrack from tracking paqet's outbound raw packets." + echo "" + echo -e " ${BOLD}Rule 3: RST DROP${NC}" + echo " iptables -t mangle -A OUTPUT -p tcp --sport PORT" + echo " --tcp-flags RST RST -j DROP" + echo "" + echo " WHY: When the kernel sees incoming TCP SYN packets on a" + echo " port with no listening socket, it sends TCP RST (reset)" + echo " back. This would kill paqet connections. This rule drops" + echo " those RST packets so paqet can handle them instead." + echo "" + echo -e " ${DIM}These rules are auto-managed by paqctl:${NC}" + echo -e " ${DIM} - Applied on service start (ExecStartPre)${NC}" + echo -e " ${DIM} - Removed on service stop (ExecStopPost)${NC}" + echo -e " ${DIM} - Persisted across reboots when possible${NC}" + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 4) + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} TROUBLESHOOTING${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " ${BOLD}Service won't start:${NC}" + echo " 1. Check logs: sudo paqctl logs" + echo " 2. Run health check: sudo paqctl health" + echo " 3. Verify libpcap: ldconfig -p | grep libpcap" + echo " 4. Check config: cat /opt/paqctl/config.yaml" + echo " 5. Test binary: sudo /opt/paqctl/bin/paqet version" + echo "" + echo -e " ${BOLD}Client can't connect to server:${NC}" + echo " 1. Verify server IP and port are correct" + echo " 2. Check encryption key matches exactly" + echo " 3. Ensure server iptables rules are active:" + echo " sudo paqctl firewall (on server)" + echo " 4. Check cloud security group allows the port" + echo " 5. Test raw connectivity:" + echo " sudo /opt/paqctl/bin/paqet ping -c /opt/paqctl/config.yaml" + echo " 6. Run packet dump to see what's happening:" + echo " sudo /opt/paqctl/bin/paqet dump -c /opt/paqctl/config.yaml" + echo "" + echo -e " ${BOLD}SOCKS5 not working (client side):${NC}" + echo " 1. Verify client is running: sudo paqctl status" + echo " 2. Test the proxy directly:" + echo " curl -v --proxy socks5h://127.0.0.1:1080 https://httpbin.org/ip" + echo " 3. Check SOCKS port is listening:" + echo " ss -tlnp | grep 1080" + echo " 4. Check if paqet output shows errors:" + echo " sudo paqctl logs" + echo "" + echo -e " ${BOLD}High CPU / Memory:${NC}" + echo " 1. Check process stats: sudo paqctl status" + echo " 2. Restart the service: sudo paqctl restart" + echo " 3. Check for latest version: sudo paqctl update" + echo "" + echo -e " ${BOLD}After system reboot:${NC}" + echo " 1. paqctl auto-starts via systemd (check: systemctl status paqctl)" + echo " 2. iptables rules are re-applied by ExecStartPre" + echo " 3. If rules are missing: sudo paqctl firewall -> Apply" + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + 5) + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} ABOUT${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " ${BOLD}paqctl v${VERSION}${NC} - Paqet Management Tool" + echo "" + echo -e " ${CYAN}── Paqet ──${NC}" + echo "" + echo -e " ${BOLD}Creator:${NC} hanselime" + echo -e " ${BOLD}Repository:${NC} https://git.samnet.dev/SamNet-dev/paqctl" + echo -e " ${BOLD}License:${NC} AGPL-3.0 - Copyright (C) 2026 SamNet Technologies, LLC" + echo -e " ${BOLD}Language:${NC} Go" + echo -e " ${BOLD}Contact:${NC} Signal @hanselime.11" + echo "" + echo " Paqet is a bidirectional packet-level proxy that uses" + echo " KCP over raw TCP packets with custom TCP flags." + echo " It operates below the OS TCP/IP stack to bypass" + echo " firewalls and deep packet inspection." + echo "" + echo " Features:" + echo " - Raw TCP packet crafting via gopacket" + echo " - KCP + AES symmetric encryption" + echo " - SOCKS5 proxy for dynamic connections" + echo " - Connection multiplexing via smux" + echo " - Cross-platform (Linux, macOS, Windows)" + echo " - Android client: github.com/AliRezaBeigy/paqetNG" + echo "" + echo -e " ${CYAN}── paqctl Management Tool ──${NC}" + echo "" + echo -e " ${BOLD}Built by:${NC} SamNet-dev" + echo -e " ${BOLD}Repository:${NC} https://git.samnet.dev/SamNet-dev/paqctl" + echo -e " ${BOLD}License:${NC} AGPL-3.0 - Copyright (C) 2026 SamNet Technologies, LLC" + echo "" + echo " paqctl provides one-click installation, configuration," + echo " service management, auto-updates, health monitoring," + echo " and Telegram notifications for paqet." + echo "" + echo -e " ${DIM}Original paqet by hanselime, improved by SamNet.${NC}" + echo "" + read -n 1 -s -r -p " Press any key..." < /dev/tty || true + redraw=true + ;; + b|B) return ;; + "") ;; + *) echo -e " ${RED}Invalid choice${NC}" ;; + esac + done +} + +#═══════════════════════════════════════════════════════════════════════ +# Connection Info Display +#═══════════════════════════════════════════════════════════════════════ + +show_connection_info() { + clear + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${BOLD} CLIENT CONNECTION INFO${NC}" + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + + _load_settings + + local local_ip + local_ip=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown") + + local paqet_installed=false + local gfk_installed=false + [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true + fi + + if [ "$paqet_installed" = true ]; then + echo -e " ${GREEN}${BOLD}━━━ PAQET ━━━${NC}" + echo "" + local paqet_port="${LISTEN_PORT:-8443}" + local paqet_key="${ENCRYPTION_KEY:-not set}" + # Try to get key from config if not in settings + if [ "$paqet_key" = "not set" ] && [ -f "$INSTALL_DIR/config.yaml" ]; then + paqet_key=$(grep -E "^key:" "$INSTALL_DIR/config.yaml" 2>/dev/null | awk '{print $2}' | tr -d '"' || echo "not set") + fi + echo -e " ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}" + echo -e " ${YELLOW}║${NC} Server: ${BOLD}${local_ip}:${paqet_port}${NC}" + echo -e " ${YELLOW}║${NC} Key: ${BOLD}${paqet_key}${NC}" + echo -e " ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${DIM}Client proxy: 127.0.0.1:1080 (SOCKS5)${NC}" + echo "" + fi + + if [ "$gfk_installed" = true ]; then + echo -e " ${MAGENTA}${BOLD}━━━ GFW-KNOCKER ━━━${NC}" + echo "" + local gfk_ip="${GFK_SERVER_IP:-$local_ip}" + local gfk_auth="${GFK_AUTH_CODE:-not set}" + local gfk_mappings="${GFK_PORT_MAPPINGS:-14000:443}" + echo -e " ${YELLOW}╔═══════════════════════════════════════════════════════════╗${NC}" + echo -e " ${YELLOW}║${NC} Server IP: ${BOLD}${gfk_ip}${NC}" + echo -e " ${YELLOW}║${NC} Auth Code: ${BOLD}${gfk_auth}${NC}" + echo -e " ${YELLOW}║${NC} Mappings: ${BOLD}${gfk_mappings}${NC}" + echo -e " ${YELLOW}╚═══════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${DIM}VIO port: ${GFK_VIO_PORT:-45000} | QUIC port: ${GFK_QUIC_PORT:-25000}${NC}" + local _gfk_proxy_port + if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + _gfk_proxy_port="$GFK_SOCKS_VIO_PORT" + else + _gfk_proxy_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + fi + echo -e " ${DIM}Client proxy: 127.0.0.1:${_gfk_proxy_port} (SOCKS5)${NC}" + echo "" + fi + + if [ "$paqet_installed" = false ] && [ "$gfk_installed" = false ]; then + echo -e " ${YELLOW}No backends installed yet.${NC}" + echo "" + echo " Run 'sudo paqctl menu' and select 'Settings & Tools'" + echo " to install a backend." + echo "" + fi + + echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true +} + +#═══════════════════════════════════════════════════════════════════════ +# Interactive Menu +#═══════════════════════════════════════════════════════════════════════ + +show_menu() { + # Auto-fix systemd service if in failed state + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + local svc_state=$(systemctl is-active paqctl.service 2>/dev/null) + if [ "$svc_state" = "failed" ]; then + systemctl reset-failed paqctl.service 2>/dev/null || true + fi + fi + + # Reload settings + _load_settings + + local paqet_installed=false + local gfk_installed=false + local redraw=true + + while true; do + if [ "$redraw" = true ]; then + # Re-check what's installed each redraw + paqet_installed=false + gfk_installed=false + [ -f "$INSTALL_DIR/bin/paqet" ] && paqet_installed=true + if [ "$ROLE" = "server" ]; then + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_server.py" ] && gfk_installed=true + else + [ -d "$GFK_DIR" ] && [ -f "$GFK_DIR/quic_client.py" ] && gfk_installed=true + fi + + clear + print_header + + # Status line showing both backends + echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}" + echo -e " ${BOLD}BACKEND STATUS${NC} (Role: ${ROLE})" + echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}" + + # Paqet status + if [ "$paqet_installed" = true ]; then + local _paqet_info="" + if [ "$ROLE" = "server" ]; then + _paqet_info="Port: ${LISTEN_PORT:-8443}" + else + _paqet_info="Server: ${REMOTE_SERVER:-N/A}" + fi + if is_paqet_running; then + echo -e " Paqet: ${GREEN}● Running${NC} | ${_paqet_info} | SOCKS5: 127.0.0.1:${SOCKS_PORT:-1080}" + else + echo -e " Paqet: ${RED}○ Stopped${NC} | ${_paqet_info}" + fi + else + echo -e " Paqet: ${DIM}not installed${NC}" + fi + + # GFK status + if [ "$gfk_installed" = true ]; then + if is_gfk_running; then + local _gfk_sv + if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + _gfk_sv="$GFK_SOCKS_VIO_PORT" + else + _gfk_sv=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + fi + echo -e " GFK: ${GREEN}● Running${NC} | VIO: ${GFK_VIO_PORT:-45000} | SOCKS5: 127.0.0.1:${_gfk_sv}" + else + echo -e " GFK: ${RED}○ Stopped${NC} | VIO: ${GFK_VIO_PORT:-45000}" + fi + else + echo -e " GFK: ${DIM}not installed${NC}" + fi + + echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}" + + echo "" + echo -e " ${CYAN}MAIN MENU${NC}" + echo "" + echo " 1. View status" + echo " 2. View logs" + echo " 3. Health check" + echo " 4. Update" + echo "" + + # Paqet controls + if [ "$paqet_installed" = true ]; then + if is_paqet_running; then + echo -e " p. ${RED}Stop${NC} Paqet" + else + echo -e " p. ${GREEN}Start${NC} Paqet" + fi + fi + + # GFK controls + if [ "$gfk_installed" = true ]; then + if is_gfk_running; then + echo -e " g. ${RED}Stop${NC} GFK" + else + echo -e " g. ${GREEN}Start${NC} GFK" + fi + fi + + # Start/Stop all + if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then + echo "" + if is_paqet_running && is_gfk_running; then + echo -e " a. ${RED}Stop ALL${NC} backends" + elif ! is_paqet_running && ! is_gfk_running; then + echo -e " a. ${GREEN}Start ALL${NC} backends" + else + echo " a. Toggle ALL backends" + fi + fi + + echo "" + echo " 8. Settings & Tools" + echo -e " ${YELLOW}c. Connection Info${NC}" + echo " i. Info & Help" + echo -e " ${RED}u. Uninstall${NC}" + echo " 0. Exit" + echo -e "${CYAN}─────────────────────────────────────────────────────────────────${NC}" + echo "" + redraw=false + fi + + echo -n " Select option: " + if ! read choice < /dev/tty 2>/dev/null; then + log_error "Cannot read input. If piped, run: sudo paqctl menu" + exit 1 + fi + + case "$choice" in + 1) show_status; read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true; redraw=true ;; + 2) show_logs; redraw=true ;; + 3) health_check; read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true; redraw=true ;; + 4) update_paqet; read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true; redraw=true ;; + p|P) + if [ "$paqet_installed" = true ]; then + if is_paqet_running; then + stop_paqet_backend + else + start_paqet_backend + fi + read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true + else + echo -e " ${YELLOW}Paqet not installed${NC}" + fi + redraw=true + ;; + g|G) + if [ "$gfk_installed" = true ]; then + if is_gfk_running; then + stop_gfk_backend + else + start_gfk_backend + fi + read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true + else + echo -e " ${YELLOW}GFK not installed${NC}" + fi + redraw=true + ;; + a|A) + if [ "$paqet_installed" = true ] && [ "$gfk_installed" = true ]; then + if is_paqet_running && is_gfk_running; then + # Stop all + stop_paqet_backend + stop_gfk_backend + elif ! is_paqet_running && ! is_gfk_running; then + # Start all + start_paqet_backend + start_gfk_backend + else + # Mixed state - ask user + echo "" + echo " 1. Start all backends" + echo " 2. Stop all backends" + echo -n " Choice: " + read subchoice < /dev/tty || true + case "$subchoice" in + 1) + [ "$paqet_installed" = true ] && ! is_paqet_running && start_paqet_backend + [ "$gfk_installed" = true ] && ! is_gfk_running && start_gfk_backend + ;; + 2) + is_paqet_running && stop_paqet_backend + is_gfk_running && stop_gfk_backend + ;; + esac + fi + read -n 1 -s -r -p " Press any key to return..." < /dev/tty || true + fi + redraw=true + ;; + 8) show_settings_menu; redraw=true ;; + c|C) show_connection_info; redraw=true ;; + i|I) show_info_menu; redraw=true ;; + u|U) uninstall_paqctl; exit 0 ;; + 0) echo " Exiting."; exit 0 ;; + "") ;; + *) echo -e " ${RED}Invalid choice: ${NC}${YELLOW}$choice${NC}" ;; + esac + done +} + +#═══════════════════════════════════════════════════════════════════════ +# CLI Command Router +#═══════════════════════════════════════════════════════════════════════ + +case "${1:-menu}" in + status) show_status ;; + start) start_paqet ;; + stop) stop_paqet ;; + restart) restart_paqet ;; + start-paqet) start_paqet_backend ;; + stop-paqet) stop_paqet_backend ;; + start-gfk) start_gfk_backend ;; + stop-gfk) stop_gfk_backend ;; + start-all) start_paqet_backend; start_gfk_backend ;; + stop-all) stop_paqet_backend; stop_gfk_backend ;; + logs) show_logs ;; + health) health_check ;; + update) update_paqet ;; + config) change_config ;; + secret) generate_secret ;; + firewall) show_firewall ;; + rollback) rollback_paqet ;; + ping) run_ping ;; + dump) run_dump ;; + backup) backup_config ;; + restore) restore_config ;; + telegram) show_telegram_menu ;; + uninstall) uninstall_paqctl ;; + version) show_version ;; + help|--help|-h) show_help ;; + menu) show_menu ;; + _apply-firewall) _apply_firewall ;; + _remove-firewall) _remove_firewall ;; + *) + echo -e "${RED}Unknown command: $1${NC}" + echo "Run 'sudo paqctl help' for usage." + exit 1 + ;; +esac +MANAGEMENT + + # Replace placeholder + sed "s#REPLACE_ME_INSTALL_DIR#$INSTALL_DIR#g" "$tmp_script" > "$tmp_script.sed" && mv "$tmp_script.sed" "$tmp_script" + + if ! chmod +x "$tmp_script"; then + log_error "Failed to make management script executable" + rm -f "$tmp_script" + return 1 + fi + if ! mv -f "$tmp_script" "$INSTALL_DIR/paqctl"; then + log_error "Failed to install management script" + rm -f "$tmp_script" + return 1 + fi + + # Create symlink + rm -f /usr/local/bin/paqctl 2>/dev/null + if ! ln -sf "$INSTALL_DIR/paqctl" /usr/local/bin/paqctl; then + log_warn "Failed to create symlink /usr/local/bin/paqctl" + fi + + log_success "Management script installed → /usr/local/bin/paqctl" +} + +#═══════════════════════════════════════════════════════════════════════ +# Main Installation Flow +#═══════════════════════════════════════════════════════════════════════ + +_load_settings() { + [ -f "$INSTALL_DIR/settings.conf" ] || return 0 + # Safe settings loading without eval + while IFS='=' read -r key value; do + [[ "$key" =~ ^[A-Z_][A-Z_0-9]*$ ]] || continue + value="${value#\"}"; value="${value%\"}" + # Skip values with dangerous shell characters + [[ "$value" =~ [\`\$\(] ]] && continue + case "$key" in + BACKEND) BACKEND="$value" ;; + ROLE) ROLE="$value" ;; + PAQET_VERSION) PAQET_VERSION="$value" ;; + PAQCTL_VERSION) PAQCTL_VERSION="$value" ;; + LISTEN_PORT) [[ "$value" =~ ^[0-9]*$ ]] && LISTEN_PORT="$value" ;; + SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && SOCKS_PORT="$value" ;; + INTERFACE) INTERFACE="$value" ;; + LOCAL_IP) LOCAL_IP="$value" ;; + GATEWAY_MAC) GATEWAY_MAC="$value" ;; + ENCRYPTION_KEY) ENCRYPTION_KEY="$value" ;; + PAQET_TCP_LOCAL_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_LOCAL_FLAG="$value" ;; + PAQET_TCP_REMOTE_FLAG) [[ "$value" =~ ^[FSRPAUEC]+(,[FSRPAUEC]+)*$ ]] && PAQET_TCP_REMOTE_FLAG="$value" ;; + REMOTE_SERVER) REMOTE_SERVER="$value" ;; + GFK_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_PORT="$value" ;; + GFK_VIO_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_VIO_CLIENT_PORT="$value" ;; + GFK_QUIC_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_PORT="$value" ;; + GFK_QUIC_CLIENT_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_QUIC_CLIENT_PORT="$value" ;; + GFK_AUTH_CODE) GFK_AUTH_CODE="$value" ;; + GFK_PORT_MAPPINGS) GFK_PORT_MAPPINGS="$value" ;; + GFK_SOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_PORT="$value" ;; + GFK_SOCKS_VIO_PORT) [[ "$value" =~ ^[0-9]*$ ]] && GFK_SOCKS_VIO_PORT="$value" ;; + XRAY_PANEL_DETECTED) XRAY_PANEL_DETECTED="$value" ;; + MICROSOCKS_PORT) [[ "$value" =~ ^[0-9]*$ ]] && MICROSOCKS_PORT="$value" ;; + GFK_SERVER_IP) GFK_SERVER_IP="$value" ;; + GFK_TCP_FLAGS) [[ "$value" =~ ^[FSRPAUEC]+$ ]] && GFK_TCP_FLAGS="$value" ;; + TELEGRAM_BOT_TOKEN) TELEGRAM_BOT_TOKEN="$value" ;; + TELEGRAM_CHAT_ID) TELEGRAM_CHAT_ID="$value" ;; + TELEGRAM_INTERVAL) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_INTERVAL="$value" ;; + TELEGRAM_ENABLED) TELEGRAM_ENABLED="$value" ;; + TELEGRAM_ALERTS_ENABLED) TELEGRAM_ALERTS_ENABLED="$value" ;; + TELEGRAM_DAILY_SUMMARY) TELEGRAM_DAILY_SUMMARY="$value" ;; + TELEGRAM_WEEKLY_SUMMARY) TELEGRAM_WEEKLY_SUMMARY="$value" ;; + TELEGRAM_SERVER_LABEL) TELEGRAM_SERVER_LABEL="$value" ;; + TELEGRAM_START_HOUR) [[ "$value" =~ ^[0-9]+$ ]] && TELEGRAM_START_HOUR="$value" ;; + esac + done < <(grep '^[A-Z_][A-Z_0-9]*=' "$INSTALL_DIR/settings.conf") +} + +# Handle --update-components flag (called during self-update) +if [ "${1:-}" = "--update-components" ]; then + INSTALL_DIR="${INSTALL_DIR:-/opt/paqctl}" + _load_settings + create_management_script + exit 0 +fi + +main() { + check_root + print_header + + # Check if already installed + if [ -f "$INSTALL_DIR/settings.conf" ] && { [ -x "$INSTALL_DIR/bin/paqet" ] || [ -f "$GFK_DIR/mainserver.py" ]; }; then + _load_settings + log_info "paqctl is already installed (backend: ${BACKEND:-paqet})." + echo "" + echo " 1. Reinstall / Reconfigure" + echo " 2. Open menu (same as: sudo paqctl menu)" + echo " 3. Exit" + echo "" + read -p " Choice [1-3]: " choice < /dev/tty || true + case "$choice" in + 1) log_info "Reinstalling..." ;; + 2) exec /usr/local/bin/paqctl menu ;; + *) exit 0 ;; + esac + fi + + # Step 1: Detect OS + log_info "Step 1/7: Detecting operating system..." + detect_os + echo "" + + # Step 2: Install dependencies + log_info "Step 2/7: Installing dependencies..." + check_dependencies + echo "" + + # Step 3: Configuration wizard (determines backend + role + config) + log_info "Step 3/7: Configuration..." + run_config_wizard + echo "" + + # Step 4: Backend-specific dependencies and download + log_info "Step 4/7: Setting up ${BACKEND} backend..." + if [ "$BACKEND" = "gfw-knocker" ]; then + install_python_deps || { log_error "Failed to install Python dependencies"; exit 1; } + download_gfk || { log_error "Failed to download GFK"; exit 1; } + generate_gfk_certs || { log_error "Failed to generate certificates"; exit 1; } + if [ "$ROLE" = "server" ]; then + # Install Xray SOCKS5 proxy (adds alongside panel if detected) + setup_xray_for_gfk || { log_error "Failed to setup Xray"; exit 1; } + # Regenerate config if mappings changed (panel detected → SOCKS5 added) + if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then + generate_gfk_config || { log_error "Failed to regenerate GFK config"; exit 1; } + fi + elif [ "$ROLE" = "client" ]; then + create_gfk_client_wrapper || { log_error "Failed to create client wrapper"; exit 1; } + fi + PAQET_VERSION="$GFK_VERSION_PINNED" + log_info "Using GFK ${PAQET_VERSION} (pinned for stability)" + else + # Fetch latest version from GitHub, fall back to pinned if API unreachable + PAQET_VERSION=$(curl -s --max-time 10 "$PAQET_API_URL" 2>/dev/null | grep -o '"tag_name"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"') + if [ -z "$PAQET_VERSION" ] || ! _validate_version_tag "$PAQET_VERSION"; then + PAQET_VERSION="$PAQET_VERSION_PINNED" + fi + log_info "Installing paqet ${PAQET_VERSION}" + download_paqet "$PAQET_VERSION" + fi + echo "" + + # Step 5: Apply firewall rules + log_info "Step 5/7: Firewall setup..." + if [ "$BACKEND" = "gfw-knocker" ]; then + if [ "$ROLE" = "server" ]; then + local _vio_port="${GFK_VIO_PORT:-45000}" + log_info "Blocking VIO TCP port $_vio_port (raw socket handles it)..." + if _is_firewalld_active; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO INPUT DROP rule via firewalld" + firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO RST DROP rule via firewalld" + firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + persist_iptables_rules + elif command -v iptables &>/dev/null; then + modprobe iptable_raw 2>/dev/null || true + iptables -t raw -C PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A PREROUTING -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + iptables -t raw -C OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A OUTPUT -p tcp --sport "$_vio_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + iptables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO INPUT DROP rule" + iptables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO RST DROP rule" + if command -v ip6tables &>/dev/null; then + ip6tables -C INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + ip6tables -A INPUT -p tcp --dport "$_vio_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + ip6tables -C OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + ip6tables -A OUTPUT -p tcp --sport "$_vio_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + fi + else + log_warn "iptables not found - firewall rules cannot be applied" + fi + else + local _vio_client_port="${GFK_VIO_CLIENT_PORT:-40000}" + log_info "Applying NOTRACK + DROP rules for VIO client port $_vio_client_port..." + if _is_firewalld_active; then + firewall-cmd --direct --query-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw PREROUTING 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 raw OUTPUT 0 -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + firewall-cmd --direct --query-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO client INPUT DROP rule via firewalld" + firewall-cmd --direct --query-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv4 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO client RST DROP rule via firewalld" + firewall-cmd --direct --query-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 filter INPUT 0 -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + firewall-cmd --direct --query-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + firewall-cmd --direct --add-rule ipv6 filter OUTPUT 0 -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + persist_iptables_rules + elif command -v iptables &>/dev/null; then + modprobe iptable_raw 2>/dev/null || true + iptables -t raw -C PREROUTING -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A PREROUTING -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + iptables -t raw -C OUTPUT -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || \ + iptables -t raw -A OUTPUT -p tcp --sport "$_vio_client_port" -m comment --comment "paqctl" -j NOTRACK 2>/dev/null || true + iptables -C INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -A INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO client INPUT DROP rule" + iptables -C OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + iptables -A OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + log_warn "Failed to add VIO client RST DROP rule" + if command -v ip6tables &>/dev/null; then + ip6tables -C INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + ip6tables -A INPUT -p tcp --dport "$_vio_client_port" -m comment --comment "paqctl" -j DROP 2>/dev/null || true + ip6tables -C OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || \ + ip6tables -A OUTPUT -p tcp --sport "$_vio_client_port" --tcp-flags RST RST -m comment --comment "paqctl" -j DROP 2>/dev/null || true + fi + else + log_warn "iptables not found - firewall rules cannot be applied" + fi + fi + elif [ "$ROLE" = "server" ]; then + apply_iptables_rules "$LISTEN_PORT" + else + log_info "Client mode - no firewall rules needed" + fi + echo "" + + # Step 6: Create service + management script + log_info "Step 6/7: Setting up service..." + if ! mkdir -p "$INSTALL_DIR/bin" "$BACKUP_DIR"; then + log_error "Failed to create installation directories" + exit 1 + fi + create_management_script + setup_service + setup_logrotate + # Save settings to persist version and config + save_settings + echo "" + + # Step 7: Start the service + log_info "Step 7/7: Starting ${BACKEND}..." + if command -v systemctl &>/dev/null && [ -d /run/systemd/system ]; then + systemctl start paqctl.service 2>/dev/null + fi + + sleep 2 + + # Final summary + echo "" + echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" + echo -e "${GREEN}${BOLD} INSTALLATION COMPLETE!${NC}" + echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " Backend: ${BOLD}${BACKEND}${NC}" + echo -e " Role: ${BOLD}${ROLE}${NC}" + echo -e " Version: ${BOLD}${PAQET_VERSION}${NC}" + + if [ "$BACKEND" = "gfw-knocker" ]; then + if [ "$ROLE" = "server" ]; then + local _xray_port + _xray_port=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f2 | cut -d, -f1) + echo -e " VIO port: ${BOLD}${GFK_VIO_PORT}${NC}" + echo -e " QUIC port: ${BOLD}${GFK_QUIC_PORT}${NC}" + if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then + echo -e " Xray: ${BOLD}Existing panel detected (forwarding to port ${_xray_port})${NC}" + if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + echo -e " SOCKS5: ${BOLD}127.0.0.1:${GFK_SOCKS_PORT} (auto-added, VIO port ${GFK_SOCKS_VIO_PORT})${NC}" + echo "" + echo -e " ${GREEN}✓ GFK forwards to panel + SOCKS5 proxy added${NC}" + else + echo "" + echo -e " ${GREEN}✓ GFK forwards to panel${NC}" + fi + local _first_vio + _first_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d: -f1 | cut -d, -f1) + echo -e " ${YELLOW}! Panel users: configure Iran outbound → 127.0.0.1:${_first_vio}${NC}" + if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + echo -e " ${YELLOW}! Direct SOCKS5: use 127.0.0.1:${GFK_SOCKS_VIO_PORT} on client${NC}" + fi + else + echo -e " Xray: ${BOLD}127.0.0.1:${_xray_port} (SOCKS5)${NC}" + echo "" + echo -e " ${GREEN}✓ Xray SOCKS5 proxy installed and running${NC}" + fi + echo "" + echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${YELLOW}║ ${BOLD}CLIENT CONNECTION INFO - SAVE THIS!${NC}${YELLOW} ║${NC}" + echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════╣${NC}" + echo -e "${YELLOW}║${NC} Server IP: ${BOLD}${GFK_SERVER_IP}${NC}" + echo -e "${YELLOW}║${NC} Auth Code: ${BOLD}${GFK_AUTH_CODE}${NC}" + echo -e "${YELLOW}║${NC} Mappings: ${BOLD}${GFK_PORT_MAPPINGS}${NC}" + if [ "${XRAY_PANEL_DETECTED:-false}" = "true" ] && [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + echo -e "${YELLOW}║${NC}" + echo -e "${YELLOW}║${NC} ${GREEN}Proxy port: 127.0.0.1:${GFK_SOCKS_VIO_PORT} (SOCKS5 — use this on client)${NC}" + local _panel_vio + _panel_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + echo -e "${YELLOW}║${NC} Panel port: 127.0.0.1:${_panel_vio} (vmess/vless — for panel-to-panel)" + elif [ "${XRAY_PANEL_DETECTED:-false}" = "true" ]; then + local _panel_vio + _panel_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + echo -e "${YELLOW}║${NC}" + echo -e "${YELLOW}║${NC} Panel port: 127.0.0.1:${_panel_vio} (vmess/vless — for panel-to-panel)" + else + local _proxy_vio + _proxy_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + echo -e "${YELLOW}║${NC}" + echo -e "${YELLOW}║${NC} ${GREEN}Proxy port: 127.0.0.1:${_proxy_vio} (SOCKS5 — use this on client)${NC}" + fi + echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}" + else + local _socks_vio + if [ -n "${GFK_SOCKS_VIO_PORT:-}" ]; then + _socks_vio="$GFK_SOCKS_VIO_PORT" + else + _socks_vio=$(echo "${GFK_PORT_MAPPINGS:-14000:443}" | cut -d, -f1 | cut -d: -f1) + fi + echo -e " Server: ${BOLD}${GFK_SERVER_IP}${NC}" + echo -e " SOCKS5: ${BOLD}127.0.0.1:${_socks_vio}${NC}" + echo "" + echo -e " ${YELLOW}Test your proxy:${NC}" + echo -e " ${BOLD} curl --proxy socks5h://127.0.0.1:${_socks_vio} https://httpbin.org/ip${NC}" + fi + elif [ "$ROLE" = "server" ]; then + echo -e " Port: ${BOLD}${LISTEN_PORT}${NC}" + echo "" + echo -e "${YELLOW}╔═══════════════════════════════════════════════════════════════╗${NC}" + echo -e "${YELLOW}║ ${BOLD}CLIENT CONNECTION INFO - SAVE THIS!${NC}${YELLOW} ║${NC}" + echo -e "${YELLOW}╠═══════════════════════════════════════════════════════════════╣${NC}" + echo -e "${YELLOW}║${NC} Server: ${BOLD}${LOCAL_IP}:${LISTEN_PORT}${NC}" + echo -e "${YELLOW}║${NC} Key: ${BOLD}${ENCRYPTION_KEY}${NC}" + echo -e "${YELLOW}╚═══════════════════════════════════════════════════════════════╝${NC}" + echo "" + echo -e " ${CYAN}Key also saved in: ${INSTALL_DIR}/config.yaml${NC}" + else + echo -e " Server: ${BOLD}${REMOTE_SERVER}${NC}" + echo -e " SOCKS5: ${BOLD}127.0.0.1:${SOCKS_PORT}${NC}" + echo "" + echo -e " ${YELLOW}Test your proxy:${NC}" + echo -e " ${BOLD} curl --proxy socks5h://127.0.0.1:${SOCKS_PORT} https://httpbin.org/ip${NC}" + fi + + echo "" + echo -e " ${CYAN}Management commands:${NC}" + echo " sudo paqctl menu Interactive menu" + echo " sudo paqctl status Check status" + echo " sudo paqctl health Health check" + echo " sudo paqctl logs View logs" + echo " sudo paqctl update Update paqet" + echo " sudo paqctl help All commands" + echo "" + echo -e "${GREEN}═══════════════════════════════════════════════════════════════${NC}" + echo "" + echo -e " ${BOLD}${YELLOW}⚠ IMPORTANT: Save the connection info above before continuing!${NC}" + echo "" + echo -e " ${CYAN}Press Y to open the management menu, or any other key to exit...${NC}" + read -n 1 -r choice < /dev/tty || true + echo "" + if [[ "$choice" =~ ^[Yy]$ ]]; then + exec /usr/local/bin/paqctl menu + else + echo -e " ${GREEN}Run 'sudo paqctl menu' when ready.${NC}" + echo "" + fi +} + +# Handle command line arguments +case "${1:-}" in + menu) + check_root + if [ -f "$INSTALL_DIR/settings.conf" ]; then + _load_settings + show_menu + else + echo -e "${RED}paqctl is not installed. Run the installer first.${NC}" + exit 1 + fi + ;; + *) + main "$@" + ;; +esac diff --git a/paqet-linux-amd64-v1.0.0-alpha.12.tar.gz b/paqet-linux-amd64-v1.0.0-alpha.12.tar.gz new file mode 100644 index 0000000..6893564 Binary files /dev/null and b/paqet-linux-amd64-v1.0.0-alpha.12.tar.gz differ diff --git a/windows/GFK-Client.bat b/windows/GFK-Client.bat new file mode 100644 index 0000000..8ff3b51 --- /dev/null +++ b/windows/GFK-Client.bat @@ -0,0 +1,42 @@ +@echo off +:: GFW-knocker Client Launcher +:: Double-click to run + +:: Check for admin rights +net session >nul 2>&1 +if %errorlevel% neq 0 ( + echo Requesting Administrator privileges... + powershell -Command "Start-Process '%~f0' -Verb RunAs" + exit /b +) + +echo. +echo =============================================== +echo GFW-KNOCKER CLIENT (Python/QUIC Proxy) +echo =============================================== +echo. +echo Requirements: +echo - Npcap (will prompt to install) +echo - Python 3.x (will prompt to install) +echo. +echo Once connected, configure your browser: +echo. +echo FIREFOX: +echo Settings ^> Network Settings ^> Settings +echo Select "Manual proxy configuration" +echo SOCKS Host: 127.0.0.1 Port: 1080 +echo Select SOCKS v5 +echo Check "Proxy DNS when using SOCKS v5" +echo. +echo CHROME (launch with proxy): +echo chrome.exe --proxy-server="socks5://127.0.0.1:1080" +echo. +echo To verify: Visit https://ifconfig.me +echo (Should show your server IP, not your home IP) +echo. +echo Press Ctrl+C to disconnect +echo =============================================== +echo. + +:: Run the PowerShell script with gfk backend +powershell -ExecutionPolicy Bypass -NoExit -File "%~dp0paqet-client.ps1" -Backend gfk diff --git a/windows/Paqet-Client.bat b/windows/Paqet-Client.bat new file mode 100644 index 0000000..a634526 --- /dev/null +++ b/windows/Paqet-Client.bat @@ -0,0 +1,38 @@ +@echo off +:: Paqet Client Launcher +:: Double-click to run + +:: Check for admin rights +net session >nul 2>&1 +if %errorlevel% neq 0 ( + echo Requesting Administrator privileges... + powershell -Command "Start-Process '%~f0' -Verb RunAs" + exit /b +) + +echo. +echo =============================================== +echo PAQET CLIENT (KCP Raw Socket Proxy) +echo =============================================== +echo. +echo Once connected, configure your browser: +echo. +echo FIREFOX: +echo Settings ^> Network Settings ^> Settings +echo Select "Manual proxy configuration" +echo SOCKS Host: 127.0.0.1 Port: 1080 +echo Select SOCKS v5 +echo Check "Proxy DNS when using SOCKS v5" +echo. +echo CHROME (launch with proxy): +echo chrome.exe --proxy-server="socks5://127.0.0.1:1080" +echo. +echo To verify: Visit https://ifconfig.me +echo (Should show your server IP, not your home IP) +echo. +echo Press Ctrl+C to disconnect +echo =============================================== +echo. + +:: Run the PowerShell script with paqet backend +powershell -ExecutionPolicy Bypass -NoExit -File "%~dp0paqet-client.ps1" -Backend paqet diff --git a/windows/paqet-client.ps1 b/windows/paqet-client.ps1 new file mode 100644 index 0000000..44013db --- /dev/null +++ b/windows/paqet-client.ps1 @@ -0,0 +1,1144 @@ +#Requires -RunAsAdministrator +<# +.SYNOPSIS + Paqet/GFK Windows Client - Bypass Firewall Restrictions + +.DESCRIPTION + This script helps you connect to your server through firewalls that block normal connections. + It supports two backends: + + PAQET (Recommended for most users) + ───────────────────────────────── + • Simple all-in-one solution with built-in SOCKS5 proxy + • Uses KCP protocol over raw sockets to bypass DPI + • Works on: Windows (with Npcap) + • Configuration: Just needs server IP, port, and encryption key + • Proxy: 127.0.0.1:1080 (SOCKS5) + + GFW-KNOCKER (For heavily restricted networks) + ───────────────────────────────────────────── + • Uses "violated TCP" packets + QUIC tunnel to evade deep packet inspection + • More complex but better at evading sophisticated firewalls (like GFW) + • Works on: Windows (with Npcap + Python) + • Requires: Xray running on server port 443 + • Proxy: 127.0.0.1:14000 (forwards to server's Xray SOCKS5) + + CAN I RUN BOTH? + ─────────────── + Yes! Both can run simultaneously on different ports: + • Paqet SOCKS5: 127.0.0.1:1080 + • GFK tunnel: 127.0.0.1:14000 + This lets you have a backup if one method gets blocked. + +.NOTES + Requirements: + • Administrator privileges (for raw socket access) + • Npcap (https://npcap.com) - auto-installed if missing + • Python 3.10+ (GFK only) - auto-installed if missing +#> + +param( + [string]$ServerAddr, + [string]$Key, + [string]$Action = "menu", # menu, run, install, config, stop, status + [string]$Backend = "" # paqet, gfk (auto-detect if not specified) +) + +$ErrorActionPreference = "Stop" + +# Directories and pinned versions (for stability - update after testing new releases) +$InstallDir = "C:\paqet" +$PaqetExe = "$InstallDir\paqet_windows_amd64.exe" +$PaqetVersionPinned = "v1.0.0-alpha.17" # Fallback if GitHub API unreachable +$GfkDir = "$InstallDir\gfk" +$ConfigFile = "$InstallDir\config.yaml" +$SettingsFile = "$InstallDir\settings.conf" + +# Npcap (pinned version) +$NpcapVersion = "1.80" +$NpcapUrl = "https://npcap.com/dist/npcap-$NpcapVersion.exe" +$NpcapInstaller = "$env:TEMP\npcap-$NpcapVersion.exe" + +# GFK scripts - bundled locally for faster setup (only works when running from downloaded repo) +# When running via "irm | iex", $MyInvocation.MyCommand.Path is null +$ScriptDir = if ($MyInvocation.MyCommand.Path) { Split-Path -Parent $MyInvocation.MyCommand.Path } else { $null } +$GfkLocalDir = if ($ScriptDir) { "$ScriptDir\..\gfk\client" } else { $null } +$GfkFiles = @("mainclient.py", "quic_client.py", "vio_client.py") # parameters.py is generated + +# Colors +function Write-Info { Write-Host "[INFO] $args" -ForegroundColor Cyan } +function Write-Success { Write-Host "[OK] $args" -ForegroundColor Green } +function Write-Warn { Write-Host "[WARN] $args" -ForegroundColor Yellow } +function Write-Err { Write-Host "[ERROR] $args" -ForegroundColor Red } + +# Fetch latest paqet version from GitHub, fall back to pinned +function Get-LatestPaqetVersion { + try { + $response = Invoke-RestMethod -Uri "https://api.github.com/repos/hanselime/paqet/releases/latest" -TimeoutSec 10 + if ($response.tag_name -match '^v?\d+\.\d+\.\d+') { + return $response.tag_name + } + } catch {} + return $PaqetVersionPinned +} +$PaqetVersion = Get-LatestPaqetVersion + +# Input validation (security: prevent config injection) +function Test-ValidIP { + param([string]$IP) + return $IP -match '^(\d{1,3}\.){3}\d{1,3}$' +} + +function Test-ValidMAC { + param([string]$MAC) + return $MAC -match '^([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}$' +} + +function Test-SafeString { + param([string]$s) + # Block characters that could break Python string literals + if ($s.Contains('"') -or $s.Contains("'") -or $s.Contains('\') -or $s.Contains([char]10) -or $s.Contains([char]13)) { + return $false + } + return $true +} + +#═══════════════════════════════════════════════════════════════════════ +# Prerequisite Checks +#═══════════════════════════════════════════════════════════════════════ + +function Test-Admin { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]$identity + return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +} + +function Test-Npcap { + $npcapPath = "C:\Windows\System32\Npcap" + $wpcapDll = "C:\Windows\System32\wpcap.dll" + return (Test-Path $npcapPath) -or (Test-Path $wpcapDll) +} + +function Test-Python { + try { + $version = & python --version 2>&1 + return $version -match "Python 3\." + } catch { + return $false + } +} + +function Install-NpcapIfMissing { + if (Test-Npcap) { return $true } + + Write-Host "" + Write-Host "===============================================" -ForegroundColor Red + Write-Host " NPCAP REQUIRED" -ForegroundColor Red + Write-Host "===============================================" -ForegroundColor Red + Write-Host "" + Write-Host " Npcap is required for raw socket access." + Write-Host "" + Write-Host " IMPORTANT: During installation, check:" -ForegroundColor Yellow + Write-Host " [x] Install Npcap in WinPcap API-compatible Mode" -ForegroundColor Yellow + Write-Host "" + + $choice = Read-Host " Download and install Npcap now? [Y/n]" + if ($choice -match "^[Nn]") { + Write-Warn "Please install Npcap from https://npcap.com" + return $false + } + + Write-Info "Downloading Npcap $NpcapVersion..." + try { + Invoke-WebRequest -Uri $NpcapUrl -OutFile $NpcapInstaller -UseBasicParsing + Write-Success "Downloaded" + } catch { + Write-Err "Download failed. Please install manually from https://npcap.com" + Start-Process "https://npcap.com/#download" + return $false + } + + Write-Info "Launching Npcap installer..." + Write-Host " Check: [x] WinPcap API-compatible Mode" -ForegroundColor Yellow + Start-Process -FilePath $NpcapInstaller -Wait | Out-Null + Remove-Item $NpcapInstaller -Force -ErrorAction SilentlyContinue + + Start-Sleep -Seconds 2 + if (Test-Npcap) { + Write-Success "Npcap installed!" + return $true + } else { + Write-Err "Npcap installation failed or cancelled" + return $false + } +} + +function Install-PythonIfMissing { + if (Test-Python) { return $true } + + Write-Host "" + Write-Host "===============================================" -ForegroundColor Red + Write-Host " PYTHON 3 REQUIRED" -ForegroundColor Red + Write-Host "===============================================" -ForegroundColor Red + Write-Host "" + Write-Host " GFW-knocker requires Python 3.x" + Write-Host "" + Write-Host " Please install Python from:" -ForegroundColor Yellow + Write-Host " https://www.python.org/downloads/" -ForegroundColor Yellow + Write-Host "" + Write-Host " IMPORTANT: Check 'Add Python to PATH' during install!" -ForegroundColor Yellow + Write-Host "" + + $choice = Read-Host " Open Python download page? [Y/n]" + if ($choice -notmatch "^[Nn]") { + Start-Process "https://www.python.org/downloads/" + } + + Read-Host " Press Enter after installing Python" + + if (Test-Python) { + Write-Success "Python detected!" + return $true + } else { + Write-Err "Python not found. Please restart PowerShell after installing." + return $false + } +} + +function Install-PythonPackages { + Write-Info "Installing Python packages (scapy, aioquic)..." + try { + & python -m pip install --quiet --upgrade pip 2>&1 | Out-Null + & python -m pip install --quiet scapy aioquic 2>&1 | Out-Null + Write-Success "Python packages installed" + return $true + } catch { + Write-Err "Failed to install Python packages: $_" + Write-Info "Try manually: pip install scapy aioquic" + return $false + } +} + +#═══════════════════════════════════════════════════════════════════════ +# Network Detection +#═══════════════════════════════════════════════════════════════════════ + +function Get-NetworkInfo { + $adapter = Get-NetAdapter | Where-Object { + $_.Status -eq "Up" -and + $_.InterfaceDescription -notmatch "Virtual|VirtualBox|VMware|Hyper-V|Loopback" + } | Select-Object -First 1 + + if (-not $adapter) { + $adapter = Get-NetAdapter | Where-Object { $_.Status -eq "Up" } | Select-Object -First 1 + } + + if (-not $adapter) { + Write-Err "No active network adapter found" + return $null + } + + $ifIndex = $adapter.ifIndex + $ipConfig = Get-NetIPAddress -InterfaceIndex $ifIndex -AddressFamily IPv4 | + Where-Object { $_.PrefixOrigin -ne "WellKnown" } | Select-Object -First 1 + $gateway = Get-NetRoute -InterfaceIndex $ifIndex -DestinationPrefix "0.0.0.0/0" -ErrorAction SilentlyContinue | + Select-Object -First 1 + + if (-not $ipConfig) { + Write-Err "No IPv4 address found on $($adapter.Name)" + return $null + } + + $gatewayIP = if ($gateway) { $gateway.NextHop } else { $null } + $gatewayMAC = $null + + if ($gatewayIP) { + $null = Test-Connection -ComputerName $gatewayIP -Count 1 -ErrorAction SilentlyContinue + $arpEntry = Get-NetNeighbor -IPAddress $gatewayIP -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($arpEntry -and $arpEntry.LinkLayerAddress) { + $gatewayMAC = $arpEntry.LinkLayerAddress -replace "-", ":" + } + } + + return @{ + Name = $adapter.Name + Guid = $adapter.InterfaceGuid + IP = $ipConfig.IPAddress + GatewayIP = $gatewayIP + GatewayMAC = $gatewayMAC + } +} + +#═══════════════════════════════════════════════════════════════════════ +# Backend Detection +#═══════════════════════════════════════════════════════════════════════ + +function Get-InstalledBackend { + if (Test-Path $SettingsFile) { + $content = Get-Content $SettingsFile -ErrorAction SilentlyContinue + foreach ($line in $content) { + if ($line -match '^BACKEND="?(\w+)"?') { + return $Matches[1] + } + } + } + if (Test-Path $PaqetExe) { return "paqet" } + if (Test-Path "$GfkDir\mainclient.py") { return "gfk" } + return $null +} + +function Save-Settings { + param([string]$Backend, [string]$ServerAddr = "", [string]$SocksPort = "1080") + + $settings = @" +BACKEND="$Backend" +SERVER_ADDR="$ServerAddr" +SOCKS_PORT="$SocksPort" +"@ + if (-not (Test-Path $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + } + [System.IO.File]::WriteAllText($SettingsFile, $settings) +} + +#═══════════════════════════════════════════════════════════════════════ +# Paqet Functions +#═══════════════════════════════════════════════════════════════════════ + +function Install-Paqet { + Write-Host "" + Write-Host " Installing PAQET" -ForegroundColor Green + Write-Host " ────────────────" -ForegroundColor Green + Write-Host " Paqet is an all-in-one proxy solution with built-in SOCKS5." + Write-Host " It uses KCP protocol over raw sockets to bypass firewalls." + Write-Host "" + Write-Host " What will be installed:" -ForegroundColor Yellow + Write-Host " 1. Npcap (for raw socket access)" + Write-Host " 2. Paqet binary" + Write-Host "" + Write-Host " After setup, configure with your server's IP:port and key." + Write-Host " Your proxy will be: 127.0.0.1:1080 (SOCKS5)" + Write-Host "" + + if (-not (Install-NpcapIfMissing)) { + Write-Err "Cannot continue without Npcap" + return $false + } + + if (-not (Test-Path $InstallDir)) { + New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null + } + + if (Test-Path $PaqetExe) { + Write-Info "paqet already installed" + return $true + } + + $zipUrl = "https://github.com/hanselime/paqet/releases/download/$PaqetVersion/paqet-windows-amd64-$PaqetVersion.zip" + $zipFile = "$env:TEMP\paqet.zip" + + Write-Info "Downloading paqet $PaqetVersion..." + try { + Invoke-WebRequest -Uri $zipUrl -OutFile $zipFile + } catch { + Write-Err "Download failed: $_" + return $false + } + + Write-Info "Extracting..." + Expand-Archive -Path $zipFile -DestinationPath $InstallDir -Force + Remove-Item $zipFile -Force + + Write-Success "paqet installed to $InstallDir" + Save-Settings -Backend "paqet" + return $true +} + +function New-PaqetConfig { + param( + [Parameter(Mandatory)][string]$Server, + [Parameter(Mandatory)][string]$SecretKey, + [string]$TcpLocalFlag = "PA", + [string]$TcpRemoteFlag = "PA" + ) + + # Validate TCP flags (uppercase letters F,S,R,P,A,U,E,C, optionally comma-separated) + if ($TcpLocalFlag -cnotmatch '^[FSRPAUEC]+(,[FSRPAUEC]+)*$') { + Write-Warn "Invalid TCP local flag. Using default: PA" + $TcpLocalFlag = "PA" + } + if ($TcpRemoteFlag -cnotmatch '^[FSRPAUEC]+(,[FSRPAUEC]+)*$') { + Write-Warn "Invalid TCP remote flag. Using default: PA" + $TcpRemoteFlag = "PA" + } + + Write-Info "Detecting network..." + $net = Get-NetworkInfo + if (-not $net) { return $false } + + Write-Info " Adapter: $($net.Name)" + Write-Info " Local IP: $($net.IP)" + Write-Info " Gateway MAC: $($net.GatewayMAC)" + + if (-not $net.GatewayMAC) { + $net.GatewayMAC = Read-Host " Enter gateway MAC (aa:bb:cc:dd:ee:ff)" + } + + # Convert comma-separated flags to YAML array format: PA,A -> ["PA", "A"] + $localFlagArray = ($TcpLocalFlag -split ',') | ForEach-Object { "`"$_`"" } + $remoteFlagArray = ($TcpRemoteFlag -split ',') | ForEach-Object { "`"$_`"" } + $localFlagYaml = "[" + ($localFlagArray -join ", ") + "]" + $remoteFlagYaml = "[" + ($remoteFlagArray -join ", ") + "]" + + $guidEscaped = "\\Device\\NPF_$($net.Guid)" + $config = @" +role: "client" + +log: + level: "info" + +socks5: + - listen: "127.0.0.1:1080" + +network: + interface: "$($net.Name)" + guid: "$guidEscaped" + ipv4: + addr: "$($net.IP):0" + router_mac: "$($net.GatewayMAC)" + tcp: + local_flag: $localFlagYaml + remote_flag: $remoteFlagYaml + +server: + addr: "$Server" + +transport: + protocol: "kcp" + kcp: + mode: "fast" + key: "$SecretKey" +"@ + + # Ensure install directory exists + if (-not (Test-Path $InstallDir)) { + Write-Err "Paqet is not installed. Please install paqet first (option 1)." + return $false + } + + [System.IO.File]::WriteAllText($ConfigFile, $config) + Save-Settings -Backend "paqet" -ServerAddr $Server + Write-Success "Configuration saved" + return $true +} + +function Start-Paqet { + if (-not (Test-Npcap)) { + if (-not (Install-NpcapIfMissing)) { return } + } + + if (-not (Test-Path $PaqetExe)) { + Write-Err "paqet not installed" + return + } + + if (-not (Test-Path $ConfigFile)) { + Write-Err "Config not found. Configure first." + return + } + + Write-Host "" + Write-Host " Starting PAQET" -ForegroundColor Green + Write-Host " ──────────────" + Write-Host " Paqet will connect to your server using KCP over raw sockets." + Write-Host "" + Write-Host " Your SOCKS5 proxy will be: 127.0.0.1:1080" + Write-Host " Configure your browser to use this proxy." + Write-Host "" + Write-Info "Starting paqet..." + Write-Info "SOCKS5 proxy: 127.0.0.1:1080" + Write-Info "Press Ctrl+C to stop" + Write-Host "" + + & $PaqetExe run -c $ConfigFile +} + +#═══════════════════════════════════════════════════════════════════════ +# GFW-knocker Functions +#═══════════════════════════════════════════════════════════════════════ + +function Install-Gfk { + Write-Host "" + Write-Host " Installing GFW-KNOCKER" -ForegroundColor Yellow + Write-Host " ──────────────────────" -ForegroundColor Yellow + Write-Host " GFK is an advanced anti-censorship tool designed for heavy DPI." + Write-Host " It uses 'violated TCP' packets + QUIC tunneling to evade detection." + Write-Host "" + Write-Host " What will be installed:" -ForegroundColor Yellow + Write-Host " 1. Npcap (for raw socket access)" + Write-Host " 2. Python 3.10+ (for QUIC protocol)" + Write-Host " 3. Python packages: scapy, aioquic" + Write-Host " 4. GFK client scripts" + Write-Host "" + Write-Host " IMPORTANT: Your server must have Xray running on port 443." -ForegroundColor Cyan + Write-Host " GFK is just a tunnel - Xray provides the actual SOCKS5 proxy." + Write-Host "" + Write-Host " After setup, your proxy will be: 127.0.0.1:14000 (SOCKS5)" + Write-Host "" + + # Check prerequisites + if (-not (Install-NpcapIfMissing)) { return $false } + if (-not (Install-PythonIfMissing)) { return $false } + if (-not (Install-PythonPackages)) { return $false } + + # Create directories + if (-not (Test-Path $GfkDir)) { + New-Item -ItemType Directory -Path $GfkDir -Force | Out-Null + } + + # Copy bundled GFK scripts or download from Gitea + Write-Info "Setting up GFW-knocker scripts..." + $GfkGitHubBase = "https://git.samnet.dev/SamNet-dev/paqctl/raw/branch/main/gfk/client" + foreach ($file in $GfkFiles) { + $dest = "$GfkDir\$file" + $src = if ($GfkLocalDir) { "$GfkLocalDir\$file" } else { $null } + + if ($src -and (Test-Path $src)) { + # Copy from local bundled files (faster) + Copy-Item -Path $src -Destination $dest -Force + Write-Info " Copied $file" + } else { + # Download from Gitea (for one-liner installation) + Write-Info " Downloading $file..." + try { + Invoke-WebRequest -Uri "$GfkGitHubBase/$file" -OutFile $dest -UseBasicParsing + Write-Info " Downloaded $file" + } catch { + Write-Err "Failed to download $file from Gitea" + return $false + } + } + } + + Write-Success "GFW-knocker installed to $GfkDir" + Save-Settings -Backend "gfk" + return $true +} + +function New-GfkConfig { + param( + [Parameter(Mandatory)][string]$ServerIP, + [Parameter(Mandatory)][string]$AuthCode, + [string]$SocksPort = "1080", + [string]$TcpFlags = "AP" + ) + + # Validate inputs (security: prevent config injection) + if (-not (Test-ValidIP $ServerIP)) { + Write-Err "Invalid server IP format" + return $false + } + if (-not (Test-SafeString $AuthCode)) { + Write-Err "Invalid auth code format" + return $false + } + # Validate TCP flags (uppercase letters only: F,S,R,P,A,U,E,C) + if ($TcpFlags -cnotmatch '^[FSRPAUEC]+$') { + Write-Warn "Invalid TCP flags. Using default: AP" + $TcpFlags = "AP" + } + + Write-Info "Detecting network..." + $net = Get-NetworkInfo + if (-not $net) { return $false } + + Write-Info " Adapter: $($net.Name)" + Write-Info " Local IP: $($net.IP)" + Write-Info " Gateway: $($net.GatewayMAC)" + + if (-not $net.GatewayMAC) { + $net.GatewayMAC = Read-Host " Enter gateway MAC (aa:bb:cc:dd:ee:ff)" + } + + # Validate detected network values + if (-not (Test-ValidIP $net.IP)) { + Write-Err "Invalid local IP detected" + return $false + } + if ($net.GatewayMAC -and -not (Test-ValidMAC $net.GatewayMAC)) { + Write-Err "Invalid gateway MAC format" + return $false + } + + # Create parameters.py for GFK (matching expected variable names) + $params = @" +# GFW-knocker client configuration (auto-generated) +from scapy.all import conf + +# Network interface for scapy (Windows Npcap) +conf.iface = r"\Device\NPF_$($net.Guid)" +my_ip = "$($net.IP)" +gateway_mac = "$($net.GatewayMAC)" + +# Server settings +vps_ip = "$ServerIP" +xray_server_ip = "127.0.0.1" + +# Port mappings (local_port: remote_port) +tcp_port_mapping = {14000: 443} +udp_port_mapping = {} + +# VIO (raw socket) ports +vio_tcp_server_port = 45000 +vio_tcp_client_port = 40000 +vio_udp_server_port = 35000 +vio_udp_client_port = 30000 + +# QUIC tunnel ports +quic_server_port = 25000 +quic_client_port = 20000 +quic_local_ip = "127.0.0.1" + +# QUIC settings +quic_verify_cert = False +quic_idle_timeout = 86400 +udp_timeout = 300 +quic_mtu = 1420 +quic_max_data = 1073741824 +quic_max_stream_data = 1073741824 +quic_auth_code = "$AuthCode" +quic_certificate = "cert.pem" +quic_private_key = "key.pem" + +# TCP flags for violated packets (default: AP = ACK+PSH) +tcp_flags = "$TcpFlags" + +# SOCKS proxy +socks_port = $SocksPort +"@ + + # Ensure GFK directory exists + if (-not (Test-Path $GfkDir)) { + Write-Err "GFK is not installed. Please install GFK first (option 2)." + return $false + } + + [System.IO.File]::WriteAllText("$GfkDir\parameters.py", $params) + Save-Settings -Backend "gfk" -ServerAddr $ServerIP -SocksPort $SocksPort + Write-Success "GFK configuration saved" + return $true +} + +function Start-Gfk { + if (-not (Test-Npcap)) { + if (-not (Install-NpcapIfMissing)) { return } + } + + if (-not (Test-Python)) { + Write-Err "Python not found" + return + } + + if (-not (Test-Path "$GfkDir\mainclient.py")) { + Write-Err "GFK not installed" + return + } + + if (-not (Test-Path "$GfkDir\parameters.py")) { + Write-Err "GFK not configured" + return + } + + Write-Host "" + Write-Host " Starting GFW-KNOCKER" -ForegroundColor Yellow + Write-Host " ────────────────────" + Write-Host " This will start:" + Write-Host " 1. VIO client (raw socket handler)" + Write-Host " 2. QUIC client (tunnel to server)" + Write-Host "" + Write-Host " Your SOCKS5 proxy will be: 127.0.0.1:14000" + Write-Host " Configure your browser to use this proxy." + Write-Host "" + Write-Info "Starting GFW-knocker client..." + Write-Info "This will start the raw socket client + Python SOCKS5 proxy" + Write-Info "Press Ctrl+C to stop" + Write-Host "" + + # Start GFK client + Push-Location $GfkDir + try { + & python mainclient.py + } finally { + Pop-Location + } +} + +function Stop-GfkClient { + # Get-Process doesn't have CommandLine property - use CIM instead + $procs = Get-CimInstance Win32_Process -Filter "Name LIKE 'python%'" -ErrorAction SilentlyContinue | + Where-Object { $_.CommandLine -match "mainclient|gfk" } + if ($procs) { + $procs | ForEach-Object { + Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue + } + Write-Success "GFK client stopped" + } else { + Write-Info "GFK client not running" + } +} + +#═══════════════════════════════════════════════════════════════════════ +# Common Functions +#═══════════════════════════════════════════════════════════════════════ + +function Stop-Client { + # Stop paqet + $paqetProc = Get-Process -Name "paqet_windows_amd64" -ErrorAction SilentlyContinue + if ($paqetProc) { + Stop-Process -Name "paqet_windows_amd64" -Force + Write-Success "paqet stopped" + } + + # Stop GFK + Stop-GfkClient +} + +function Get-ClientStatus { + Write-Host "`n=== Client Status ===" -ForegroundColor Cyan + + $backend = Get-InstalledBackend + Write-Host "Backend: $(if ($backend) { $backend } else { 'Not installed' })" + + # Npcap + if (Test-Npcap) { + Write-Success "Npcap: Installed" + } else { + Write-Err "Npcap: NOT installed" + } + + # Python (for GFK) + if ($backend -eq "gfk" -or -not $backend) { + if (Test-Python) { + Write-Success "Python: Installed" + } else { + Write-Warn "Python: Not found (needed for GFK)" + } + } + + # Paqet + if (Test-Path $PaqetExe) { + Write-Success "Paqet binary: Found" + } + + # GFK + if (Test-Path "$GfkDir\mainclient.py") { + Write-Success "GFK scripts: Found" + } + + # Config + if (Test-Path $ConfigFile) { + Write-Success "Paqet config: Found" + } + if (Test-Path "$GfkDir\parameters.py") { + Write-Success "GFK config: Found" + } + + # Running processes + $paqetRunning = Get-Process -Name "paqet_windows_amd64" -ErrorAction SilentlyContinue + if ($paqetRunning) { + Write-Success "Paqet: RUNNING (PID: $($paqetRunning.Id))" + Write-Info " SOCKS5 proxy: 127.0.0.1:1080" + } + + Write-Host "" +} + +#═══════════════════════════════════════════════════════════════════════ +# Update Function +#═══════════════════════════════════════════════════════════════════════ + +function Get-InstalledPaqetVersion { + # Check settings file first for tracked version + if (Test-Path $SettingsFile) { + $content = Get-Content $SettingsFile -ErrorAction SilentlyContinue + foreach ($line in $content) { + if ($line -match '^PAQET_VERSION="?([^"]+)"?') { + return $Matches[1] + } + } + } + # Fall back to pinned version if paqet is installed + if (Test-Path $PaqetExe) { + return $PaqetVersion + } + return $null +} + +function Save-PaqetVersion { + param([string]$Version) + + if (-not (Test-Path $SettingsFile)) { + return + } + + $content = Get-Content $SettingsFile -Raw -ErrorAction SilentlyContinue + if ($content -match 'PAQET_VERSION=') { + # Update existing + $content = $content -replace 'PAQET_VERSION="[^"]*"', "PAQET_VERSION=`"$Version`"" + } else { + # Add new line + $content = $content.TrimEnd() + "`nPAQET_VERSION=`"$Version`"" + } + [System.IO.File]::WriteAllText($SettingsFile, $content) +} + +function Update-Paqet { + Write-Host "" + Write-Host " CHECKING FOR UPDATES" -ForegroundColor Cyan + Write-Host " ────────────────────" -ForegroundColor Cyan + Write-Host "" + + # Check if paqet is installed + if (-not (Test-Path $PaqetExe)) { + Write-Warn "Paqet is not installed. Use option 1 to install first." + return $false + } + + # Get installed version + $installedVersion = Get-InstalledPaqetVersion + if (-not $installedVersion) { + $installedVersion = $PaqetVersion + } + + # Query GitHub API for latest release + Write-Info "Querying GitHub for latest release..." + try { + $apiUrl = "https://api.github.com/repos/hanselime/paqet/releases/latest" + $response = Invoke-RestMethod -Uri $apiUrl -TimeoutSec 30 + $latestVersion = $response.tag_name + } catch { + Write-Err "Failed to check for updates: $_" + return $false + } + + # Show version info + Write-Host "" + Write-Host " Installed version: $installedVersion" -ForegroundColor White + Write-Host " Latest version: $latestVersion" -ForegroundColor White + Write-Host "" + + # Compare versions + if ($installedVersion -eq $latestVersion) { + Write-Success "You are already on the latest version!" + return $true + } + + # Confirm update + Write-Host " A new version is available!" -ForegroundColor Yellow + $confirm = Read-Host " Update to $latestVersion? [y/N]" + if ($confirm -notmatch "^[Yy]") { + Write-Info "Update cancelled" + return $false + } + + # Stop running paqet first + $paqetProc = Get-Process -Name "paqet_windows_amd64" -ErrorAction SilentlyContinue + if ($paqetProc) { + Write-Info "Stopping paqet..." + Stop-Process -Name "paqet_windows_amd64" -Force + Start-Sleep -Seconds 2 + } + + # Download new version + $zipUrl = "https://github.com/hanselime/paqet/releases/download/$latestVersion/paqet-windows-amd64-$latestVersion.zip" + $zipFile = "$env:TEMP\paqet-update.zip" + $extractDir = "$env:TEMP\paqet-update" + + Write-Info "Downloading paqet $latestVersion..." + try { + Invoke-WebRequest -Uri $zipUrl -OutFile $zipFile -TimeoutSec 120 + } catch { + Write-Err "Download failed: $_" + return $false + } + + # Validate download + if (-not (Test-Path $zipFile) -or (Get-Item $zipFile).Length -lt 1000) { + Write-Err "Downloaded file is invalid or too small" + return $false + } + + # Extract + Write-Info "Extracting..." + try { + if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force } + Expand-Archive -Path $zipFile -DestinationPath $extractDir -Force + } catch { + Write-Err "Extraction failed: $_" + Remove-Item $zipFile -Force -ErrorAction SilentlyContinue + return $false + } + + # Find the binary + $newBinary = Get-ChildItem -Path $extractDir -Filter "paqet_windows_amd64.exe" -Recurse | Select-Object -First 1 + if (-not $newBinary) { + Write-Err "Could not find paqet binary in archive" + Remove-Item $zipFile -Force -ErrorAction SilentlyContinue + Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue + return $false + } + + # Backup old binary + $backupPath = "$InstallDir\paqet_windows_amd64.exe.bak" + try { + Copy-Item $PaqetExe $backupPath -Force + Write-Info "Backed up old binary" + } catch { + Write-Warn "Could not backup old binary: $_" + } + + # Install new binary + try { + Copy-Item $newBinary.FullName $PaqetExe -Force + } catch { + Write-Err "Failed to install new binary: $_" + # Try to restore backup + if (Test-Path $backupPath) { + Copy-Item $backupPath $PaqetExe -Force -ErrorAction SilentlyContinue + } + return $false + } + + # Save version to settings + Save-PaqetVersion -Version $latestVersion + + # Cleanup + Remove-Item $zipFile -Force -ErrorAction SilentlyContinue + Remove-Item $extractDir -Recurse -Force -ErrorAction SilentlyContinue + + Write-Host "" + Write-Success "Updated to $latestVersion!" + Write-Host "" + Write-Info "Restart the client to use the new version" + Write-Host "" + + return $true +} + +#═══════════════════════════════════════════════════════════════════════ +# Interactive Menu +#═══════════════════════════════════════════════════════════════════════ + +function Show-Menu { + param([string]$InitBackend = "") + + # Use passed backend parameter, or detect if not specified + $backend = if ($InitBackend) { $InitBackend } else { Get-InstalledBackend } + + while ($true) { + Write-Host "" + Write-Host "===============================================" -ForegroundColor Cyan + Write-Host " PAQET/GFK CLIENT MANAGER" -ForegroundColor Cyan + Write-Host "===============================================" -ForegroundColor Cyan + Write-Host "" + if ($backend) { + Write-Host " Active backend: " -NoNewline + Write-Host "$backend" -ForegroundColor Green + if ($backend -eq "paqet") { + Write-Host " Proxy: 127.0.0.1:1080 (SOCKS5)" -ForegroundColor DarkGray + } else { + Write-Host " Proxy: 127.0.0.1:14000 (SOCKS5 via tunnel)" -ForegroundColor DarkGray + } + } else { + Write-Host " No backend installed yet" -ForegroundColor Yellow + } + Write-Host "" + Write-Host " 1. Install paqet (simple, all-in-one SOCKS5)" + Write-Host " 2. Install GFW-knocker (advanced, for heavy DPI)" + Write-Host " 3. Configure connection" + Write-Host " 4. Start client" + Write-Host " 5. Stop client" + Write-Host " 6. Show status" + Write-Host " 7. Update paqet" + Write-Host " 8. About (how it works)" + Write-Host " 0. Exit" + Write-Host "" + + $choice = Read-Host " Select option" + + switch ($choice) { + "1" { + if (Install-Paqet) { $backend = "paqet" } + } + "2" { + if (Install-Gfk) { $backend = "gfk" } + } + "3" { + if (-not $backend) { + Write-Warn "Install a backend first (option 1 or 2)" + continue + } + + if ($backend -eq "paqet") { + Write-Host "" + Write-Host " PAQET CONFIGURATION" -ForegroundColor Green + Write-Host " Get these values from your server admin or 'paqctl info' on server" + Write-Host "" + $server = Read-Host " Server address (e.g., 1.2.3.4:8443)" + $key = Read-Host " Encryption key (16+ chars)" + + # Advanced options (hidden by default - just press Enter) + Write-Host "" + Write-Host " Advanced options (press Enter for defaults - recommended):" -ForegroundColor DarkGray + Write-Host " TCP flags must match your server config. Only change if server admin says so." -ForegroundColor DarkGray + Write-Host " Valid flags: S A P R F U E C | Multiple: PA,A" -ForegroundColor DarkGray + $tcpLocal = Read-Host " TCP local flag [PA]" + $tcpRemote = Read-Host " TCP remote flag [PA]" + if (-not $tcpLocal) { $tcpLocal = "PA" } + if (-not $tcpRemote) { $tcpRemote = "PA" } + + if ($server -and $key) { + if (New-PaqetConfig -Server $server -SecretKey $key -TcpLocalFlag $tcpLocal -TcpRemoteFlag $tcpRemote) { + Write-Host "" + Write-Host " Your SOCKS5 proxy: 127.0.0.1:1080" -ForegroundColor Green + } + } + } else { + Write-Host "" + Write-Host " GFK CONFIGURATION" -ForegroundColor Yellow + Write-Host " Get these values from your server admin or 'paqctl info' on server" + Write-Host "" + $server = Read-Host " Server IP (e.g., 1.2.3.4)" + $auth = Read-Host " Auth code (from server setup)" + + # Advanced options (hidden by default - just press Enter) + Write-Host "" + Write-Host " Advanced options (press Enter for defaults - recommended):" -ForegroundColor DarkGray + Write-Host " TCP flags must match your server config. Only change if server admin says so." -ForegroundColor DarkGray + Write-Host " Valid flags: S A P R F U E C" -ForegroundColor DarkGray + $tcpFlags = Read-Host " TCP flags [AP]" + if (-not $tcpFlags) { $tcpFlags = "AP" } + + if ($server -and $auth) { + if (New-GfkConfig -ServerIP $server -AuthCode $auth -SocksPort "14000" -TcpFlags $tcpFlags) { + Write-Host "" + Write-Host " Your SOCKS5 proxy: 127.0.0.1:14000" -ForegroundColor Green + } + } + } + } + "4" { + if (-not $backend) { + Write-Warn "Install a backend first" + continue + } + if ($backend -eq "paqet") { + Start-Paqet + } else { + Start-Gfk + } + } + "5" { Stop-Client } + "6" { Get-ClientStatus } + "7" { Update-Paqet } + "8" { Show-About } + "0" { return } + default { Write-Warn "Invalid option" } + } + } +} + +function Show-About { + Write-Host "" + Write-Host "===============================================" -ForegroundColor Cyan + Write-Host " HOW IT WORKS" -ForegroundColor Cyan + Write-Host "===============================================" -ForegroundColor Cyan + Write-Host "" + Write-Host " This tool helps bypass firewall restrictions" + Write-Host " by disguising your traffic. You have TWO options:" + Write-Host "" + Write-Host " --- PAQET - Simple and Fast ---" -ForegroundColor Green + Write-Host " How: Uses KCP protocol over raw sockets" + Write-Host " Proxy: 127.0.0.1:1080 (SOCKS5)" + Write-Host " Best for: Most situations, easy setup" + Write-Host "" + Write-Host " --- GFW-KNOCKER - Advanced Anti-DPI ---" -ForegroundColor Yellow + Write-Host " How: Violated TCP packets + QUIC tunnel" + Write-Host " Proxy: 127.0.0.1:14000 (SOCKS5 via Xray)" + Write-Host " Best for: When paqet is blocked, heavy censorship" + Write-Host "" + Write-Host " --- CAN I RUN BOTH? ---" -ForegroundColor Magenta + Write-Host " YES! They use different ports:" + Write-Host " - Paqet: 127.0.0.1:1080" + Write-Host " - GFK: 127.0.0.1:14000" + Write-Host " Install both as backup - if one gets blocked, use the other!" + Write-Host "" + Write-Host " Press Enter to continue..." -ForegroundColor DarkGray + Read-Host | Out-Null +} + +#═══════════════════════════════════════════════════════════════════════ +# Main Entry Point +#═══════════════════════════════════════════════════════════════════════ + +if (-not (Test-Admin)) { + Write-Err "Administrator privileges required" + Write-Info "Right-click PowerShell -> Run as Administrator" + exit 1 +} + +# Auto-detect backend if not specified +if (-not $Backend) { + $Backend = Get-InstalledBackend +} + +switch ($Action.ToLower()) { + "install" { + if ($Backend -eq "gfk") { + Install-Gfk + } else { + Install-Paqet + } + } + "config" { + if ($Backend -eq "gfk") { + if (-not $ServerAddr -or -not $Key) { + Write-Err "Usage: -Action config -ServerAddr [ip] -Key [authcode]" + exit 1 + } + New-GfkConfig -ServerIP $ServerAddr -AuthCode $Key + } else { + if (-not $ServerAddr -or -not $Key) { + Write-Err "Usage: -Action config -ServerAddr [ip:port] -Key [key]" + exit 1 + } + New-PaqetConfig -Server $ServerAddr -SecretKey $Key + } + } + "run" { + if ($ServerAddr -and $Key) { + if ($Backend -eq "gfk") { + Install-Gfk + New-GfkConfig -ServerIP $ServerAddr -AuthCode $Key + Start-Gfk + } else { + Install-Paqet + New-PaqetConfig -Server $ServerAddr -SecretKey $Key + Start-Paqet + } + } else { + if ($Backend -eq "gfk") { + Start-Gfk + } else { + Start-Paqet + } + } + } + "start" { + if ($Backend -eq "gfk") { Start-Gfk } else { Start-Paqet } + } + "stop" { Stop-Client } + "status" { Get-ClientStatus } + "menu" { Show-Menu -InitBackend $Backend } + default { Show-Menu -InitBackend $Backend } +}