commit be63cb624c0c71cfce10402bcf254a2b9bf8786f Author: Aero Date: Mon Dec 9 10:40:56 2019 +0800 init diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c4f3145 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +name: Build +on: [push] + +jobs: + + test: + name: Test + runs-on: ubuntu-latest + steps: + + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + + - name: Test + run: go test -v . + + build: + name: Build + runs-on: ubuntu-latest + needs: [test] + steps: + + - name: Set up Go 1.13 + uses: actions/setup-go@v1 + with: + go-version: 1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v1 + + - name: Build + run: go build -v . \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..93904e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,22 @@ +name: Release +on: + create: + tags: + - v* + +jobs: + release: + name: Release on GitHub + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v1 + + - name: Create release on GitHub + uses: docker://goreleaser/goreleaser:latest + env: + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + with: + args: release + if: success() + \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e74173 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# custom +.idea +.vscode + +dist + +*.zip +/*.conf +/*.rule + +config/rules.d/*.rule +config/rules.d/*.list + +glider +/bak/ +/rules.d/ diff --git a/.goreleaser.yml b/.goreleaser.yml new file mode 100644 index 0000000..059bd02 --- /dev/null +++ b/.goreleaser.yml @@ -0,0 +1,62 @@ +# Make sure to check the documentation at http://goreleaser.com + +# release: +# git tag -a v0.1.0 -m "v0.1.0" +# git push origin v0.1.0 +# goreleaser release --skip-publish --rm-dist + +# snapshot: +# goreleaser --snapshot --rm-dist + +# https://goreleaser.com/customization/ + +before: + hooks: + - go mod tidy + +# https://goreleaser.com/build/ +builds: + - env: + - CGO_ENABLED=0 + goos: + - windows + - linux + - darwin + goarch: + - 386 + - amd64 + - arm + - arm64 + - mips + - mipsle + - mips64 + - mips64le + goarm: + - 6 + - 7 + + ignore: + - goos: darwin + goarch: 386 + +# https://goreleaser.com/archive/ +archive: + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" + wrap_in_directory: true + format: tar.gz + format_overrides: + - goos: windows + format: zip + files: + - LICENSE + - README.md + - config/**/* + - systemd/* + +# https://goreleaser.com/snapshots/ +snapshot: + name_template: "dev@{{.ShortCommit}}" + +# https://goreleaser.com/checksum/ +checksum: + name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cecc1d --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + 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 General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {one line to give the program's name and a brief idea of what it does.} + Copyright (C) {year} {name of author} + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + {project} Copyright (C) {year} {fullname} + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..23a0497 --- /dev/null +++ b/README.md @@ -0,0 +1,385 @@ +# [glider](https://github.com/nadoo/glider) + +[![Go Report Card](https://goreportcard.com/badge/github.com/nadoo/glider?style=flat-square)](https://goreportcard.com/report/github.com/nadoo/glider) +[![GitHub release](https://img.shields.io/github/v/release/nadoo/glider.svg?include_prereleases&style=flat-square)](https://github.com/nadoo/glider/releases) + +glider is a forward proxy with multiple protocols support, and also a dns forwarding server with ipset management features(like dnsmasq). + +we can set up local listeners as proxy servers, and forward requests to internet via forwarders. + +```bash + |Forwarder ----------------->| + Listener --> | | Internet + |Forwarder --> Forwarder->...| +``` + +## Features + +Listen (local proxy server): + +- Socks5 proxy(tcp&udp) +- Http proxy(tcp) +- SS proxy(tcp&udp) +- Linux transparent proxy(iptables redirect) +- TCP tunnel +- UDP tunnel +- UDP over TCP tunnel +- TLS, use it together with above proxy protocols(tcp) +- Unix domain socket, use it together with above proxy protocols(tcp) +- KCP protocol, use it together with above proxy protocols(tcp) + +Forward (local proxy client/upstream proxy server): + +- Socks5 proxy(tcp&udp) +- Http proxy(tcp) +- SS proxy(tcp&udp&uot) +- SSR proxy(tcp) +- VMess proxy(tcp) +- TLS, use it together with above proxy protocols(tcp) +- Websocket, use it together with above proxy protocols(tcp) +- Unix domain socket, use it together with above proxy protocols(tcp) +- KCP protocol, use it together with above proxy protocols(tcp) +- Simple-Obfs, use it together with above proxy protocols(tcp) + +DNS Forwarding Server (udp2tcp): + +- Listen on UDP and forward dns requests to remote dns server in TCP via forwarders +- Specify different upstream dns server based on destinations(in rule file) +- Tunnel mode: forward to a fixed upstream dns server +- Add resolved IPs to proxy rules +- Add resolved IPs to ipset +- DNS cache +- Custom dns record + +IPSet Management (Linux kernel version >= 2.6.32): + +- Add ip/cidrs from rule files on startup +- Add resolved ips for domains from rule files by dns forwarding server + +General: + +- Http and socks5 on the same port +- Forwarder chain +- RR/HA/LHA/DH strategy for multiple forwarders +- Periodical proxy checking +- Rule proxy based on destinations: [Config Examples](config/examples) +- Send requests from specific ip/interface + +TODO: + +- [ ] IPv6 support in ipset manager +- [ ] Transparent UDP proxy (iptables tproxy) +- [ ] Performance tuning +- [ ] TUN/TAP device support +- [ ] SSH tunnel support (maybe) + +## Install + +Binary: + +- [https://github.com/nadoo/glider/releases](https://github.com/nadoo/glider/releases) + +Go Get (requires **Go 1.13+** ): + +```bash +go get -u github.com/nadoo/glider +``` + +ArchLinux: + +```bash +sudo pacman -S glider +``` + +## Run + +command line: + +```bash +glider -listen :8443 -verbose +``` + +config file: + +```bash +glider -config CONFIGPATH +``` + +command line with config file: + +```bash +glider -config CONFIGPATH -listen :8080 -verbose +``` + +## Usage + +```bash +glider 0.8.0 usage: + -checkinterval int + proxy check interval(seconds) (default 30) + -checktimeout int + proxy check timeout(seconds) (default 10) + -checkwebsite string + proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80 (default "www.apple.com") + -config string + config file path + -dns string + local dns server listen address + -dnsalwaystcp + always use tcp to query upstream dns servers no matter there is a forwarder or not + -dnsmaxttl int + maximum TTL value for entries in the CACHE(seconds) (default 1800) + -dnsminttl int + minimum TTL value for entries in the CACHE(seconds) + -dnsrecord value + custom dns record, format: domain/ip + -dnsserver value + remote dns server address + -dnstimeout int + timeout value used in multiple dnsservers switch(seconds) (default 3) + -forward value + forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS] + -include value + include file + -interface string + source ip or source interface + -listen value + listen url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS + -maxfailures int + max failures to change forwarder status to disabled (default 3) + -rulefile value + rule file path + -rules-dir string + rule file folder + -strategy string + forward strategy, default: rr (default "rr") + -verbose + verbose mode + +Available Schemes: + mixed: serve as a http/socks5 proxy on the same port. (default) + ss: ss proxy + socks5: socks5 proxy + http: http proxy + ssr: ssr proxy + vmess: vmess proxy + tls: tls transport + ws: websocket transport + redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules) + redir6: redirect proxy(ipv6) + tcptun: tcp tunnel + udptun: udp tunnel + uottun: udp over tcp tunnel + unix: unix domain socket + kcp: kcp protocol + simple-obfs: simple-obfs protocol + reject: a virtual proxy which just reject connections + +Available schemes for different modes: + listen: mixed ss socks5 http redir redir6 tcptun udptun uottun tls unix kcp + forward: reject ss socks5 http ssr vmess tls ws unix kcp simple-obfs + +SS scheme: + ss://method:pass@host:port + +Available methods for ss: + AEAD Ciphers: + AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AEAD_XCHACHA20_POLY1305 + Stream Ciphers: + AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5 + Alias: + chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305 + +SSR scheme: + ssr://method:pass@host:port?protocol=xxx&protocol_param=yyy&obfs=zzz&obfs_param=xyz + +VMess scheme: + vmess://[security:]uuid@host:port?alterID=num + +Available securities for vmess: + none, aes-128-gcm, chacha20-poly1305 + +TLS client scheme: + tls://host:port[?skipVerify=true] + +Proxy over tls client: + tls://host:port[?skipVerify=true],scheme:// + tls://host:port[?skipVerify=true],http://[user:pass@] + tls://host:port[?skipVerify=true],socks5://[user:pass@] + tls://host:port[?skipVerify=true],vmess://[security:]uuid@?alterID=num + +TLS server scheme: + tls://host:port?cert=PATH&key=PATH + +Proxy over tls server: + tls://host:port?cert=PATH&key=PATH,scheme:// + tls://host:port?cert=PATH&key=PATH,http:// + tls://host:port?cert=PATH&key=PATH,socks5:// + tls://host:port?cert=PATH&key=PATH,ss://method:pass@ + +Websocket scheme: + ws://host:port[/path] + +Websocket with a specified proxy protocol: + ws://host:port[/path],scheme:// + ws://host:port[/path],http://[user:pass@] + ws://host:port[/path],socks5://[user:pass@] + ws://host:port[/path],vmess://[security:]uuid@?alterID=num + +TLS and Websocket with a specified proxy protocol: + tls://host:port[?skipVerify=true],ws://[@/path],scheme:// + tls://host:port[?skipVerify=true],ws://[@/path],http://[user:pass@] + tls://host:port[?skipVerify=true],ws://[@/path],socks5://[user:pass@] + tls://host:port[?skipVerify=true],ws://[@/path],vmess://[security:]uuid@?alterID=num + +Unix domain socket scheme: + unix://path + +KCP scheme: + kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM] + +Available crypt types for KCP: + none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20 + +Simple-Obfs scheme: + simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA] + +Available types for simple-obfs: + http, tls + +DNS forwarding server: + dns=:53 + dnsserver=8.8.8.8:53 + dnsserver=1.1.1.1:53 + dnsrecord=www.example.com/1.2.3.4 + dnsrecord=www.example.com/2606:2800:220:1:248:1893:25c8:1946 + +Available forward strategies: + rr: Round Robin mode + ha: High Availability mode + lha: Latency based High Availability mode + dh: Destination Hashing mode + +Forwarder option scheme: FORWARD_URL#OPTIONS + priority: set the priority of that forwarder, default:0 + interface: set local interface or ip address used to connect remote server + - + Examples: + socks5://1.1.1.1:1080#priority=100 + vmess://[security:]uuid@host:port?alterID=num#priority=200 + vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=192.168.1.99 + vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=eth0 + +Config file format(see `glider.conf.example` as an example): + # COMMENT LINE + KEY=VALUE + KEY=VALUE + # KEY equals to command line flag name: listen forward strategy... + +Examples: + glider -config glider.conf + -run glider with specified config file. + + glider -config glider.conf -rulefile office.rule -rulefile home.rule + -run glider with specified global config file and rule config files. + + glider -listen :8443 + -listen on :8443, serve as http/socks5 proxy on the same port. + + glider -listen ss://AEAD_CHACHA20_POLY1305:pass@:8443 + -listen on 0.0.0.0:8443 as a ss server. + + glider -listen socks5://:1080 -verbose + -listen on :1080 as a socks5 proxy server, in verbose mode. + + glider -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose + -listen on :443 as a https(http over tls) proxy server. + + glider -listen http://:8080 -forward socks5://127.0.0.1:1080 + -listen on :8080 as a http proxy server, forward all requests via socks5 server. + + glider -listen redir://:1081 -forward ss://method:pass@1.1.1.1:8443 + -listen on :1081 as a transparent redirect server, forward all requests via remote ss server. + + glider -listen redir://:1081 -forward "ssr://method:pass@1.1.1.1:8444?protocol=a&protocol_param=b&obfs=c&obfs_param=d" + -listen on :1081 as a transparent redirect server, forward all requests via remote ssr server. + + glider -listen redir://:1081 -forward "tls://1.1.1.1:443,vmess://security:uuid@?alterID=10" + -listen on :1081 as a transparent redirect server, forward all requests via remote tls+vmess server. + + glider -listen redir://:1081 -forward "ws://1.1.1.1:80,vmess://security:uuid@?alterID=10" + -listen on :1081 as a transparent redirect server, forward all requests via remote ws+vmess server. + + glider -listen tcptun://:80=2.2.2.2:80 -forward ss://method:pass@1.1.1.1:8443 + -listen on :80 and forward all requests to 2.2.2.2:80 via remote ss server. + + glider -listen udptun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443 + -listen on :53 and forward all udp requests to 8.8.8.8:53 via remote ss server. + + glider -listen uottun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443 + -listen on :53 and forward all udp requests via udp over tcp tunnel. + + glider -listen socks5://:1080 -listen http://:8080 -forward ss://method:pass@1.1.1.1:8443 + -listen on :1080 as socks5 server, :8080 as http proxy server, forward all requests via remote ss server. + + glider -listen redir://:1081 -dns=:53 -dnsserver=8.8.8.8:53 -forward ss://method:pass@server1:port1,ss://method:pass@server2:port2 + -listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2. + + glider -listen socks5://:1080 -forward ss://method:pass@server1:port1 -forward ss://method:pass@server2:port2 -strategy rr + -listen on :1080 as socks5 server, forward requests via server1 and server2 in round robin mode. + + glider -verbose -dns=:53 -dnsserver=8.8.8.8:53 -dnsrecord=www.example.com/1.2.3.4 + -listen on :53 as dns server, forward dns requests to 8.8.8.8:53, return 1.2.3.4 when resolving www.example.com. +``` + +## Advanced Usage + +- [ConfigFile](config) + - [glider.conf.example](config/glider.conf.example) + - [office.rule.example](config/rules.d/office.rule.example) +- [Examples](config/examples) + - [transparent proxy with dnsmasq](config/examples/8.transparent_proxy_with_dnsmasq) + - [transparent proxy without dnsmasq](config/examples/9.transparent_proxy_without_dnsmasq) + +### Proxy & Protocol Chain +In glider, you can easily chain several proxy servers or protocols together, e.g: + +- Chain proxy servers: + +```bash +forward=http://1.1.1.1:80,socks5://2.2.2.2:1080,ss://method:pass@3.3.3.3:8443@ +``` + +- Chain protocols: https proxy (http over tls) + +```bash +forward=tls://1.1.1.1:443,http:// +``` + +- Chain protocols: vmess over ws over tls + +```bash +forward=tls://1.1.1.1:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2 +``` + +- Chain protocols and servers: + +``` bash +forward=socks5://1.1.1.1:1080,tls://2.2.2.2:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2 +``` + +- Chain protocols in listener: https proxy server + +``` bash +listen=tls://:443?cert=crtFilePath&key=keyFilePath,http:// +``` + + +## Service + +- systemd: [https://github.com/nadoo/glider/blob/master/systemd/](https://github.com/nadoo/glider/blob/master/systemd/) + +## Links + +- [conflag](https://github.com/nadoo/conflag): command line and config file parse support +- [ArchLinux](https://www.archlinux.org/packages/community/x86_64/glider): a great linux distribution with glider pre-built package diff --git a/common/conn/conn.go b/common/conn/conn.go new file mode 100644 index 0000000..aca363b --- /dev/null +++ b/common/conn/conn.go @@ -0,0 +1,90 @@ +package conn + +import ( + "bufio" + "io" + "net" + "time" +) + +// UDPBufSize is the size of udp buffer. +const UDPBufSize = 65536 + +// Conn is a base conn struct. +type Conn struct { + r *bufio.Reader + net.Conn +} + +// NewConn returns a new conn. +func NewConn(c net.Conn) *Conn { + return &Conn{bufio.NewReader(c), c} +} + +// Peek returns the next n bytes without advancing the reader. +func (c *Conn) Peek(n int) ([]byte, error) { + return c.r.Peek(n) +} + +func (c *Conn) Read(p []byte) (int, error) { + return c.r.Read(p) +} + +// Reader returns the internal bufio.Reader. +func (c *Conn) Reader() *bufio.Reader { + return c.r +} + +// Relay relays between left and right. +func Relay(left, right net.Conn) (int64, int64, error) { + type res struct { + N int64 + Err error + } + ch := make(chan res) + + go func() { + n, err := io.Copy(right, left) + right.SetDeadline(time.Now()) // wake up the other goroutine blocking on right + left.SetDeadline(time.Now()) // wake up the other goroutine blocking on left + ch <- res{n, err} + }() + + n, err := io.Copy(left, right) + right.SetDeadline(time.Now()) // wake up the other goroutine blocking on right + left.SetDeadline(time.Now()) // wake up the other goroutine blocking on left + rs := <-ch + + if err == nil { + err = rs.Err + } + return n, rs.N, err +} + +// RelayUDP copys from src to dst at target with read timeout. +func RelayUDP(dst net.PacketConn, target net.Addr, src net.PacketConn, timeout time.Duration) error { + buf := make([]byte, UDPBufSize) + for { + src.SetReadDeadline(time.Now().Add(timeout)) + n, _, err := src.ReadFrom(buf) + if err != nil { + return err + } + + _, err = dst.WriteTo(buf[:n], target) + if err != nil { + return err + } + } +} + +// OutboundIP returns preferred outbound ip of this machine. +func OutboundIP() string { + conn, err := net.Dial("udp", "8.8.8.8:80") + if err != nil { + return "" + } + defer conn.Close() + + return conn.LocalAddr().(*net.UDPAddr).IP.String() +} diff --git a/common/log/log.go b/common/log/log.go new file mode 100644 index 0000000..9bf4e1c --- /dev/null +++ b/common/log/log.go @@ -0,0 +1,19 @@ +package log + +import stdlog "log" + +// Func defines a simple log function +type Func func(f string, v ...interface{}) + +// F is the main log function +var F Func = func(string, ...interface{}) {} + +// Fatal log and exit +func Fatal(v ...interface{}) { + stdlog.Fatal(v...) +} + +// Fatalf log and exit +func Fatalf(f string, v ...interface{}) { + stdlog.Fatalf(f, v...) +} diff --git a/common/socks/socks.go b/common/socks/socks.go new file mode 100644 index 0000000..d5a5ec1 --- /dev/null +++ b/common/socks/socks.go @@ -0,0 +1,177 @@ +package socks + +import ( + "errors" + "io" + "net" + "strconv" +) + +// SOCKS auth type +const ( + AuthNone = 0 + AuthPassword = 2 +) + +// SOCKS request commands as defined in RFC 1928 section 4. +const ( + CmdConnect = 1 + CmdBind = 2 + CmdUDPAssociate = 3 +) + +// SOCKS address types as defined in RFC 1928 section 5. +const ( + ATypIP4 = 1 + ATypDomain = 3 + ATypIP6 = 4 +) + +// MaxAddrLen is the maximum size of SOCKS address in bytes. +const MaxAddrLen = 1 + 1 + 255 + 2 + +// Errors are socks5 errors +var Errors = []error{ + errors.New(""), + errors.New("general failure"), + errors.New("connection forbidden"), + errors.New("network unreachable"), + errors.New("host unreachable"), + errors.New("connection refused"), + errors.New("TTL expired"), + errors.New("command not supported"), + errors.New("address type not supported"), + errors.New("socks5UDPAssociate"), +} + +// Addr . +type Addr []byte + +// String serializes SOCKS address a to string form. +func (a Addr) String() string { + var host, port string + + switch ATYP(a[0]) { // address type + case ATypDomain: + host = string(a[2 : 2+int(a[1])]) + port = strconv.Itoa((int(a[2+int(a[1])]) << 8) | int(a[2+int(a[1])+1])) + case ATypIP4: + host = net.IP(a[1 : 1+net.IPv4len]).String() + port = strconv.Itoa((int(a[1+net.IPv4len]) << 8) | int(a[1+net.IPv4len+1])) + case ATypIP6: + host = net.IP(a[1 : 1+net.IPv6len]).String() + port = strconv.Itoa((int(a[1+net.IPv6len]) << 8) | int(a[1+net.IPv6len+1])) + } + + return net.JoinHostPort(host, port) +} + +// UoT returns whether it is udp over tcp +func UoT(b byte) bool { + return b&0x8 == 0x8 +} + +// ATYP returns the address type +func ATYP(b byte) int { + return int(b &^ 0x8) +} + +// ReadAddrBuf reads just enough bytes from r to get a valid Addr. +func ReadAddrBuf(r io.Reader, b []byte) (Addr, error) { + if len(b) < MaxAddrLen { + return nil, io.ErrShortBuffer + } + _, err := io.ReadFull(r, b[:1]) // read 1st byte for address type + if err != nil { + return nil, err + } + + switch ATYP(b[0]) { + case ATypDomain: + _, err = io.ReadFull(r, b[1:2]) // read 2nd byte for domain length + if err != nil { + return nil, err + } + _, err = io.ReadFull(r, b[2:2+int(b[1])+2]) + return b[:1+1+int(b[1])+2], err + case ATypIP4: + _, err = io.ReadFull(r, b[1:1+net.IPv4len+2]) + return b[:1+net.IPv4len+2], err + case ATypIP6: + _, err = io.ReadFull(r, b[1:1+net.IPv6len+2]) + return b[:1+net.IPv6len+2], err + } + + return nil, Errors[8] +} + +// ReadAddr reads just enough bytes from r to get a valid Addr. +func ReadAddr(r io.Reader) (Addr, error) { + return ReadAddrBuf(r, make([]byte, MaxAddrLen)) +} + +// SplitAddr slices a SOCKS address from beginning of b. Returns nil if failed. +func SplitAddr(b []byte) Addr { + addrLen := 1 + if len(b) < addrLen { + return nil + } + + switch ATYP(b[0]) { + case ATypDomain: + if len(b) < 2 { + return nil + } + addrLen = 1 + 1 + int(b[1]) + 2 + case ATypIP4: + addrLen = 1 + net.IPv4len + 2 + case ATypIP6: + addrLen = 1 + net.IPv6len + 2 + default: + return nil + + } + + if len(b) < addrLen { + return nil + } + + return b[:addrLen] +} + +// ParseAddr parses the address in string s. Returns nil if failed. +func ParseAddr(s string) Addr { + var addr Addr + host, port, err := net.SplitHostPort(s) + if err != nil { + return nil + } + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + addr = make([]byte, 1+net.IPv4len+2) + addr[0] = ATypIP4 + copy(addr[1:], ip4) + } else { + addr = make([]byte, 1+net.IPv6len+2) + addr[0] = ATypIP6 + copy(addr[1:], ip) + } + } else { + if len(host) > 255 { + return nil + } + addr = make([]byte, 1+1+len(host)+2) + addr[0] = ATypDomain + addr[1] = byte(len(host)) + copy(addr[2:], host) + } + + portnum, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return nil + } + + addr[len(addr)-2], addr[len(addr)-1] = byte(portnum>>8), byte(portnum) + + return addr +} diff --git a/conf.go b/conf.go new file mode 100644 index 0000000..020fc2f --- /dev/null +++ b/conf.go @@ -0,0 +1,308 @@ +package main + +import ( + "fmt" + "log" + "os" + "path" + + "github.com/nadoo/conflag" + + "github.com/nadoo/glider/dns" + "github.com/nadoo/glider/rule" + "github.com/nadoo/glider/strategy" +) + +var flag = conflag.New() + +var conf struct { + Verbose bool + + Listen []string + + Forward []string + StrategyConfig strategy.Config + + RuleFile []string + RulesDir string + + DNS string + DNSConfig dns.Config + + rules []*rule.Config +} + +func confInit() { + flag.BoolVar(&conf.Verbose, "verbose", false, "verbose mode") + flag.StringSliceUniqVar(&conf.Listen, "listen", nil, "listen url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS") + + flag.StringSliceUniqVar(&conf.Forward, "forward", nil, "forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]") + flag.StringVar(&conf.StrategyConfig.Strategy, "strategy", "rr", "forward strategy, default: rr") + flag.StringVar(&conf.StrategyConfig.CheckWebSite, "checkwebsite", "www.apple.com", "proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80") + flag.IntVar(&conf.StrategyConfig.CheckInterval, "checkinterval", 30, "proxy check interval(seconds)") + flag.IntVar(&conf.StrategyConfig.CheckTimeout, "checktimeout", 10, "proxy check timeout(seconds)") + flag.IntVar(&conf.StrategyConfig.MaxFailures, "maxfailures", 3, "max failures to change forwarder status to disabled") + flag.StringVar(&conf.StrategyConfig.IntFace, "interface", "", "source ip or source interface") + + flag.StringSliceUniqVar(&conf.RuleFile, "rulefile", nil, "rule file path") + flag.StringVar(&conf.RulesDir, "rules-dir", "", "rule file folder") + + flag.StringVar(&conf.DNS, "dns", "", "local dns server listen address") + flag.StringSliceUniqVar(&conf.DNSConfig.Servers, "dnsserver", []string{"8.8.8.8:53"}, "remote dns server address") + flag.BoolVar(&conf.DNSConfig.AlwaysTCP, "dnsalwaystcp", false, "always use tcp to query upstream dns servers no matter there is a forwarder or not") + flag.IntVar(&conf.DNSConfig.Timeout, "dnstimeout", 3, "timeout value used in multiple dnsservers switch(seconds)") + flag.IntVar(&conf.DNSConfig.MaxTTL, "dnsmaxttl", 1800, "maximum TTL value for entries in the CACHE(seconds)") + flag.IntVar(&conf.DNSConfig.MinTTL, "dnsminttl", 0, "minimum TTL value for entries in the CACHE(seconds)") + flag.StringSliceUniqVar(&conf.DNSConfig.Records, "dnsrecord", nil, "custom dns record, format: domain/ip") + + flag.Usage = usage + err := flag.Parse() + if err != nil { + // flag.Usage() + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + os.Exit(-1) + } + + if len(conf.Listen) == 0 && conf.DNS == "" { + // flag.Usage() + fmt.Fprintf(os.Stderr, "ERROR: listen url must be specified.\n") + os.Exit(-1) + } + + // rulefiles + for _, ruleFile := range conf.RuleFile { + if !path.IsAbs(ruleFile) { + ruleFile = path.Join(flag.ConfDir(), ruleFile) + } + + rule, err := rule.NewConfFromFile(ruleFile) + if err != nil { + log.Fatal(err) + } + + conf.rules = append(conf.rules, rule) + } + + if conf.RulesDir != "" { + if !path.IsAbs(conf.RulesDir) { + conf.RulesDir = path.Join(flag.ConfDir(), conf.RulesDir) + } + + ruleFolderFiles, _ := rule.ListDir(conf.RulesDir, ".rule") + for _, ruleFile := range ruleFolderFiles { + rule, err := rule.NewConfFromFile(ruleFile) + if err != nil { + log.Fatal(err) + } + conf.rules = append(conf.rules, rule) + } + } + +} + +func usage() { + app := os.Args[0] + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, "%s %s usage:\n", app, version) + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available Schemes:\n") + fmt.Fprintf(os.Stderr, " mixed: serve as a http/socks5 proxy on the same port. (default)\n") + fmt.Fprintf(os.Stderr, " ss: ss proxy\n") + fmt.Fprintf(os.Stderr, " socks5: socks5 proxy\n") + fmt.Fprintf(os.Stderr, " http: http proxy\n") + fmt.Fprintf(os.Stderr, " ssr: ssr proxy\n") + fmt.Fprintf(os.Stderr, " vmess: vmess proxy\n") + fmt.Fprintf(os.Stderr, " tls: tls transport\n") + fmt.Fprintf(os.Stderr, " ws: websocket transport\n") + fmt.Fprintf(os.Stderr, " redir: redirect proxy. (used on linux as a transparent proxy with iptables redirect rules)\n") + fmt.Fprintf(os.Stderr, " redir6: redirect proxy(ipv6)\n") + fmt.Fprintf(os.Stderr, " tcptun: tcp tunnel\n") + fmt.Fprintf(os.Stderr, " udptun: udp tunnel\n") + fmt.Fprintf(os.Stderr, " uottun: udp over tcp tunnel\n") + fmt.Fprintf(os.Stderr, " unix: unix domain socket\n") + fmt.Fprintf(os.Stderr, " kcp: kcp protocol\n") + fmt.Fprintf(os.Stderr, " simple-obfs: simple-obfs protocol\n") + fmt.Fprintf(os.Stderr, " reject: a virtual proxy which just reject connections\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available schemes for different modes:\n") + fmt.Fprintf(os.Stderr, " listen: mixed ss socks5 http redir redir6 tcptun udptun uottun tls unix kcp\n") + fmt.Fprintf(os.Stderr, " forward: reject ss socks5 http ssr vmess tls ws unix kcp simple-obfs\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "SS scheme:\n") + fmt.Fprintf(os.Stderr, " ss://method:pass@host:port\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available methods for ss:\n") + fmt.Fprintf(os.Stderr, " AEAD Ciphers:\n") + fmt.Fprintf(os.Stderr, " AEAD_AES_128_GCM AEAD_AES_192_GCM AEAD_AES_256_GCM AEAD_CHACHA20_POLY1305 AEAD_XCHACHA20_POLY1305\n") + fmt.Fprintf(os.Stderr, " Stream Ciphers:\n") + fmt.Fprintf(os.Stderr, " AES-128-CFB AES-128-CTR AES-192-CFB AES-192-CTR AES-256-CFB AES-256-CTR CHACHA20-IETF XCHACHA20 CHACHA20 RC4-MD5\n") + fmt.Fprintf(os.Stderr, " Alias:\n") + fmt.Fprintf(os.Stderr, " chacha20-ietf-poly1305 = AEAD_CHACHA20_POLY1305, xchacha20-ietf-poly1305 = AEAD_XCHACHA20_POLY1305\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "SSR scheme:\n") + fmt.Fprintf(os.Stderr, " ssr://method:pass@host:port?protocol=xxx&protocol_param=yyy&obfs=zzz&obfs_param=xyz\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "VMess scheme:\n") + fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available securities for vmess:\n") + fmt.Fprintf(os.Stderr, " none, aes-128-gcm, chacha20-poly1305\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "TLS client scheme:\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true]\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Proxy over tls client:\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],scheme://\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],http://[user:pass@]\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],socks5://[user:pass@]\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],vmess://[security:]uuid@?alterID=num\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "TLS server scheme:\n") + fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Proxy over tls server:\n") + fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,scheme://\n") + fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,http://\n") + fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,socks5://\n") + fmt.Fprintf(os.Stderr, " tls://host:port?cert=PATH&key=PATH,ss://method:pass@\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Websocket scheme:\n") + fmt.Fprintf(os.Stderr, " ws://host:port[/path]\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Websocket with a specified proxy protocol:\n") + fmt.Fprintf(os.Stderr, " ws://host:port[/path],scheme://\n") + fmt.Fprintf(os.Stderr, " ws://host:port[/path],http://[user:pass@]\n") + fmt.Fprintf(os.Stderr, " ws://host:port[/path],socks5://[user:pass@]\n") + fmt.Fprintf(os.Stderr, " ws://host:port[/path],vmess://[security:]uuid@?alterID=num\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "TLS and Websocket with a specified proxy protocol:\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],scheme://\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],http://[user:pass@]\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],socks5://[user:pass@]\n") + fmt.Fprintf(os.Stderr, " tls://host:port[?skipVerify=true],ws://[@/path],vmess://[security:]uuid@?alterID=num\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Unix domain socket scheme:\n") + fmt.Fprintf(os.Stderr, " unix://path\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "KCP scheme:\n") + fmt.Fprintf(os.Stderr, " kcp://CRYPT:KEY@host:port[?dataShards=NUM&parityShards=NUM]\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available crypt types for KCP:\n") + fmt.Fprintf(os.Stderr, " none, sm4, tea, xor, aes, aes-128, aes-192, blowfish, twofish, cast5, 3des, xtea, salsa20\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Simple-Obfs scheme:\n") + fmt.Fprintf(os.Stderr, " simple-obfs://host:port[?type=TYPE&host=HOST&uri=URI&ua=UA]\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available types for simple-obfs:\n") + fmt.Fprintf(os.Stderr, " http, tls\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "DNS forwarding server:\n") + fmt.Fprintf(os.Stderr, " dns=:53\n") + fmt.Fprintf(os.Stderr, " dnsserver=8.8.8.8:53\n") + fmt.Fprintf(os.Stderr, " dnsserver=1.1.1.1:53\n") + fmt.Fprintf(os.Stderr, " dnsrecord=www.example.com/1.2.3.4\n") + fmt.Fprintf(os.Stderr, " dnsrecord=www.example.com/2606:2800:220:1:248:1893:25c8:1946\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Available forward strategies:\n") + fmt.Fprintf(os.Stderr, " rr: Round Robin mode\n") + fmt.Fprintf(os.Stderr, " ha: High Availability mode\n") + fmt.Fprintf(os.Stderr, " lha: Latency based High Availability mode\n") + fmt.Fprintf(os.Stderr, " dh: Destination Hashing mode\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Forwarder option scheme: FORWARD_URL#OPTIONS\n") + fmt.Fprintf(os.Stderr, " priority: set the priority of that forwarder, default:0\n") + fmt.Fprintf(os.Stderr, " interface: set local interface or ip address used to connect remote server\n") + fmt.Fprintf(os.Stderr, " -\n") + fmt.Fprintf(os.Stderr, " Examples:\n") + fmt.Fprintf(os.Stderr, " socks5://1.1.1.1:1080#priority=100\n") + fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num#priority=200\n") + fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=192.168.1.99\n") + fmt.Fprintf(os.Stderr, " vmess://[security:]uuid@host:port?alterID=num#priority=200&interface=eth0\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Config file format(see `"+app+".conf.example` as an example):\n") + fmt.Fprintf(os.Stderr, " # COMMENT LINE\n") + fmt.Fprintf(os.Stderr, " KEY=VALUE\n") + fmt.Fprintf(os.Stderr, " KEY=VALUE\n") + fmt.Fprintf(os.Stderr, " # KEY equals to command line flag name: listen forward strategy...\n") + fmt.Fprintf(os.Stderr, "\n") + + fmt.Fprintf(os.Stderr, "Examples:\n") + fmt.Fprintf(os.Stderr, " "+app+" -config glider.conf\n") + fmt.Fprintf(os.Stderr, " -run glider with specified config file.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -config glider.conf -rulefile office.rule -rulefile home.rule\n") + fmt.Fprintf(os.Stderr, " -run glider with specified global config file and rule config files.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen :8443\n") + fmt.Fprintf(os.Stderr, " -listen on :8443, serve as http/socks5 proxy on the same port.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen ss://AEAD_CHACHA20_POLY1305:pass@:8443\n") + fmt.Fprintf(os.Stderr, " -listen on 0.0.0.0:8443 as a ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -verbose\n") + fmt.Fprintf(os.Stderr, " -listen on :1080 as a socks5 proxy server, in verbose mode.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen tls://:443?cert=crtFilePath&key=keyFilePath,http:// -verbose\n") + fmt.Fprintf(os.Stderr, " -listen on :443 as a https(http over tls) proxy server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen http://:8080 -forward socks5://127.0.0.1:1080\n") + fmt.Fprintf(os.Stderr, " -listen on :8080 as a http proxy server, forward all requests via socks5 server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward ss://method:pass@1.1.1.1:8443\n") + fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward \"ssr://method:pass@1.1.1.1:8444?protocol=a&protocol_param=b&obfs=c&obfs_param=d\"\n") + fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ssr server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward \"tls://1.1.1.1:443,vmess://security:uuid@?alterID=10\"\n") + fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote tls+vmess server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -forward \"ws://1.1.1.1:80,vmess://security:uuid@?alterID=10\"\n") + fmt.Fprintf(os.Stderr, " -listen on :1081 as a transparent redirect server, forward all requests via remote ws+vmess server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen tcptun://:80=2.2.2.2:80 -forward ss://method:pass@1.1.1.1:8443\n") + fmt.Fprintf(os.Stderr, " -listen on :80 and forward all requests to 2.2.2.2:80 via remote ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen udptun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443\n") + fmt.Fprintf(os.Stderr, " -listen on :53 and forward all udp requests to 8.8.8.8:53 via remote ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen uottun://:53=8.8.8.8:53 -forward ss://method:pass@1.1.1.1:8443\n") + fmt.Fprintf(os.Stderr, " -listen on :53 and forward all udp requests via udp over tcp tunnel.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -listen http://:8080 -forward ss://method:pass@1.1.1.1:8443\n") + fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, :8080 as http proxy server, forward all requests via remote ss server.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen redir://:1081 -dns=:53 -dnsserver=8.8.8.8:53 -forward ss://method:pass@server1:port1,ss://method:pass@server2:port2\n") + fmt.Fprintf(os.Stderr, " -listen on :1081 as transparent redirect server, :53 as dns server, use forward chain: server1 -> server2.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -listen socks5://:1080 -forward ss://method:pass@server1:port1 -forward ss://method:pass@server2:port2 -strategy rr\n") + fmt.Fprintf(os.Stderr, " -listen on :1080 as socks5 server, forward requests via server1 and server2 in round robin mode.\n") + fmt.Fprintf(os.Stderr, "\n") + fmt.Fprintf(os.Stderr, " "+app+" -verbose -dns=:53 -dnsserver=8.8.8.8:53 -dnsrecord=www.example.com/1.2.3.4\n") + fmt.Fprintf(os.Stderr, " -listen on :53 as dns server, forward dns requests to 8.8.8.8:53, return 1.2.3.4 when resolving www.example.com.\n") + fmt.Fprintf(os.Stderr, "\n") +} diff --git a/config/README.md b/config/README.md new file mode 100644 index 0000000..b6846f4 --- /dev/null +++ b/config/README.md @@ -0,0 +1,94 @@ + +## Config File +Command: +```bash +glider -config glider.conf +``` +Config file, **just use the command line flag name as the key name**: +```bash +### glider config file + +# verbose mode, print logs +verbose + +# listen on 8443, serve as http/socks5 proxy on the same port. +listen=:8443 + +# upstream forward proxy +forward=socks5://192.168.1.10:1080 + +# upstream forward proxy +forward=ss://method:pass@1.1.1.1:8443 + +# upstream forward proxy (forward chain) +forward=http://1.1.1.1:8080,socks5://2.2.2.2:1080 + +# multiple upstream proxies forwad strategy +strategy=rr + +# Used to connect via forwarders, if the host is unreachable, the forwarder +# will be set to disabled. +# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED. +checkwebsite=www.apple.com + +# check interval +checkinterval=30 + + +# Setup a dns forwarding server +dns=:53 +# global remote dns server (you can specify different dns server in rule file) +dnsserver=8.8.8.8:53 + +# RULE FILES +rules-dir=rules.d +#rulefile=office.rule +#rulefile=home.rule + +# INCLUDE MORE CONFIG FILES +#include=dnsrecord.inc.conf +#include=more.inc.conf +``` +See: +- [glider.conf.example](config/glider.conf.example) +- [examples](config/examples) + +## Rule File +Rule file, **same as the config file but specify forwarders based on destinations**: +```bash +# YOU CAN USE ALL KEYS IN THE GLOBAL CONFIG FILE EXCEPT "listen", "rulefile" +forward=socks5://192.168.1.10:1080 +forward=ss://method:pass@1.1.1.1:8443 +forward=http://192.168.2.1:8080,socks5://192.168.2.2:1080 +strategy=rr +checkwebsite=www.apple.com +checkinterval=30 + +# DNS SERVER for domains in this rule file +dnsserver=208.67.222.222:53 + +# IPSET MANAGEMENT +# ---------------- +# Create and mange ipset on linux based on destinations in rule files +# - add ip/cidrs in rule files on startup +# - add resolved ips for domains in rule files by dns forwarding server +# Usually used in transparent proxy mode on linux +ipset=glider + +# YOU CAN SPECIFY DESTINATIONS TO USE THE ABOVE FORWARDERS +# matches abc.com and *.abc.com +domain=abc.com + +# matches 1.1.1.1 +ip=1.1.1.1 + +# matches 192.168.100.0/24 +cidr=192.168.100.0/24 + +# we can include a list file with only destinations settings +include=office.list.example + +``` +See: +- [office.rule.example](rules.d/office.rule.example) +- [examples](examples) diff --git a/config/dnsrecord.inc.conf.example b/config/dnsrecord.inc.conf.example new file mode 100644 index 0000000..4e7ebc1 --- /dev/null +++ b/config/dnsrecord.inc.conf.example @@ -0,0 +1,7 @@ + +# intranet +dnsrecord=oa.yourcompany.local/10.0.0.1 +dnsrecord=git.yourcompany.local/10.0.0.2 + +# ad +#dnsrecord=ad.domain/127.0.0.1 diff --git a/config/examples/1.simple_proxy_service/glider.conf b/config/examples/1.simple_proxy_service/glider.conf new file mode 100644 index 0000000..8c8e8c9 --- /dev/null +++ b/config/examples/1.simple_proxy_service/glider.conf @@ -0,0 +1,5 @@ + +# Verbose mode, print logs +verbose=True + +listen=:8443 \ No newline at end of file diff --git a/config/examples/2.one_forwarder/glider.conf b/config/examples/2.one_forwarder/glider.conf new file mode 100644 index 0000000..a519c2e --- /dev/null +++ b/config/examples/2.one_forwarder/glider.conf @@ -0,0 +1,7 @@ + +# Verbose mode, print logs +verbose=True + +listen=:8443 + +forward=socks5://192.168.1.10:1080 \ No newline at end of file diff --git a/config/examples/3.forward_chain/glider.conf b/config/examples/3.forward_chain/glider.conf new file mode 100644 index 0000000..168dec9 --- /dev/null +++ b/config/examples/3.forward_chain/glider.conf @@ -0,0 +1,8 @@ + +# Verbose mode, print logs +verbose=True + +listen=:8443 + +# first connect forwarder1 then forwarder2 then internet +forward=http://forwarder1:8080,socks5://forwarder2:1080 \ No newline at end of file diff --git a/config/examples/4.multiple_forwarders/glider.conf b/config/examples/4.multiple_forwarders/glider.conf new file mode 100644 index 0000000..2c982f1 --- /dev/null +++ b/config/examples/4.multiple_forwarders/glider.conf @@ -0,0 +1,21 @@ + +# Verbose mode, print logs +verbose=True + +listen=:8443 + +# first connect forwarder1 then forwarder2 then internet +forward=http://forwarder1:8080,socks5://forwarder2:1080 +forward=http://1.1.1.1:8080 + +# Round Robin mode: rr +# High Availability mode: ha +strategy=rr + +# Used to connect via forwarders, if the host is unreachable, the forwarder +# will be set to disabled. +# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED. +checkwebsite=www.apple.com + +# check interval(seconds) +checkinterval=30 diff --git a/config/examples/5.rule_default_direct/glider.conf b/config/examples/5.rule_default_direct/glider.conf new file mode 100644 index 0000000..507a34f --- /dev/null +++ b/config/examples/5.rule_default_direct/glider.conf @@ -0,0 +1,9 @@ + +# Verbose mode, print logs +verbose=True + +listen=:8443 + +# NOTE HERE: +# Specify a rule file +rulefile=office.rule \ No newline at end of file diff --git a/config/examples/5.rule_default_direct/office.rule b/config/examples/5.rule_default_direct/office.rule new file mode 100644 index 0000000..9999085 --- /dev/null +++ b/config/examples/5.rule_default_direct/office.rule @@ -0,0 +1,29 @@ + + +# first connect forwarder1 then forwarder2 then internet +forward=http://forwarder1:8080,socks5://forwarder2:1080 +forward=http://1.1.1.1:8080 + +# Round Robin mode: rr +# High Availability mode: ha +strategy=rr + +checkwebsite=www.apple.com +checkinterval=30 + + +# matches abc.com and *.abc.com +domain=abc.com + +# matches 1.1.1.1 +ip=1.1.1.1 + +# matches 192.168.100.0/24 +cidr=192.168.100.0/24 + +domain=example1.com +domain=example2.com +domain=example3.com +ip=2.2.2.2 +ip=3.3.3.3 +cidr=172.16.0.0/24 diff --git a/config/examples/6.rule_default_forwarder/bypass.rule b/config/examples/6.rule_default_forwarder/bypass.rule new file mode 100644 index 0000000..3cda452 --- /dev/null +++ b/config/examples/6.rule_default_forwarder/bypass.rule @@ -0,0 +1,8 @@ + +# matches abc.com and *.abc.com +domain=abc.com + +ip=127.0.0.1 +cidr=192.168.0.0/24 +cidr=192.168.1.0/24 +cidr=172.16.0.0/24 \ No newline at end of file diff --git a/config/examples/6.rule_default_forwarder/glider.conf b/config/examples/6.rule_default_forwarder/glider.conf new file mode 100644 index 0000000..90240ec --- /dev/null +++ b/config/examples/6.rule_default_forwarder/glider.conf @@ -0,0 +1,22 @@ + + +# Verbose mode, print logs +verbose=True + +listen=:8443 + +# first connect forwarder1 then forwarder2 then internet +forward=http://forwarder1:8080,socks5://forwarder2:1080 +forward=http://1.1.1.1:8080 + +# Round Robin mode: rr +# High Availability mode: ha +strategy=rr + +checkwebsite=www.apple.com +checkinterval=30 + + +# NOTE HERE: +# Specify a rule file +rulefile=bypass.rule \ No newline at end of file diff --git a/config/examples/7.rule_multiple_rule_files/glider.conf b/config/examples/7.rule_multiple_rule_files/glider.conf new file mode 100644 index 0000000..07a480f --- /dev/null +++ b/config/examples/7.rule_multiple_rule_files/glider.conf @@ -0,0 +1,8 @@ + +# Verbose mode, print logs +verbose=True + +listen=:8443 + +# parse all *.rule files in rules.d folder +rules-dir=rules.d diff --git a/config/examples/7.rule_multiple_rule_files/rules.d/home.rule b/config/examples/7.rule_multiple_rule_files/rules.d/home.rule new file mode 100644 index 0000000..0c84e64 --- /dev/null +++ b/config/examples/7.rule_multiple_rule_files/rules.d/home.rule @@ -0,0 +1,18 @@ + + +forward=http://forwarder4:8080 + +# first connect forwarder1 then forwarder2 then internet +forward=http://forwarder5:8080,socks6://forwarder3:1080 + + +# Round Robin mode: rr +# High Availability mode: ha +strategy=rr + +checkwebsite=www.apple.com +checkinterval=30 + + +# matches 192.168.0.0/16 +cidr=192.168.0.0/16 diff --git a/config/examples/7.rule_multiple_rule_files/rules.d/office.rule b/config/examples/7.rule_multiple_rule_files/rules.d/office.rule new file mode 100644 index 0000000..5294968 --- /dev/null +++ b/config/examples/7.rule_multiple_rule_files/rules.d/office.rule @@ -0,0 +1,18 @@ + + +forward=http://forwarder1:8080 + +# first connect forwarder2 then forwarder3 then internet +forward=http://forwarder2:8080,socks5://forwarder3:1080 + + +# Round Robin mode: rr +# High Availability mode: ha +strategy=rr + +checkwebsite=www.apple.com +checkinterval=30 + + +# matches 172.16.0.0/24 +cidr=172.16.0.0/24 diff --git a/config/examples/8.transparent_proxy_with_dnsmasq/README.md b/config/examples/8.transparent_proxy_with_dnsmasq/README.md new file mode 100644 index 0000000..b38c251 --- /dev/null +++ b/config/examples/8.transparent_proxy_with_dnsmasq/README.md @@ -0,0 +1,44 @@ + +## 8. Transparent Proxy with dnsmasq + +#### Setup a redirect proxy and a dns server with glider +glider.conf +```bash +verbose=True +listen=redir://:1081 +forward=http://forwarder1:8080,socks5://forwarder2:1080 +forward=http://1.1.1.1:8080 +dns=:5353 +dnsserver=8.8.8.8:53 +strategy=rr +checkwebsite=www.apple.com +checkinterval=30 +``` + +#### Create a ipset manually +```bash +ipset create myset hash:ip +``` + +#### Config dnsmasq +```bash +server=/example1.com/127.0.0.1#5353 +ipset=/example1.com/myset +server=/example2.com/127.0.0.1#5353 +ipset=/example2.com/myset +server=/example3.com/127.0.0.1#5353 +ipset=/example4.com/myset +``` + +#### Config iptables on your linux gateway +```bash +iptables -t nat -I PREROUTING -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081 +#iptables -t nat -I OUTPUT -p tcp -m set --match-set myset dst -j REDIRECT --to-ports 1081 +``` + +#### When client requests network, the whole process: +1. all dns requests for domain example1.com will be forward to glider(:5353) by dnsmasq +2. glider will forward dns requests to 8.8.8.8:53 in tcp via forwarders +3. the resolved ip address will be added to ipset "myset" by dnsmasq +4. all tcp requests to example1.com will be redirect to glider(:1081) by iptables +5. glider then forward requests to example1.com via forwarders diff --git a/config/examples/8.transparent_proxy_with_dnsmasq/glider.conf b/config/examples/8.transparent_proxy_with_dnsmasq/glider.conf new file mode 100644 index 0000000..1632525 --- /dev/null +++ b/config/examples/8.transparent_proxy_with_dnsmasq/glider.conf @@ -0,0 +1,16 @@ + +# Verbose mode, print logs +verbose=True + +listen=redir://:1081 + +forward=http://forwarder1:8080,socks5://forwarder2:1080 +forward=http://1.1.1.1:8080 + +dns=:5353 +dnsserver=8.8.8.8:53 + + +strategy=rr +checkwebsite=www.apple.com +checkinterval=30 diff --git a/config/examples/9.transparent_proxy_without_dnsmasq/README.md b/config/examples/9.transparent_proxy_without_dnsmasq/README.md new file mode 100644 index 0000000..4971a38 --- /dev/null +++ b/config/examples/9.transparent_proxy_without_dnsmasq/README.md @@ -0,0 +1,101 @@ + +## 9. Transparent Proxy without dnsmasq + +PC Client -> Gateway with glider running(linux box) -> Upstream Forwarders -> Internet + +#### In this mode, glider will act as the following roles: +1. A transparent proxy server +2. A dns forwarding server +3. A ipset manager + +so you don't need any dns server in your network. + +#### Create a ipset manually +```bash +ipset create glider hash:net +``` + +#### Glider Configuration +##### glider.conf +```bash +verbose=True + +# as a redir proxy +listen=redir://:1081 + +# as a dns forwarding server +dns=:53 +dnsserver=8.8.8.8:53 +dnsserver=8.8.4.4:53 + +# specify rule files +rules-dir=rules.d +``` + +##### office.rule +```bash +# add your forwarders +forward=http://forwarder1:8080,socks5://forwarder2:1080 +forward=http://1.1.1.1:8080 +strategy=rr +checkwebsite=www.apple.com +checkinterval=30 + +# specify a different dns server(if need) +dnsserver=208.67.222.222:53 + +# as a ipset manager +ipset=glider + +# specify destinations +include=office.list + +domain=example1.com +domain=example2.com +# matches ip +ip=1.1.1.1 +ip=2.2.2.2 +# matches a ip net +cidr=192.168.100.0/24 +cidr=172.16.100.0/24 +``` + +##### office.list +```bash +# destinations list +domain=mycompany.com +domain=mycompany1.com +ip=4.4.4.4 +ip=5.5.5.5 +cidr=172.16.101.0/24 +cidr=172.16.102.0/24 +``` + +#### Configure iptables on your linux gateway +```bash +iptables -t nat -I PREROUTING -p tcp -m set --match-set glider dst -j REDIRECT --to-ports 1081 +iptables -t nat -I OUTPUT -p tcp -m set --match-set glider dst -j REDIRECT --to-ports 1081 +``` + +#### Server DNS settings +Set server's nameserver to glider: +```bash +echo nameserver 127.0.0.1 > /etc/resolv.conf +``` + +#### Client DNS settings +Use the linux server's ip as your dns server. + +#### When client requesting to access http://example1.com (in office.rule), the whole process: +DNS Resolving: +1. client sends a udp dns request to linux server, and glider will receive the request(as it listen on default dns port :53) +2. upstream dns server choice: glider will lookup it's rule config and find out the dns server to use for this domain(matched "example1.com" in office.rule, so 208.67.222.222:53 will be chosen) +3. glider uses the forwarder in office.rule to ask 208.67.222.222:53 for the resolve answers. +4. glider updates it's office rule config, add the resolved ip address to it. +5. glider adds the resolved ip into ipset "glider", and return the dns answer to client. + +Destination Accessing: +1. client sends http request to the resolved ip of example1.com. +2. linux gateway server will get the request. +3. iptabes matches the ip in ipset "glider" and redirect this request to :1081(glider) +4. glider finds the ip in office rule, and then choose a forwarder in office.rule to complete the request. diff --git a/config/examples/9.transparent_proxy_without_dnsmasq/glider.conf b/config/examples/9.transparent_proxy_without_dnsmasq/glider.conf new file mode 100644 index 0000000..47e14ea --- /dev/null +++ b/config/examples/9.transparent_proxy_without_dnsmasq/glider.conf @@ -0,0 +1,13 @@ + +# Verbose mode, print logs +verbose=True + +# as a redir proxy +listen=redir://:1081 + +# as a dns forwarding server +dns=:53 +dnsserver=8.8.8.8:53 + +# parse all *.rule files in rules.d folder +rules-dir=rules.d diff --git a/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/home.rule b/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/home.rule new file mode 100644 index 0000000..b863aba --- /dev/null +++ b/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/home.rule @@ -0,0 +1,20 @@ + + +forward=http://forwarder4:8080 + +# first connect forwarder1 then forwarder2 then internet +forward=http://forwarder5:8080,socks5://forwarder3:1080 + + +# Round Robin mode: rr +# High Availability mode: ha +strategy=rr + +checkwebsite=www.apple.com +checkinterval=30 + +# as a ipset manager +ipset=glider + +# matches 192.168.0.0/16 +cidr=192.168.0.0/16 diff --git a/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/office.list b/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/office.list new file mode 100644 index 0000000..7a53ca3 --- /dev/null +++ b/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/office.list @@ -0,0 +1,7 @@ + +domain=mycompany.com +domain=mycompany1.com +ip=4.4.4.4 +ip=5.5.5.5 +cidr=172.16.101.0/24 +cidr=172.16.102.0/24 diff --git a/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/office.rule b/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/office.rule new file mode 100644 index 0000000..23b9e45 --- /dev/null +++ b/config/examples/9.transparent_proxy_without_dnsmasq/rules.d/office.rule @@ -0,0 +1,31 @@ + + +forward=http://forwarder1:8080 + +# first connect forwarder2 then forwarder3 then internet +forward=http://forwarder2:8080,socks5://forwarder3:1080 + + +# Round Robin mode: rr +# High Availability mode: ha +strategy=rr +checkwebsite=www.apple.com +checkinterval=30 + +# specify a different dns server(if need) +dnsserver=208.67.222.222:53 + +# as a ipset manager +ipset=glider + +# specify destinations +include=office.list + +domain=example1.com +domain=example2.com +# matches ip +ip=1.1.1.1 +ip=2.2.2.2 +# matches a ip net +cidr=192.168.100.0/24 +cidr=172.16.100.0/24 diff --git a/config/examples/README.md b/config/examples/README.md new file mode 100644 index 0000000..f4af294 --- /dev/null +++ b/config/examples/README.md @@ -0,0 +1,98 @@ + +# Glider Configuration Examples + +## 1. Simple Proxy Service +Just listen on 8443 as HTTP/SOCKS5 proxy on the same port, forward all requests directly. + +``` + Clients --> Listener --> Internet +``` + +- [simple_proxy_service](1.simple_proxy_service) + +## 2. One remote upstream proxy + +``` + Clients --> Listener --> Forwarder --> Internet +``` + +- [one_forwarder](2.one_forwarder) + +## 3. One remote upstream PROXY CHAIN + +``` + Clients --> Listener --> Forwarder1 --> Forwarder2 --> Internet +``` + +- [forward_chain](3.forward_chain) + +## 4. Multiple upstream proxies + +``` + |Forwarder ----------------->| + Clients --> Listener --> | | Internet + |Forwarder --> Forwarder->...| +``` + +- [multiple_forwarders](4.multiple_forwarders) + + +## 5. With Rule File: Default Direct, Rule file use forwarder + +Default: +``` + Clients --> Listener --> Internet +``` +Destinations specified in rule file: +``` + |Forwarder ----------------->| + Clients --> Listener --> | | Internet + |Forwarder --> Forwarder->...| +``` + +- [rule_default_direct](5.rule_default_direct) + + +## 6. With Rule File: Default use forwarder, rule file use direct + +Default: +``` + |Forwarder ----------------->| + Clients --> Listener --> | | Internet + |Forwarder --> Forwarder->...| +``` + +Destinations specified in rule file: +``` + Clients --> Listener --> Internet +``` + +- [rule_default_forwarder](6.rule_default_forwarder) + + +## 7. With Rule File: multiple rule files + +Default: +``` + Clients --> Listener --> Internet +``` +Destinations specified in rule file1: +``` + |Forwarder1 ----------------->| + Clients --> Listener --> | | Internet + |Forwarder2 --> Forwarder3->...| +``` +Destinations specified in rule file2: +``` + |Forwarder4 ----------------->| + Clients --> Listener --> | | Internet + |Forwarder5 --> Forwarder6->...| +``` + +- [rule_multiple_rule_files](7.rule_multiple_rule_files) + +## 8. Transparent Proxy with Dnsmasq +- [transparent_proxy_with_dnsmasq](8.transparent_proxy_with_dnsmasq) + +## 9. Transparent Proxy without Dnsmasq +- [transparent_proxy_without_dnsmasq](9.transparent_proxy_without_dnsmasq) \ No newline at end of file diff --git a/config/glider.conf.example b/config/glider.conf.example new file mode 100644 index 0000000..6dd66b7 --- /dev/null +++ b/config/glider.conf.example @@ -0,0 +1,212 @@ +########################################## +# __ _ _ ___ ____ ___ +# / /`_ | | | | | | \ | |_ | |_) +# \_\_/ |_|__ |_| |_|_/ |_|__ |_| \ +# +# Glider is a forward proxy with multiple protocols support, and also a dns forwarding server with ipset management features(like dnsmasq). +# +# We can set up local listeners as proxy, and forward requests to internet via forwarders. +# +# |Forwarder ----------------->| +# Listener --> | | Internet +# |Forwarder --> Forwarder->...| +# +# ----------------------------------------------------------- +# +# This is a sample configuration file for glider. +# +# Format is one option per line, legal options are the same +# as the options legal on the command line. See "glider -help" for details. +# +# Comment line starts with "#", values set in the format: +# KEY=VALUE +# +# ----------------------------------------------------------- + +# Verbose mode, print logs +verbose=True + +# LISTENERS +# --------- +# Local listeners, we can set up multiple listeners on different port with +# different protocols. + +# listen on 8443, serve as http/socks5 proxy on the same port. +listen=:8443 + +# listen on 8448 as a ss server. +# listen=ss://AEAD_CHACHA20_POLY1305:pass@:8448 + +# listen on 8080 as a http proxy server. +listen=http://:8080 + +# listen on 1080 as a socks5 proxy server. +listen=socks5://:1080 + +# listen on 1081 as a linux transparent proxy server. +# listen=redir://:1081 + +# listen on 1082 as a tcp tunnel, all requests to :1082 will be forward to 1.1.1.1:80 +# listen=tcptun://:1082=1.1.1.1:80 + +# listen on 1083 as a udp tunnel, all requests to :1083 will be forward to 1.1.1.1:53 +# listen=udptun://:1083=1.1.1.1:53 + +# listen on 1084 as a udp over tcp tunnel, all requests to :1084 will be forward to 1.1.1.1:53 +# listen=uottun://:1084=1.1.1.1:53 + +# http over tls (HTTPS proxy) +# listen=tls://:443?cert=crtFilePath&key=keyFilePath,http:// + +# ss over tls +# listen=tls://:443?cert=crtFilePath&key=keyFilePath,ss://AEAD_CHACHA20_POLY1305:pass@ + +# socks5 over unix domain socket +# listen=unix:///tmp/glider.socket,socks5:// + +# socks5 over kcp +# listen=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3,socks5:// + +# FORWARDERS +# ---------- +# Forwarders, we can setup multiple forwarders. +# forward=SCHEME#OPTIONS + +# FORWARDER OPTIONS +# priority: set the priority of that forwarder, default:0 +# interface: set local interface or ip address used to connect remote server + +# Socks5 proxy as forwarder +# forward=socks5://192.168.1.10:1080 + +# Socks5 proxy as forwarder with priority 100 +# forward=socks5://192.168.1.10:1080#priority=100 + +# Socks5 proxy as forwarder with priority 100 and use `eth0` as source interface +# forward=socks5://192.168.1.10:1080#priority=100&interface=eth0 + +# Socks5 proxy as forwarder with priority 100 and use `192.168.1.100` as source ip +# forward=socks5://192.168.1.10:1080#priority=100&interface=192.168.1.100 + +# SS proxy as forwarder +# forward=ss://method:pass@1.1.1.1:8443 + +# SSR proxy as forwarder +# forward=ssr://method:pass@1.1.1.1:8443?protocol=auth_aes128_md5&protocol_param=xxx&obfs=tls1.2_ticket_auth&obfs_param=yyy + +# http proxy as forwarder +# forward=http://1.1.1.1:8080 + +# vmess with none security +# forward=vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443?alterID=2 + +# vmess with aes-128-gcm security +# forward=vmess://aes-128-gcm:5a146038-0b56-4e95-b1dc-5c6f5a32cd98@1.1.1.1:443?alterID=2 + +# vmess over tls +# forward=tls://1.1.1.1:443,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2 + +# vmess over websocket +# forward=ws://1.1.1.1:80/path,vmess://chacha20-poly1305:5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2 + +# vmess over ws over tls +# forward=tls://1.1.1.1:443,ws://,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2 +# forward=tls://1.1.1.1:443,ws://@/path,vmess://5a146038-0b56-4e95-b1dc-5c6f5a32cd98@?alterID=2 + +# ss over tls +# forward=tls://1.1.1.1:443,ss://AEAD_CHACHA20_POLY1305:pass@ + +# ss over kcp +# forward=kcp://aes:key@127.0.0.1:8444?dataShards=10&parityShards=3,ss://AEAD_CHACHA20_POLY1305:pass@ + +# ss with simple-obfs +# forward=simple-obfs://1.1.1.1:443?type=tls&host=apple.com,ss://AEAD_CHACHA20_POLY1305:pass@ + +# socks5 over unix domain socket +# forward=unix:///tmp/glider.socket,socks5:// + +# FORWARDER CHAIN +# --------------- +# We can setup a forward chain using 1 forward option, +# use comma to separate different upstream forward proxies. +#forward=http://1.1.1.1:8080,socks5://2.2.2.2:1080 + + +# FORWARDE STRATEGY +# ----------------- +# If we set up multiple forwarders, we can use them in our own strategy. + +# Round Robin mode: rr +# High Availability mode: ha +# Latency based High Availability mode: lha +# Destination Hashing mode: dh +strategy=rr + + +# FORWARDERS CHECK +# ---------------- +# We can check whether a forwarder is available. + +# Used to connect via forwarders, if the host is unreachable, the forwarder +# will be set to disabled. +# MUST be a HTTP website server address, format: HOST[:PORT]. HTTPS NOT SUPPORTED. +checkwebsite=www.apple.com + +# check interval(seconds) +checkinterval=30 + +# check timeout(seconds) +checktimeout=10 + +# DNS FORWARDING SERVER +# ---------------- +# we can specify different upstream dns server in rule file for different destinations + +# Setup a dns forwarding server +dns=:53 + +# global remote dns server (you can specify different dns server in rule file) +dnsserver=8.8.8.8:53 +dnsserver=1.1.1.1:53 + +# By default, when glider received udp dns request and there's no forwarder specified, +# it will use udp to query upstream dns servers, otherwise, use tcp; +# you can set dnsalwaystcp=true to always use tcp no matter there is a forwarder or not. +# dnsalwaystcp=false + +# timeout value used in multiple dnsservers switch(seconds) +dnstimeout=3 + +# maximum TTL value for entries in the CACHE(seconds) +dnsmaxttl=1800 + +# minimum TTL value for entries in the CACHE(seconds) +dnsminttl=0 + +# custom records +dnsrecord=www.example.com/1.2.3.4 +dnsrecord=www.example.com/2606:2800:220:1:248:1893:25c8:1946 + +# INTERFACE SPECIFIC +# ------------------ +# Specify the outbound ip/interface. +# +# interface="" +# interface="192.168.1.100" +# interface="eth0" + +# RULE FILES +# ---------- +# Specify additional forward rules. + +# specify rules folder, so all *.rule files under this folder will be parsed as rule file +rules-dir=rules.d + +# specify a rule file +#rulefile=office.rule +#rulefile=home.rule + + +# INCLUDE MORE CONFIG FILES +#include=dnsrecord.inc.conf +#include=more.conf diff --git a/config/rules.d/direct.rule.example b/config/rules.d/direct.rule.example new file mode 100644 index 0000000..a7fafa0 --- /dev/null +++ b/config/rules.d/direct.rule.example @@ -0,0 +1,7 @@ + +# Specify destinations in rule file without forwarders, so glider will bypass +# all forwarders and direct connect them instead + +ip=127.0.0.1 +cidr=192.168.1.0/24 +domain=bypass.com diff --git a/config/rules.d/office.list.example b/config/rules.d/office.list.example new file mode 100644 index 0000000..7a53ca3 --- /dev/null +++ b/config/rules.d/office.list.example @@ -0,0 +1,7 @@ + +domain=mycompany.com +domain=mycompany1.com +ip=4.4.4.4 +ip=5.5.5.5 +cidr=172.16.101.0/24 +cidr=172.16.102.0/24 diff --git a/config/rules.d/office.rule.example b/config/rules.d/office.rule.example new file mode 100644 index 0000000..3ffe08d --- /dev/null +++ b/config/rules.d/office.rule.example @@ -0,0 +1,52 @@ +# Glider rule configuration file. +# +# Format is the same as glider main config file. +# EXCEPTION: Listeners are NOT allowed to setup here. + +# FORWARDERS +# ---------- +# Forwarders, we can setup multiple forwarders. +forward=socks5://192.168.1.10:1080 +forward=ss://method:pass@1.1.1.1:8443 +forward=http://192.168.2.1:8080,socks5://192.168.2.2:1080 + +# STRATEGY for multiple forwarders. rr|ha +strategy=rr + +# FORWARDER CHECK SETTINGS +checkwebsite=www.apple.com +checkinterval=30 + +# DNS SERVER for domains in this rule file +dnsserver=208.67.222.222:53 + +# IPSET MANAGEMENT +# ---------------- +# Create and mange ipset on linux based on destinations in rule files +# - add ip/cidrs in rule files on startup +# - add resolved ips for domains in rule files by dns forwarding server +# Usually used in transparent proxy mode on linux +ipset=glider + +# DESTINATIONS +# ------------ +# ALL destinations matches the following rules will be forward using forwarders specified above + +# INCLUDE FILE +# we can include a list file with only destinations settings +include=office.list + +# matches example.com and *.example.com +domain=example.com +domain=example1.com +domain=example2.com +domain=example3.com + +# matches ip +ip=1.1.1.1 +ip=2.2.2.2 +ip=3.3.3.3 + +# matches a ip net +cidr=192.168.100.0/24 +cidr=172.16.100.0/24 diff --git a/config/rules.d/reject.rule.example b/config/rules.d/reject.rule.example new file mode 100644 index 0000000..8c47b4b --- /dev/null +++ b/config/rules.d/reject.rule.example @@ -0,0 +1,7 @@ + +forward=reject:// + +ipset=glider + +domain=pornhub.com +domain=amazon.com diff --git a/dev.go b/dev.go new file mode 100644 index 0000000..538890a --- /dev/null +++ b/dev.go @@ -0,0 +1,18 @@ +//+build dev + +package main + +import ( + "fmt" + "net/http" + _ "net/http/pprof" +) + +func init() { + go func() { + err := http.ListenAndServe(":6060", nil) + if err != nil { + fmt.Printf("Create pprof server error: %s\n", err) + } + }() +} diff --git a/dev_linux.go b/dev_linux.go new file mode 100644 index 0000000..52e7dd3 --- /dev/null +++ b/dev_linux.go @@ -0,0 +1,7 @@ +//+build dev + +package main + +import ( + _ "github.com/nadoo/glider/proxy/tproxy" +) diff --git a/dns/cache.go b/dns/cache.go new file mode 100644 index 0000000..80e6e9e --- /dev/null +++ b/dns/cache.go @@ -0,0 +1,66 @@ +package dns + +import ( + "sync" + "time" +) + +// LongTTL is 50 years duration in seconds, used for none-expired items. +const LongTTL = 50 * 365 * 24 * 3600 + +type item struct { + value []byte + expire time.Time +} + +// Cache is the struct of cache. +type Cache struct { + m map[string]*item + l sync.RWMutex +} + +// NewCache returns a new cache. +func NewCache() (c *Cache) { + c = &Cache{m: make(map[string]*item)} + go func() { + for now := range time.Tick(time.Second) { + c.l.Lock() + for k, v := range c.m { + if now.After(v.expire) { + delete(c.m, k) + } + } + c.l.Unlock() + } + }() + return +} + +// Len returns the length of cache. +func (c *Cache) Len() int { + return len(c.m) +} + +// Put an item into cache, invalid after ttl seconds. +func (c *Cache) Put(k string, v []byte, ttl int) { + if len(v) != 0 { + c.l.Lock() + it, ok := c.m[k] + if !ok { + it = &item{value: v} + c.m[k] = it + } + it.expire = time.Now().Add(time.Duration(ttl) * time.Second) + c.l.Unlock() + } +} + +// Get an item from cache. +func (c *Cache) Get(k string) (v []byte) { + c.l.RLock() + if it, ok := c.m[k]; ok { + v = it.value + } + c.l.RUnlock() + return +} diff --git a/dns/client.go b/dns/client.go new file mode 100644 index 0000000..d94fd03 --- /dev/null +++ b/dns/client.go @@ -0,0 +1,299 @@ +package dns + +import ( + "bytes" + "encoding/binary" + "errors" + "io" + "net" + "strings" + "time" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// HandleFunc function handles the dns TypeA or TypeAAAA answer. +type HandleFunc func(Domain, ip string) error + +// Config for dns. +type Config struct { + Servers []string + Timeout int + MaxTTL int + MinTTL int + Records []string + AlwaysTCP bool +} + +// Client is a dns client struct. +type Client struct { + proxy proxy.Proxy + cache *Cache + config *Config + upServers []string + upServerMap map[string][]string + handlers []HandleFunc +} + +// NewClient returns a new dns client. +func NewClient(proxy proxy.Proxy, config *Config) (*Client, error) { + c := &Client{ + proxy: proxy, + cache: NewCache(), + config: config, + upServers: config.Servers, + upServerMap: make(map[string][]string), + } + + // custom records + for _, record := range config.Records { + c.AddRecord(record) + } + + return c, nil +} + +// Exchange handles request message and returns response message. +// reqBytes = reqLen + reqMsg +func (c *Client) Exchange(reqBytes []byte, clientAddr string, preferTCP bool) ([]byte, error) { + req, err := UnmarshalMessage(reqBytes[2:]) + if err != nil { + return nil, err + } + + if req.Question.QTYPE == QTypeA || req.Question.QTYPE == QTypeAAAA { + v := c.cache.Get(getKey(req.Question)) + if v != nil { + binary.BigEndian.PutUint16(v[2:4], req.ID) + log.F("[dns] %s <-> cache, type: %d, %s", + clientAddr, req.Question.QTYPE, req.Question.QNAME) + + return v, nil + } + } + + dnsServer, network, dialerAddr, respBytes, err := c.exchange(req.Question.QNAME, reqBytes, preferTCP) + if err != nil { + return nil, err + } + + if req.Question.QTYPE != QTypeA && req.Question.QTYPE != QTypeAAAA { + log.F("[dns] %s <-> %s(%s) via %s, type: %d, %s", + clientAddr, dnsServer, network, dialerAddr, req.Question.QTYPE, req.Question.QNAME) + return respBytes, nil + } + + resp, err := UnmarshalMessage(respBytes[2:]) + if err != nil { + return respBytes, err + } + + ttl := c.config.MinTTL + ips := []string{} + for _, answer := range resp.Answers { + if answer.TYPE == QTypeA || answer.TYPE == QTypeAAAA { + for _, h := range c.handlers { + h(resp.Question.QNAME, answer.IP) + } + if answer.IP != "" { + ips = append(ips, answer.IP) + } + if answer.TTL != 0 { + ttl = int(answer.TTL) + } + } + } + + if ttl > c.config.MaxTTL { + ttl = c.config.MaxTTL + } else if ttl < c.config.MinTTL { + ttl = c.config.MinTTL + } + + // add to cache only when there's a valid ip address + if len(ips) != 0 && ttl > 0 { + c.cache.Put(getKey(resp.Question), respBytes, ttl) + } + + log.F("[dns] %s <-> %s(%s) via %s, type: %d, %s: %s", + clientAddr, dnsServer, network, dialerAddr, resp.Question.QTYPE, resp.Question.QNAME, strings.Join(ips, ",")) + + return respBytes, nil +} + +// exchange choose a upstream dns server based on qname, communicate with it on the network. +func (c *Client) exchange(qname string, reqBytes []byte, preferTCP bool) ( + server, network, dialerAddr string, respBytes []byte, err error) { + + // use tcp to connect upstream server default + network = "tcp" + dialer := c.proxy.NextDialer(qname + ":53") + + // if we are resolving the dialer's domain, then use Direct to avoid denpency loop + // TODO: dialer.Addr() == "REJECT", tricky + if strings.Contains(dialer.Addr(), qname) || dialer.Addr() == "REJECT" { + dialer = proxy.Default + } + + // If client uses udp and no forwarders specified, use udp + // TODO: dialer.Addr() == "DIRECT", tricky + if !preferTCP && !c.config.AlwaysTCP && dialer.Addr() == "DIRECT" { + network = "udp" + } + + servers := c.GetServers(qname) + for _, server = range servers { + var rc net.Conn + rc, err = dialer.Dial(network, server) + if err != nil { + log.F("[dns] failed to connect to server %v: %v", server, err) + continue + } + defer rc.Close() + + // TODO: support timeout setting for different upstream server + rc.SetDeadline(time.Now().Add(time.Duration(c.config.Timeout) * time.Second)) + + switch network { + case "tcp": + respBytes, err = c.exchangeTCP(rc, reqBytes) + case "udp": + respBytes, err = c.exchangeUDP(rc, reqBytes) + } + + if err == nil { + break + } + + log.F("[dns] failed to exchange with server %v: %v", server, err) + } + + return server, network, dialer.Addr(), respBytes, err +} + +// exchangeTCP exchange with server over tcp. +func (c *Client) exchangeTCP(rc net.Conn, reqBytes []byte) ([]byte, error) { + if _, err := rc.Write(reqBytes); err != nil { + log.F("[dns] failed to write req message: %v", err) + return nil, err + } + + var respLen uint16 + if err := binary.Read(rc, binary.BigEndian, &respLen); err != nil { + log.F("[dns] failed to read response length: %v", err) + return nil, err + } + + respBytes := make([]byte, respLen+2) + binary.BigEndian.PutUint16(respBytes[:2], respLen) + + _, err := io.ReadFull(rc, respBytes[2:]) + if err != nil { + log.F("[dns] error in read respMsg %s\n", err) + return nil, err + } + + return respBytes, nil +} + +// exchangeUDP exchange with server over udp. +func (c *Client) exchangeUDP(rc net.Conn, reqBytes []byte) ([]byte, error) { + if _, err := rc.Write(reqBytes[2:]); err != nil { + log.F("[dns] failed to write req message: %v", err) + return nil, err + } + + reqBytes = make([]byte, 2+UDPMaxLen) + n, err := rc.Read(reqBytes[2:]) + if err != nil { + return nil, err + } + binary.BigEndian.PutUint16(reqBytes[:2], uint16(n)) + + return reqBytes[:2+n], nil +} + +// SetServers sets upstream dns servers for the given domain. +func (c *Client) SetServers(domain string, servers ...string) { + c.upServerMap[domain] = append(c.upServerMap[domain], servers...) +} + +// GetServers gets upstream dns servers for the given domain +func (c *Client) GetServers(domain string) []string { + domainParts := strings.Split(domain, ".") + length := len(domainParts) + for i := length - 1; i >= 0; i-- { + domain := strings.Join(domainParts[i:length], ".") + + if servers, ok := c.upServerMap[domain]; ok { + return servers + } + } + + return c.upServers +} + +// AddHandler adds a custom handler to handle the resolved result (A and AAAA). +func (c *Client) AddHandler(h HandleFunc) { + c.handlers = append(c.handlers, h) +} + +// AddRecord adds custom record to dns cache, format: +// www.example.com/1.2.3.4 or www.example.com/2606:2800:220:1:248:1893:25c8:1946 +func (c *Client) AddRecord(record string) error { + r := strings.Split(record, "/") + domain, ip := r[0], r[1] + m, err := c.GenResponse(domain, ip) + if err != nil { + return err + } + + b, _ := m.Marshal() + + var buf bytes.Buffer + binary.Write(&buf, binary.BigEndian, uint16(len(b))) + buf.Write(b) + + c.cache.Put(getKey(m.Question), buf.Bytes(), LongTTL) + + return nil +} + +// GenResponse generates a dns response message for the given domain and ip address. +func (c *Client) GenResponse(domain string, ip string) (*Message, error) { + ipb := net.ParseIP(ip) + if ipb == nil { + return nil, errors.New("GenResponse: invalid ip format") + } + + var rdata []byte + var qtype, rdlen uint16 + if rdata = ipb.To4(); rdata != nil { + qtype = QTypeA + rdlen = net.IPv4len + } else { + qtype = QTypeAAAA + rdlen = net.IPv6len + rdata = ipb + } + + m := NewMessage(0, Response) + m.SetQuestion(NewQuestion(qtype, domain)) + rr := &RR{NAME: domain, TYPE: qtype, CLASS: ClassINET, + TTL: uint32(c.config.MinTTL), RDLENGTH: rdlen, RDATA: rdata} + m.AddAnswer(rr) + + return m, nil +} + +func getKey(q *Question) string { + qtype := "" + switch q.QTYPE { + case QTypeA: + qtype = "A" + case QTypeAAAA: + qtype = "AAAA" + } + return q.QNAME + "/" + qtype +} diff --git a/dns/message.go b/dns/message.go new file mode 100644 index 0000000..efc0f23 --- /dev/null +++ b/dns/message.go @@ -0,0 +1,467 @@ +package dns + +import ( + "bytes" + "encoding/binary" + "errors" + "math/rand" + "net" + "strings" +) + +// UDPMaxLen is the max size of udp dns request. +// https://tools.ietf.org/html/rfc1035#section-4.2.1 +// Messages carried by UDP are restricted to 512 bytes (not counting the IP +// or UDP headers). Longer messages are truncated and the TC bit is set in +// the header. +const UDPMaxLen = 512 + +// HeaderLen is the length of dns msg header. +const HeaderLen = 12 + +// Message types +const ( + Query = 0 + Response = 1 +) + +// Query types +const ( + QTypeA uint16 = 1 //ipv4 + QTypeAAAA uint16 = 28 ///ipv6 +) + +// ClassINET . +const ClassINET uint16 = 1 + +// Message format +// https://tools.ietf.org/html/rfc1035#section-4.1 +// All communications inside of the domain protocol are carried in a single +// format called a message. The top level format of message is divided +// into 5 sections (some of which are empty in certain cases) shown below: +// +// +---------------------+ +// | Header | +// +---------------------+ +// | Question | the question for the name server +// +---------------------+ +// | Answer | RRs answering the question +// +---------------------+ +// | Authority | RRs pointing toward an authority +// +---------------------+ +// | Additional | RRs holding additional information +type Message struct { + Header + // most dns implementation only support 1 question + Question *Question + Answers []*RR + Authority []*RR + Additional []*RR + + // used in UnmarshalMessage + unMarshaled []byte +} + +// NewMessage returns a new message. +func NewMessage(id uint16, msgType int) *Message { + if id == 0 { + id = uint16(rand.Uint32()) + } + + m := &Message{Header: Header{ID: id}} + m.SetMsgType(msgType) + + return m +} + +// SetQuestion sets a question to dns message. +func (m *Message) SetQuestion(q *Question) error { + m.Question = q + m.Header.SetQdcount(1) + return nil +} + +// AddAnswer adds an answer to dns message. +func (m *Message) AddAnswer(rr *RR) error { + m.Answers = append(m.Answers, rr) + return nil +} + +// Marshal marshals message struct to []byte. +func (m *Message) Marshal() ([]byte, error) { + var buf bytes.Buffer + + m.Header.SetQdcount(1) + m.Header.SetAncount(len(m.Answers)) + + b, err := m.Header.Marshal() + if err != nil { + return nil, err + } + buf.Write(b) + + b, err = m.Question.Marshal() + if err != nil { + return nil, err + } + buf.Write(b) + + for _, answer := range m.Answers { + b, err := answer.Marshal() + if err != nil { + return nil, err + } + buf.Write(b) + } + + return buf.Bytes(), nil +} + +// UnmarshalMessage unmarshals []bytes to Message. +func UnmarshalMessage(b []byte) (*Message, error) { + if len(b) < HeaderLen { + return nil, errors.New("UnmarshalMessage: not enough data") + } + + m := &Message{unMarshaled: b} + err := UnmarshalHeader(b[:HeaderLen], &m.Header) + if err != nil { + return nil, err + } + + q := &Question{} + qLen, err := m.UnmarshalQuestion(b[HeaderLen:], q) + if err != nil { + return nil, err + } + m.SetQuestion(q) + + // resp answers + rrIdx := HeaderLen + qLen + for i := 0; i < int(m.Header.ANCOUNT); i++ { + rr := &RR{} + rrLen, err := m.UnmarshalRR(rrIdx, rr) + if err != nil { + return nil, err + } + m.AddAnswer(rr) + + rrIdx += rrLen + } + + m.Header.SetAncount(len(m.Answers)) + + return m, nil +} + +// Header format +// https://tools.ietf.org/html/rfc1035#section-4.1.1 +// The header contains the following fields: +// +// 1 1 1 1 1 1 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | ID | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// |QR| Opcode |AA|TC|RD|RA| Z | RCODE | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | QDCOUNT | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | ANCOUNT | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | NSCOUNT | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | ARCOUNT | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// +type Header struct { + ID uint16 + Bits uint16 + QDCOUNT uint16 + ANCOUNT uint16 + NSCOUNT uint16 + ARCOUNT uint16 +} + +// SetMsgType sets the message type. +func (h *Header) SetMsgType(qr int) { + h.Bits |= uint16(qr) << 15 +} + +// SetTC sets the tc flag. +func (h *Header) SetTC(tc int) { + h.Bits |= uint16(tc) << 9 +} + +// SetQdcount sets query count, most dns servers only support 1 query per request. +func (h *Header) SetQdcount(qdcount int) { + h.QDCOUNT = uint16(qdcount) +} + +// SetAncount sets answers count. +func (h *Header) SetAncount(ancount int) { + h.ANCOUNT = uint16(ancount) +} + +func (h *Header) setFlag(QR uint16, Opcode uint16, AA uint16, + TC uint16, RD uint16, RA uint16, RCODE uint16) { + h.Bits = QR<<15 + Opcode<<11 + AA<<10 + TC<<9 + RD<<8 + RA<<7 + RCODE +} + +// Marshal marshals header struct to []byte. +func (h *Header) Marshal() ([]byte, error) { + var buf bytes.Buffer + err := binary.Write(&buf, binary.BigEndian, h) + return buf.Bytes(), err +} + +// UnmarshalHeader unmarshals []bytes to Header. +func UnmarshalHeader(b []byte, h *Header) error { + if h == nil { + return errors.New("unmarshal header must not be nil") + } + + if len(b) != HeaderLen { + return errors.New("unmarshal header bytes has an unexpected size") + } + + h.ID = binary.BigEndian.Uint16(b[:2]) + h.Bits = binary.BigEndian.Uint16(b[2:4]) + h.QDCOUNT = binary.BigEndian.Uint16(b[4:6]) + h.ANCOUNT = binary.BigEndian.Uint16(b[6:8]) + h.NSCOUNT = binary.BigEndian.Uint16(b[8:10]) + h.ARCOUNT = binary.BigEndian.Uint16(b[10:]) + + return nil +} + +// Question format +// https://tools.ietf.org/html/rfc1035#section-4.1.2 +// The question section is used to carry the "question" in most queries, +// i.e., the parameters that define what is being asked. The section +// contains QDCOUNT (usually 1) entries, each of the following format: +// +// 1 1 1 1 1 1 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | | +// / QNAME / +// / / +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | QTYPE | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | QCLASS | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +type Question struct { + QNAME string + QTYPE uint16 + QCLASS uint16 +} + +// NewQuestion returns a new dns question. +func NewQuestion(qtype uint16, domain string) *Question { + return &Question{ + QNAME: domain, + QTYPE: qtype, + QCLASS: ClassINET, + } +} + +// Marshal marshals Question struct to []byte. +func (q *Question) Marshal() ([]byte, error) { + var buf bytes.Buffer + + buf.Write(MarshalDomain(q.QNAME)) + binary.Write(&buf, binary.BigEndian, q.QTYPE) + binary.Write(&buf, binary.BigEndian, q.QCLASS) + + return buf.Bytes(), nil +} + +// UnmarshalQuestion unmarshals []bytes to Question. +func (m *Message) UnmarshalQuestion(b []byte, q *Question) (n int, err error) { + if q == nil { + return 0, errors.New("unmarshal question must not be nil") + } + + if len(b) <= 5 { + return 0, errors.New("UnmarshalQuestion: not enough data") + } + + domain, idx, err := m.UnmarshalDomain(b) + if err != nil { + return 0, err + } + + q.QNAME = domain + q.QTYPE = binary.BigEndian.Uint16(b[idx : idx+2]) + q.QCLASS = binary.BigEndian.Uint16(b[idx+2 : idx+4]) + + return idx + 3 + 1, nil +} + +// RR format +// https://tools.ietf.org/html/rfc1035#section-3.2.1 +// https://tools.ietf.org/html/rfc1035#section-4.1.3 +// The answer, authority, and additional sections all share the same +// format: a variable number of resource records, where the number of +// records is specified in the corresponding count field in the header. +// Each resource record has the following format: +// +// 1 1 1 1 1 1 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | | +// / / +// / NAME / +// | | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | TYPE | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | CLASS | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | TTL | +// | | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +// | RDLENGTH | +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--| +// / RDATA / +// / / +// +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ +type RR struct { + NAME string + TYPE uint16 + CLASS uint16 + TTL uint32 + RDLENGTH uint16 + RDATA []byte + + IP string +} + +// NewRR returns a new dns rr. +func NewRR() *RR { + rr := &RR{} + return rr +} + +// Marshal marshals RR struct to []byte. +func (rr *RR) Marshal() ([]byte, error) { + var buf bytes.Buffer + + buf.Write(MarshalDomain(rr.NAME)) + binary.Write(&buf, binary.BigEndian, rr.TYPE) + binary.Write(&buf, binary.BigEndian, rr.CLASS) + binary.Write(&buf, binary.BigEndian, rr.TTL) + binary.Write(&buf, binary.BigEndian, rr.RDLENGTH) + buf.Write(rr.RDATA) + + return buf.Bytes(), nil +} + +// UnmarshalRR unmarshals []bytes to RR. +func (m *Message) UnmarshalRR(start int, rr *RR) (n int, err error) { + if rr == nil { + return 0, errors.New("unmarshal rr must not be nil") + } + + p := m.unMarshaled[start:] + + domain, n, err := m.UnmarshalDomain(p) + if err != nil { + return 0, err + } + rr.NAME = domain + + if len(p) <= n+10 { + return 0, errors.New("UnmarshalRR: not enough data") + } + + rr.TYPE = binary.BigEndian.Uint16(p[n:]) + rr.CLASS = binary.BigEndian.Uint16(p[n+2:]) + rr.TTL = binary.BigEndian.Uint32(p[n+4:]) + rr.RDLENGTH = binary.BigEndian.Uint16(p[n+8:]) + + if len(p) < n+10+int(rr.RDLENGTH) { + return 0, errors.New("UnmarshalRR: not enough data for RDATA") + } + + rr.RDATA = p[n+10 : n+10+int(rr.RDLENGTH)] + + if rr.TYPE == QTypeA { + rr.IP = net.IP(rr.RDATA[:net.IPv4len]).String() + } else if rr.TYPE == QTypeAAAA { + rr.IP = net.IP(rr.RDATA[:net.IPv6len]).String() + } + + n = n + 10 + int(rr.RDLENGTH) + + return n, nil +} + +// MarshalDomain marshals domain string struct to []byte. +func MarshalDomain(domain string) []byte { + var buf bytes.Buffer + + for _, seg := range strings.Split(domain, ".") { + binary.Write(&buf, binary.BigEndian, byte(len(seg))) + binary.Write(&buf, binary.BigEndian, []byte(seg)) + } + binary.Write(&buf, binary.BigEndian, byte(0x00)) + + return buf.Bytes() +} + +// UnmarshalDomain gets domain from bytes. +func (m *Message) UnmarshalDomain(b []byte) (string, int, error) { + var idx, size int + var labels = []string{} + + for { + // https://tools.ietf.org/html/rfc1035#section-4.1.4 + // "Message compression", + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + // | 1 1| OFFSET | + // +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ + if b[idx]&0xC0 == 0xC0 { + offset := binary.BigEndian.Uint16(b[idx : idx+2]) + label, err := m.UnmarshalDomainPoint(int(offset & 0x3FFF)) + if err != nil { + return "", 0, err + } + + labels = append(labels, label) + idx += 2 + break + } else { + size = int(b[idx]) + if size == 0 { + idx++ + break + } + + if size > 63 { + return "", 0, errors.New("UnmarshalDomain: label size larger than 63") + } + + if idx+size+1 > len(b) { + return "", 0, errors.New("UnmarshalDomain: label size larger than msg length") + } + + labels = append(labels, string(b[idx+1:idx+size+1])) + idx += (size + 1) + } + } + + domain := strings.Join(labels, ".") + return domain, idx, nil +} + +// UnmarshalDomainPoint gets domain from offset point. +func (m *Message) UnmarshalDomainPoint(offset int) (string, error) { + if offset > len(m.unMarshaled) { + return "", errors.New("UnmarshalDomainPoint: offset larger than msg length") + } + domain, _, err := m.UnmarshalDomain(m.unMarshaled[offset:]) + return domain, err +} diff --git a/dns/server.go b/dns/server.go new file mode 100644 index 0000000..d46dc58 --- /dev/null +++ b/dns/server.go @@ -0,0 +1,144 @@ +package dns + +import ( + "encoding/binary" + "io" + "net" + "sync" + "time" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// conn timeout, seconds +const timeout = 30 + +// Server is a dns server struct. +type Server struct { + addr string + // Client is used to communicate with upstream dns servers + *Client +} + +// NewServer returns a new dns server. +func NewServer(addr string, p proxy.Proxy, config *Config) (*Server, error) { + c, err := NewClient(p, config) + s := &Server{ + addr: addr, + Client: c, + } + + return s, err +} + +// Start starts the dns forwarding server. +// We use WaitGroup here to ensure both udp and tcp serer are completly running, +// so we can start any other services later, since they may rely on dns service. +func (s *Server) Start() { + var wg sync.WaitGroup + wg.Add(2) + go s.ListenAndServeTCP(&wg) + go s.ListenAndServeUDP(&wg) + wg.Wait() +} + +// ListenAndServeUDP listen and serves on udp port. +func (s *Server) ListenAndServeUDP(wg *sync.WaitGroup) { + c, err := net.ListenPacket("udp", s.addr) + wg.Done() + if err != nil { + log.F("[dns] failed to listen on %s, error: %v", s.addr, err) + return + } + defer c.Close() + + log.F("[dns] listening UDP on %s", s.addr) + + for { + reqBytes := make([]byte, 2+UDPMaxLen) + n, caddr, err := c.ReadFrom(reqBytes[2:]) + if err != nil { + log.F("[dns] local read error: %v", err) + continue + } + + reqLen := uint16(n) + if reqLen <= HeaderLen+2 { + log.F("[dns] not enough message data") + continue + } + binary.BigEndian.PutUint16(reqBytes[:2], reqLen) + + go func() { + respBytes, err := s.Client.Exchange(reqBytes[:2+n], caddr.String(), false) + if err != nil { + log.F("[dns] error in exchange: %s", err) + return + } + + _, err = c.WriteTo(respBytes[2:], caddr) + if err != nil { + log.F("[dns] error in local write: %s", err) + return + } + + }() + } + +} + +// ListenAndServeTCP listen and serves on tcp port. +func (s *Server) ListenAndServeTCP(wg *sync.WaitGroup) { + l, err := net.Listen("tcp", s.addr) + wg.Done() + if err != nil { + log.F("[dns-tcp] error: %v", err) + return + } + defer l.Close() + + log.F("[dns-tcp] listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[dns-tcp] error: failed to accept: %v", err) + continue + } + go s.ServeTCP(c) + } +} + +// ServeTCP serves a tcp connection. +func (s *Server) ServeTCP(c net.Conn) { + defer c.Close() + + c.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) + + var reqLen uint16 + if err := binary.Read(c, binary.BigEndian, &reqLen); err != nil { + log.F("[dns-tcp] failed to get request length: %v", err) + return + } + + reqBytes := make([]byte, reqLen+2) + _, err := io.ReadFull(c, reqBytes[2:]) + if err != nil { + log.F("[dns-tcp] error in read reqBytes %s", err) + return + } + + binary.BigEndian.PutUint16(reqBytes[:2], reqLen) + + respBytes, err := s.Exchange(reqBytes, c.RemoteAddr().String(), true) + if err != nil { + log.F("[dns-tcp] error in exchange: %s", err) + return + } + + if err := binary.Write(c, binary.BigEndian, respBytes); err != nil { + log.F("[dns-tcp] error in local write respBytes: %s", err) + return + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e68a88b --- /dev/null +++ b/go.mod @@ -0,0 +1,33 @@ +module github.com/nadoo/glider + +go 1.13 + +require ( + github.com/Yawning/chacha20 v0.0.0-20170904085104-e3b1f968fc63 // indirect + github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/dgryski/go-camellia v0.0.0-20140412174459-3be6b3054dd1 // indirect + github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb // indirect + github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 // indirect + github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 // indirect + github.com/klauspost/cpuid v1.2.1 // indirect + github.com/klauspost/reedsolomon v1.9.2 // indirect + github.com/nadoo/conflag v0.2.0 + github.com/nadoo/go-shadowsocks2 v0.1.0 + github.com/pkg/errors v0.8.1 // indirect + github.com/sun8911879/shadowsocksR v0.0.0-20180529042039-da20fda4804f + github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 // indirect + github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b // indirect + github.com/tjfoc/gmsm v1.0.1 // indirect + github.com/xtaci/kcp-go v5.4.4+incompatible + github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae // indirect + golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 + golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 // indirect + golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a // indirect +) + +// Replace dependency modules with local developing copy +// use `go list -m all` to confirm the final module used +// replace ( +// github.com/nadoo/conflag => ../conflag +// github.com/nadoo/go-shadowsocks2 => ../go-shadowsocks2 +// ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0b80609 --- /dev/null +++ b/go.sum @@ -0,0 +1,47 @@ +github.com/Yawning/chacha20 v0.0.0-20170904085104-e3b1f968fc63 h1:I6/SJSN9wJMJ+ZyQaCHUlzoTA4ypU5Bb44YWR1wTY/0= +github.com/Yawning/chacha20 v0.0.0-20170904085104-e3b1f968fc63/go.mod h1:nf+Komq6fVP4SwmKEaVGxHTyQGKREVlwjQKpvOV39yE= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= +github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/dgryski/go-camellia v0.0.0-20140412174459-3be6b3054dd1 h1:/5UddQ9I3CXetvBVN2ipRc209YUB0AMR8bufErftAxI= +github.com/dgryski/go-camellia v0.0.0-20140412174459-3be6b3054dd1/go.mod h1:QX5ZVULjAfZJux/W62Y91HvCh9hyW6enAwcrrv/sLj0= +github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb h1:zXpN5126w/mhECTkqazBkrOJIMatbPP71aSIDR5UuW4= +github.com/dgryski/go-idea v0.0.0-20170306091226-d2fb45a411fb/go.mod h1:F7WkpqJj9t98ePxB/WJGQTIDeOVPuSJ3qdn6JUjg170= +github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152 h1:ED31mPIxDJnrLt9W9dH5xgd/6KjzEACKHBVGQ33czc0= +github.com/dgryski/go-rc2 v0.0.0-20150621095337-8a9021637152/go.mod h1:I9fhc/EvSg88cDxmfQ47v35Ssz9rlFunL/KY0A1JAYI= +github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521 h1:fBHFH+Y/GPGFGo7LIrErQc3p2MeAhoIQNgaxPWYsSxk= +github.com/ebfe/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:ucvhdsUCE3TH0LoLRb6ShHiJl8e39dGlx6A4g/ujlow= +github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w= +github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/reedsolomon v1.9.2 h1:E9CMS2Pqbv+C7tsrYad4YC9MfhnMVWhMRsTi7U0UB18= +github.com/klauspost/reedsolomon v1.9.2/go.mod h1:CwCi+NUr9pqSVktrkN+Ondf06rkhYZ/pcNv7fu+8Un4= +github.com/nadoo/conflag v0.2.0 h1:xao13tYqfD+5bjQ1A/jT2kBL8tUcVpFhq3seuN5kpeM= +github.com/nadoo/conflag v0.2.0/go.mod h1:Ayl83klaw7fagwYaI6luTmbOi4psAf7FqJNRRv5YMvU= +github.com/nadoo/go-shadowsocks2 v0.1.0 h1:NkdUrZrI8uYq8R0YDmHLttLqKt0Z9i7dUKtGvBqZQl8= +github.com/nadoo/go-shadowsocks2 v0.1.0/go.mod h1:J0B/QoRZtqUwE9BJqkP3F3M5+N8t+b5fXeNrkUarveM= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/sun8911879/shadowsocksR v0.0.0-20180529042039-da20fda4804f h1:66c28UIO0JbJi5he9n+QN9Ya0OAW0eKb8Eu02kMSXHI= +github.com/sun8911879/shadowsocksR v0.0.0-20180529042039-da20fda4804f/go.mod h1:uEm3LP/z9l1+zfo2FTzUvWnxua7rbrUoGAMiLaHdujk= +github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161 h1:89CEmDvlq/F7SJEOqkIdNDGJXrQIhuIx9D2DBXjavSU= +github.com/templexxx/cpufeat v0.0.0-20180724012125-cef66df7f161/go.mod h1:wM7WEvslTq+iOEAMDLSzhVuOt5BRZ05WirO+b09GHQU= +github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b h1:mnG1fcsIB1d/3vbkBak2MM0u+vhGhlQwpeimUi7QncM= +github.com/templexxx/xor v0.0.0-20181023030647-4e92f724b73b/go.mod h1:5XA7W9S6mni3h5uvOC75dA3m9CCCaS83lltmc0ukdi4= +github.com/tjfoc/gmsm v1.0.1 h1:R11HlqhXkDospckjZEihx9SW/2VW0RgdwrykyWMFOQU= +github.com/tjfoc/gmsm v1.0.1/go.mod h1:XxO4hdhhrzAd+G4CjDqaOkd0hUzmtPR/d3EiBBMn/wc= +github.com/xtaci/kcp-go v5.4.4+incompatible h1:QIJ0a0Q0N1G20yLHL2+fpdzyy2v/Cb3PI+xiwx/KK9c= +github.com/xtaci/kcp-go v5.4.4+incompatible/go.mod h1:bN6vIwHQbfHaHtFpEssmWsN45a+AZwO7eyRCmEIbtvE= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM= +github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586 h1:7KByu05hhLed2MO29w7p1XfZvZ13m8mub3shuVftRs0= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7 h1:fHDIZ2oxGnUZRN6WgWFCbYBjH9uqVPRCUVUDhs0wnbA= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= diff --git a/ipset/ipset_linux.go b/ipset/ipset_linux.go new file mode 100644 index 0000000..eb995e7 --- /dev/null +++ b/ipset/ipset_linux.go @@ -0,0 +1,475 @@ +// Apache License 2.0 +// @mdlayher https://github.com/mdlayher/netlink +// Ref: https://github.com/vishvananda/netlink/blob/master/nl/nl_linux.go + +package ipset + +import ( + "bytes" + "encoding/binary" + "net" + "strings" + "sync" + "sync/atomic" + "syscall" + "unsafe" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/rule" +) + +// NFNL_SUBSYS_IPSET netfilter netlink message types +// https://github.com/torvalds/linux/blob/9e66317d3c92ddaab330c125dfe9d06eee268aff/include/uapi/linux/netfilter/nfnetlink.h#L56 +const NFNL_SUBSYS_IPSET = 6 + +// IPSET_PROTOCOL The protocol version +// http://git.netfilter.org/ipset/tree/include/libipset/linux_ip_set.h +const IPSET_PROTOCOL = 6 + +// IPSET_MAXNAMELEN The max length of strings including NUL: set and type identifiers +const IPSET_MAXNAMELEN = 32 + +// Message types and commands +const ( + IPSET_CMD_CREATE = 2 + IPSET_CMD_FLUSH = 4 + IPSET_CMD_ADD = 9 + IPSET_CMD_DEL = 10 +) + +// Attributes at command level +const ( + IPSET_ATTR_PROTOCOL = 1 /* 1: Protocol version */ + IPSET_ATTR_SETNAME = 2 /* 2: Name of the set */ + IPSET_ATTR_TYPENAME = 3 /* 3: Typename */ + IPSET_ATTR_REVISION = 4 /* 4: Settype revision */ + IPSET_ATTR_FAMILY = 5 /* 5: Settype family */ + IPSET_ATTR_DATA = 7 /* 7: Nested attributes */ +) + +// CADT specific attributes +const ( + IPSET_ATTR_IP = 1 + IPSET_ATTR_CIDR = 3 +) + +// IP specific attributes +const ( + IPSET_ATTR_IPADDR_IPV4 = 1 + IPSET_ATTR_IPADDR_IPV6 = 2 +) + +// ATTR flags +const ( + NLA_F_NESTED = (1 << 15) + NLA_F_NET_BYTEORDER = (1 << 14) +) + +var nextSeqNr uint32 +var nativeEndian binary.ByteOrder + +// Manager struct +type Manager struct { + fd int + lsa syscall.SockaddrNetlink + + domainSet sync.Map +} + +// NewManager returns a Manager +func NewManager(rules []*rule.Config) (*Manager, error) { + fd, err := syscall.Socket(syscall.AF_NETLINK, syscall.SOCK_RAW, syscall.NETLINK_NETFILTER) + if err != nil { + log.F("%s", err) + return nil, err + } + // defer syscall.Close(fd) + + lsa := syscall.SockaddrNetlink{ + Family: syscall.AF_NETLINK, + } + + if err = syscall.Bind(fd, &lsa); err != nil { + log.F("%s", err) + return nil, err + } + + m := &Manager{fd: fd, lsa: lsa} + + // create ipset + for _, r := range rules { + if r.IPSet != "" { + CreateSet(fd, lsa, r.IPSet) + } + } + + // init ipset + for _, r := range rules { + if r.IPSet != "" { + for _, domain := range r.Domain { + m.domainSet.Store(domain, r.IPSet) + } + for _, ip := range r.IP { + AddToSet(fd, lsa, r.IPSet, ip) + } + for _, cidr := range r.CIDR { + AddToSet(fd, lsa, r.IPSet, cidr) + } + } + } + + return m, nil +} + +// AddDomainIP implements the DNSAnswerHandler function, used to update ipset according to domainSet rule +func (m *Manager) AddDomainIP(domain, ip string) error { + if ip != "" { + domainParts := strings.Split(domain, ".") + length := len(domainParts) + for i := length - 1; i >= 0; i-- { + domain := strings.Join(domainParts[i:length], ".") + + // find in domainMap + if ipset, ok := m.domainSet.Load(domain); ok { + AddToSet(m.fd, m.lsa, ipset.(string), ip) + } + } + } + + return nil +} + +// CreateSet create a ipset +func CreateSet(fd int, lsa syscall.SockaddrNetlink, setName string) { + if setName == "" { + return + } + + if len(setName) > IPSET_MAXNAMELEN { + log.Fatal("ipset: name too long") + } + + log.F("ipset create %s hash:net", setName) + + req := NewNetlinkRequest(IPSET_CMD_CREATE|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST) + + // TODO: support AF_INET6 + req.AddData(NewNfGenMsg(syscall.AF_INET, 0, 0)) + req.AddData(NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL))) + req.AddData(NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName))) + req.AddData(NewRtAttr(IPSET_ATTR_TYPENAME, ZeroTerminated("hash:net"))) + req.AddData(NewRtAttr(IPSET_ATTR_REVISION, Uint8Attr(1))) + req.AddData(NewRtAttr(IPSET_ATTR_FAMILY, Uint8Attr(2))) + req.AddData(NewRtAttr(IPSET_ATTR_DATA|NLA_F_NESTED, nil)) + + err := syscall.Sendto(fd, req.Serialize(), 0, &lsa) + if err != nil { + log.F("%s", err) + } + + FlushSet(fd, lsa, setName) +} + +// FlushSet flush a ipset +func FlushSet(fd int, lsa syscall.SockaddrNetlink, setName string) { + log.F("ipset flush %s", setName) + + req := NewNetlinkRequest(IPSET_CMD_FLUSH|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST) + + // TODO: support AF_INET6 + req.AddData(NewNfGenMsg(syscall.AF_INET, 0, 0)) + req.AddData(NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL))) + req.AddData(NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName))) + + err := syscall.Sendto(fd, req.Serialize(), 0, &lsa) + if err != nil { + log.F("%s", err) + } + +} + +// AddToSet adds an entry to ipset +func AddToSet(fd int, lsa syscall.SockaddrNetlink, setName, entry string) { + if setName == "" { + return + } + + if len(setName) > IPSET_MAXNAMELEN { + log.F("ipset: name too long") + } + + log.F("ipset add %s %s", setName, entry) + + var ip net.IP + var cidr *net.IPNet + + ip, cidr, err := net.ParseCIDR(entry) + if err != nil { + ip = net.ParseIP(entry) + } + + if ip == nil { + log.F("ipset: parse %s error", entry) + return + } + + req := NewNetlinkRequest(IPSET_CMD_ADD|(NFNL_SUBSYS_IPSET<<8), syscall.NLM_F_REQUEST) + + // TODO: support AF_INET6 + req.AddData(NewNfGenMsg(syscall.AF_INET, 0, 0)) + req.AddData(NewRtAttr(IPSET_ATTR_PROTOCOL, Uint8Attr(IPSET_PROTOCOL))) + req.AddData(NewRtAttr(IPSET_ATTR_SETNAME, ZeroTerminated(setName))) + + attrNested := NewRtAttr(IPSET_ATTR_DATA|NLA_F_NESTED, nil) + attrIP := NewRtAttrChild(attrNested, IPSET_ATTR_IP|NLA_F_NESTED, nil) + + // TODO: support ipV6 + NewRtAttrChild(attrIP, IPSET_ATTR_IPADDR_IPV4|NLA_F_NET_BYTEORDER, ip.To4()) + + // for cidr prefix + if cidr != nil { + cidrPrefix, _ := cidr.Mask.Size() + NewRtAttrChild(attrNested, IPSET_ATTR_CIDR, Uint8Attr(uint8(cidrPrefix))) + } + + NewRtAttrChild(attrNested, 9|NLA_F_NET_BYTEORDER, Uint32Attr(0)) + req.AddData(attrNested) + + err = syscall.Sendto(fd, req.Serialize(), 0, &lsa) + if err != nil { + log.F("%s", err) + } +} + +// NativeEndian get native endianness for the system +func NativeEndian() binary.ByteOrder { + if nativeEndian == nil { + var x uint32 = 0x01020304 + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + nativeEndian = binary.BigEndian + } else { + nativeEndian = binary.LittleEndian + } + } + return nativeEndian +} + +func rtaAlignOf(attrlen int) int { + return (attrlen + syscall.RTA_ALIGNTO - 1) & ^(syscall.RTA_ALIGNTO - 1) +} + +// NetlinkRequestData . +type NetlinkRequestData interface { + Len() int + Serialize() []byte +} + +// NfGenMsg . +type NfGenMsg struct { + nfgenFamily uint8 + version uint8 + resID uint16 +} + +// NewNfGenMsg . +func NewNfGenMsg(nfgenFamily, version, resID int) *NfGenMsg { + return &NfGenMsg{ + nfgenFamily: uint8(nfgenFamily), + version: uint8(version), + resID: uint16(resID), + } +} + +// Len . +func (m *NfGenMsg) Len() int { + return rtaAlignOf(4) +} + +// Serialize . +func (m *NfGenMsg) Serialize() []byte { + native := NativeEndian() + + length := m.Len() + buf := make([]byte, rtaAlignOf(length)) + buf[0] = m.nfgenFamily + buf[1] = m.version + native.PutUint16(buf[2:4], m.resID) + return buf +} + +// RtAttr Extend RtAttr to handle data and children +type RtAttr struct { + syscall.RtAttr + Data []byte + children []NetlinkRequestData +} + +// NewRtAttr Create a new Extended RtAttr object +func NewRtAttr(attrType int, data []byte) *RtAttr { + return &RtAttr{ + RtAttr: syscall.RtAttr{ + Type: uint16(attrType), + }, + children: []NetlinkRequestData{}, + Data: data, + } +} + +// NewRtAttrChild Create a new RtAttr obj anc add it as a child of an existing object +func NewRtAttrChild(parent *RtAttr, attrType int, data []byte) *RtAttr { + attr := NewRtAttr(attrType, data) + parent.children = append(parent.children, attr) + return attr +} + +// Len . +func (a *RtAttr) Len() int { + if len(a.children) == 0 { + return (syscall.SizeofRtAttr + len(a.Data)) + } + + l := 0 + for _, child := range a.children { + l += rtaAlignOf(child.Len()) + } + l += syscall.SizeofRtAttr + return rtaAlignOf(l + len(a.Data)) +} + +// Serialize the RtAttr into a byte array +// This can't just unsafe.cast because it must iterate through children. +func (a *RtAttr) Serialize() []byte { + native := NativeEndian() + + length := a.Len() + buf := make([]byte, rtaAlignOf(length)) + + next := 4 + if a.Data != nil { + copy(buf[next:], a.Data) + next += rtaAlignOf(len(a.Data)) + } + if len(a.children) > 0 { + for _, child := range a.children { + childBuf := child.Serialize() + copy(buf[next:], childBuf) + next += rtaAlignOf(len(childBuf)) + } + } + + if l := uint16(length); l != 0 { + native.PutUint16(buf[0:2], l) + } + native.PutUint16(buf[2:4], a.Type) + return buf +} + +// NetlinkRequest . +type NetlinkRequest struct { + syscall.NlMsghdr + Data []NetlinkRequestData + RawData []byte +} + +// NewNetlinkRequest create a new netlink request from proto and flags +// Note the Len value will be inaccurate once data is added until +// the message is serialized +func NewNetlinkRequest(proto, flags int) *NetlinkRequest { + return &NetlinkRequest{ + NlMsghdr: syscall.NlMsghdr{ + Len: uint32(syscall.SizeofNlMsghdr), + Type: uint16(proto), + Flags: syscall.NLM_F_REQUEST | uint16(flags), + Seq: atomic.AddUint32(&nextSeqNr, 1), + // Pid: uint32(os.Getpid()), + }, + } +} + +// Serialize the Netlink Request into a byte array +func (req *NetlinkRequest) Serialize() []byte { + length := syscall.SizeofNlMsghdr + dataBytes := make([][]byte, len(req.Data)) + for i, data := range req.Data { + dataBytes[i] = data.Serialize() + length = length + len(dataBytes[i]) + } + length += len(req.RawData) + + req.Len = uint32(length) + b := make([]byte, length) + hdr := (*(*[syscall.SizeofNlMsghdr]byte)(unsafe.Pointer(req)))[:] + next := syscall.SizeofNlMsghdr + copy(b[0:next], hdr) + for _, data := range dataBytes { + for _, dataByte := range data { + b[next] = dataByte + next = next + 1 + } + } + // Add the raw data if any + if len(req.RawData) > 0 { + copy(b[next:length], req.RawData) + } + return b +} + +// AddData add data to request +func (req *NetlinkRequest) AddData(data NetlinkRequestData) { + if data != nil { + req.Data = append(req.Data, data) + } +} + +// AddRawData adds raw bytes to the end of the NetlinkRequest object during serialization +func (req *NetlinkRequest) AddRawData(data []byte) { + if data != nil { + req.RawData = append(req.RawData, data...) + } +} + +// Uint8Attr . +func Uint8Attr(v uint8) []byte { + return []byte{byte(v)} +} + +// Uint16Attr . +func Uint16Attr(v uint16) []byte { + native := NativeEndian() + bytes := make([]byte, 2) + native.PutUint16(bytes, v) + return bytes +} + +// Uint32Attr . +func Uint32Attr(v uint32) []byte { + native := NativeEndian() + bytes := make([]byte, 4) + native.PutUint32(bytes, v) + return bytes +} + +// ZeroTerminated . +func ZeroTerminated(s string) []byte { + bytes := make([]byte, len(s)+1) + for i := 0; i < len(s); i++ { + bytes[i] = s[i] + } + bytes[len(s)] = 0 + return bytes +} + +// NonZeroTerminated . +func NonZeroTerminated(s string) []byte { + bytes := make([]byte, len(s)) + for i := 0; i < len(s); i++ { + bytes[i] = s[i] + } + return bytes +} + +// BytesToString . +func BytesToString(b []byte) string { + n := bytes.Index(b, []byte{0}) + return string(b[:n]) +} diff --git a/ipset/ipset_other.go b/ipset/ipset_other.go new file mode 100644 index 0000000..91445ad --- /dev/null +++ b/ipset/ipset_other.go @@ -0,0 +1,22 @@ +// +build !linux + +package ipset + +import ( + "errors" + + "github.com/nadoo/glider/rule" +) + +// Manager struct +type Manager struct{} + +// NewManager returns a Manager +func NewManager(rules []*rule.Config) (*Manager, error) { + return nil, errors.New("ipset not supported on this os") +} + +// AddDomainIP implements the DNSAnswerHandler function +func (m *Manager) AddDomainIP(domain, ip string) error { + return errors.New("ipset not supported on this os") +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..e500b6a --- /dev/null +++ b/main.go @@ -0,0 +1,92 @@ +package main + +import ( + stdlog "log" + "os" + "os/signal" + "syscall" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/dns" + "github.com/nadoo/glider/ipset" + "github.com/nadoo/glider/proxy" + "github.com/nadoo/glider/rule" + "github.com/nadoo/glider/strategy" + + _ "github.com/nadoo/glider/proxy/http" + _ "github.com/nadoo/glider/proxy/kcp" + _ "github.com/nadoo/glider/proxy/mixed" + _ "github.com/nadoo/glider/proxy/obfs" + _ "github.com/nadoo/glider/proxy/reject" + _ "github.com/nadoo/glider/proxy/socks5" + _ "github.com/nadoo/glider/proxy/ss" + _ "github.com/nadoo/glider/proxy/ssr" + _ "github.com/nadoo/glider/proxy/tcptun" + _ "github.com/nadoo/glider/proxy/tls" + _ "github.com/nadoo/glider/proxy/udptun" + _ "github.com/nadoo/glider/proxy/uottun" + _ "github.com/nadoo/glider/proxy/vmess" + _ "github.com/nadoo/glider/proxy/ws" +) + +var version = "0.8.2" + +func main() { + // read configs + confInit() + + // setup a log func + log.F = func(f string, v ...interface{}) { + if conf.Verbose { + stdlog.Printf(f, v...) + } + } + + // global rule proxy + p := rule.NewProxy(conf.rules, strategy.NewProxy(conf.Forward, &conf.StrategyConfig)) + + // ipset manager + ipsetM, _ := ipset.NewManager(conf.rules) + + // check and setup dns server + if conf.DNS != "" { + d, err := dns.NewServer(conf.DNS, p, &conf.DNSConfig) + if err != nil { + log.Fatal(err) + } + + // rule + for _, r := range conf.rules { + for _, domain := range r.Domain { + if len(r.DNSServers) > 0 { + d.SetServers(domain, r.DNSServers...) + } + } + } + + // add a handler to update proxy rules when a domain resolved + d.AddHandler(p.AddDomainIP) + if ipsetM != nil { + d.AddHandler(ipsetM.AddDomainIP) + } + + d.Start() + } + + // enable checkers + p.Check() + + // Proxy Servers + for _, listen := range conf.Listen { + local, err := proxy.ServerFromURL(listen, p) + if err != nil { + log.Fatal(err) + } + + go local.ListenAndServe() + } + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + <-sigCh +} diff --git a/main_linux.go b/main_linux.go new file mode 100644 index 0000000..ce57ebd --- /dev/null +++ b/main_linux.go @@ -0,0 +1,6 @@ +package main + +import ( + _ "github.com/nadoo/glider/proxy/redir" + _ "github.com/nadoo/glider/proxy/unix" +) diff --git a/proxy/dialer.go b/proxy/dialer.go new file mode 100644 index 0000000..ec5df2f --- /dev/null +++ b/proxy/dialer.go @@ -0,0 +1,55 @@ +package proxy + +import ( + "errors" + "net" + "net/url" + "strings" + + "github.com/nadoo/glider/common/log" +) + +// Dialer is used to create connection. +type Dialer interface { + // Addr is the dialer's addr + Addr() string + + // Dial connects to the given address + Dial(network, addr string) (c net.Conn, err error) + + // DialUDP connects to the given address + DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) +} + +// DialerCreator is a function to create dialers. +type DialerCreator func(s string, dialer Dialer) (Dialer, error) + +var ( + dialerMap = make(map[string]DialerCreator) +) + +// RegisterDialer is used to register a dialer. +func RegisterDialer(name string, c DialerCreator) { + dialerMap[name] = c +} + +// DialerFromURL calls the registered creator to create dialers. +// dialer is the default upstream dialer so cannot be nil, we can use Default when calling this function. +func DialerFromURL(s string, dialer Dialer) (Dialer, error) { + if dialer == nil { + return nil, errors.New("DialerFromURL: dialer cannot be nil") + } + + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + c, ok := dialerMap[strings.ToLower(u.Scheme)] + if ok { + return c(s, dialer) + } + + return nil, errors.New("unknown scheme '" + u.Scheme + "'") +} diff --git a/proxy/direct.go b/proxy/direct.go new file mode 100644 index 0000000..5561376 --- /dev/null +++ b/proxy/direct.go @@ -0,0 +1,122 @@ +package proxy + +import ( + "errors" + "net" + + "github.com/nadoo/glider/common/log" +) + +// Direct proxy +type Direct struct { + iface *net.Interface // interface specified by user + ip net.IP +} + +// Default dialer +var Default = &Direct{} + +// NewDirect returns a Direct dialer +func NewDirect(intface string) (*Direct, error) { + if intface == "" { + return &Direct{}, nil + } + + ip := net.ParseIP(intface) + if ip != nil { + return &Direct{ip: ip}, nil + } + + iface, err := net.InterfaceByName(intface) + if err != nil { + return nil, errors.New(err.Error() + ": " + intface) + } + + return &Direct{iface: iface}, nil +} + +// Addr returns forwarder's address +func (d *Direct) Addr() string { return "DIRECT" } + +// Dial connects to the address addr on the network net +func (d *Direct) Dial(network, addr string) (c net.Conn, err error) { + if d.iface == nil || d.ip != nil { + c, err = dial(network, addr, d.ip) + if err == nil { + return + } + } + + for _, ip := range d.IFaceIPs() { + c, err = dial(network, addr, ip) + if err == nil { + d.ip = ip + break + } + } + + // no ip available (so no dials made), maybe the interface link is down + if c == nil && err == nil { + err = errors.New("dial failed, maybe the interface link is down, please check it") + } + + return c, err +} + +func dial(network, addr string, localIP net.IP) (net.Conn, error) { + if network == "uot" { + network = "udp" + } + + var la net.Addr + switch network { + case "tcp": + la = &net.TCPAddr{IP: localIP} + case "udp": + la = &net.UDPAddr{IP: localIP} + } + + dialer := &net.Dialer{LocalAddr: la} + c, err := dialer.Dial(network, addr) + if err != nil { + return nil, err + } + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + return c, err +} + +// DialUDP connects to the given address +func (d *Direct) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + // TODO: support specifying local interface + la := "" + if d.ip != nil { + la = d.ip.String() + ":0" + } + + pc, err := net.ListenPacket(network, la) + if err != nil { + log.F("ListenPacket error: %s", err) + return nil, nil, err + } + + uAddr, err := net.ResolveUDPAddr("udp", addr) + return pc, uAddr, err +} + +// IFaceIPs returns ip addresses according to the specified interface +func (d *Direct) IFaceIPs() (ips []net.IP) { + ipnets, err := d.iface.Addrs() + if err != nil { + return + } + + for _, ipnet := range ipnets { + ips = append(ips, ipnet.(*net.IPNet).IP) //!ip.IsLinkLocalUnicast() + } + + return +} diff --git a/proxy/http/http.go b/proxy/http/http.go new file mode 100644 index 0000000..a7d89b6 --- /dev/null +++ b/proxy/http/http.go @@ -0,0 +1,320 @@ +// http proxy +// NOTE: never keep-alive so the implementation can be much easier. + +package http + +import ( + "bufio" + "bytes" + "encoding/base64" + "errors" + "fmt" + "io" + "net" + "net/textproto" + "net/url" + "strings" + "time" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// HTTP struct. +type HTTP struct { + dialer proxy.Dialer + proxy proxy.Proxy + addr string + user string + password string + pretendAsWebServer bool +} + +func init() { + proxy.RegisterDialer("http", NewHTTPDialer) + proxy.RegisterServer("http", NewHTTPServer) +} + +// NewHTTP returns a http proxy. +func NewHTTP(s string, d proxy.Dialer, p proxy.Proxy) (*HTTP, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + user := u.User.Username() + pass, _ := u.User.Password() + + h := &HTTP{ + dialer: d, + proxy: p, + addr: addr, + user: user, + password: pass, + pretendAsWebServer: false, + } + + pretend := u.Query().Get("pretend") + if pretend == "true" { + h.pretendAsWebServer = true + } + + return h, nil +} + +// NewHTTPDialer returns a http proxy dialer. +func NewHTTPDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewHTTP(s, d, nil) +} + +// NewHTTPServer returns a http proxy server. +func NewHTTPServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewHTTP(s, nil, p) +} + +// ListenAndServe listens on server's addr and serves connections. +func (s *HTTP) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + log.F("[http] failed to listen on %s: %v", s.addr, err) + return + } + defer l.Close() + + log.F("[http] listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[http] failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +// Serve serves a connection. +func (s *HTTP) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + reqR := bufio.NewReader(c) + reqTP := textproto.NewReader(reqR) + method, requestURI, proto, ok := parseFirstLine(reqTP) + if !ok { + return + } + + if s.pretendAsWebServer { + fmt.Fprintf(c, "%s 404 Not Found\r\nServer: nginx\r\n\r\n404 Not Found\r\n", proto) + log.F("[http pretender] being accessed as web server from %s", c.RemoteAddr().String()) + return + } + + if method == "CONNECT" { + s.servHTTPS(method, requestURI, proto, c) + return + } + + reqHeader, err := reqTP.ReadMIMEHeader() + if err != nil { + log.F("[http] read header error:%s", err) + return + } + cleanHeaders(reqHeader) + + // tell the remote server not to keep alive + reqHeader.Set("Connection", "close") + + u, err := url.ParseRequestURI(requestURI) + if err != nil { + log.F("[http] parse request url error: %s", err) + return + } + + var tgt = u.Host + if !strings.Contains(u.Host, ":") { + tgt += ":80" + } + + rc, p, err := s.proxy.Dial("tcp", tgt) + if err != nil { + fmt.Fprintf(c, "%s 502 ERROR\r\n\r\n", proto) + log.F("[http] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, p, err) + return + } + defer rc.Close() + + // GET http://example.com/a/index.htm HTTP/1.1 --> + // GET /a/index.htm HTTP/1.1 + u.Scheme = "" + u.Host = "" + uri := u.String() + + var reqBuf bytes.Buffer + writeFirstLine(&reqBuf, method, uri, proto) + writeHeaders(&reqBuf, reqHeader) + + // send request to remote server + rc.Write(reqBuf.Bytes()) + + // copy the left request bytes to remote server. eg. length specificed or chunked body + go func() { + if _, err := reqR.Peek(1); err == nil { + io.Copy(rc, reqR) + rc.SetDeadline(time.Now()) + c.SetDeadline(time.Now()) + } + }() + + respR := bufio.NewReader(rc) + respTP := textproto.NewReader(respR) + proto, code, status, ok := parseFirstLine(respTP) + if !ok { + return + } + + respHeader, err := respTP.ReadMIMEHeader() + if err != nil { + log.F("[http] %s <-> %s via %s, read header error: %v", c.RemoteAddr(), tgt, p, err) + return + } + + respHeader.Set("Proxy-Connection", "close") + respHeader.Set("Connection", "close") + + var respBuf bytes.Buffer + writeFirstLine(&respBuf, proto, code, status) + writeHeaders(&respBuf, respHeader) + + log.F("[http] %s <-> %s via %s", c.RemoteAddr(), tgt, p) + c.Write(respBuf.Bytes()) + + io.Copy(c, respR) + +} + +func (s *HTTP) servHTTPS(method, requestURI, proto string, c net.Conn) { + rc, p, err := s.proxy.Dial("tcp", requestURI) + if err != nil { + c.Write([]byte(proto)) + c.Write([]byte(" 502 ERROR\r\n\r\n")) + log.F("[http] %s <-> %s [c] via %s, error in dial: %v", c.RemoteAddr(), requestURI, p, err) + return + } + + c.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n")) + + log.F("[http] %s <-> %s [c] via %s", c.RemoteAddr(), requestURI, p) + + _, _, err = conn.Relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + log.F("[http] relay error: %v", err) + } +} + +// Addr returns forwarder's address. +func (s *HTTP) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *HTTP) Dial(network, addr string) (net.Conn, error) { + rc, err := s.dialer.Dial(network, s.addr) + if err != nil { + log.F("[http] dial to %s error: %s", s.addr, err) + return nil, err + } + + var buf bytes.Buffer + buf.Write([]byte("CONNECT " + addr + " HTTP/1.1\r\n")) + // TODO: add host header for compatibility? + buf.Write([]byte("Proxy-Connection: Keep-Alive\r\n")) + + if s.user != "" && s.password != "" { + auth := s.user + ":" + s.password + buf.Write([]byte("Proxy-Authorization: Basic " + base64.StdEncoding.EncodeToString([]byte(auth)) + "\r\n")) + } + + // header ended + buf.Write([]byte("\r\n")) + _, err = rc.Write(buf.Bytes()) + if err != nil { + return nil, err + } + + c := conn.NewConn(rc) + tpr := textproto.NewReader(c.Reader()) + _, code, _, ok := parseFirstLine(tpr) + if ok && code == "200" { + tpr.ReadMIMEHeader() + return c, err + } + + if code == "407" { + log.F("[http] authencation needed by proxy %s", s.addr) + } else if code == "405" { + log.F("[http] 'CONNECT' method not allowed by proxy %s", s.addr) + } + + return nil, errors.New("[http] can not connect remote address: " + addr + ". error code: " + code) +} + +// DialUDP connects to the given address via the proxy. +func (s *HTTP) DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) { + return nil, nil, errors.New("http client does not support udp") +} + +// parseFirstLine parses "GET /foo HTTP/1.1" OR "HTTP/1.1 200 OK" into its three parts. +func parseFirstLine(tp *textproto.Reader) (r1, r2, r3 string, ok bool) { + line, err := tp.ReadLine() + if err != nil { + return + } + + s1 := strings.Index(line, " ") + s2 := strings.Index(line[s1+1:], " ") + if s1 < 0 || s2 < 0 { + return + } + s2 += s1 + 1 + return line[:s1], line[s1+1 : s2], line[s2+1:], true +} + +func cleanHeaders(header textproto.MIMEHeader) { + header.Del("Proxy-Connection") + header.Del("Connection") + header.Del("Keep-Alive") + header.Del("Proxy-Authenticate") + header.Del("Proxy-Authorization") + header.Del("TE") + header.Del("Trailers") + header.Del("Transfer-Encoding") + header.Del("Upgrade") +} + +func writeFirstLine(buf *bytes.Buffer, s1, s2, s3 string) { + buf.WriteString(s1 + " " + s2 + " " + s3 + "\r\n") +} + +func writeHeaders(buf *bytes.Buffer, header textproto.MIMEHeader) { + for key, values := range header { + for _, v := range values { + buf.WriteString(key + ": " + v + "\r\n") + } + } + buf.WriteString("\r\n") +} diff --git a/proxy/kcp/kcp.go b/proxy/kcp/kcp.go new file mode 100644 index 0000000..cd3bf68 --- /dev/null +++ b/proxy/kcp/kcp.go @@ -0,0 +1,230 @@ +package kcp + +import ( + "crypto/sha1" + "errors" + "net" + "net/url" + "strconv" + "strings" + + kcp "github.com/xtaci/kcp-go" + "golang.org/x/crypto/pbkdf2" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// KCP struct. +type KCP struct { + dialer proxy.Dialer + proxy proxy.Proxy + addr string + + key string + crypt string + block kcp.BlockCrypt + + dataShards int + parityShards int + + server proxy.Server +} + +func init() { + proxy.RegisterDialer("kcp", NewKCPDialer) + proxy.RegisterServer("kcp", NewKCPServer) +} + +// NewKCP returns a kcp proxy struct. +func NewKCP(s string, d proxy.Dialer, p proxy.Proxy) (*KCP, error) { + u, err := url.Parse(s) + if err != nil { + log.F("[kcp] parse url err: %s", err) + return nil, err + } + + addr := u.Host + crypt := u.User.Username() + key, _ := u.User.Password() + + query := u.Query() + + // dataShards + dShards := query.Get("dataShards") + if dShards == "" { + dShards = "10" + } + + dataShards, err := strconv.ParseUint(dShards, 10, 32) + if err != nil { + log.F("[kcp] parse dataShards err: %s", err) + return nil, err + } + + // parityShards + pShards := query.Get("parityShards") + if pShards == "" { + pShards = "3" + } + + parityShards, err := strconv.ParseUint(pShards, 10, 32) + if err != nil { + log.F("[kcp] parse parityShards err: %s", err) + return nil, err + } + + k := &KCP{ + dialer: d, + proxy: p, + addr: addr, + key: key, + crypt: crypt, + dataShards: int(dataShards), + parityShards: int(parityShards), + } + + if k.crypt != "" { + pass := pbkdf2.Key([]byte(k.key), []byte("kcp-go"), 4096, 32, sha1.New) + var block kcp.BlockCrypt + switch k.crypt { + case "sm4": + block, _ = kcp.NewSM4BlockCrypt(pass[:16]) + case "tea": + block, _ = kcp.NewTEABlockCrypt(pass[:16]) + case "xor": + block, _ = kcp.NewSimpleXORBlockCrypt(pass) + case "none": + block, _ = kcp.NewNoneBlockCrypt(pass) + case "aes": + block, _ = kcp.NewAESBlockCrypt(pass) + case "aes-128": + block, _ = kcp.NewAESBlockCrypt(pass[:16]) + case "aes-192": + block, _ = kcp.NewAESBlockCrypt(pass[:24]) + case "blowfish": + block, _ = kcp.NewBlowfishBlockCrypt(pass) + case "twofish": + block, _ = kcp.NewTwofishBlockCrypt(pass) + case "cast5": + block, _ = kcp.NewCast5BlockCrypt(pass[:16]) + case "3des": + block, _ = kcp.NewTripleDESBlockCrypt(pass[:24]) + case "xtea": + block, _ = kcp.NewXTEABlockCrypt(pass[:16]) + case "salsa20": + block, _ = kcp.NewSalsa20BlockCrypt(pass) + default: + return nil, errors.New("[kcp] unknown crypt type '" + k.crypt + "'") + } + + k.block = block + } + + return k, nil +} + +// NewKCPDialer returns a kcp proxy dialer. +func NewKCPDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewKCP(s, d, nil) +} + +// NewKCPServer returns a kcp proxy server. +func NewKCPServer(s string, p proxy.Proxy) (proxy.Server, error) { + transport := strings.Split(s, ",") + + // prepare transport listener + // TODO: check here + if len(transport) < 2 { + return nil, errors.New("[kcp] malformd listener:" + s) + } + + k, err := NewKCP(transport[0], nil, p) + if err != nil { + return nil, err + } + + k.server, err = proxy.ServerFromURL(transport[1], p) + if err != nil { + return nil, err + } + + return k, nil +} + +// ListenAndServe listens on server's addr and serves connections. +func (s *KCP) ListenAndServe() { + l, err := kcp.ListenWithOptions(s.addr, s.block, s.dataShards, s.parityShards) + if err != nil { + log.F("[kcp] failed to listen on %s: %v", s.addr, err) + return + } + defer l.Close() + + log.F("[kcp] listening on %s", s.addr) + + for { + c, err := l.AcceptKCP() + if err != nil { + log.F("[kcp] failed to accept: %v", err) + continue + } + + // TODO: change them to customizable later? + c.SetStreamMode(true) + c.SetWriteDelay(false) + c.SetNoDelay(0, 30, 2, 1) + c.SetWindowSize(1024, 1024) + c.SetMtu(1350) + c.SetACKNoDelay(true) + + go s.Serve(c) + } +} + +// Serve serves connections. +func (s *KCP) Serve(c net.Conn) { + // we know the internal server will close the connection after serve + // defer c.Close() + + if s.server != nil { + s.server.Serve(c) + } +} + +// Addr returns forwarder's address. +func (s *KCP) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *KCP) Dial(network, addr string) (net.Conn, error) { + // NOTE: kcp uses udp, we should dial remote server directly here + c, err := kcp.DialWithOptions(s.addr, s.block, s.dataShards, s.parityShards) + if err != nil { + log.F("[kcp] dial to %s error: %s", s.addr, err) + return nil, err + } + + // TODO: change them to customizable later? + c.SetStreamMode(true) + c.SetWriteDelay(false) + c.SetNoDelay(0, 30, 2, 1) + c.SetWindowSize(1024, 1024) + c.SetMtu(1350) + c.SetACKNoDelay(true) + + c.SetDSCP(0) + c.SetReadBuffer(4194304) + c.SetWriteBuffer(4194304) + + return c, err +} + +// DialUDP connects to the given address via the proxy. +func (s *KCP) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("kcp client does not support udp now") +} diff --git a/proxy/mixed/mixed.go b/proxy/mixed/mixed.go new file mode 100644 index 0000000..f07236d --- /dev/null +++ b/proxy/mixed/mixed.go @@ -0,0 +1,127 @@ +package mixed + +import ( + "bytes" + "net" + "net/url" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" + "github.com/nadoo/glider/proxy/http" + "github.com/nadoo/glider/proxy/socks5" +) + +// https://www.ietf.org/rfc/rfc2616.txt, http methods must be uppercase +var httpMethods = [...][]byte{ + []byte("GET"), + []byte("POST"), + []byte("PUT"), + []byte("DELETE"), + []byte("CONNECT"), + []byte("HEAD"), + []byte("OPTIONS"), + []byte("TRACE"), + []byte("PATCH"), +} + +// Mixed struct. +type Mixed struct { + proxy proxy.Proxy + addr string + + http *http.HTTP + socks5 *socks5.Socks5 +} + +func init() { + proxy.RegisterServer("mixed", NewMixedServer) +} + +// NewMixed returns a mixed proxy. +func NewMixed(s string, p proxy.Proxy) (*Mixed, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + m := &Mixed{ + proxy: p, + addr: u.Host, + } + + m.http, _ = http.NewHTTP(s, nil, p) + m.socks5, _ = socks5.NewSocks5(s, nil, p) + + return m, nil +} + +// NewMixedServer returns a mixed server. +func NewMixedServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewMixed(s, p) +} + +// ListenAndServe listens on server's addr and serves connections. +func (m *Mixed) ListenAndServe() { + go m.socks5.ListenAndServeUDP() + + l, err := net.Listen("tcp", m.addr) + if err != nil { + log.F("[mixed] failed to listen on %s: %v", m.addr, err) + return + } + + log.F("[mixed] listening TCP on %s", m.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[mixed] failed to accept: %v", err) + continue + } + + go m.Serve(c) + } +} + +// Serve serves connections. +func (m *Mixed) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + cc := conn.NewConn(c) + + if m.socks5 != nil { + head, err := cc.Peek(1) + if err != nil { + // log.F("[mixed] socks5 peek error: %s", err) + return + } + + // check socks5, client send socksversion: 5 as the first byte + if head[0] == socks5.Version { + m.socks5.Serve(cc) + return + } + } + + if m.http != nil { + head, err := cc.Peek(8) + if err != nil { + log.F("[mixed] http peek error: %s", err) + return + } + + for _, method := range httpMethods { + if bytes.HasPrefix(head, method) { + m.http.Serve(cc) + return + } + } + } + +} diff --git a/proxy/obfs/http.go b/proxy/obfs/http.go new file mode 100644 index 0000000..3942824 --- /dev/null +++ b/proxy/obfs/http.go @@ -0,0 +1,78 @@ +package obfs + +import ( + "bufio" + "bytes" + "crypto/rand" + "encoding/base64" + "io" + "net" +) + +// HTTPObfs struct +type HTTPObfs struct { + obfsHost string + obfsURI string + obfsUA string +} + +// NewHTTPObfs returns a HTTPObfs object +func NewHTTPObfs(obfsHost, obfsURI, obfsUA string) *HTTPObfs { + return &HTTPObfs{obfsHost, obfsURI, obfsUA} +} + +// HTTPObfsConn struct +type HTTPObfsConn struct { + *HTTPObfs + + net.Conn + reader io.Reader +} + +// NewConn returns a new obfs connection +func (p *HTTPObfs) NewConn(c net.Conn) (net.Conn, error) { + cc := &HTTPObfsConn{ + Conn: c, + HTTPObfs: p, + } + + // send http header to remote server + _, err := cc.writeHeader() + return cc, err +} + +func (c *HTTPObfsConn) writeHeader() (int, error) { + buf := new(bytes.Buffer) + buf.WriteString("GET " + c.obfsURI + " HTTP/1.1\r\n") + buf.WriteString("Host: " + c.obfsHost + "\r\n") + buf.WriteString("User-Agent: " + c.obfsUA + "\r\n") + buf.WriteString("Upgrade: websocket\r\n") + buf.WriteString("Connection: Upgrade\r\n") + + p := make([]byte, 16) + rand.Read(p) + buf.WriteString("Sec-WebSocket-Key: " + base64.StdEncoding.EncodeToString(p) + "\r\n") + + buf.WriteString("\r\n") + + return c.Conn.Write(buf.Bytes()) +} + +func (c *HTTPObfsConn) Read(b []byte) (n int, err error) { + if c.reader == nil { + r := bufio.NewReader(c.Conn) + c.reader = r + for { + l, _, err := r.ReadLine() + if err != nil { + return 0, err + } + + if len(l) == 0 { + break + } + } + } + + return c.reader.Read(b) +} diff --git a/proxy/obfs/obfs.go b/proxy/obfs/obfs.go new file mode 100644 index 0000000..cd0f390 --- /dev/null +++ b/proxy/obfs/obfs.go @@ -0,0 +1,111 @@ +// Package obfs implements simple-obfs of ss +package obfs + +import ( + "errors" + "net" + "net/url" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// Obfs struct. +type Obfs struct { + dialer proxy.Dialer + addr string + + obfsType string + obfsHost string + obfsURI string + obfsUA string + + obfsConn func(c net.Conn) (net.Conn, error) +} + +func init() { + proxy.RegisterDialer("simple-obfs", NewObfsDialer) +} + +// NewObfs returns a proxy struct. +func NewObfs(s string, d proxy.Dialer) (*Obfs, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse url err: %s", err) + return nil, err + } + + addr := u.Host + + query := u.Query() + obfsType := query.Get("type") + if obfsType == "" { + obfsType = "http" + } + + obfsHost := query.Get("host") + if obfsHost == "" { + return nil, errors.New("[obfs] host cannot be null") + } + + obfsURI := query.Get("uri") + if obfsURI == "" { + obfsURI = "/" + } + + obfsUA := query.Get("ua") + if obfsUA == "" { + obfsUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" + } + + p := &Obfs{ + dialer: d, + addr: addr, + obfsType: obfsType, + obfsHost: obfsHost, + obfsURI: obfsURI, + obfsUA: obfsUA, + } + + switch obfsType { + case "http": + httpObfs := NewHTTPObfs(obfsHost, obfsURI, obfsUA) + p.obfsConn = httpObfs.NewConn + case "tls": + tlsObfs := NewTLSObfs(obfsHost) + p.obfsConn = tlsObfs.NewConn + default: + return nil, errors.New("[obfs] unknown obfs type: " + obfsType) + } + + return p, nil +} + +// NewObfsDialer returns a proxy dialer. +func NewObfsDialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) { + return NewObfs(s, dialer) +} + +// Addr returns forwarder's address. +func (s *Obfs) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *Obfs) Dial(network, addr string) (net.Conn, error) { + c, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + log.F("[obfs] dial to %s error: %s", s.addr, err) + return nil, err + } + + return s.obfsConn(c) +} + +// DialUDP connects to the given address via the proxy. +func (s *Obfs) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("obfs client does not support udp now") +} diff --git a/proxy/obfs/tls.go b/proxy/obfs/tls.go new file mode 100644 index 0000000..ffb070a --- /dev/null +++ b/proxy/obfs/tls.go @@ -0,0 +1,268 @@ +// https://www.ietf.org/rfc/rfc5246.txt +// https://golang.org/src/crypto/tls/handshake_messages.go + +// NOTE: +// https://github.com/shadowsocks/simple-obfs/blob/master/src/obfs_tls.c +// The official obfs-server only checks 6 static bytes of client hello packet, +// so if we send a malformed packet, e.g: set a wrong length number of extensions, +// obfs-server will treat it as a correct packet, but in wireshak, it's malformed. + +package obfs + +import ( + "bufio" + "bytes" + "crypto/rand" + "encoding/binary" + "io" + "net" + "time" +) + +const ( + lenSize = 2 + chunkSize = 1 << 13 // 8192 +) + +// TLSObfs struct +type TLSObfs struct { + obfsHost string +} + +// NewTLSObfs returns a TLSObfs object +func NewTLSObfs(obfsHost string) *TLSObfs { + return &TLSObfs{obfsHost: obfsHost} +} + +// TLSObfsConn struct +type TLSObfsConn struct { + *TLSObfs + + net.Conn + reqSent bool + reader *bufio.Reader + buf []byte + leftBytes int +} + +// NewConn returns a new obfs connection +func (p *TLSObfs) NewConn(c net.Conn) (net.Conn, error) { + cc := &TLSObfsConn{ + Conn: c, + TLSObfs: p, + buf: make([]byte, lenSize), + } + + return cc, nil +} + +func (c *TLSObfsConn) Write(b []byte) (int, error) { + if !c.reqSent { + c.reqSent = true + return c.handshake(b) + } + + n := len(b) + for i := 0; i < n; i += chunkSize { + end := i + chunkSize + if end > n { + end = n + } + + buf := new(bytes.Buffer) + buf.Write([]byte{0x17, 0x03, 0x03}) + binary.Write(buf, binary.BigEndian, uint16(len(b[i:end]))) + buf.Write(b[i:end]) + + _, err := c.Conn.Write(buf.Bytes()) + if err != nil { + return 0, err + } + } + + return n, nil +} + +func (c *TLSObfsConn) Read(b []byte) (int, error) { + if c.reader == nil { + c.reader = bufio.NewReader(c.Conn) + // Server Hello + // TLSv1.2 Record Layer: Handshake Protocol: Server Hello (96 bytes) + // TLSv1.2 Record Layer: Change Cipher Spec Protocol: Change Cipher Spec (6 bytes) + c.reader.Discard(102) + } + + if c.leftBytes == 0 { + // TLSv1.2 Record Layer: + // 1st packet: handshake encrypted message / following packets: application data + // 1 byte: Content Type: Handshake (22) / Application Data (23) + // 2 bytes: Version: TLS 1.2 (0x0303) + c.reader.Discard(3) + + // get length + _, err := io.ReadFull(c.reader, c.buf[:lenSize]) + if err != nil { + return 0, err + } + + c.leftBytes = int(binary.BigEndian.Uint16(c.buf[:lenSize])) + } + + readLen := len(b) + if readLen > c.leftBytes { + readLen = c.leftBytes + } + + m, err := c.reader.Read(b[:readLen]) + if err != nil { + return 0, err + } + + c.leftBytes -= m + + return m, nil +} + +func (c *TLSObfsConn) handshake(b []byte) (int, error) { + buf := new(bytes.Buffer) + + // prepare extension & clientHello content + bufExt, bufHello := extension(b, c.obfsHost), clientHello() + + // prepare lengths + extLen := bufExt.Len() + helloLen := bufHello.Len() + 2 + extLen // 2: len(extContentLength) + handshakeLen := 4 + helloLen // 1: len(0x01) + 3: len(clientHelloContentLength) + + // TLS Record Layer Begin + // Content Type: Handshake (22) + buf.WriteByte(0x16) + + // Version: TLS 1.0 (0x0301) + buf.Write([]byte{0x03, 0x01}) + + // length + binary.Write(buf, binary.BigEndian, uint16(handshakeLen)) + + // Handshake Begin + // Handshake Type: Client Hello (1) + buf.WriteByte(0x01) + + // length: uint24(3 bytes), but golang doesn't have this type + buf.Write([]byte{uint8(helloLen >> 16), uint8(helloLen >> 8), uint8(helloLen)}) + + // clientHello content + buf.Write(bufHello.Bytes()) + + // Extension Begin + // ext content length + binary.Write(buf, binary.BigEndian, uint16(extLen)) + + // ext content + buf.Write(bufExt.Bytes()) + + _, err := c.Conn.Write(buf.Bytes()) + if err != nil { + return 0, err + } + + return len(b), nil +} + +func clientHello() *bytes.Buffer { + buf := new(bytes.Buffer) + + // Version: TLS 1.2 (0x0303) + buf.Write([]byte{0x03, 0x03}) + + // Random + // https://tools.ietf.org/id/draft-mathewson-no-gmtunixtime-00.txt + // NOTE: + // Most tls implementations do not deal with the first 4 bytes unix time, + // clients do not send current time, and server do not check it, + // golang tls client and chrome browser send random bytes instead. + // + binary.Write(buf, binary.BigEndian, uint32(time.Now().Unix())) + random := make([]byte, 28) + // The above 2 lines of codes was added to make it compatible with some server implementation, + // if we don't need the compatibility, just use the following code instead. + // random := make([]byte, 32) + + rand.Read(random) + buf.Write(random) + + // Session ID Length: 32 + buf.WriteByte(32) + // Session ID + sessionID := make([]byte, 32) + rand.Read(sessionID) + buf.Write(sessionID) + + // https://github.com/shadowsocks/simple-obfs/blob/7659eeccf473aa41eb294e92c32f8f60a8747325/src/obfs_tls.c#L57 + // Cipher Suites Length: 56 + binary.Write(buf, binary.BigEndian, uint16(56)) + // Cipher Suites (28 suites) + buf.Write([]byte{ + 0xc0, 0x2c, 0xc0, 0x30, 0x00, 0x9f, 0xcc, 0xa9, 0xcc, 0xa8, 0xcc, 0xaa, 0xc0, 0x2b, 0xc0, 0x2f, + 0x00, 0x9e, 0xc0, 0x24, 0xc0, 0x28, 0x00, 0x6b, 0xc0, 0x23, 0xc0, 0x27, 0x00, 0x67, 0xc0, 0x0a, + 0xc0, 0x14, 0x00, 0x39, 0xc0, 0x09, 0xc0, 0x13, 0x00, 0x33, 0x00, 0x9d, 0x00, 0x9c, 0x00, 0x3d, + 0x00, 0x3c, 0x00, 0x35, 0x00, 0x2f, 0x00, 0xff, + }) + + // Compression Methods Length: 1 + buf.WriteByte(0x01) + // Compression Methods (1 method) + buf.WriteByte(0x00) + + return buf +} + +func extension(b []byte, server string) *bytes.Buffer { + buf := new(bytes.Buffer) + + // Extension: SessionTicket TLS + buf.Write([]byte{0x00, 0x23}) // type + // NOTE: send some data in sessionticket, the server will treat it as data too + binary.Write(buf, binary.BigEndian, uint16(len(b))) // length + buf.Write(b) + + // Extension: server_name + buf.Write([]byte{0x00, 0x00}) // type + binary.Write(buf, binary.BigEndian, uint16(len(server)+5)) // length + binary.Write(buf, binary.BigEndian, uint16(len(server)+3)) // Server Name list length + buf.WriteByte(0x00) // Server Name Type: host_name (0) + binary.Write(buf, binary.BigEndian, uint16(len(server))) // Server Name length + buf.Write([]byte(server)) + + // https://github.com/shadowsocks/simple-obfs/blob/7659eeccf473aa41eb294e92c32f8f60a8747325/src/obfs_tls.c#L88 + // Extension: ec_point_formats (len=4) + buf.Write([]byte{0x00, 0x0b}) // type + binary.Write(buf, binary.BigEndian, uint16(4)) // length + buf.WriteByte(0x03) // format length + buf.Write([]byte{0x01, 0x00, 0x02}) + + // Extension: supported_groups (len=10) + buf.Write([]byte{0x00, 0x0a}) // type + binary.Write(buf, binary.BigEndian, uint16(10)) // length + binary.Write(buf, binary.BigEndian, uint16(8)) // Supported Groups List Length: 8 + buf.Write([]byte{0x00, 0x1d, 0x00, 0x17, 0x00, 0x19, 0x00, 0x18}) + + // Extension: signature_algorithms (len=32) + buf.Write([]byte{0x00, 0x0d}) // type + binary.Write(buf, binary.BigEndian, uint16(32)) // length + binary.Write(buf, binary.BigEndian, uint16(30)) // Signature Hash Algorithms Length: 30 + buf.Write([]byte{ + 0x06, 0x01, 0x06, 0x02, 0x06, 0x03, 0x05, 0x01, 0x05, 0x02, 0x05, 0x03, 0x04, 0x01, 0x04, 0x02, + 0x04, 0x03, 0x03, 0x01, 0x03, 0x02, 0x03, 0x03, 0x02, 0x01, 0x02, 0x02, 0x02, 0x03, + }) + + // Extension: encrypt_then_mac (len=0) + buf.Write([]byte{0x00, 0x16}) // type + binary.Write(buf, binary.BigEndian, uint16(0)) // length + + // Extension: extended_master_secret (len=0) + buf.Write([]byte{0x00, 0x17}) // type + binary.Write(buf, binary.BigEndian, uint16(0)) // length + + return buf +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..da80bc1 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,15 @@ +package proxy + +import "net" + +// Proxy is a dialer manager +type Proxy interface { + // Dial connects to the given address via the proxy. + Dial(network, addr string) (c net.Conn, proxy string, err error) + + // DialUDP connects to the given address via the proxy. + DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) + + // Get the dialer by dstAddr + NextDialer(dstAddr string) Dialer +} diff --git a/proxy/redir/redir_linux.go b/proxy/redir/redir_linux.go new file mode 100644 index 0000000..f03e8db --- /dev/null +++ b/proxy/redir/redir_linux.go @@ -0,0 +1,183 @@ +// getOrigDst: +// https://github.com/shadowsocks/go-shadowsocks2/blob/master/tcp_linux.go#L30 + +package redir + +import ( + "errors" + "net" + "net/url" + "syscall" + "unsafe" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/common/socks" + "github.com/nadoo/glider/proxy" +) + +const ( + // SO_ORIGINAL_DST from linux/include/uapi/linux/netfilter_ipv4.h + SO_ORIGINAL_DST = 80 + // IP6T_SO_ORIGINAL_DST from linux/include/uapi/linux/netfilter_ipv6/ip6_tables.h + IP6T_SO_ORIGINAL_DST = 80 +) + +// RedirProxy struct. +type RedirProxy struct { + proxy proxy.Proxy + addr string + ipv6 bool +} + +func init() { + proxy.RegisterServer("redir", NewRedirServer) + proxy.RegisterServer("redir6", NewRedir6Server) +} + +// NewRedirProxy returns a redirect proxy. +func NewRedirProxy(s string, p proxy.Proxy, ipv6 bool) (*RedirProxy, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + r := &RedirProxy{ + proxy: p, + addr: addr, + ipv6: ipv6, + } + + return r, nil +} + +// NewRedirServer returns a redir server. +func NewRedirServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewRedirProxy(s, p, false) +} + +// NewRedir6Server returns a redir server for ipv6. +func NewRedir6Server(s string, p proxy.Proxy) (proxy.Server, error) { + return NewRedirProxy(s, p, true) +} + +// ListenAndServe listens on server's addr and serves connections. +func (s *RedirProxy) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + log.F("[redir] failed to listen on %s: %v", s.addr, err) + return + } + + log.F("[redir] listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[redir] failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +// Serve serves connections. +func (s *RedirProxy) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + tgt, err := getOrigDst(c, s.ipv6) + if err != nil { + log.F("[redir] failed to get target address: %v", err) + return + } + + // loop request + if c.LocalAddr().String() == tgt.String() { + log.F("[redir] %s <-> %s, unallowed request to redir port", c.RemoteAddr(), tgt) + return + } + + rc, p, err := s.proxy.Dial("tcp", tgt.String()) + if err != nil { + log.F("[redir] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, p, err) + return + } + defer rc.Close() + + log.F("[redir] %s <-> %s via %s", c.RemoteAddr(), tgt, p) + + _, _, err = conn.Relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + log.F("[redir] relay error: %v", err) + } +} + +// Get the original destination of a TCP connection. +func getOrigDst(conn net.Conn, ipv6 bool) (socks.Addr, error) { + c, ok := conn.(*net.TCPConn) + if !ok { + return nil, errors.New("only work with TCP connection") + } + f, err := c.File() + if err != nil { + return nil, err + } + defer f.Close() + + fd := f.Fd() + + // The File() call above puts both the original socket fd and the file fd in blocking mode. + // Set the file fd back to non-blocking mode and the original socket fd will become non-blocking as well. + // Otherwise blocking I/O will waste OS threads. + if err := syscall.SetNonblock(int(fd), true); err != nil { + return nil, err + } + + if ipv6 { + return getorigdstIPv6(fd) + } + + return getorigdst(fd) +} + +// Call getorigdst() from linux/net/ipv4/netfilter/nf_conntrack_l3proto_ipv4.c +func getorigdst(fd uintptr) (socks.Addr, error) { + raw := syscall.RawSockaddrInet4{} + siz := unsafe.Sizeof(raw) + if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil { + return nil, err + } + + addr := make([]byte, 1+net.IPv4len+2) + addr[0] = socks.ATypIP4 + copy(addr[1:1+net.IPv4len], raw.Addr[:]) + port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian + addr[1+net.IPv4len], addr[1+net.IPv4len+1] = port[0], port[1] + return addr, nil +} + +// Call ipv6_getorigdst() from linux/net/ipv6/netfilter/nf_conntrack_l3proto_ipv6.c +func getorigdstIPv6(fd uintptr) (socks.Addr, error) { + raw := syscall.RawSockaddrInet6{} + siz := unsafe.Sizeof(raw) + if err := socketcall(GETSOCKOPT, fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, uintptr(unsafe.Pointer(&raw)), uintptr(unsafe.Pointer(&siz)), 0); err != nil { + return nil, err + } + + addr := make([]byte, 1+net.IPv6len+2) + addr[0] = socks.ATypIP6 + copy(addr[1:1+net.IPv6len], raw.Addr[:]) + port := (*[2]byte)(unsafe.Pointer(&raw.Port)) // big-endian + addr[1+net.IPv6len], addr[1+net.IPv6len+1] = port[0], port[1] + return addr, nil +} diff --git a/proxy/redir/redir_linux_386.go b/proxy/redir/redir_linux_386.go new file mode 100644 index 0000000..c8fca33 --- /dev/null +++ b/proxy/redir/redir_linux_386.go @@ -0,0 +1,18 @@ +package redir + +import ( + "syscall" + "unsafe" +) + +// https://github.com/golang/go/blob/9e6b79a5dfb2f6fe4301ced956419a0da83bd025/src/syscall/syscall_linux_386.go#L196 +const GETSOCKOPT = 15 + +func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error { + var a [6]uintptr + a[0], a[1], a[2], a[3], a[4], a[5] = a0, a1, a2, a3, a4, a5 + if _, _, errno := syscall.Syscall6(syscall.SYS_SOCKETCALL, call, uintptr(unsafe.Pointer(&a)), 0, 0, 0, 0); errno != 0 { + return errno + } + return nil +} diff --git a/proxy/redir/redir_linux_other.go b/proxy/redir/redir_linux_other.go new file mode 100644 index 0000000..a2677b9 --- /dev/null +++ b/proxy/redir/redir_linux_other.go @@ -0,0 +1,15 @@ +// +build linux,!386 + +package redir + +import "syscall" + +// GETSOCKOPT from syscall +const GETSOCKOPT = syscall.SYS_GETSOCKOPT + +func socketcall(call, a0, a1, a2, a3, a4, a5 uintptr) error { + if _, _, errno := syscall.Syscall6(call, a0, a1, a2, a3, a4, a5); errno != 0 { + return errno + } + return nil +} diff --git a/proxy/reject/reject.go b/proxy/reject/reject.go new file mode 100644 index 0000000..13935c4 --- /dev/null +++ b/proxy/reject/reject.go @@ -0,0 +1,39 @@ +// Package reject implements a virtual proxy which always reject requests. +package reject + +import ( + "errors" + "net" + + "github.com/nadoo/glider/proxy" +) + +// A Reject represents the base struct of a reject proxy. +type Reject struct{} + +func init() { + proxy.RegisterDialer("reject", NewRejectDialer) +} + +// NewReject returns a reject proxy, reject://. +func NewReject(s string, d proxy.Dialer) (*Reject, error) { + return &Reject{}, nil +} + +// NewRejectDialer returns a reject proxy dialer. +func NewRejectDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewReject(s, d) +} + +// Addr returns forwarder's address. +func (s *Reject) Addr() string { return "REJECT" } + +// Dial connects to the address addr on the network net via the proxy. +func (s *Reject) Dial(network, addr string) (net.Conn, error) { + return nil, errors.New("REJECT") +} + +// DialUDP connects to the given address via the proxy. +func (s *Reject) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("REJECT") +} diff --git a/proxy/server.go b/proxy/server.go new file mode 100644 index 0000000..78fdac1 --- /dev/null +++ b/proxy/server.go @@ -0,0 +1,56 @@ +package proxy + +import ( + "errors" + "net" + "net/url" + "strings" + + "github.com/nadoo/glider/common/log" +) + +// Server interface +type Server interface { + // ListenAndServe sets up a listener and serve on it + ListenAndServe() + + // Serve serves a connection + Serve(c net.Conn) +} + +// ServerCreator is a function to create proxy servers +type ServerCreator func(s string, proxy Proxy) (Server, error) + +var ( + serverMap = make(map[string]ServerCreator) +) + +// RegisterServer is used to register a proxy server +func RegisterServer(name string, c ServerCreator) { + serverMap[name] = c +} + +// ServerFromURL calls the registered creator to create proxy servers +// dialer is the default upstream dialer so cannot be nil, we can use Default when calling this function +func ServerFromURL(s string, p Proxy) (Server, error) { + if p == nil { + return nil, errors.New("ServerFromURL: dialer cannot be nil") + } + + if !strings.Contains(s, "://") { + s = "mixed://" + s + } + + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + c, ok := serverMap[strings.ToLower(u.Scheme)] + if ok { + return c(s, p) + } + + return nil, errors.New("unknown scheme '" + u.Scheme + "'") +} diff --git a/proxy/socks5/packet.go b/proxy/socks5/packet.go new file mode 100644 index 0000000..0a28601 --- /dev/null +++ b/proxy/socks5/packet.go @@ -0,0 +1,99 @@ +package socks5 + +import ( + "net" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/common/socks" +) + +// PktConn . +type PktConn struct { + net.PacketConn + + writeAddr net.Addr // write to and read from addr + + tgtAddr socks.Addr + tgtHeader bool + + ctrlConn net.Conn // tcp control conn +} + +// NewPktConn returns a PktConn +func NewPktConn(c net.PacketConn, writeAddr net.Addr, tgtAddr socks.Addr, tgtHeader bool, ctrlConn net.Conn) *PktConn { + pc := &PktConn{ + PacketConn: c, + writeAddr: writeAddr, + tgtAddr: tgtAddr, + tgtHeader: tgtHeader, + ctrlConn: ctrlConn} + + if ctrlConn != nil { + go func() { + buf := make([]byte, 1) + for { + _, err := ctrlConn.Read(buf) + if err, ok := err.(net.Error); ok && err.Timeout() { + continue + } + log.F("[socks5] dialudp udp associate end") + return + } + }() + } + + return pc +} + +// ReadFrom overrides the original function from net.PacketConn +func (pc *PktConn) ReadFrom(b []byte) (int, net.Addr, error) { + if !pc.tgtHeader { + return pc.PacketConn.ReadFrom(b) + } + + buf := make([]byte, len(b)) + n, raddr, err := pc.PacketConn.ReadFrom(buf) + if err != nil { + return n, raddr, err + } + + // https://tools.ietf.org/html/rfc1928#section-7 + // +----+------+------+----------+----------+----------+ + // |RSV | FRAG | ATYP | DST.ADDR | DST.PORT | DATA | + // +----+------+------+----------+----------+----------+ + // | 2 | 1 | 1 | Variable | 2 | Variable | + // +----+------+------+----------+----------+----------+ + tgtAddr := socks.SplitAddr(buf[3:]) + copy(b, buf[3+len(tgtAddr):]) + + //test + if pc.writeAddr == nil { + pc.writeAddr = raddr + } + + if pc.tgtAddr == nil { + pc.tgtAddr = tgtAddr + } + + return n - len(tgtAddr) - 3, raddr, err +} + +// WriteTo overrides the original function from net.PacketConn +func (pc *PktConn) WriteTo(b []byte, addr net.Addr) (int, error) { + if !pc.tgtHeader { + return pc.PacketConn.WriteTo(b, addr) + } + + buf := append([]byte{0, 0, 0}, pc.tgtAddr...) + buf = append(buf, b[:]...) + return pc.PacketConn.WriteTo(buf, pc.writeAddr) +} + +// Close . +func (pc *PktConn) Close() error { + if pc.ctrlConn != nil { + pc.ctrlConn.Close() + } + + return pc.PacketConn.Close() +} diff --git a/proxy/socks5/socks5.go b/proxy/socks5/socks5.go new file mode 100644 index 0000000..0de87f9 --- /dev/null +++ b/proxy/socks5/socks5.go @@ -0,0 +1,467 @@ +// https://tools.ietf.org/html/rfc1928 + +// socks5 client: +// https://github.com/golang/net/tree/master/proxy +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// socks5 server: +// https://github.com/shadowsocks/go-shadowsocks2/tree/master/socks + +// Package socks5 implements a socks5 proxy. +package socks5 + +import ( + "errors" + "io" + "net" + "net/url" + "strconv" + "sync" + "time" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/common/socks" + "github.com/nadoo/glider/proxy" +) + +// Version is socks5 version number. +const Version = 5 + +// Socks5 is a base socks5 struct. +type Socks5 struct { + dialer proxy.Dialer + proxy proxy.Proxy + addr string + user string + password string +} + +func init() { + proxy.RegisterDialer("socks5", NewSocks5Dialer) + proxy.RegisterServer("socks5", NewSocks5Server) +} + +// NewSocks5 returns a Proxy that makes SOCKS v5 connections to the given address +// with an optional username and password. (RFC 1928) +func NewSocks5(s string, d proxy.Dialer, p proxy.Proxy) (*Socks5, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + user := u.User.Username() + pass, _ := u.User.Password() + + h := &Socks5{ + dialer: d, + proxy: p, + addr: addr, + user: user, + password: pass, + } + + return h, nil +} + +// NewSocks5Dialer returns a socks5 proxy dialer. +func NewSocks5Dialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewSocks5(s, d, nil) +} + +// NewSocks5Server returns a socks5 proxy server. +func NewSocks5Server(s string, p proxy.Proxy) (proxy.Server, error) { + return NewSocks5(s, nil, p) +} + +// ListenAndServe serves socks5 requests. +func (s *Socks5) ListenAndServe() { + go s.ListenAndServeUDP() + s.ListenAndServeTCP() +} + +// ListenAndServeTCP listen and serve on tcp port. +func (s *Socks5) ListenAndServeTCP() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + log.F("[socks5] failed to listen on %s: %v", s.addr, err) + return + } + + log.F("[socks5] listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[socks5] failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +// Serve serves a connection. +func (s *Socks5) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + tgt, err := s.handshake(c) + if err != nil { + // UDP: keep the connection until disconnect then free the UDP socket + if err == socks.Errors[9] { + buf := make([]byte, 1) + // block here + for { + _, err := c.Read(buf) + if err, ok := err.(net.Error); ok && err.Timeout() { + continue + } + // log.F("[socks5] servetcp udp associate end") + return + } + } + + log.F("[socks5] failed to get target address: %v", err) + return + } + + rc, p, err := s.proxy.Dial("tcp", tgt.String()) + if err != nil { + log.F("[socks5] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, p, err) + return + } + defer rc.Close() + + log.F("[socks5] %s <-> %s via %s", c.RemoteAddr(), tgt, p) + + _, _, err = conn.Relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + log.F("[socks5] relay error: %v", err) + } +} + +// ListenAndServeUDP serves udp requests. +func (s *Socks5) ListenAndServeUDP() { + lc, err := net.ListenPacket("udp", s.addr) + if err != nil { + log.F("[socks5-udp] failed to listen on %s: %v", s.addr, err) + return + } + defer lc.Close() + + log.F("[socks5-udp] listening UDP on %s", s.addr) + + var nm sync.Map + buf := make([]byte, conn.UDPBufSize) + + for { + c := NewPktConn(lc, nil, nil, true, nil) + + n, raddr, err := c.ReadFrom(buf) + if err != nil { + log.F("[socks5-udp] remote read error: %v", err) + continue + } + + var pc *PktConn + v, ok := nm.Load(raddr.String()) + if !ok && v == nil { + if c.tgtAddr == nil { + log.F("[socks5-udp] can not get target address, not a valid request") + continue + } + + lpc, nextHop, err := s.proxy.DialUDP("udp", c.tgtAddr.String()) + if err != nil { + log.F("[socks5-udp] remote dial error: %v", err) + continue + } + + pc = NewPktConn(lpc, nextHop, nil, false, nil) + nm.Store(raddr.String(), pc) + + go func() { + conn.RelayUDP(c, raddr, pc, 2*time.Minute) + pc.Close() + nm.Delete(raddr.String()) + }() + + log.F("[socks5-udp] %s <-> %s", raddr, c.tgtAddr) + + } else { + pc = v.(*PktConn) + } + + _, err = pc.WriteTo(buf[:n], pc.writeAddr) + if err != nil { + log.F("[socks5-udp] remote write error: %v", err) + continue + } + + // log.F("[socks5-udp] %s <-> %s", raddr, c.tgtAddr) + } + +} + +// Addr returns forwarder's address. +func (s *Socks5) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the SOCKS5 proxy. +func (s *Socks5) Dial(network, addr string) (net.Conn, error) { + switch network { + case "tcp", "tcp6", "tcp4": + default: + return nil, errors.New("[socks5]: no support for connection type " + network) + } + + c, err := s.dialer.Dial(network, s.addr) + if err != nil { + log.F("[socks5]: dial to %s error: %s", s.addr, err) + return nil, err + } + + if err := s.connect(c, addr); err != nil { + c.Close() + return nil, err + } + + return c, nil +} + +// DialUDP connects to the given address via the proxy. +func (s *Socks5) DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) { + c, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + log.F("[socks5] dialudp dial tcp to %s error: %s", s.addr, err) + return nil, nil, err + } + + // send VER, NMETHODS, METHODS + c.Write([]byte{5, 1, 0}) + + buf := make([]byte, socks.MaxAddrLen) + // read VER METHOD + if _, err := io.ReadFull(c, buf[:2]); err != nil { + return nil, nil, err + } + + dstAddr := socks.ParseAddr(addr) + // write VER CMD RSV ATYP DST.ADDR DST.PORT + c.Write(append([]byte{5, socks.CmdUDPAssociate, 0}, dstAddr...)) + + // read VER REP RSV ATYP BND.ADDR BND.PORT + if _, err := io.ReadFull(c, buf[:3]); err != nil { + return nil, nil, err + } + + rep := buf[1] + if rep != 0 { + log.F("[socks5] server reply: %d, not succeeded", rep) + return nil, nil, errors.New("server connect failed") + } + + uAddr, err := socks.ReadAddrBuf(c, buf) + if err != nil { + return nil, nil, err + } + + pc, nextHop, err := s.dialer.DialUDP(network, uAddr.String()) + if err != nil { + log.F("[socks5] dialudp to %s error: %s", uAddr.String(), err) + return nil, nil, err + } + + pkc := NewPktConn(pc, nextHop, dstAddr, true, c) + return pkc, nextHop, err +} + +// connect takes an existing connection to a socks5 proxy server, +// and commands the server to extend that connection to target, +// which must be a canonical address with a host and port. +func (s *Socks5) connect(conn net.Conn, target string) error { + host, portStr, err := net.SplitHostPort(target) + if err != nil { + return err + } + + port, err := strconv.Atoi(portStr) + if err != nil { + return errors.New("proxy: failed to parse port number: " + portStr) + } + if port < 1 || port > 0xffff { + return errors.New("proxy: port number out of range: " + portStr) + } + + // the size here is just an estimate + buf := make([]byte, 0, 6+len(host)) + + buf = append(buf, Version) + if len(s.user) > 0 && len(s.user) < 256 && len(s.password) < 256 { + buf = append(buf, 2 /* num auth methods */, socks.AuthNone, socks.AuthPassword) + } else { + buf = append(buf, 1 /* num auth methods */, socks.AuthNone) + } + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write greeting to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read greeting from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + if buf[0] != 5 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " has unexpected version " + strconv.Itoa(int(buf[0]))) + } + if buf[1] == 0xff { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " requires authentication") + } + + if buf[1] == socks.AuthPassword { + buf = buf[:0] + buf = append(buf, 1 /* password protocol version */) + buf = append(buf, uint8(len(s.user))) + buf = append(buf, s.user...) + buf = append(buf, uint8(len(s.password))) + buf = append(buf, s.password...) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write authentication request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read authentication reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if buf[1] != 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " rejected username/password") + } + } + + buf = buf[:0] + buf = append(buf, Version, socks.CmdConnect, 0 /* reserved */) + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + buf = append(buf, socks.ATypIP4) + ip = ip4 + } else { + buf = append(buf, socks.ATypIP6) + } + buf = append(buf, ip...) + } else { + if len(host) > 255 { + return errors.New("proxy: destination hostname too long: " + host) + } + buf = append(buf, socks.ATypDomain) + buf = append(buf, byte(len(host))) + buf = append(buf, host...) + } + buf = append(buf, byte(port>>8), byte(port)) + + if _, err := conn.Write(buf); err != nil { + return errors.New("proxy: failed to write connect request to SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + if _, err := io.ReadFull(conn, buf[:4]); err != nil { + return errors.New("proxy: failed to read connect reply from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + failure := "unknown error" + if int(buf[1]) < len(socks.Errors) { + failure = socks.Errors[buf[1]].Error() + } + + if len(failure) > 0 { + return errors.New("proxy: SOCKS5 proxy at " + s.addr + " failed to connect: " + failure) + } + + bytesToDiscard := 0 + switch buf[3] { + case socks.ATypIP4: + bytesToDiscard = net.IPv4len + case socks.ATypIP6: + bytesToDiscard = net.IPv6len + case socks.ATypDomain: + _, err := io.ReadFull(conn, buf[:1]) + if err != nil { + return errors.New("proxy: failed to read domain length from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + bytesToDiscard = int(buf[0]) + default: + return errors.New("proxy: got unknown address type " + strconv.Itoa(int(buf[3])) + " from SOCKS5 proxy at " + s.addr) + } + + if cap(buf) < bytesToDiscard { + buf = make([]byte, bytesToDiscard) + } else { + buf = buf[:bytesToDiscard] + } + if _, err := io.ReadFull(conn, buf); err != nil { + return errors.New("proxy: failed to read address from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + // Also need to discard the port number + if _, err := io.ReadFull(conn, buf[:2]); err != nil { + return errors.New("proxy: failed to read port from SOCKS5 proxy at " + s.addr + ": " + err.Error()) + } + + return nil +} + +// Handshake fast-tracks SOCKS initialization to get target address to connect. +func (s *Socks5) handshake(rw io.ReadWriter) (socks.Addr, error) { + // Read RFC 1928 for request and reply structure and sizes + buf := make([]byte, socks.MaxAddrLen) + // read VER, NMETHODS, METHODS + if _, err := io.ReadFull(rw, buf[:2]); err != nil { + return nil, err + } + nmethods := buf[1] + if _, err := io.ReadFull(rw, buf[:nmethods]); err != nil { + return nil, err + } + // write VER METHOD + if _, err := rw.Write([]byte{5, 0}); err != nil { + return nil, err + } + // read VER CMD RSV ATYP DST.ADDR DST.PORT + if _, err := io.ReadFull(rw, buf[:3]); err != nil { + return nil, err + } + cmd := buf[1] + addr, err := socks.ReadAddrBuf(rw, buf) + if err != nil { + return nil, err + } + switch cmd { + case socks.CmdConnect: + _, err = rw.Write([]byte{5, 0, 0, 1, 0, 0, 0, 0, 0, 0}) // SOCKS v5, reply succeeded + case socks.CmdUDPAssociate: + listenAddr := socks.ParseAddr(rw.(net.Conn).LocalAddr().String()) + _, err = rw.Write(append([]byte{5, 0, 0}, listenAddr...)) // SOCKS v5, reply succeeded + if err != nil { + return nil, socks.Errors[7] + } + err = socks.Errors[9] + default: + return nil, socks.Errors[7] + } + + return addr, err // skip VER, CMD, RSV fields +} diff --git a/proxy/ss/packet.go b/proxy/ss/packet.go new file mode 100644 index 0000000..91edf60 --- /dev/null +++ b/proxy/ss/packet.go @@ -0,0 +1,67 @@ +package ss + +import ( + "net" + + "github.com/nadoo/glider/common/socks" +) + +// PktConn . +type PktConn struct { + net.PacketConn + + writeAddr net.Addr // write to and read from addr + + tgtAddr socks.Addr + tgtHeader bool +} + +// NewPktConn returns a PktConn +func NewPktConn(c net.PacketConn, writeAddr net.Addr, tgtAddr socks.Addr, tgtHeader bool) *PktConn { + pc := &PktConn{ + PacketConn: c, + writeAddr: writeAddr, + tgtAddr: tgtAddr, + tgtHeader: tgtHeader} + return pc +} + +// ReadFrom overrides the original function from net.PacketConn +func (pc *PktConn) ReadFrom(b []byte) (int, net.Addr, error) { + if !pc.tgtHeader { + return pc.PacketConn.ReadFrom(b) + } + + buf := make([]byte, len(b)) + n, raddr, err := pc.PacketConn.ReadFrom(buf) + if err != nil { + return n, raddr, err + } + + tgtAddr := socks.SplitAddr(buf) + copy(b, buf[len(tgtAddr):]) + + //test + if pc.writeAddr == nil { + pc.writeAddr = raddr + } + + if pc.tgtAddr == nil { + pc.tgtAddr = tgtAddr + } + + return n - len(tgtAddr), raddr, err +} + +// WriteTo overrides the original function from net.PacketConn +func (pc *PktConn) WriteTo(b []byte, addr net.Addr) (int, error) { + if !pc.tgtHeader { + return pc.PacketConn.WriteTo(b, addr) + } + + buf := make([]byte, len(pc.tgtAddr)+len(b)) + copy(buf, pc.tgtAddr) + copy(buf[len(pc.tgtAddr):], b) + + return pc.PacketConn.WriteTo(buf, pc.writeAddr) +} diff --git a/proxy/ss/ss.go b/proxy/ss/ss.go new file mode 100644 index 0000000..5516bee --- /dev/null +++ b/proxy/ss/ss.go @@ -0,0 +1,280 @@ +package ss + +import ( + "errors" + "net" + "net/url" + "strings" + "sync" + "time" + + "github.com/nadoo/go-shadowsocks2/core" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/common/socks" + "github.com/nadoo/glider/proxy" +) + +// SS is a base ss struct. +type SS struct { + dialer proxy.Dialer + proxy proxy.Proxy + addr string + + core.Cipher +} + +func init() { + proxy.RegisterDialer("ss", NewSSDialer) + proxy.RegisterServer("ss", NewSSServer) +} + +// NewSS returns a ss proxy. +func NewSS(s string, d proxy.Dialer, p proxy.Proxy) (*SS, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + method := u.User.Username() + pass, _ := u.User.Password() + + ciph, err := core.PickCipher(method, nil, pass) + if err != nil { + log.Fatalf("[ss] PickCipher for '%s', error: %s", method, err) + } + + ss := &SS{ + dialer: d, + proxy: p, + addr: addr, + Cipher: ciph, + } + + return ss, nil +} + +// NewSSDialer returns a ss proxy dialer. +func NewSSDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewSS(s, d, nil) +} + +// NewSSServer returns a ss proxy server. +func NewSSServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewSS(s, nil, p) +} + +// ListenAndServe serves ss requests. +func (s *SS) ListenAndServe() { + go s.ListenAndServeUDP() + s.ListenAndServeTCP() +} + +// ListenAndServeTCP serves tcp ss requests. +func (s *SS) ListenAndServeTCP() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + log.F("[ss] failed to listen on %s: %v", s.addr, err) + return + } + + log.F("[ss] listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[ss] failed to accept: %v", err) + continue + } + go s.Serve(c) + } + +} + +// Serve serves a connection. +func (s *SS) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + c = s.StreamConn(c) + + tgt, err := socks.ReadAddr(c) + if err != nil { + log.F("[ss] failed to get target address: %v", err) + return + } + + dialer := s.proxy.NextDialer(tgt.String()) + + // udp over tcp? + uot := socks.UoT(tgt[0]) + if uot && dialer.Addr() == "DIRECT" { + rc, err := net.ListenPacket("udp", "") + if err != nil { + log.F("[ss-uottun] UDP remote listen error: %v", err) + } + defer rc.Close() + + req := make([]byte, conn.UDPBufSize) + n, err := c.Read(req) + if err != nil { + log.F("[ss-uottun] error in ioutil.ReadAll: %s\n", err) + return + } + + tgtAddr, _ := net.ResolveUDPAddr("udp", tgt.String()) + rc.WriteTo(req[:n], tgtAddr) + + buf := make([]byte, conn.UDPBufSize) + n, _, err = rc.ReadFrom(buf) + if err != nil { + log.F("[ss-uottun] read error: %v", err) + } + + c.Write(buf[:n]) + + log.F("[ss] %s <-tcp-> %s - %s <-udp-> %s ", c.RemoteAddr(), c.LocalAddr(), rc.LocalAddr(), tgt) + + return + } + + network := "tcp" + if uot { + network = "udp" + } + + rc, err := dialer.Dial(network, tgt.String()) + if err != nil { + log.F("[ss] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), tgt, dialer.Addr(), err) + return + } + defer rc.Close() + + log.F("[ss] %s <-> %s via %s", c.RemoteAddr(), tgt, dialer.Addr()) + + _, _, err = conn.Relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + log.F("[ss] relay error: %v", err) + } + +} + +// ListenAndServeUDP serves udp ss requests. +func (s *SS) ListenAndServeUDP() { + lc, err := net.ListenPacket("udp", s.addr) + if err != nil { + log.F("[ss-udp] failed to listen on %s: %v", s.addr, err) + return + } + defer lc.Close() + + lc = s.PacketConn(lc) + + log.F("[ss-udp] listening UDP on %s", s.addr) + + var nm sync.Map + buf := make([]byte, conn.UDPBufSize) + + for { + c := NewPktConn(lc, nil, nil, true) + + n, raddr, err := c.ReadFrom(buf) + if err != nil { + log.F("[ss-udp] remote read error: %v", err) + continue + } + + var pc *PktConn + v, ok := nm.Load(raddr.String()) + if !ok && v == nil { + lpc, nextHop, err := s.proxy.DialUDP("udp", c.tgtAddr.String()) + if err != nil { + log.F("[ss-udp] remote dial error: %v", err) + continue + } + + pc = NewPktConn(lpc, nextHop, nil, false) + nm.Store(raddr.String(), pc) + + go func() { + conn.RelayUDP(c, raddr, pc, 2*time.Minute) + pc.Close() + nm.Delete(raddr.String()) + }() + + log.F("[ss-udp] %s <-> %s", raddr, c.tgtAddr) + + } else { + pc = v.(*PktConn) + } + + _, err = pc.WriteTo(buf[:n], pc.writeAddr) + if err != nil { + log.F("[ss-udp] remote write error: %v", err) + continue + } + + // log.F("[ss-udp] %s <-> %s", raddr, c.tgtAddr) + } +} + +// ListCipher returns all the ciphers supported. +func ListCipher() string { + return strings.Join(core.ListCipher(), " ") +} + +// Addr returns forwarder's address. +func (s *SS) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *SS) Dial(network, addr string) (net.Conn, error) { + target := socks.ParseAddr(addr) + if target == nil { + return nil, errors.New("[ss] unable to parse address: " + addr) + } + + if network == "uot" { + target[0] = target[0] | 0x8 + } + + c, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + log.F("[ss] dial to %s error: %s", s.addr, err) + return nil, err + } + + c = s.StreamConn(c) + if _, err = c.Write(target); err != nil { + c.Close() + return nil, err + } + + return c, err + +} + +// DialUDP connects to the given address via the proxy. +func (s *SS) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + pc, nextHop, err := s.dialer.DialUDP(network, s.addr) + if err != nil { + log.F("[ss] dialudp to %s error: %s", s.addr, err) + return nil, nil, err + } + + pkc := NewPktConn(s.PacketConn(pc), nextHop, socks.ParseAddr(addr), true) + return pkc, nextHop, err +} diff --git a/proxy/ssr/ssr.go b/proxy/ssr/ssr.go new file mode 100644 index 0000000..4da8704 --- /dev/null +++ b/proxy/ssr/ssr.go @@ -0,0 +1,154 @@ +package ssr + +import ( + "errors" + "net" + "net/url" + "strconv" + "strings" + + shadowsocksr "github.com/sun8911879/shadowsocksR" + "github.com/sun8911879/shadowsocksR/obfs" + "github.com/sun8911879/shadowsocksR/protocol" + "github.com/sun8911879/shadowsocksR/ssr" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/common/socks" + "github.com/nadoo/glider/proxy" +) + +// SSR struct. +type SSR struct { + dialer proxy.Dialer + addr string + + EncryptMethod string + EncryptPassword string + Obfs string + ObfsParam string + ObfsData interface{} + Protocol string + ProtocolParam string + ProtocolData interface{} +} + +func init() { + proxy.RegisterDialer("ssr", NewSSRDialer) +} + +// NewSSR returns a shadowsocksr proxy, ssr://method:pass@host:port/query +func NewSSR(s string, d proxy.Dialer) (*SSR, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + method := u.User.Username() + pass, _ := u.User.Password() + + p := &SSR{ + dialer: d, + addr: addr, + EncryptMethod: method, + EncryptPassword: pass, + } + + query := u.Query() + p.Protocol = query.Get("protocol") + p.ProtocolParam = query.Get("protocol_param") + p.Obfs = query.Get("obfs") + p.ObfsParam = query.Get("obfs_param") + + return p, nil +} + +// NewSSRDialer returns a ssr proxy dialer. +func NewSSRDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewSSR(s, d) +} + +// Addr returns forwarder's address +func (s *SSR) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *SSR) Dial(network, addr string) (net.Conn, error) { + target := socks.ParseAddr(addr) + if target == nil { + return nil, errors.New("[ssr] unable to parse address: " + addr) + } + + cipher, err := shadowsocksr.NewStreamCipher(s.EncryptMethod, s.EncryptPassword) + if err != nil { + return nil, err + } + + c, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + log.F("[ssr] dial to %s error: %s", s.addr, err) + return nil, err + } + + ssrconn := shadowsocksr.NewSSTCPConn(c, cipher) + if ssrconn.Conn == nil || ssrconn.RemoteAddr() == nil { + return nil, errors.New("[ssr] nil connection") + } + + // should initialize obfs/protocol now + rs := strings.Split(ssrconn.RemoteAddr().String(), ":") + port, _ := strconv.Atoi(rs[1]) + + ssrconn.IObfs = obfs.NewObfs(s.Obfs) + if ssrconn.IObfs == nil { + return nil, errors.New("[ssr] unsupported obfs type: " + s.Obfs) + } + + obfsServerInfo := &ssr.ServerInfoForObfs{ + Host: rs[0], + Port: uint16(port), + TcpMss: 1460, + Param: s.ObfsParam, + } + ssrconn.IObfs.SetServerInfo(obfsServerInfo) + + ssrconn.IProtocol = protocol.NewProtocol(s.Protocol) + if ssrconn.IProtocol == nil { + return nil, errors.New("[ssr] unsupported protocol type: " + s.Protocol) + } + + protocolServerInfo := &ssr.ServerInfoForObfs{ + Host: rs[0], + Port: uint16(port), + TcpMss: 1460, + Param: s.ProtocolParam, + } + ssrconn.IProtocol.SetServerInfo(protocolServerInfo) + + if s.ObfsData == nil { + s.ObfsData = ssrconn.IObfs.GetData() + } + ssrconn.IObfs.SetData(s.ObfsData) + + if s.ProtocolData == nil { + s.ProtocolData = ssrconn.IProtocol.GetData() + } + ssrconn.IProtocol.SetData(s.ProtocolData) + + if _, err := ssrconn.Write(target); err != nil { + ssrconn.Close() + return nil, err + } + + return ssrconn, err +} + +// DialUDP connects to the given address via the proxy. +func (s *SSR) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("[ssr] udp not supported now") +} diff --git a/proxy/tcptun/tcptun.go b/proxy/tcptun/tcptun.go new file mode 100644 index 0000000..1898863 --- /dev/null +++ b/proxy/tcptun/tcptun.go @@ -0,0 +1,95 @@ +package tcptun + +import ( + "net" + "net/url" + "strings" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// TCPTun struct. +type TCPTun struct { + proxy proxy.Proxy + addr string + + raddr string +} + +func init() { + proxy.RegisterServer("tcptun", NewTCPTunServer) +} + +// NewTCPTun returns a tcptun proxy. +func NewTCPTun(s string, p proxy.Proxy) (*TCPTun, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + d := strings.Split(addr, "=") + + t := &TCPTun{ + proxy: p, + addr: d[0], + raddr: d[1], + } + + return t, nil +} + +// NewTCPTunServer returns a udp tunnel server. +func NewTCPTunServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewTCPTun(s, p) +} + +// ListenAndServe listens on server's addr and serves connections. +func (s *TCPTun) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + log.F("failed to listen on %s: %v", s.addr, err) + return + } + + log.F("[tcptun] listening TCP on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[tcptun] failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +// Serve serves a connection. +func (s *TCPTun) Serve(c net.Conn) { + defer c.Close() + + if c, ok := c.(*net.TCPConn); ok { + c.SetKeepAlive(true) + } + + rc, p, err := s.proxy.Dial("tcp", s.raddr) + if err != nil { + log.F("[tcptun] %s <-> %s via %s, error in dial: %v", c.RemoteAddr(), s.addr, p, err) + return + } + defer rc.Close() + + log.F("[tcptun] %s <-> %s via %s", c.RemoteAddr(), s.raddr, p) + + _, _, err = conn.Relay(c, rc) + if err != nil { + if err, ok := err.(net.Error); ok && err.Timeout() { + return // ignore i/o timeout + } + log.F("relay error: %v", err) + } +} diff --git a/proxy/tls/tls.go b/proxy/tls/tls.go new file mode 100644 index 0000000..a23262f --- /dev/null +++ b/proxy/tls/tls.go @@ -0,0 +1,183 @@ +package tls + +import ( + stdtls "crypto/tls" + "errors" + "net" + "net/url" + "strings" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// TLS struct. +type TLS struct { + dialer proxy.Dialer + proxy proxy.Proxy + addr string + + tlsConfig *stdtls.Config + + serverName string + skipVerify bool + + certFile string + keyFile string + cert stdtls.Certificate + + server proxy.Server +} + +func init() { + proxy.RegisterDialer("tls", NewTLSDialer) + proxy.RegisterServer("tls", NewTLSServer) +} + +// NewTLS returns a tls proxy struct. +func NewTLS(s string, d proxy.Dialer, p proxy.Proxy) (*TLS, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse url err: %s", err) + return nil, err + } + + addr := u.Host + + colonPos := strings.LastIndex(addr, ":") + if colonPos == -1 { + colonPos = len(addr) + } + serverName := addr[:colonPos] + + query := u.Query() + skipVerify := query.Get("skipVerify") + certFile := query.Get("cert") + keyFile := query.Get("key") + + t := &TLS{ + dialer: d, + proxy: p, + addr: addr, + serverName: serverName, + skipVerify: false, + certFile: certFile, + keyFile: keyFile, + } + + if skipVerify == "true" { + t.skipVerify = true + } + + return t, nil +} + +// NewTLSDialer returns a tls proxy dialer. +func NewTLSDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + p, err := NewTLS(s, d, nil) + if err != nil { + return nil, err + } + + p.tlsConfig = &stdtls.Config{ + ServerName: p.serverName, + InsecureSkipVerify: p.skipVerify, + ClientSessionCache: stdtls.NewLRUClientSessionCache(64), + MinVersion: stdtls.VersionTLS10, + } + + return p, err +} + +// NewTLSServer returns a tls transport layer before the real server. +func NewTLSServer(s string, p proxy.Proxy) (proxy.Server, error) { + transport := strings.Split(s, ",") + + // prepare transport listener + // TODO: check here + if len(transport) < 2 { + return nil, errors.New("[tls] malformd listener:" + s) + } + + t, err := NewTLS(transport[0], nil, p) + if err != nil { + return nil, err + } + + cert, err := stdtls.LoadX509KeyPair(t.certFile, t.keyFile) + if err != nil { + log.F("[tls] unable to load cert: %s, key %s", t.certFile, t.keyFile) + return nil, err + } + + t.tlsConfig = &stdtls.Config{ + Certificates: []stdtls.Certificate{cert}, + MinVersion: stdtls.VersionTLS10, + } + + t.server, err = proxy.ServerFromURL(transport[1], p) + if err != nil { + return nil, err + } + + return t, nil +} + +// ListenAndServe listens on server's addr and serves connections. +func (s *TLS) ListenAndServe() { + l, err := net.Listen("tcp", s.addr) + if err != nil { + log.F("[tls] failed to listen on %s: %v", s.addr, err) + return + } + defer l.Close() + + log.F("[tls] listening TCP on %s with TLS", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[tls] failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +// Serve serves a connection. +func (s *TLS) Serve(c net.Conn) { + // we know the internal server will close the connection after serve + // defer c.Close() + + if s.server != nil { + cc := stdtls.Server(c, s.tlsConfig) + s.server.Serve(cc) + } +} + +// Addr returns forwarder's address. +func (s *TLS) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *TLS) Dial(network, addr string) (net.Conn, error) { + cc, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + log.F("[tls] dial to %s error: %s", s.addr, err) + return nil, err + } + + c := stdtls.Client(cc, s.tlsConfig) + err = c.Handshake() + return c, err +} + +// DialUDP connects to the given address via the proxy. +func (s *TLS) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("tls client does not support udp now") +} diff --git a/proxy/tproxy/tproxy_linux.go b/proxy/tproxy/tproxy_linux.go new file mode 100644 index 0000000..6c3149e --- /dev/null +++ b/proxy/tproxy/tproxy_linux.go @@ -0,0 +1,177 @@ +// ref: https://www.kernel.org/doc/Documentation/networking/tproxy.txt +// @LiamHaworth: https://github.com/LiamHaworth/go-tproxy/blob/master/tproxy_udp.go + +package tproxy + +import ( + "bytes" + "encoding/binary" + "fmt" + "net" + "net/url" + "strconv" + "syscall" + "unsafe" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// TProxy struct. +type TProxy struct { + proxy proxy.Proxy + addr string +} + +func init() { + proxy.RegisterServer("tproxy", NewTProxyServer) +} + +// NewTProxy returns a tproxy. +func NewTProxy(s string, p proxy.Proxy) (*TProxy, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + + tp := &TProxy{ + proxy: p, + addr: addr, + } + + return tp, nil +} + +// NewTProxyServer returns a udp tunnel server. +func NewTProxyServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewTProxy(s, p) +} + +// ListenAndServe listens on server's addr and serves connections. +func (s *TProxy) ListenAndServe() { + // go s.ListenAndServeTCP() + s.ListenAndServeUDP() +} + +// ListenAndServeTCP . +func (s *TProxy) ListenAndServeTCP() { + log.F("[tproxy] tcp mode not supported now, please use 'redir' instead") +} + +// ListenAndServeUDP . +func (s *TProxy) ListenAndServeUDP() { + laddr, err := net.ResolveUDPAddr("udp", s.addr) + if err != nil { + log.F("[tproxy] failed to resolve addr %s: %v", s.addr, err) + return + } + + lc, err := net.ListenUDP("udp", laddr) + if err != nil { + log.F("[tproxy] failed to listen on %s: %v", s.addr, err) + return + } + + fd, err := lc.File() + if err != nil { + log.F("[tproxy] failed to get file descriptor: %v", err) + return + } + defer fd.Close() + + fileDescriptor := int(fd.Fd()) + if err = syscall.SetsockoptInt(fileDescriptor, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { + syscall.Close(fileDescriptor) + log.F("[tproxy] failed to set socket option IP_TRANSPARENT: %v", err) + return + } + + if err = syscall.SetsockoptInt(fileDescriptor, syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1); err != nil { + syscall.Close(fileDescriptor) + log.F("[tproxy] failed to set socket option IP_RECVORIGDSTADDR: %v", err) + return + } + + for { + buf := make([]byte, 1024) + _, srcAddr, dstAddr, err := ReadFromUDP(lc, buf) + if err != nil { + if netErr, ok := err.(net.Error); ok && netErr.Temporary() { + log.F("[tproxy] temporary reading data error: %s", netErr) + continue + } + + log.F("[tproxy] Unrecoverable error while reading data: %s", err) + continue + } + + log.F("[tproxy] Accepting UDP connection from %s with destination of %s", srcAddr.String(), dstAddr.String()) + + } + +} + +// Serve . +func (s *TProxy) Serve(c net.Conn) { + log.F("[tproxy] func Serve: can not be called directly") +} + +// ReadFromUDP reads a UDP packet from c, copying the payload into b. +// It returns the number of bytes copied into b and the return address +// that was on the packet. +// +// Out-of-band data is also read in so that the original destination +// address can be identified and parsed. +func ReadFromUDP(conn *net.UDPConn, b []byte) (int, *net.UDPAddr, *net.UDPAddr, error) { + oob := make([]byte, 1024) + n, oobn, _, addr, err := conn.ReadMsgUDP(b, oob) + if err != nil { + return 0, nil, nil, err + } + + msgs, err := syscall.ParseSocketControlMessage(oob[:oobn]) + if err != nil { + return 0, nil, nil, fmt.Errorf("parsing socket control message: %s", err) + } + + var originalDst *net.UDPAddr + for _, msg := range msgs { + if msg.Header.Level == syscall.SOL_IP && msg.Header.Type == syscall.IP_RECVORIGDSTADDR { + originalDstRaw := &syscall.RawSockaddrInet4{} + if err = binary.Read(bytes.NewReader(msg.Data), binary.LittleEndian, originalDstRaw); err != nil { + return 0, nil, nil, fmt.Errorf("reading original destination address: %s", err) + } + + switch originalDstRaw.Family { + case syscall.AF_INET: + pp := (*syscall.RawSockaddrInet4)(unsafe.Pointer(originalDstRaw)) + p := (*[2]byte)(unsafe.Pointer(&pp.Port)) + originalDst = &net.UDPAddr{ + IP: net.IPv4(pp.Addr[0], pp.Addr[1], pp.Addr[2], pp.Addr[3]), + Port: int(p[0])<<8 + int(p[1]), + } + + case syscall.AF_INET6: + pp := (*syscall.RawSockaddrInet6)(unsafe.Pointer(originalDstRaw)) + p := (*[2]byte)(unsafe.Pointer(&pp.Port)) + originalDst = &net.UDPAddr{ + IP: net.IP(pp.Addr[:]), + Port: int(p[0])<<8 + int(p[1]), + Zone: strconv.Itoa(int(pp.Scope_id)), + } + + default: + return 0, nil, nil, fmt.Errorf("original destination is an unsupported network family") + } + } + } + + if originalDst == nil { + return 0, nil, nil, fmt.Errorf("unable to obtain original destination: %s", err) + } + + return n, addr, originalDst, nil +} diff --git a/proxy/udptun/udptun.go b/proxy/udptun/udptun.go new file mode 100644 index 0000000..d2d66a6 --- /dev/null +++ b/proxy/udptun/udptun.go @@ -0,0 +1,110 @@ +package udptun + +import ( + "net" + "net/url" + "strings" + "sync" + "time" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// UDPTun is a base udptun struct. +type UDPTun struct { + proxy proxy.Proxy + addr string + taddr string // tunnel addr string + tuaddr *net.UDPAddr // tunnel addr +} + +func init() { + proxy.RegisterServer("udptun", NewUDPTunServer) +} + +// NewUDPTun returns a UDPTun proxy. +func NewUDPTun(s string, p proxy.Proxy) (*UDPTun, error) { + u, err := url.Parse(s) + if err != nil { + log.F("[udptun] parse err: %s", err) + return nil, err + } + + addr := u.Host + d := strings.Split(addr, "=") + + ut := &UDPTun{ + proxy: p, + addr: d[0], + taddr: d[1], + } + + ut.tuaddr, err = net.ResolveUDPAddr("udp", ut.taddr) + return ut, err +} + +// NewUDPTunServer returns a udp tunnel server. +func NewUDPTunServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewUDPTun(s, p) +} + +// ListenAndServe listen and serves on the given address. +func (s *UDPTun) ListenAndServe() { + c, err := net.ListenPacket("udp", s.addr) + if err != nil { + log.F("[udptun] failed to listen on %s: %v", s.addr, err) + return + } + defer c.Close() + + log.F("[udptun] listening UDP on %s", s.addr) + + var nm sync.Map + buf := make([]byte, conn.UDPBufSize) + + for { + n, raddr, err := c.ReadFrom(buf) + if err != nil { + log.F("[udptun] read error: %v", err) + continue + } + + var pc net.PacketConn + + v, ok := nm.Load(raddr.String()) + if !ok && v == nil { + pc, _, err = s.proxy.DialUDP("udp", s.taddr) + if err != nil { + log.F("[udptun] remote dial error: %v", err) + continue + } + + nm.Store(raddr.String(), pc) + + go func() { + conn.RelayUDP(c, raddr, pc, 2*time.Minute) + pc.Close() + nm.Delete(raddr.String()) + }() + + } else { + pc = v.(net.PacketConn) + } + + _, err = pc.WriteTo(buf[:n], s.tuaddr) + if err != nil { + log.F("[udptun] remote write error: %v", err) + continue + } + + log.F("[udptun] %s <-> %s", raddr, s.taddr) + + } +} + +// Serve serves a net.Conn, can not be called directly. +func (s *UDPTun) Serve(c net.Conn) { + log.F("[udptun] func Serve: can not be called directly") +} diff --git a/proxy/unix/unix.go b/proxy/unix/unix.go new file mode 100644 index 0000000..c38fdce --- /dev/null +++ b/proxy/unix/unix.go @@ -0,0 +1,123 @@ +package unix + +import ( + "errors" + "net" + "net/url" + "os" + "strings" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// Unix domain socket struct. +type Unix struct { + dialer proxy.Dialer + proxy proxy.Proxy + addr string + + server proxy.Server +} + +func init() { + proxy.RegisterServer("unix", NewUnixServer) + proxy.RegisterDialer("unix", NewUnixDialer) +} + +// NewUnix returns unix fomain socket proxy. +func NewUnix(s string, d proxy.Dialer, p proxy.Proxy) (*Unix, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse url err: %s", err) + return nil, err + } + + unix := &Unix{ + dialer: d, + proxy: p, + addr: u.Path, + } + + return unix, nil +} + +// NewUnixDialer returns a unix domain socket dialer. +func NewUnixDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewUnix(s, d, nil) +} + +// NewUnixServer returns a unix domain socket server. +func NewUnixServer(s string, p proxy.Proxy) (proxy.Server, error) { + transport := strings.Split(s, ",") + + // prepare transport listener + // TODO: check here + if len(transport) < 2 { + return nil, errors.New("[unix] malformd listener:" + s) + } + + unix, err := NewUnix(transport[0], nil, p) + if err != nil { + return nil, err + } + + unix.server, err = proxy.ServerFromURL(transport[1], p) + if err != nil { + return nil, err + } + + return unix, nil +} + +// ListenAndServe serves requests. +func (s *Unix) ListenAndServe() { + os.Remove(s.addr) + l, err := net.Listen("unix", s.addr) + if err != nil { + log.F("[unix] failed to listen on %s: %v", s.addr, err) + return + } + defer l.Close() + + log.F("[unix] listening on %s", s.addr) + + for { + c, err := l.Accept() + if err != nil { + log.F("[unix] failed to accept: %v", err) + continue + } + + go s.Serve(c) + } +} + +// Serve serves requests. +func (s *Unix) Serve(c net.Conn) { + // we know the internal server will close the connection after serve + // defer c.Close() + + if s.server != nil { + s.server.Serve(c) + } +} + +// Addr returns forwarder's address. +func (s *Unix) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *Unix) Dial(network, addr string) (net.Conn, error) { + // NOTE: must be the first dialer in a chain + return net.Dial("unix", s.addr) +} + +// DialUDP connects to the given address via the proxy. +func (s *Unix) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("unix domain socket client does not support udp now") +} diff --git a/proxy/uottun/uottun.go b/proxy/uottun/uottun.go new file mode 100644 index 0000000..1f6be96 --- /dev/null +++ b/proxy/uottun/uottun.go @@ -0,0 +1,110 @@ +package uottun + +import ( + "io/ioutil" + "net" + "net/url" + "strings" + "time" + + "github.com/nadoo/glider/common/conn" + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// UoTTun is a base udp over tcp tunnel struct. +type UoTTun struct { + proxy proxy.Proxy + addr string + + raddr string +} + +func init() { + proxy.RegisterServer("uottun", NewUoTTunServer) +} + +// NewUoTTun returns a UoTTun proxy. +func NewUoTTun(s string, p proxy.Proxy) (*UoTTun, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse err: %s", err) + return nil, err + } + + addr := u.Host + d := strings.Split(addr, "=") + + ut := &UoTTun{ + proxy: p, + addr: d[0], + raddr: d[1], + } + + return ut, nil +} + +// NewUoTTunServer returns a uot tunnel server. +func NewUoTTunServer(s string, p proxy.Proxy) (proxy.Server, error) { + return NewUoTTun(s, p) +} + +// ListenAndServe listen and serve on tcp. +func (s *UoTTun) ListenAndServe() { + c, err := net.ListenPacket("udp", s.addr) + if err != nil { + log.F("[uottun] failed to listen on %s: %v", s.addr, err) + return + } + defer c.Close() + + log.F("[uottun] listening UDP on %s", s.addr) + + buf := make([]byte, conn.UDPBufSize) + + for { + n, clientAddr, err := c.ReadFrom(buf) + if err != nil { + log.F("[uottun] read error: %v", err) + continue + } + + rc, p, err := s.proxy.Dial("uot", s.raddr) + if err != nil { + log.F("[uottun] failed to connect to server %v: %v", s.raddr, err) + continue + } + + go func() { + // no remote forwarder, just a local udp forwarder + if urc, ok := rc.(*net.UDPConn); ok { + conn.RelayUDP(c, clientAddr, urc, 2*time.Minute) + urc.Close() + return + } + + // remote forwarder, udp over tcp + resp, err := ioutil.ReadAll(rc) + if err != nil { + log.F("error in ioutil.ReadAll: %s\n", err) + return + } + rc.Close() + c.WriteTo(resp, clientAddr) + }() + + _, err = rc.Write(buf[:n]) + if err != nil { + log.F("[uottun] remote write error: %v", err) + continue + } + + log.F("[uottun] %s <-> %s via %s", clientAddr, s.raddr, p) + } +} + +// Serve is not allowed to be called directly. +func (s *UoTTun) Serve(c net.Conn) { + // TODO + log.F("[uottun] func Serve: can not be called directly") +} diff --git a/proxy/vmess/addr.go b/proxy/vmess/addr.go new file mode 100644 index 0000000..dfc494d --- /dev/null +++ b/proxy/vmess/addr.go @@ -0,0 +1,61 @@ +package vmess + +import ( + "net" + "strconv" +) + +// Atyp is vmess addr type +type Atyp byte + +// Atyp +const ( + AtypErr Atyp = 0 + AtypIP4 Atyp = 1 + AtypDomain Atyp = 2 + AtypIP6 Atyp = 3 +) + +// Addr is vmess addr +type Addr []byte + +// Port is vmess addr port +type Port uint16 + +// ParseAddr parses the address in string s +func ParseAddr(s string) (Atyp, Addr, Port, error) { + var atyp Atyp + var addr Addr + + host, port, err := net.SplitHostPort(s) + if err != nil { + return 0, nil, 0, err + } + + if ip := net.ParseIP(host); ip != nil { + if ip4 := ip.To4(); ip4 != nil { + addr = make([]byte, net.IPv4len) + atyp = AtypIP4 + copy(addr[:], ip4) + } else { + addr = make([]byte, net.IPv6len) + atyp = AtypIP6 + copy(addr[:], ip) + } + } else { + if len(host) > 255 { + return 0, nil, 0, err + } + addr = make([]byte, 1+len(host)) + atyp = AtypDomain + addr[0] = byte(len(host)) + copy(addr[1:], host) + } + + portnum, err := strconv.ParseUint(port, 10, 16) + if err != nil { + return 0, nil, 0, err + } + + return atyp, addr, Port(portnum), err +} diff --git a/proxy/vmess/aead.go b/proxy/vmess/aead.go new file mode 100644 index 0000000..d0e334c --- /dev/null +++ b/proxy/vmess/aead.go @@ -0,0 +1,136 @@ +package vmess + +import ( + "bytes" + "crypto/cipher" + "encoding/binary" + "io" +) + +type aeadWriter struct { + io.Writer + cipher.AEAD + nonce []byte + buf []byte + count uint16 + iv []byte +} + +// AEADWriter returns a aead writer +func AEADWriter(w io.Writer, aead cipher.AEAD, iv []byte) io.Writer { + return &aeadWriter{ + Writer: w, + AEAD: aead, + buf: make([]byte, lenSize+maxChunkSize), + nonce: make([]byte, aead.NonceSize()), + count: 0, + iv: iv, + } +} + +func (w *aeadWriter) Write(b []byte) (int, error) { + n, err := w.ReadFrom(bytes.NewBuffer(b)) + return int(n), err +} + +func (w *aeadWriter) ReadFrom(r io.Reader) (n int64, err error) { + for { + buf := w.buf + payloadBuf := buf[lenSize : lenSize+defaultChunkSize-w.Overhead()] + + nr, er := r.Read(payloadBuf) + if nr > 0 { + n += int64(nr) + buf = buf[:lenSize+nr+w.Overhead()] + payloadBuf = payloadBuf[:nr] + binary.BigEndian.PutUint16(buf[:lenSize], uint16(nr+w.Overhead())) + + binary.BigEndian.PutUint16(w.nonce[:2], w.count) + copy(w.nonce[2:], w.iv[2:12]) + + w.Seal(payloadBuf[:0], w.nonce, payloadBuf, nil) + w.count++ + + _, ew := w.Writer.Write(buf) + if ew != nil { + err = ew + break + } + } + + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + break + } + } + + return n, err +} + +type aeadReader struct { + io.Reader + cipher.AEAD + nonce []byte + buf []byte + leftover []byte + count uint16 + iv []byte +} + +// AEADReader returns a aead reader +func AEADReader(r io.Reader, aead cipher.AEAD, iv []byte) io.Reader { + return &aeadReader{ + Reader: r, + AEAD: aead, + buf: make([]byte, lenSize+maxChunkSize), + nonce: make([]byte, aead.NonceSize()), + count: 0, + iv: iv, + } +} + +func (r *aeadReader) Read(b []byte) (int, error) { + if len(r.leftover) > 0 { + n := copy(b, r.leftover) + r.leftover = r.leftover[n:] + return n, nil + } + + // get length + _, err := io.ReadFull(r.Reader, r.buf[:lenSize]) + if err != nil { + return 0, err + } + + // if length == 0, then this is the end + l := binary.BigEndian.Uint16(r.buf[:lenSize]) + if l == 0 { + return 0, nil + } + + // get payload + buf := r.buf[:l] + _, err = io.ReadFull(r.Reader, buf) + if err != nil { + return 0, err + } + + binary.BigEndian.PutUint16(r.nonce[:2], r.count) + copy(r.nonce[2:], r.iv[2:12]) + + _, err = r.Open(buf[:0], r.nonce, buf, nil) + r.count++ + if err != nil { + return 0, err + } + + dataLen := int(l) - r.Overhead() + m := copy(b, r.buf[:dataLen]) + if m < int(dataLen) { + r.leftover = r.buf[m:dataLen] + } + + return m, err +} diff --git a/proxy/vmess/chunk.go b/proxy/vmess/chunk.go new file mode 100644 index 0000000..6acd145 --- /dev/null +++ b/proxy/vmess/chunk.go @@ -0,0 +1,104 @@ +package vmess + +import ( + "bytes" + "encoding/binary" + "io" +) + +const ( + lenSize = 2 + maxChunkSize = 1 << 14 // 16384 + defaultChunkSize = 1 << 13 // 8192 +) + +type chunkedWriter struct { + io.Writer + buf []byte +} + +// ChunkedWriter returns a chunked writer +func ChunkedWriter(w io.Writer) io.Writer { + return &chunkedWriter{ + Writer: w, + buf: make([]byte, lenSize+maxChunkSize), + } +} + +func (w *chunkedWriter) Write(b []byte) (int, error) { + n, err := w.ReadFrom(bytes.NewBuffer(b)) + return int(n), err +} + +func (w *chunkedWriter) ReadFrom(r io.Reader) (n int64, err error) { + for { + buf := w.buf + payloadBuf := buf[lenSize : lenSize+defaultChunkSize] + + nr, er := r.Read(payloadBuf) + if nr > 0 { + n += int64(nr) + buf = buf[:lenSize+nr] + payloadBuf = payloadBuf[:nr] + binary.BigEndian.PutUint16(buf[:lenSize], uint16(nr)) + + _, ew := w.Writer.Write(buf) + if ew != nil { + err = ew + break + } + } + + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + break + } + } + + return n, err +} + +type chunkedReader struct { + io.Reader + buf []byte + leftBytes int +} + +// ChunkedReader returns a chunked reader +func ChunkedReader(r io.Reader) io.Reader { + return &chunkedReader{ + Reader: r, + buf: make([]byte, lenSize), // NOTE: buf only used to save header bytes now + } +} + +func (r *chunkedReader) Read(b []byte) (int, error) { + if r.leftBytes == 0 { + // get length + _, err := io.ReadFull(r.Reader, r.buf[:lenSize]) + if err != nil { + return 0, err + } + r.leftBytes = int(binary.BigEndian.Uint16(r.buf[:lenSize])) + + // if length == 0, then this is the end + if r.leftBytes == 0 { + return 0, nil + } + } + + readLen := len(b) + if readLen > r.leftBytes { + readLen = r.leftBytes + } + + m, err := r.Reader.Read(b[:readLen]) + if err != nil { + return 0, err + } + + r.leftBytes -= m + return m, err +} diff --git a/proxy/vmess/client.go b/proxy/vmess/client.go new file mode 100644 index 0000000..0901a9b --- /dev/null +++ b/proxy/vmess/client.go @@ -0,0 +1,308 @@ +package vmess + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/md5" + "encoding/binary" + "errors" + "hash/fnv" + "io" + "math/rand" + "net" + "strings" + "time" + + "golang.org/x/crypto/chacha20poly1305" +) + +// Request Options +const ( + OptBasicFormat byte = 0 + OptChunkStream byte = 1 + // OptReuseTCPConnection byte = 2 + // OptMetadataObfuscate byte = 4 +) + +// Security types +const ( + SecurityAES128GCM byte = 3 + SecurityChacha20Poly1305 byte = 4 + SecurityNone byte = 5 +) + +// CMD types +const ( + CmdTCP byte = 1 + CmdUDP byte = 2 +) + +// Client vmess client +type Client struct { + users []*User + count int + opt byte + security byte +} + +// Conn is a connection to vmess server +type Conn struct { + user *User + opt byte + security byte + + atyp Atyp + addr Addr + port Port + + reqBodyIV [16]byte + reqBodyKey [16]byte + reqRespV byte + respBodyIV [16]byte + respBodyKey [16]byte + + net.Conn + dataReader io.Reader + dataWriter io.Writer +} + +// NewClient . +func NewClient(uuidStr, security string, alterID int) (*Client, error) { + uuid, err := StrToUUID(uuidStr) + if err != nil { + return nil, err + } + + c := &Client{} + user := NewUser(uuid) + c.users = append(c.users, user) + c.users = append(c.users, user.GenAlterIDUsers(alterID)...) + c.count = len(c.users) + + c.opt = OptChunkStream + + security = strings.ToLower(security) + switch security { + case "aes-128-gcm": + c.security = SecurityAES128GCM + case "chacha20-poly1305": + c.security = SecurityChacha20Poly1305 + case "none": + c.security = SecurityNone + case "": + // NOTE: use basic format when no method specified + c.opt = OptBasicFormat + c.security = SecurityNone + default: + return nil, errors.New("unknown security type: " + security) + } + + // NOTE: give rand a new seed to avoid the same sequence of values + rand.Seed(time.Now().UnixNano()) + + return c, nil +} + +// NewConn . +func (c *Client) NewConn(rc net.Conn, target string) (*Conn, error) { + r := rand.Intn(c.count) + conn := &Conn{user: c.users[r], opt: c.opt, security: c.security} + + var err error + conn.atyp, conn.addr, conn.port, err = ParseAddr(target) + if err != nil { + return nil, err + } + + randBytes := make([]byte, 33) + rand.Read(randBytes) + + copy(conn.reqBodyIV[:], randBytes[:16]) + copy(conn.reqBodyKey[:], randBytes[16:32]) + conn.reqRespV = randBytes[32] + + conn.respBodyIV = md5.Sum(conn.reqBodyIV[:]) + conn.respBodyKey = md5.Sum(conn.reqBodyKey[:]) + + // AuthInfo + _, err = rc.Write(conn.EncodeAuthInfo()) + if err != nil { + return nil, err + } + // Request + req, err := conn.EncodeRequest() + if err != nil { + return nil, err + } + + _, err = rc.Write(req) + if err != nil { + return nil, err + } + + conn.Conn = rc + + return conn, nil +} + +// EncodeAuthInfo returns HMAC("md5", UUID, UTC) result +func (c *Conn) EncodeAuthInfo() []byte { + ts := make([]byte, 8) + binary.BigEndian.PutUint64(ts, uint64(time.Now().UTC().Unix())) + + h := hmac.New(md5.New, c.user.UUID[:]) + h.Write(ts) + + return h.Sum(nil) +} + +// EncodeRequest encodes requests to network bytes +func (c *Conn) EncodeRequest() ([]byte, error) { + buf := new(bytes.Buffer) + + // Request + buf.WriteByte(1) // Ver + buf.Write(c.reqBodyIV[:]) // IV + buf.Write(c.reqBodyKey[:]) // Key + buf.WriteByte(c.reqRespV) // V + buf.WriteByte(c.opt) // Opt + + // pLen and Sec + paddingLen := rand.Intn(16) + pSec := byte(paddingLen<<4) | c.security // P(4bit) and Sec(4bit) + buf.WriteByte(pSec) + + buf.WriteByte(0) // reserved + buf.WriteByte(CmdTCP) // cmd + + // target + err := binary.Write(buf, binary.BigEndian, uint16(c.port)) // port + if err != nil { + return nil, err + } + + buf.WriteByte(byte(c.atyp)) // atyp + buf.Write(c.addr) // addr + + // padding + if paddingLen > 0 { + padding := make([]byte, paddingLen) + rand.Read(padding) + buf.Write(padding) + } + + // F + fnv1a := fnv.New32a() + _, err = fnv1a.Write(buf.Bytes()) + if err != nil { + return nil, err + } + buf.Write(fnv1a.Sum(nil)) + + block, err := aes.NewCipher(c.user.CmdKey[:]) + if err != nil { + return nil, err + } + + stream := cipher.NewCFBEncrypter(block, TimestampHash(time.Now().UTC())) + stream.XORKeyStream(buf.Bytes(), buf.Bytes()) + + return buf.Bytes(), nil +} + +// DecodeRespHeader . +func (c *Conn) DecodeRespHeader() error { + block, err := aes.NewCipher(c.respBodyKey[:]) + if err != nil { + return err + } + + stream := cipher.NewCFBDecrypter(block, c.respBodyIV[:]) + + buf := make([]byte, 4) + _, err = io.ReadFull(c.Conn, buf) + if err != nil { + return err + } + + stream.XORKeyStream(buf, buf) + + if buf[0] != c.reqRespV { + return errors.New("unexpected response header") + } + + // TODO: Dynamic port support + if buf[2] != 0 { + // dataLen := int32(buf[3]) + return errors.New("dynamic port is not supported now") + } + + return nil +} + +func (c *Conn) Write(b []byte) (n int, err error) { + if c.dataWriter != nil { + return c.dataWriter.Write(b) + } + + c.dataWriter = c.Conn + if c.opt&OptChunkStream == OptChunkStream { + switch c.security { + case SecurityNone: + c.dataWriter = ChunkedWriter(c.Conn) + + case SecurityAES128GCM: + block, _ := aes.NewCipher(c.reqBodyKey[:]) + aead, _ := cipher.NewGCM(block) + c.dataWriter = AEADWriter(c.Conn, aead, c.reqBodyIV[:]) + + case SecurityChacha20Poly1305: + key := make([]byte, 32) + t := md5.Sum(c.reqBodyKey[:]) + copy(key, t[:]) + t = md5.Sum(key[:16]) + copy(key[16:], t[:]) + aead, _ := chacha20poly1305.New(key) + c.dataWriter = AEADWriter(c.Conn, aead, c.reqBodyIV[:]) + } + } + + return c.dataWriter.Write(b) +} + +func (c *Conn) Read(b []byte) (n int, err error) { + if c.dataReader != nil { + return c.dataReader.Read(b) + } + + err = c.DecodeRespHeader() + if err != nil { + return 0, err + } + + c.dataReader = c.Conn + if c.opt&OptChunkStream == OptChunkStream { + switch c.security { + case SecurityNone: + c.dataReader = ChunkedReader(c.Conn) + + case SecurityAES128GCM: + block, _ := aes.NewCipher(c.respBodyKey[:]) + aead, _ := cipher.NewGCM(block) + c.dataReader = AEADReader(c.Conn, aead, c.respBodyIV[:]) + + case SecurityChacha20Poly1305: + key := make([]byte, 32) + t := md5.Sum(c.respBodyKey[:]) + copy(key, t[:]) + t = md5.Sum(key[:16]) + copy(key[16:], t[:]) + aead, _ := chacha20poly1305.New(key) + c.dataReader = AEADReader(c.Conn, aead, c.respBodyIV[:]) + } + } + + return c.dataReader.Read(b) +} diff --git a/proxy/vmess/user.go b/proxy/vmess/user.go new file mode 100644 index 0000000..ca2d6a3 --- /dev/null +++ b/proxy/vmess/user.go @@ -0,0 +1,85 @@ +package vmess + +import ( + "bytes" + "crypto/md5" + "encoding/binary" + "encoding/hex" + "errors" + "strings" + "time" +) + +// User of vmess client +type User struct { + UUID [16]byte + CmdKey [16]byte +} + +// NewUser . +func NewUser(uuid [16]byte) *User { + u := &User{UUID: uuid} + copy(u.CmdKey[:], GetKey(uuid)) + return u +} + +func nextID(oldID [16]byte) (newID [16]byte) { + md5hash := md5.New() + md5hash.Write(oldID[:]) + md5hash.Write([]byte("16167dc8-16b6-4e6d-b8bb-65dd68113a81")) + for { + md5hash.Sum(newID[:0]) + if !bytes.Equal(oldID[:], newID[:]) { + return + } + md5hash.Write([]byte("533eff8a-4113-4b10-b5ce-0f5d76b98cd2")) + } +} + +// GenAlterIDUsers generates users according to primary user's id and alterID +func (u *User) GenAlterIDUsers(alterID int) []*User { + users := make([]*User, alterID) + preID := u.UUID + for i := 0; i < alterID; i++ { + newID := nextID(preID) + // NOTE: alterID user is a user which have a different uuid but a same cmdkey with the primary user. + users[i] = &User{UUID: newID, CmdKey: u.CmdKey} + preID = newID + } + + return users +} + +// StrToUUID converts string to uuid. +// s fomat: "6ba7b810-9dad-11d1-80b4-00c04fd430c8" +func StrToUUID(s string) (uuid [16]byte, err error) { + b := []byte(strings.Replace(s, "-", "", -1)) + if len(b) != 32 { + return uuid, errors.New("invalid UUID: " + s) + } + _, err = hex.Decode(uuid[:], b) + return +} + +// GetKey returns the key of AES-128-CFB encrypter +// Key:MD5(UUID + []byte('c48619fe-8f02-49e0-b9e9-edf763e17e21')) +func GetKey(uuid [16]byte) []byte { + md5hash := md5.New() + md5hash.Write(uuid[:]) + md5hash.Write([]byte("c48619fe-8f02-49e0-b9e9-edf763e17e21")) + return md5hash.Sum(nil) +} + +// TimestampHash returns the iv of AES-128-CFB encrypter +// IV:MD5(X + X + X + X),X = []byte(timestamp.now) (8 bytes, Big Endian) +func TimestampHash(t time.Time) []byte { + md5hash := md5.New() + + ts := make([]byte, 8) + binary.BigEndian.PutUint64(ts, uint64(t.UTC().Unix())) + md5hash.Write(ts) + md5hash.Write(ts) + md5hash.Write(ts) + md5hash.Write(ts) + return md5hash.Sum(nil) +} diff --git a/proxy/vmess/vmess.go b/proxy/vmess/vmess.go new file mode 100644 index 0000000..c3d6c93 --- /dev/null +++ b/proxy/vmess/vmess.go @@ -0,0 +1,102 @@ +package vmess + +import ( + "errors" + "net" + "net/url" + "strconv" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// VMess struct. +type VMess struct { + dialer proxy.Dialer + addr string + + uuid string + alterID int + security string + + client *Client +} + +func init() { + proxy.RegisterDialer("vmess", NewVMessDialer) +} + +// NewVMess returns a vmess proxy. +func NewVMess(s string, d proxy.Dialer) (*VMess, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse url err: %s", err) + return nil, err + } + + addr := u.Host + security := u.User.Username() + uuid, ok := u.User.Password() + if !ok { + // no security type specified, vmess://uuid@server + uuid = security + security = "" + } + + query := u.Query() + aid := query.Get("alterID") + if aid == "" { + aid = "0" + } + + alterID, err := strconv.ParseUint(aid, 10, 32) + if err != nil { + log.F("parse alterID err: %s", err) + return nil, err + } + + client, err := NewClient(uuid, security, int(alterID)) + if err != nil { + log.F("create vmess client err: %s", err) + return nil, err + } + + p := &VMess{ + dialer: d, + addr: addr, + uuid: uuid, + alterID: int(alterID), + security: security, + client: client, + } + + return p, nil +} + +// NewVMessDialer returns a vmess proxy dialer. +func NewVMessDialer(s string, dialer proxy.Dialer) (proxy.Dialer, error) { + return NewVMess(s, dialer) +} + +// Addr returns forwarder's address. +func (s *VMess) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *VMess) Dial(network, addr string) (net.Conn, error) { + rc, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + return nil, err + } + + return s.client.NewConn(rc, addr) +} + +// DialUDP connects to the given address via the proxy. +func (s *VMess) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("vmess client does not support udp now") +} diff --git a/proxy/ws/client.go b/proxy/ws/client.go new file mode 100644 index 0000000..291ccc9 --- /dev/null +++ b/proxy/ws/client.go @@ -0,0 +1,128 @@ +package ws + +import ( + "bufio" + "bytes" + "crypto/rand" + "crypto/sha1" + "encoding/base64" + "errors" + "io" + "net" + "net/textproto" + "strings" +) + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +// Client is ws client struct. +type Client struct { + host string + path string +} + +// Conn is a connection to ws server. +type Conn struct { + net.Conn + reader io.Reader + writer io.Writer +} + +// NewClient creates a new ws client. +func NewClient(host, path string) (*Client, error) { + if path == "" { + path = "/" + } + c := &Client{host: host, path: path} + return c, nil +} + +// NewConn creates a new ws client connection. +func (c *Client) NewConn(rc net.Conn, target string) (*Conn, error) { + conn := &Conn{Conn: rc} + return conn, conn.Handshake(c.host, c.path) +} + +// Handshake handshakes with the server using HTTP to request a protocol upgrade. +func (c *Conn) Handshake(host, path string) error { + clientKey := generateClientKey() + + var buf bytes.Buffer + buf.WriteString("GET " + path + " HTTP/1.1\r\n") + buf.WriteString("Host: " + host + "\r\n") + buf.WriteString("Upgrade: websocket\r\n") + buf.WriteString("Connection: Upgrade\r\n") + buf.WriteString("Origin: http://" + host + "\r\n") + buf.WriteString("Sec-WebSocket-Key: " + clientKey + "\r\n") + buf.WriteString("Sec-WebSocket-Protocol: binary\r\n") + buf.WriteString("Sec-WebSocket-Version: 13\r\n") + buf.WriteString(("\r\n")) + + if _, err := c.Conn.Write(buf.Bytes()); err != nil { + return err + } + + tpr := textproto.NewReader(bufio.NewReader(c.Conn)) + line, err := tpr.ReadLine() + if err != nil { + return err + } + + _, code, _, ok := parseFirstLine(line) + if !ok || code != "101" { + return errors.New("[ws] error in ws handshake parseFirstLine") + } + + respHeader, err := tpr.ReadMIMEHeader() + if err != nil { + return err + } + + serverKey := respHeader.Get("Sec-WebSocket-Accept") + if serverKey != computeServerKey(clientKey) { + return errors.New("[ws] error in ws handshake, got wrong Sec-Websocket-Key") + } + + return nil +} + +func (c *Conn) Write(b []byte) (n int, err error) { + if c.writer == nil { + c.writer = FrameWriter(c.Conn) + } + + return c.writer.Write(b) +} + +func (c *Conn) Read(b []byte) (n int, err error) { + if c.reader == nil { + c.reader = FrameReader(c.Conn) + } + + return c.reader.Read(b) +} + +// parseFirstLine parses "GET /foo HTTP/1.1" OR "HTTP/1.1 200 OK" into its three parts. +// TODO: move to separate http lib package for reuse(also for http proxy module) +func parseFirstLine(line string) (r1, r2, r3 string, ok bool) { + s1 := strings.Index(line, " ") + s2 := strings.Index(line[s1+1:], " ") + if s1 < 0 || s2 < 0 { + return + } + s2 += s1 + 1 + return line[:s1], line[s1+1 : s2], line[s2+1:], true +} + +func generateClientKey() string { + p := make([]byte, 16) + rand.Read(p) + return base64.StdEncoding.EncodeToString(p) +} + +func computeServerKey(clientKey string) string { + h := sha1.New() + h.Write([]byte(clientKey)) + h.Write(keyGUID) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/proxy/ws/frame.go b/proxy/ws/frame.go new file mode 100644 index 0000000..7682cf8 --- /dev/null +++ b/proxy/ws/frame.go @@ -0,0 +1,181 @@ +// https://tools.ietf.org/html/rfc6455#section-5.2 +// +// Frame Format +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-------+-+-------------+-------------------------------+ +// |F|R|R|R| opcode|M| Payload len | Extended payload length | +// |I|S|S|S| (4) |A| (7) | (16/64) | +// |N|V|V|V| |S| | (if payload len==126/127) | +// | |1|2|3| |K| | | +// +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + +// | Extended payload length continued, if payload len == 127 | +// + - - - - - - - - - - - - - - - +-------------------------------+ +// | |Masking-key, if MASK set to 1 | +// +-------------------------------+-------------------------------+ +// | Masking-key (continued) | Payload Data | +// +-------------------------------- - - - - - - - - - - - - - - - + +// : Payload Data continued ... : +// + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// | Payload Data continued ... | +// +---------------------------------------------------------------+ + +package ws + +import ( + "bytes" + "encoding/binary" + "io" + "math/rand" +) + +const ( + defaultFrameSize = 4096 + maxFrameHeaderSize = 2 + 8 + 4 // Fixed header + length + mask + maskKeyLen = 4 + + finalBit byte = 1 << 7 + maskBit byte = 1 << 7 + opCodeBinary byte = 2 +) + +type frameWriter struct { + io.Writer + buf []byte + maskKey []byte +} + +// FrameWriter returns a frame writer. +func FrameWriter(w io.Writer) io.Writer { + n := rand.Uint32() + return &frameWriter{ + Writer: w, + buf: make([]byte, maxFrameHeaderSize+defaultFrameSize), + maskKey: []byte{byte(n), byte(n >> 8), byte(n >> 16), byte(n >> 24)}, + } +} + +func (w *frameWriter) Write(b []byte) (int, error) { + n, err := w.ReadFrom(bytes.NewBuffer(b)) + return int(n), err +} + +func (w *frameWriter) ReadFrom(r io.Reader) (n int64, err error) { + for { + buf := w.buf + payloadBuf := buf[maxFrameHeaderSize:] + + nr, er := r.Read(payloadBuf) + if nr > 0 { + n += int64(nr) + buf[0] = finalBit | opCodeBinary + buf[1] = maskBit + + lengthFieldLen := 0 + switch { + case nr <= 125: + buf[1] |= byte(nr) + case nr < 65536: + buf[1] |= 126 + lengthFieldLen = 2 + binary.BigEndian.PutUint16(buf[2:2+lengthFieldLen], uint16(nr)) + default: + buf[1] |= 127 + lengthFieldLen = 8 + binary.BigEndian.PutUint64(buf[2:2+lengthFieldLen], uint64(nr)) + } + + // header and length + _, ew := w.Writer.Write(buf[:2+lengthFieldLen]) + if ew != nil { + err = ew + break + } + + // maskkey + _, ew = w.Writer.Write(w.maskKey) + if ew != nil { + err = ew + break + } + + // payload + payloadBuf = payloadBuf[:nr] + for i := range payloadBuf { + payloadBuf[i] = payloadBuf[i] ^ w.maskKey[i%4] + } + + _, ew = w.Writer.Write(payloadBuf) + if ew != nil { + err = ew + break + } + } + + if er != nil { + if er != io.EOF { // ignore EOF as per io.ReaderFrom contract + err = er + } + break + } + + } + + return n, err +} + +type frameReader struct { + io.Reader + buf []byte + leftBytes int64 +} + +// FrameReader returns a chunked reader. +func FrameReader(r io.Reader) io.Reader { + return &frameReader{ + Reader: r, + buf: make([]byte, maxFrameHeaderSize), // NOTE: buf only used to save header bytes now + } +} + +func (r *frameReader) Read(b []byte) (int, error) { + if r.leftBytes == 0 { + // get msg header + _, err := io.ReadFull(r.Reader, r.buf[:2]) + if err != nil { + return 0, err + } + + // final := r.buf[0]&finalBit != 0 + // frameType := int(r.buf[0] & 0xf) + // mask := r.buf[1]&maskBit != 0 + r.leftBytes = int64(r.buf[1] & 0x7f) + switch r.leftBytes { + case 126: + _, err := io.ReadFull(r.Reader, r.buf[:2]) + if err != nil { + return 0, err + } + r.leftBytes = int64(binary.BigEndian.Uint16(r.buf[0:])) + case 127: + _, err := io.ReadFull(r.Reader, r.buf[:8]) + if err != nil { + return 0, err + } + r.leftBytes = int64(binary.BigEndian.Uint64(r.buf[0:])) + } + } + + readLen := int64(len(b)) + if readLen > r.leftBytes { + readLen = r.leftBytes + } + + m, err := r.Reader.Read(b[:readLen]) + if err != nil { + return 0, err + } + + r.leftBytes -= int64(m) + return m, err +} diff --git a/proxy/ws/ws.go b/proxy/ws/ws.go new file mode 100644 index 0000000..c0b98ce --- /dev/null +++ b/proxy/ws/ws.go @@ -0,0 +1,88 @@ +// Package ws implements a simple websocket client. +package ws + +import ( + "errors" + "net" + "net/url" + "strings" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// WS is the base ws proxy struct. +type WS struct { + dialer proxy.Dialer + addr string + + client *Client +} + +func init() { + proxy.RegisterDialer("ws", NewWSDialer) +} + +// NewWS returns a websocket proxy. +func NewWS(s string, d proxy.Dialer) (*WS, error) { + u, err := url.Parse(s) + if err != nil { + log.F("parse url err: %s", err) + return nil, err + } + + addr := u.Host + + // TODO: + if addr == "" { + addr = d.Addr() + } + + colonPos := strings.LastIndex(addr, ":") + if colonPos == -1 { + colonPos = len(addr) + } + serverName := addr[:colonPos] + + client, err := NewClient(serverName, u.Path) + if err != nil { + log.F("create ws client err: %s", err) + return nil, err + } + + p := &WS{ + dialer: d, + addr: addr, + client: client, + } + + return p, nil +} + +// NewWSDialer returns a ws proxy dialer. +func NewWSDialer(s string, d proxy.Dialer) (proxy.Dialer, error) { + return NewWS(s, d) +} + +// Addr returns forwarder's address. +func (s *WS) Addr() string { + if s.addr == "" { + return s.dialer.Addr() + } + return s.addr +} + +// Dial connects to the address addr on the network net via the proxy. +func (s *WS) Dial(network, addr string) (net.Conn, error) { + rc, err := s.dialer.Dial("tcp", s.addr) + if err != nil { + return nil, err + } + + return s.client.NewConn(rc, addr) +} + +// DialUDP connects to the given address via the proxy. +func (s *WS) DialUDP(network, addr string) (net.PacketConn, net.Addr, error) { + return nil, nil, errors.New("ws client does not support udp now") +} diff --git a/rule/config.go b/rule/config.go new file mode 100644 index 0000000..68efc92 --- /dev/null +++ b/rule/config.go @@ -0,0 +1,75 @@ +package rule + +import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/nadoo/conflag" + + "github.com/nadoo/glider/strategy" +) + +// Config , every rule dialer points to a rule file +type Config struct { + Name string + + Forward []string + StrategyConfig strategy.Config + + DNSServers []string + IPSet string + + Domain []string + IP []string + CIDR []string +} + +// NewConfFromFile . +func NewConfFromFile(ruleFile string) (*Config, error) { + p := &Config{Name: ruleFile} + + f := conflag.NewFromFile("rule", ruleFile) + f.StringSliceUniqVar(&p.Forward, "forward", nil, "forward url, format: SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS[,SCHEME://[USER|METHOD:PASSWORD@][HOST]:PORT?PARAMS]") + f.StringVar(&p.StrategyConfig.Strategy, "strategy", "rr", "forward strategy, default: rr") + f.StringVar(&p.StrategyConfig.CheckWebSite, "checkwebsite", "www.apple.com", "proxy check HTTP(NOT HTTPS) website address, format: HOST[:PORT], default port: 80") + f.IntVar(&p.StrategyConfig.CheckInterval, "checkinterval", 30, "proxy check interval(seconds)") + f.IntVar(&p.StrategyConfig.CheckTimeout, "checktimeout", 10, "proxy check timeout(seconds)") + f.StringVar(&p.StrategyConfig.IntFace, "interface", "", "source ip or source interface") + + f.StringSliceUniqVar(&p.DNSServers, "dnsserver", nil, "remote dns server") + f.StringVar(&p.IPSet, "ipset", "", "ipset name") + + f.StringSliceUniqVar(&p.Domain, "domain", nil, "domain") + f.StringSliceUniqVar(&p.IP, "ip", nil, "ip") + f.StringSliceUniqVar(&p.CIDR, "cidr", nil, "cidr") + + err := f.Parse() + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + return nil, err + } + + return p, err +} + +// ListDir returns file list named with suffix in dirPth +func ListDir(dirPth string, suffix string) (files []string, err error) { + files = make([]string, 0, 10) + dir, err := ioutil.ReadDir(dirPth) + if err != nil { + return nil, err + } + PthSep := string(os.PathSeparator) + suffix = strings.ToUpper(suffix) + for _, fi := range dir { + if fi.IsDir() { + continue + } + if strings.HasSuffix(strings.ToUpper(fi.Name()), suffix) { + files = append(files, dirPth+PthSep+fi.Name()) + } + } + return files, nil +} diff --git a/rule/rule.go b/rule/rule.go new file mode 100644 index 0000000..b8474f9 --- /dev/null +++ b/rule/rule.go @@ -0,0 +1,137 @@ +package rule + +import ( + "net" + "strings" + "sync" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" + "github.com/nadoo/glider/strategy" +) + +// Proxy struct +type Proxy struct { + proxy *strategy.Proxy + proxies []*strategy.Proxy + + domainMap sync.Map + ipMap sync.Map + cidrMap sync.Map +} + +// NewProxy returns a new rule proxy +func NewProxy(rules []*Config, proxy *strategy.Proxy) *Proxy { + rd := &Proxy{proxy: proxy} + + for _, r := range rules { + sd := strategy.NewProxy(r.Forward, &r.StrategyConfig) + rd.proxies = append(rd.proxies, sd) + + for _, domain := range r.Domain { + rd.domainMap.Store(strings.ToLower(domain), sd) + } + + for _, ip := range r.IP { + rd.ipMap.Store(ip, sd) + } + + for _, s := range r.CIDR { + if _, cidr, err := net.ParseCIDR(s); err == nil { + rd.cidrMap.Store(cidr, sd) + } + } + } + + return rd +} + +// Dial dials to targer addr and return a conn +func (p *Proxy) Dial(network, addr string) (net.Conn, string, error) { + return p.nextProxy(addr).Dial(network, addr) +} + +// DialUDP connects to the given address via the proxy +func (p *Proxy) DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) { + return p.nextProxy(addr).DialUDP(network, addr) +} + +// nextProxy return next proxy according to rule +func (p *Proxy) nextProxy(dstAddr string) *strategy.Proxy { + host, _, err := net.SplitHostPort(dstAddr) + if err != nil { + // TODO: check here + // logf("[rule] SplitHostPort ERROR: %s", err) + return p.proxy + } + + // find ip + if ip := net.ParseIP(host); ip != nil { + // check ip + if proxy, ok := p.ipMap.Load(ip.String()); ok { + return proxy.(*strategy.Proxy) + } + + var ret *strategy.Proxy + // check cidr + p.cidrMap.Range(func(key, value interface{}) bool { + cidr := key.(*net.IPNet) + if cidr.Contains(ip) { + ret = value.(*strategy.Proxy) + return false + } + + return true + }) + + if ret != nil { + return ret + } + + } + + domainParts := strings.Split(host, ".") + length := len(domainParts) + for i := length - 1; i >= 0; i-- { + domain := strings.Join(domainParts[i:length], ".") + + // find in domainMap + if proxy, ok := p.domainMap.Load(domain); ok { + return proxy.(*strategy.Proxy) + } + } + + return p.proxy +} + +// NextDialer return next dialer according to rule +func (p *Proxy) NextDialer(dstAddr string) proxy.Dialer { + return p.nextProxy(dstAddr).NextDialer(dstAddr) +} + +// AddDomainIP used to update ipMap rules according to domainMap rule +func (p *Proxy) AddDomainIP(domain, ip string) error { + if ip != "" { + domainParts := strings.Split(domain, ".") + length := len(domainParts) + for i := length - 1; i >= 0; i-- { + pDomain := strings.ToLower(strings.Join(domainParts[i:length], ".")) + + // find in domainMap + if dialer, ok := p.domainMap.Load(pDomain); ok { + p.ipMap.Store(ip, dialer) + log.F("[rule] add ip=%s, based on rule: domain=%s & domain/ip: %s/%s\n", ip, pDomain, domain, ip) + } + } + } + return nil +} + +// Check . +func (p *Proxy) Check() { + p.proxy.Check() + + for _, d := range p.proxies { + d.Check() + } +} diff --git a/strategy/forward.go b/strategy/forward.go new file mode 100644 index 0000000..81f4b32 --- /dev/null +++ b/strategy/forward.go @@ -0,0 +1,184 @@ +package strategy + +import ( + "net" + "net/url" + "strconv" + "strings" + "sync/atomic" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// StatusHandler function will be called when the forwarder's status changed +type StatusHandler func(*Forwarder) + +// Forwarder is a forwarder +type Forwarder struct { + proxy.Dialer + addr string + priority uint32 + maxFailures uint32 // maxfailures to set to Disabled + disabled uint32 + failures uint32 + latency int64 + intface string // local interface or ip address + handlers []StatusHandler +} + +// ForwarderFromURL parses `forward=` command value and returns a new forwarder +func ForwarderFromURL(s, intface string) (f *Forwarder, err error) { + f = &Forwarder{} + + ss := strings.Split(s, "#") + if len(ss) > 1 { + err = f.parseOption(ss[1]) + } + + iface := intface + if f.intface != "" && f.intface != intface { + iface = f.intface + } + + var d proxy.Dialer + d, err = proxy.NewDirect(iface) + if err != nil { + return nil, err + } + + for _, url := range strings.Split(ss[0], ",") { + d, err = proxy.DialerFromURL(url, d) + if err != nil { + return nil, err + } + } + + f.Dialer = d + f.addr = d.Addr() + + // set forwarder to disabled by default + f.Disable() + + return f, err +} + +// DirectForwarder returns a direct forwarder +func DirectForwarder(intface string) *Forwarder { + d, err := proxy.NewDirect(intface) + if err != nil { + return nil + } + + return &Forwarder{Dialer: d, addr: d.Addr()} +} + +func (f *Forwarder) parseOption(option string) error { + query, err := url.ParseQuery(option) + if err != nil { + return err + } + + var priority uint64 + p := query.Get("priority") + if p != "" { + priority, err = strconv.ParseUint(p, 10, 32) + } + f.SetPriority(uint32(priority)) + + f.intface = query.Get("interface") + + return err +} + +// Addr . +func (f *Forwarder) Addr() string { + return f.addr +} + +// Dial . +func (f *Forwarder) Dial(network, addr string) (c net.Conn, err error) { + c, err = f.Dialer.Dial(network, addr) + if err != nil { + f.IncFailures() + if f.Failures() >= f.MaxFailures() && f.Enabled() { + f.Disable() + log.F("[forwarder] %s reaches maxfailures.", f.addr) + } + } + + return c, err +} + +// Failures returns the failuer count of forwarder +func (f *Forwarder) Failures() uint32 { + return atomic.LoadUint32(&f.failures) +} + +// IncFailures increase the failuer count by 1 +func (f *Forwarder) IncFailures() { + atomic.AddUint32(&f.failures, 1) +} + +// AddHandler adds a custom handler to handle the status change event +func (f *Forwarder) AddHandler(h StatusHandler) { + f.handlers = append(f.handlers, h) +} + +// Enable the forwarder +func (f *Forwarder) Enable() { + if atomic.CompareAndSwapUint32(&f.disabled, 1, 0) { + for _, h := range f.handlers { + h(f) + } + } + atomic.StoreUint32(&f.failures, 0) +} + +// Disable the forwarder +func (f *Forwarder) Disable() { + if atomic.CompareAndSwapUint32(&f.disabled, 0, 1) { + for _, h := range f.handlers { + h(f) + } + } +} + +// Enabled returns the status of forwarder +func (f *Forwarder) Enabled() bool { + return !isTrue(atomic.LoadUint32(&f.disabled)) +} + +func isTrue(n uint32) bool { + return n&1 == 1 +} + +// Priority returns the priority of forwarder +func (f *Forwarder) Priority() uint32 { + return atomic.LoadUint32(&f.priority) +} + +// SetPriority sets the priority of forwarder +func (f *Forwarder) SetPriority(l uint32) { + atomic.StoreUint32(&f.priority, l) +} + +// MaxFailures returns the maxFailures of forwarder +func (f *Forwarder) MaxFailures() uint32 { + return atomic.LoadUint32(&f.maxFailures) +} + +// SetMaxFailures sets the maxFailures of forwarder +func (f *Forwarder) SetMaxFailures(l uint32) { + atomic.StoreUint32(&f.maxFailures, l) +} + +// Latency returns the latency of forwarder +func (f *Forwarder) Latency() int64 { + return atomic.LoadInt64(&f.latency) +} + +// SetLatency sets the latency of forwarder +func (f *Forwarder) SetLatency(l int64) { + atomic.StoreInt64(&f.latency, l) +} diff --git a/strategy/strategy.go b/strategy/strategy.go new file mode 100644 index 0000000..ba3e932 --- /dev/null +++ b/strategy/strategy.go @@ -0,0 +1,274 @@ +package strategy + +import ( + "bytes" + "hash/fnv" + "io" + "net" + "sort" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/nadoo/glider/common/log" + "github.com/nadoo/glider/proxy" +) + +// Config is strategy config struct. +type Config struct { + Strategy string + CheckWebSite string + CheckInterval int + CheckTimeout int + MaxFailures int + IntFace string +} + +// forwarder slice orderd by priority +type priSlice []*Forwarder + +func (p priSlice) Len() int { return len(p) } +func (p priSlice) Less(i, j int) bool { return p[i].Priority() > p[j].Priority() } +func (p priSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +// Proxy is base proxy struct. +type Proxy struct { + config *Config + fwdrs priSlice + available []*Forwarder + mu sync.RWMutex + index uint32 + priority uint32 + + nextForwarder func(addr string) *Forwarder +} + +// NewProxy returns a new strategy proxy. +func NewProxy(s []string, c *Config) *Proxy { + var fwdrs []*Forwarder + for _, chain := range s { + fwdr, err := ForwarderFromURL(chain, c.IntFace) + if err != nil { + log.Fatal(err) + } + fwdr.SetMaxFailures(uint32(c.MaxFailures)) + fwdrs = append(fwdrs, fwdr) + } + + if len(fwdrs) == 0 { + // direct forwarder + fwdrs = append(fwdrs, DirectForwarder(c.IntFace)) + c.Strategy = "rr" + } + + return newProxy(fwdrs, c) +} + +// newProxy returns a new rrProxy +func newProxy(fwdrs []*Forwarder, c *Config) *Proxy { + d := &Proxy{fwdrs: fwdrs, config: c} + sort.Sort(d.fwdrs) + + d.initAvailable() + + if strings.IndexByte(d.config.CheckWebSite, ':') == -1 { + d.config.CheckWebSite += ":80" + } + + switch c.Strategy { + case "rr": + d.nextForwarder = d.scheduleRR + log.F("[strategy] forward to remote servers in round robin mode.") + case "ha": + d.nextForwarder = d.scheduleHA + log.F("[strategy] forward to remote servers in high availability mode.") + case "lha": + d.nextForwarder = d.scheduleLHA + log.F("[strategy] forward to remote servers in latency based high availability mode.") + case "dh": + d.nextForwarder = d.scheduleDH + log.F("[strategy] forward to remote servers in destination hashing mode.") + default: + d.nextForwarder = d.scheduleRR + log.F("[strategy] not supported forward mode '%s', use round robin mode.", c.Strategy) + } + + for _, f := range fwdrs { + f.AddHandler(d.onStatusChanged) + } + + return d +} + +// Dial connects to the address addr on the network net. +func (p *Proxy) Dial(network, addr string) (net.Conn, string, error) { + nd := p.NextDialer(addr) + c, err := nd.Dial(network, addr) + return c, nd.Addr(), err +} + +// DialUDP connects to the given address. +func (p *Proxy) DialUDP(network, addr string) (pc net.PacketConn, writeTo net.Addr, err error) { + return p.NextDialer(addr).DialUDP(network, addr) +} + +// NextDialer returns the next dialer. +func (p *Proxy) NextDialer(dstAddr string) proxy.Dialer { + p.mu.RLock() + defer p.mu.RUnlock() + + return p.nextForwarder(dstAddr) +} + +// Priority returns the active priority of dialer. +func (p *Proxy) Priority() uint32 { return atomic.LoadUint32(&p.priority) } + +// SetPriority sets the active priority of daler. +func (p *Proxy) SetPriority(pri uint32) { atomic.StoreUint32(&p.priority, pri) } + +// initAvailable traverse d.fwdrs and init the available forwarder slice. +func (p *Proxy) initAvailable() { + for _, f := range p.fwdrs { + if f.Enabled() { + p.SetPriority(f.Priority()) + break + } + } + + p.available = nil + for _, f := range p.fwdrs { + if f.Enabled() && f.Priority() >= p.Priority() { + p.available = append(p.available, f) + } + } + + if len(p.available) == 0 { + // no available forwarders, set priority to 0 to check all forwarders in check func + p.SetPriority(0) + log.F("[strategy] no available forwarders, just use: %s, please check your settings or network", p.fwdrs[0].Addr()) + p.available = append(p.available, p.fwdrs[0]) + } +} + +// onStatusChanged will be called when fwdr's status changed. +func (p *Proxy) onStatusChanged(fwdr *Forwarder) { + p.mu.Lock() + defer p.mu.Unlock() + + if fwdr.Enabled() { + log.F("[strategy] %s changed status from Disabled to Enabled ", fwdr.Addr()) + if fwdr.Priority() == p.Priority() { + p.available = append(p.available, fwdr) + } else if fwdr.Priority() > p.Priority() { + p.initAvailable() + } + } else { + log.F("[strategy] %s changed status from Enabled to Disabled", fwdr.Addr()) + for i, f := range p.available { + if f == fwdr { + p.available[i], p.available = p.available[len(p.available)-1], p.available[:len(p.available)-1] + break + } + } + } + + if len(p.available) == 0 { + p.initAvailable() + } +} + +// Check implements the Checker interface. +func (p *Proxy) Check() { + // no need to check when there's only 1 forwarder + if len(p.fwdrs) > 1 { + for i := 0; i < len(p.fwdrs); i++ { + go p.check(i) + } + } +} + +func (p *Proxy) check(i int) { + f := p.fwdrs[i] + retry := 1 + buf := make([]byte, 4) + + for { + time.Sleep(time.Duration(p.config.CheckInterval) * time.Second * time.Duration(retry>>1)) + + // check all forwarders at least one time + if retry > 1 && f.Priority() < p.Priority() { + continue + } + + retry <<= 1 + if retry > 16 { + retry = 16 + } + + startTime := time.Now() + rc, err := f.Dial("tcp", p.config.CheckWebSite) + if err != nil { + f.Disable() + log.F("[check] %s(%d) -> %s, DISABLED. error in dial: %s", f.Addr(), f.Priority(), p.config.CheckWebSite, err) + continue + } + + rc.Write([]byte("GET / HTTP/1.0\r\n\r\n")) + + _, err = io.ReadFull(rc, buf) + if err != nil { + f.Disable() + log.F("[check] %s(%d) -> %s, DISABLED. error in read: %s", f.Addr(), f.Priority(), p.config.CheckWebSite, err) + } else if bytes.Equal([]byte("HTTP"), buf) { + + readTime := time.Since(startTime) + f.SetLatency(int64(readTime)) + + if readTime > time.Duration(p.config.CheckTimeout)*time.Second { + f.Disable() + log.F("[check] %s(%d) -> %s, DISABLED. check timeout: %s", f.Addr(), f.Priority(), p.config.CheckWebSite, readTime) + } else { + retry = 2 + f.Enable() + log.F("[check] %s(%d) -> %s, ENABLED. connect time: %s", f.Addr(), f.Priority(), p.config.CheckWebSite, readTime) + } + + } else { + f.Disable() + log.F("[check] %s(%d) -> %s, DISABLED. server response: %s", f.Addr(), f.Priority(), p.config.CheckWebSite, buf) + } + + rc.Close() + } +} + +// Round Robin +func (p *Proxy) scheduleRR(dstAddr string) *Forwarder { + return p.available[atomic.AddUint32(&p.index, 1)%uint32(len(p.available))] +} + +// High Availability +func (p *Proxy) scheduleHA(dstAddr string) *Forwarder { + return p.available[0] +} + +// Latency based High Availability +func (p *Proxy) scheduleLHA(dstAddr string) *Forwarder { + fwdr := p.available[0] + lowest := fwdr.Latency() + for _, f := range p.available { + if f.Latency() < lowest { + lowest = f.Latency() + fwdr = f + } + } + return fwdr +} + +// Destination Hashing +func (p *Proxy) scheduleDH(dstAddr string) *Forwarder { + fnv1a := fnv.New32a() + fnv1a.Write([]byte(dstAddr)) + return p.available[fnv1a.Sum32()%uint32(len(p.available))] +} diff --git a/systemd/README.md b/systemd/README.md new file mode 100644 index 0000000..f1ee49e --- /dev/null +++ b/systemd/README.md @@ -0,0 +1,34 @@ +## Service + +### Install + +#### 1. copy binary file + +```bash +cp glider /usr/bin/ +``` + +#### 2. add service file + +```bash +# copy service file to systemd +cp systemd/glider@.service /etc/systemd/system/ +``` + +#### 3. add config file: ***server***.conf + +```bash +# copy config file to /etc/glider/ +mkdir /etc/glider/ +cp ./config/glider.conf.example /etc/glider/server.conf +``` + +#### 4. enable and start service: glider@***server*** + +```bash +# enable and start service +systemctl enable glider@server +systemctl start glider@server +``` + +See [glider@.service](glider%40.service) diff --git a/systemd/glider@.service b/systemd/glider@.service new file mode 100644 index 0000000..ae6ef46 --- /dev/null +++ b/systemd/glider@.service @@ -0,0 +1,22 @@ +[Unit] +Description=Glider Service (%i) +After=network.target iptables.service ip6tables.service + +[Service] +Type=simple +User=nobody +Restart=always +LimitNOFILE=102400 + +# NOTE: change to your glider path +ExecStart=/usr/bin/glider -config /etc/glider/%i.conf + +# work with systemd v229 or later, so glider can listen on port below 1024 with none-root user +# CAP_NET_ADMIN: ipset +# CAP_NET_BIND_SERVICE: bind ports under 1024 +CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE +NoNewPrivileges=true + +[Install] +WantedBy=multi-user.target