Repository: p4gefau1t/trojan-go Branch: master Commit: 2dc60f52e79f Files: 205 Total size: 513.3 KB Directory structure: gitextract_d00n_ui_/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug-report.md │ ├── linters/ │ │ └── .golangci.yml │ └── workflows/ │ ├── docker-build.yml │ ├── docker-nightly-build.yml │ ├── gh-pages.yml │ ├── linter.yml │ ├── nightly-build.yml │ ├── release-build.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── api/ │ ├── api.go │ ├── control/ │ │ └── control.go │ └── service/ │ ├── api.pb.go │ ├── api.proto │ ├── api_grpc.pb.go │ ├── client.go │ ├── client_test.go │ ├── config.go │ ├── gen.sh │ ├── server.go │ └── server_test.go ├── common/ │ ├── common.go │ ├── error.go │ ├── geodata/ │ │ ├── cache.go │ │ ├── decode.go │ │ ├── decode_test.go │ │ ├── interface.go │ │ └── loader.go │ ├── io.go │ ├── io_test.go │ ├── net.go │ └── sync.go ├── component/ │ ├── api.go │ ├── base.go │ ├── client.go │ ├── custom.go │ ├── forward.go │ ├── mysql.go │ ├── nat.go │ ├── other.go │ └── server.go ├── config/ │ ├── config.go │ └── config_test.go ├── constant/ │ └── constant.go ├── docs/ │ ├── .gitignore │ ├── Makefile │ ├── archetypes/ │ │ └── default.md │ ├── config.toml │ └── content/ │ ├── _index.md │ ├── advance/ │ │ ├── _index.md │ │ ├── aead.md │ │ ├── api.md │ │ ├── customize-protocol-stack.md │ │ ├── forward.md │ │ ├── mux.md │ │ ├── nat.md │ │ ├── nginx-relay.md │ │ ├── plugin.md │ │ ├── router.md │ │ └── websocket.md │ ├── basic/ │ │ ├── _index.md │ │ ├── config.md │ │ ├── full-config.md │ │ └── trojan.md │ └── developer/ │ ├── _index.md │ ├── api.md │ ├── build.md │ ├── mux.md │ ├── overview.md │ ├── plugin.md │ ├── simplesocks.md │ ├── trojan.md │ ├── url.md │ └── websocket.md ├── easy/ │ └── easy.go ├── example/ │ ├── client.json │ ├── client.yaml │ ├── server.json │ ├── server.yaml │ ├── trojan-go.service │ └── trojan-go@.service ├── go.mod ├── go.sum ├── log/ │ ├── golog/ │ │ ├── buffer/ │ │ │ ├── buffer.go │ │ │ └── buffer_test.go │ │ ├── colorful/ │ │ │ ├── colorful.go │ │ │ └── colorful_test.go │ │ └── golog.go │ ├── log.go │ └── simplelog/ │ └── simplelog.go ├── main.go ├── option/ │ └── option.go ├── proxy/ │ ├── client/ │ │ ├── client.go │ │ └── config.go │ ├── config.go │ ├── custom/ │ │ ├── config.go │ │ └── custom.go │ ├── forward/ │ │ └── forward.go │ ├── nat/ │ │ ├── nat.go │ │ └── nat_stub.go │ ├── option.go │ ├── proxy.go │ ├── server/ │ │ ├── config.go │ │ └── server.go │ └── stack.go ├── redirector/ │ ├── redirector.go │ └── redirector_test.go ├── statistic/ │ ├── memory/ │ │ ├── config.go │ │ ├── memory.go │ │ └── memory_test.go │ ├── mysql/ │ │ ├── config.go │ │ └── mysql.go │ └── statistics.go ├── test/ │ ├── scenario/ │ │ ├── custom_test.go │ │ └── proxy_test.go │ └── util/ │ ├── target.go │ └── util.go ├── tunnel/ │ ├── adapter/ │ │ ├── config.go │ │ ├── server.go │ │ └── tunnel.go │ ├── dokodemo/ │ │ ├── config.go │ │ ├── conn.go │ │ ├── dokodemo_test.go │ │ ├── server.go │ │ └── tunnel.go │ ├── freedom/ │ │ ├── client.go │ │ ├── config.go │ │ ├── conn.go │ │ ├── freedom_test.go │ │ └── tunnel.go │ ├── http/ │ │ ├── http_test.go │ │ ├── server.go │ │ └── tunnel.go │ ├── metadata.go │ ├── mux/ │ │ ├── client.go │ │ ├── config.go │ │ ├── conn.go │ │ ├── mux_test.go │ │ ├── server.go │ │ └── tunnel.go │ ├── router/ │ │ ├── client.go │ │ ├── config.go │ │ ├── conn.go │ │ ├── data.go │ │ ├── router_test.go │ │ └── tunnel.go │ ├── shadowsocks/ │ │ ├── client.go │ │ ├── config.go │ │ ├── conn.go │ │ ├── server.go │ │ ├── shadowsocks_test.go │ │ └── tunnel.go │ ├── simplesocks/ │ │ ├── client.go │ │ ├── conn.go │ │ ├── server.go │ │ ├── simplesocks_test.go │ │ └── tunnel.go │ ├── socks/ │ │ ├── config.go │ │ ├── conn.go │ │ ├── server.go │ │ ├── socks_test.go │ │ └── tunnel.go │ ├── tls/ │ │ ├── client.go │ │ ├── config.go │ │ ├── fingerprint/ │ │ │ └── tls.go │ │ ├── server.go │ │ ├── tls_test.go │ │ └── tunnel.go │ ├── tproxy/ │ │ ├── config.go │ │ ├── conn.go │ │ ├── getsockopt.go │ │ ├── getsockopt_i386.go │ │ ├── server.go │ │ ├── tcp.go │ │ ├── tproxy_stub.go │ │ ├── tunnel.go │ │ └── udp.go │ ├── transport/ │ │ ├── client.go │ │ ├── config.go │ │ ├── conn.go │ │ ├── server.go │ │ ├── transport_test.go │ │ └── tunnel.go │ ├── trojan/ │ │ ├── client.go │ │ ├── config.go │ │ ├── packet.go │ │ ├── server.go │ │ ├── trojan_test.go │ │ └── tunnel.go │ ├── tunnel.go │ └── websocket/ │ ├── client.go │ ├── config.go │ ├── conn.go │ ├── server.go │ ├── tunnel.go │ └── websocket_test.go ├── url/ │ ├── option.go │ ├── option_test.go │ ├── share_link.go │ └── share_link_test.go └── version/ └── version.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.md ================================================ --- name: Bug 报告 about: 提交项目中存在的漏洞和问题 title: "[BUG]" labels: '' assignees: '' --- - [ ] **我确定我已经尝试多次复现此次问题,并且将会提供涉及此问题的系统和网络环境,软件及其版本。** 我们建议您按照下方模板填写 Bug Report,以便我们收集更多的有效信息 ## 简单描述这个 Bug ## 如何复现这个 Bug 在此描述复现这个Bug所需要的操作步骤 ## 服务器和客户端环境信息 在此描述你的服务器和客户端所处的网络环境,系统架构,以及其他信息 ## 服务端和客户端日志 粘贴**故障发生时**,服务端和客户端日志 ## 服务端和客户端配置文件 可以复现该问题的客户端和服务端的完整配置(请隐去域名和IP等隐私信息) ## 服务端和客户端版本信息 请执行./trojan-go -version并将输出完整粘贴在此处 ## 其他信息 你认为对我们修复bug有帮助的任何信息都可以在这里写出来 ================================================ FILE: .github/linters/.golangci.yml ================================================ run: timeout: 5m skip-files: - \.pb\.go$ issues: new: true linters: enable: - asciicheck - bodyclose - depguard - gci - gocritic - gofmt - gofumpt - goimports - govet - ineffassign - misspell - typecheck - unconvert - whitespace disable: - deadcode - errcheck - goprintffuncname - gosimple - nilerr - rowserrcheck - staticcheck - structcheck - stylecheck - unparam - unused - varcheck linters-settings: gci: local-prefixes: github.com/p4gefau1t/trojan-go goimports: local-prefixes: github.com/p4gefau1t/trojan-go ================================================ FILE: .github/workflows/docker-build.yml ================================================ on: push: branches: - master paths-ignore: - "**.md" - "docs/**" tags: - "v*.*.*" name: docker-build jobs: build: if: github.repository == 'p4gefau1t/trojan-go' runs-on: ubuntu-latest steps: - name: Checkout the code uses: actions/checkout@v2 - name: Setup QEMU uses: docker/setup-qemu-action@v1 - name: Setup Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Prepare id: prepare run: | if [[ $GITHUB_REF == refs/tags/* ]]; then echo ::set-output name=version::${GITHUB_REF#refs/tags/} echo ::set-output name=ref::${GITHUB_REF#refs/tags/} else echo ::set-output name=version::snapshot echo ::set-output name=ref::${{ github.sha }} fi echo ::set-output name=docker_platforms::linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 echo ::set-output name=docker_image::${{ secrets.DOCKER_USERNAME }}/trojan-go - name: Build and push docker image run: | docker buildx build --platform ${{ steps.prepare.outputs.docker_platforms }} \ --output "type=image,push=true" \ --tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}" \ --tag "${{ steps.prepare.outputs.docker_image }}:latest" \ --build-arg REF=${{ steps.prepare.outputs.ref }} \ --file Dockerfile . test: needs: build runs-on: ubuntu-latest steps: - name: Test docker image run: | docker run --rm --entrypoint /usr/local/bin/trojan-go ${{ secrets.DOCKER_USERNAME }}/trojan-go -version ================================================ FILE: .github/workflows/docker-nightly-build.yml ================================================ on: push: branches: - master paths-ignore: - '**.md' - 'docs/**' name: docker-nightly-build jobs: build: if: github.repository == 'p4gefau1t/trojan-go' runs-on: ubuntu-latest steps: - name: Checkout the code uses: actions/checkout@v2 - name: Setup QEMU uses: docker/setup-qemu-action@v1 - name: Setup Docker Buildx uses: docker/setup-buildx-action@v1 - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Prepare id: prepare run: | echo ::set-output name=docker_platforms::linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 echo ::set-output name=docker_image::${{ secrets.DOCKER_USERNAME }}/trojan-go echo ::set-output name=ref::${{ github.sha }} - name: Build and push docker image run: | docker buildx build --platform ${{ steps.prepare.outputs.docker_platforms }} \ --output "type=image,push=true" \ --tag "${{ steps.prepare.outputs.docker_image }}:nightly" \ --build-arg REF=${{ steps.prepare.outputs.ref }} \ --file Dockerfile . test: needs: build runs-on: ubuntu-latest steps: - name: Test docker image run: | docker run --rm --entrypoint /usr/local/bin/trojan-go ${{ secrets.DOCKER_USERNAME }}/trojan-go -version ================================================ FILE: .github/workflows/gh-pages.yml ================================================ on: push: branches: - master paths: - "docs/**" - ".github/workflows/gh-pages.yml" pull_request: types: [opened, synchronize, reopened] branches: - master paths: - "docs/**" - ".github/workflows/gh-pages.yml" name: github-pages jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: submodules: true # Fetch Hugo themes fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod - name: Setup Hugo uses: peaceiris/actions-hugo@v2 with: hugo-version: "0.83.1" # extended: true - name: Build run: | cd docs make hugo-themes hugo - name: Deploy if: ${{ github.event_name == 'push' }} uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./docs/public ================================================ FILE: .github/workflows/linter.yml ================================================ name: Linter on: push: branches: - master paths: - "**/*.go" - ".github/workflows/linter.yml" - ".github/linters/.golangci.yml" pull_request: types: [opened, synchronize, reopened] branches: - master paths: - "**/*.go" - ".github/workflows/linter.yml" - ".github/linters/.golangci.yml" jobs: lint: runs-on: ubuntu-latest steps: - name: Checkout codebase uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: version: latest args: --config=.github/linters/.golangci.yml only-new-issues: true ================================================ FILE: .github/workflows/nightly-build.yml ================================================ on: push: branches: - master paths: - "**/*.go" - "go.mod" - "go.sum" - "Makefile" - ".github/workflows/nightly-build.yml" pull_request: types: [opened, synchronize, reopened] branches: - master paths: - "**/*.go" - "go.mod" - "go.sum" - "Makefile" - ".github/workflows/nightly-build.yml" name: nightly-build jobs: build: strategy: fail-fast: false runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ^1.17.1 - name: Checkout code uses: actions/checkout@v2 - name: Build run: | make release -j$(nproc) ================================================ FILE: .github/workflows/release-build.yml ================================================ on: push: tags: - "v*.*.*" name: release-build jobs: build: runs-on: ubuntu-latest steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ^1.17.1 - name: Checkout code uses: actions/checkout@v2 - name: Checkout tag run: | git fetch --depth=1 origin +refs/tags/*:refs/tags/* tag_name="${GITHUB_REF##*/}" echo Tag $tag_name git checkout $tag_name echo "TAG_NAME=${tag_name}" >> $GITHUB_ENV - name: Build run: | make release -j$(nproc) - name: Release uses: svenstaro/upload-release-action@v2 with: repo_token: ${{ secrets.GITHUB_TOKEN }} tag: ${{ env.TAG_NAME }} file: ./trojan-go-*.zip file_glob: true prerelease: true ================================================ FILE: .github/workflows/test.yml ================================================ name: Test on: push: branches: - master paths: - "**/*.go" - "go.mod" - "go.sum" - ".github/workflows/test.yml" pull_request: types: [opened, synchronize, reopened] branches: - master paths: - "**/*.go" - "go.mod" - "go.sum" - ".github/workflows/test.yml" jobs: test: strategy: fail-fast: false matrix: platform: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.platform }} steps: - name: Install Go uses: actions/setup-go@v2 with: go-version: ^1.17.1 - name: Checkout code uses: actions/checkout@v2 - name: Check Go modules run: | go mod tidy -compat=1.17 git diff --exit-code go.mod go.sum go mod verify - name: Test run: make test ================================================ FILE: .gitignore ================================================ # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ build/ *.DS_Store *.zip *.tar.gz *.crt *.key *.dat trojan-go ================================================ FILE: Dockerfile ================================================ FROM golang:alpine AS builder WORKDIR / ARG REF RUN apk add git make &&\ git clone https://github.com/p4gefau1t/trojan-go.git RUN if [[ -z "${REF}" ]]; then \ echo "No specific commit provided, use the latest one." \ ;else \ echo "Use commit ${REF}" &&\ cd trojan-go &&\ git checkout ${REF} \ ;fi RUN cd trojan-go &&\ make &&\ wget https://github.com/v2fly/domain-list-community/raw/release/dlc.dat -O build/geosite.dat &&\ wget https://github.com/v2fly/geoip/raw/release/geoip.dat -O build/geoip.dat &&\ wget https://github.com/v2fly/geoip/raw/release/geoip-only-cn-private.dat -O build/geoip-only-cn-private.dat FROM alpine WORKDIR / RUN apk add --no-cache tzdata ca-certificates COPY --from=builder /trojan-go/build /usr/local/bin/ COPY --from=builder /trojan-go/example/server.json /etc/trojan-go/config.json ENTRYPOINT ["/usr/local/bin/trojan-go", "-config"] CMD ["/etc/trojan-go/config.json"] ================================================ FILE: LICENSE ================================================ 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. Copyright (C) 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: Copyright (C) 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 . ================================================ FILE: Makefile ================================================ NAME := trojan-go PACKAGE_NAME := github.com/p4gefau1t/trojan-go VERSION := `git describe --dirty` COMMIT := `git rev-parse HEAD` PLATFORM := linux BUILD_DIR := build VAR_SETTING := -X $(PACKAGE_NAME)/constant.Version=$(VERSION) -X $(PACKAGE_NAME)/constant.Commit=$(COMMIT) GOBUILD = env CGO_ENABLED=0 $(GO_DIR)go build -tags "full" -trimpath -ldflags="-s -w -buildid= $(VAR_SETTING)" -o $(BUILD_DIR) .PHONY: trojan-go release test normal: clean trojan-go clean: rm -rf $(BUILD_DIR) rm -f *.zip rm -f *.dat geoip.dat: wget https://github.com/v2fly/geoip/raw/release/geoip.dat geoip-only-cn-private.dat: wget https://github.com/v2fly/geoip/raw/release/geoip-only-cn-private.dat geosite.dat: wget https://github.com/v2fly/domain-list-community/raw/release/dlc.dat -O geosite.dat test: # Disable Bloomfilter when testing SHADOWSOCKS_SF_CAPACITY="-1" $(GO_DIR)go test -v ./... trojan-go: mkdir -p $(BUILD_DIR) $(GOBUILD) install: $(BUILD_DIR)/$(NAME) geoip.dat geoip-only-cn-private.dat geosite.dat mkdir -p /etc/$(NAME) mkdir -p /usr/share/$(NAME) cp example/*.json /etc/$(NAME) cp $(BUILD_DIR)/$(NAME) /usr/bin/$(NAME) cp example/$(NAME).service /usr/lib/systemd/system/ cp example/$(NAME)@.service /usr/lib/systemd/system/ systemctl daemon-reload cp geosite.dat /usr/share/$(NAME)/geosite.dat cp geoip.dat /usr/share/$(NAME)/geoip.dat cp geoip-only-cn-private.dat /usr/share/$(NAME)/geoip-only-cn-private.dat ln -fs /usr/share/$(NAME)/geoip.dat /usr/bin/ ln -fs /usr/share/$(NAME)/geoip-only-cn-private.dat /usr/bin/ ln -fs /usr/share/$(NAME)/geosite.dat /usr/bin/ uninstall: rm /usr/lib/systemd/system/$(NAME).service rm /usr/lib/systemd/system/$(NAME)@.service systemctl daemon-reload rm /usr/bin/$(NAME) rm -rd /etc/$(NAME) rm -rd /usr/share/$(NAME) rm /usr/bin/geoip.dat rm /usr/bin/geoip-only-cn-private.dat rm /usr/bin/geosite.dat %.zip: % geosite.dat geoip.dat geoip-only-cn-private.dat @zip -du $(NAME)-$@ -j $(BUILD_DIR)/$ 启用多路复用并不能提高测速得到的链路速度,但能降低延迟、提升大量并发请求时的网络体验,例如浏览含有大量图片的网页等。 你可以通过设置客户端的 `mux` 选项 `enabled` 字段启用它: ```json "mux": { "enabled": true } ``` 只需开启客户端 mux 配置即可,服务端会自动检测是否启用多路复用并提供支持。完整的选项说明参见 [Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。 ### 路由模块 Trojan-Go 客户端内建一个简单实用的路由模块,以方便实现国内直连、海外代理等自定义路由功能。 路由策略有三种: - `Proxy` 代理:将请求通过 TLS 隧道进行代理,由 Trojan 服务端与目的地址进行连接。 - `Bypass` 绕过:直接使用本地设备与目的地址进行连接。 - `Block` 封锁:不发送请求,直接关闭连接。 要激活路由模块,请在配置文件中添加 `router` 选项,并设置 `enabled` 字段为 `true`: ```json "router": { "enabled": true, "bypass": [ "geoip:cn", "geoip:private", "full:localhost" ], "block": [ "cidr:192.168.1.1/24", ], "proxy": [ "domain:google.com", ], "default_policy": "proxy" } ``` 完整的选项说明参见 [Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。 ### AEAD 加密 Trojan-Go 支持基于 Shadowsocks AEAD 对 Trojan 协议流量进行二次加密,以保证 Websocket 传输流量无法被不可信的 CDN 识别和审查: ```json "shadowsocks": { "enabled": true, "password": "my-password" } ``` 如需开启,服务端和客户端必须同时开启并保证密码一致。 ### 传输层插件 Trojan-Go 支持可插拔的传输层插件,并支持 Shadowsocks [SIP003](https://shadowsocks.org/en/wiki/Plugin.html) 标准的混淆插件。下面是使用 `v2ray-plugin` 的一个例子: > **此配置并不安全,仅作为演示** 服务端配置: ```json "transport_plugin": { "enabled": true, "type": "shadowsocks", "command": "./v2ray-plugin", "arg": ["-server", "-host", "www.baidu.com"] } ``` 客户端配置: ```json "transport_plugin": { "enabled": true, "type": "shadowsocks", "command": "./v2ray-plugin", "arg": ["-host", "www.baidu.com"] } ``` 完整的选项说明参见 [Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。 ## 构建 > 请确保 Go 版本 >= 1.14 使用 `make` 进行编译: ```shell git clone https://github.com/p4gefau1t/trojan-go.git cd trojan-go make make install #安装systemd服务等,可选 ``` 或者使用 Go 自行编译: ```shell go build -tags "full" ``` Go 支持通过设置环境变量进行交叉编译,例如: 编译适用于 64 位 Windows 操作系统的可执行文件: ```shell CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -tags "full" ``` 编译适用于 Apple Silicon 的可执行文件: ```shell CGO_ENABLED=0 GOOS=macos GOARCH=arm64 go build -tags "full" ``` 编译适用于 64 位 Linux 操作系统的可执行文件: ```shell CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -tags "full" ``` ## 致谢 - [Trojan](https://github.com/trojan-gfw/trojan) - [V2Fly](https://github.com/v2fly) - [utls](https://github.com/refraction-networking/utls) - [smux](https://github.com/xtaci/smux) - [go-tproxy](https://github.com/LiamHaworth/go-tproxy) ## Stargazers over time [![Stargazers over time](https://starchart.cc/p4gefau1t/trojan-go.svg)](https://starchart.cc/p4gefau1t/trojan-go) ================================================ FILE: api/api.go ================================================ package api import ( "context" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/statistic" ) type Handler func(ctx context.Context, auth statistic.Authenticator) error var handlers = make(map[string]Handler) func RegisterHandler(name string, handler Handler) { handlers[name] = handler } func RunService(ctx context.Context, name string, auth statistic.Authenticator) error { if h, ok := handlers[name]; ok { log.Debug("api handler found", name) return h(ctx, auth) } log.Debug("api handler not found", name) return nil } ================================================ FILE: api/control/control.go ================================================ package control import ( "context" "encoding/json" "flag" "fmt" "io" "google.golang.org/grpc" "github.com/p4gefau1t/trojan-go/api/service" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/option" ) type apiController struct { address *string key *string hash *string cert *string cmd *string password *string add *bool delete *bool modify *bool list *bool uploadSpeedLimit *int downloadSpeedLimit *int ipLimit *int ctx context.Context } func (apiController) Name() string { return "api" } func (o *apiController) listUsers(apiClient service.TrojanServerServiceClient) error { stream, err := apiClient.ListUsers(o.ctx, &service.ListUsersRequest{}) if err != nil { return err } defer stream.CloseSend() result := []*service.ListUsersResponse{} for { resp, err := stream.Recv() if err != nil { if err == io.EOF { break } return err } result = append(result, resp) } data, err := json.Marshal(result) common.Must(err) fmt.Println(string(data)) return nil } func (o *apiController) getUsers(apiClient service.TrojanServerServiceClient) error { stream, err := apiClient.GetUsers(o.ctx) if err != nil { return err } defer stream.CloseSend() err = stream.Send(&service.GetUsersRequest{ User: &service.User{ Password: *o.password, Hash: *o.hash, }, }) if err != nil { return err } resp, err := stream.Recv() if err != nil { return err } data, err := json.Marshal(resp) common.Must(err) fmt.Print(string(data)) return nil } func (o *apiController) setUsers(apiClient service.TrojanServerServiceClient) error { stream, err := apiClient.SetUsers(o.ctx) if err != nil { return err } defer stream.CloseSend() req := &service.SetUsersRequest{ Status: &service.UserStatus{ User: &service.User{ Password: *o.password, Hash: *o.hash, }, IpLimit: int32(*o.ipLimit), SpeedLimit: &service.Speed{ UploadSpeed: uint64(*o.uploadSpeedLimit), DownloadSpeed: uint64(*o.downloadSpeedLimit), }, }, } switch { case *o.add: req.Operation = service.SetUsersRequest_Add case *o.modify: req.Operation = service.SetUsersRequest_Modify case *o.delete: req.Operation = service.SetUsersRequest_Delete default: return common.NewError("Invalid operation") } err = stream.Send(req) if err != nil { return err } resp, err := stream.Recv() if err != nil { return err } if resp.Success { fmt.Println("Done") } else { fmt.Println("Failed: " + resp.Info) } return nil } func (o *apiController) Handle() error { if *o.cmd == "" { return common.NewError("") } conn, err := grpc.Dial(*o.address, grpc.WithInsecure()) if err != nil { log.Error(err) return nil } defer conn.Close() apiClient := service.NewTrojanServerServiceClient(conn) switch *o.cmd { case "list": err := o.listUsers(apiClient) if err != nil { log.Error(err) } case "get": err := o.getUsers(apiClient) if err != nil { log.Error(err) } case "set": err := o.setUsers(apiClient) if err != nil { log.Error(err) } default: log.Error("unknown command " + *o.cmd) } return nil } func (o *apiController) Priority() int { return 50 } func init() { option.RegisterHandler(&apiController{ cmd: flag.String("api", "", "Connect to a Trojan-Go API service. \"-api add/get/list\""), address: flag.String("api-addr", "127.0.0.1:10000", "Address of Trojan-Go API service"), password: flag.String("target-password", "", "Password of the target user"), hash: flag.String("target-hash", "", "Hash of the target user"), add: flag.Bool("add-profile", false, "Add a new profile with API"), delete: flag.Bool("delete-profile", false, "Delete an existing profile with API"), modify: flag.Bool("modify-profile", false, "Modify an existing profile with API"), uploadSpeedLimit: flag.Int("upload-speed-limit", 0, "Limit the upload speed with API"), downloadSpeedLimit: flag.Int("download-speed-limit", 0, "Limit the download speed with API"), ipLimit: flag.Int("ip-limit", 0, "Limit the number of IP with API"), ctx: context.Background(), }) } ================================================ FILE: api/service/api.pb.go ================================================ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.27.1 // protoc v3.17.3 // source: api.proto package service import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( // Verify that this generated code is sufficiently up-to-date. _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) // Verify that runtime/protoimpl is sufficiently up-to-date. _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) type SetUsersRequest_Operation int32 const ( SetUsersRequest_Add SetUsersRequest_Operation = 0 SetUsersRequest_Delete SetUsersRequest_Operation = 1 SetUsersRequest_Modify SetUsersRequest_Operation = 2 ) // Enum value maps for SetUsersRequest_Operation. var ( SetUsersRequest_Operation_name = map[int32]string{ 0: "Add", 1: "Delete", 2: "Modify", } SetUsersRequest_Operation_value = map[string]int32{ "Add": 0, "Delete": 1, "Modify": 2, } ) func (x SetUsersRequest_Operation) Enum() *SetUsersRequest_Operation { p := new(SetUsersRequest_Operation) *p = x return p } func (x SetUsersRequest_Operation) String() string { return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) } func (SetUsersRequest_Operation) Descriptor() protoreflect.EnumDescriptor { return file_api_proto_enumTypes[0].Descriptor() } func (SetUsersRequest_Operation) Type() protoreflect.EnumType { return &file_api_proto_enumTypes[0] } func (x SetUsersRequest_Operation) Number() protoreflect.EnumNumber { return protoreflect.EnumNumber(x) } // Deprecated: Use SetUsersRequest_Operation.Descriptor instead. func (SetUsersRequest_Operation) EnumDescriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{10, 0} } type Traffic struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields UploadTraffic uint64 `protobuf:"varint,1,opt,name=upload_traffic,json=uploadTraffic,proto3" json:"upload_traffic,omitempty"` DownloadTraffic uint64 `protobuf:"varint,2,opt,name=download_traffic,json=downloadTraffic,proto3" json:"download_traffic,omitempty"` } func (x *Traffic) Reset() { *x = Traffic{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Traffic) String() string { return protoimpl.X.MessageStringOf(x) } func (*Traffic) ProtoMessage() {} func (x *Traffic) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[0] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Traffic.ProtoReflect.Descriptor instead. func (*Traffic) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{0} } func (x *Traffic) GetUploadTraffic() uint64 { if x != nil { return x.UploadTraffic } return 0 } func (x *Traffic) GetDownloadTraffic() uint64 { if x != nil { return x.DownloadTraffic } return 0 } type Speed struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields UploadSpeed uint64 `protobuf:"varint,1,opt,name=upload_speed,json=uploadSpeed,proto3" json:"upload_speed,omitempty"` DownloadSpeed uint64 `protobuf:"varint,2,opt,name=download_speed,json=downloadSpeed,proto3" json:"download_speed,omitempty"` } func (x *Speed) Reset() { *x = Speed{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[1] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *Speed) String() string { return protoimpl.X.MessageStringOf(x) } func (*Speed) ProtoMessage() {} func (x *Speed) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[1] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use Speed.ProtoReflect.Descriptor instead. func (*Speed) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{1} } func (x *Speed) GetUploadSpeed() uint64 { if x != nil { return x.UploadSpeed } return 0 } func (x *Speed) GetDownloadSpeed() uint64 { if x != nil { return x.DownloadSpeed } return 0 } type User struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Password string `protobuf:"bytes,1,opt,name=password,proto3" json:"password,omitempty"` Hash string `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"` } func (x *User) Reset() { *x = User{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[2] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *User) String() string { return protoimpl.X.MessageStringOf(x) } func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[2] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use User.ProtoReflect.Descriptor instead. func (*User) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{2} } func (x *User) GetPassword() string { if x != nil { return x.Password } return "" } func (x *User) GetHash() string { if x != nil { return x.Hash } return "" } type UserStatus struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` TrafficTotal *Traffic `protobuf:"bytes,2,opt,name=traffic_total,json=trafficTotal,proto3" json:"traffic_total,omitempty"` SpeedCurrent *Speed `protobuf:"bytes,3,opt,name=speed_current,json=speedCurrent,proto3" json:"speed_current,omitempty"` SpeedLimit *Speed `protobuf:"bytes,4,opt,name=speed_limit,json=speedLimit,proto3" json:"speed_limit,omitempty"` IpCurrent int32 `protobuf:"varint,5,opt,name=ip_current,json=ipCurrent,proto3" json:"ip_current,omitempty"` IpLimit int32 `protobuf:"varint,6,opt,name=ip_limit,json=ipLimit,proto3" json:"ip_limit,omitempty"` } func (x *UserStatus) Reset() { *x = UserStatus{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[3] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *UserStatus) String() string { return protoimpl.X.MessageStringOf(x) } func (*UserStatus) ProtoMessage() {} func (x *UserStatus) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[3] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use UserStatus.ProtoReflect.Descriptor instead. func (*UserStatus) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{3} } func (x *UserStatus) GetUser() *User { if x != nil { return x.User } return nil } func (x *UserStatus) GetTrafficTotal() *Traffic { if x != nil { return x.TrafficTotal } return nil } func (x *UserStatus) GetSpeedCurrent() *Speed { if x != nil { return x.SpeedCurrent } return nil } func (x *UserStatus) GetSpeedLimit() *Speed { if x != nil { return x.SpeedLimit } return nil } func (x *UserStatus) GetIpCurrent() int32 { if x != nil { return x.IpCurrent } return 0 } func (x *UserStatus) GetIpLimit() int32 { if x != nil { return x.IpLimit } return 0 } type GetTrafficRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` } func (x *GetTrafficRequest) Reset() { *x = GetTrafficRequest{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[4] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetTrafficRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetTrafficRequest) ProtoMessage() {} func (x *GetTrafficRequest) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[4] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetTrafficRequest.ProtoReflect.Descriptor instead. func (*GetTrafficRequest) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{4} } func (x *GetTrafficRequest) GetUser() *User { if x != nil { return x.User } return nil } type GetTrafficResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` Info string `protobuf:"bytes,2,opt,name=info,proto3" json:"info,omitempty"` TrafficTotal *Traffic `protobuf:"bytes,3,opt,name=traffic_total,json=trafficTotal,proto3" json:"traffic_total,omitempty"` SpeedCurrent *Speed `protobuf:"bytes,4,opt,name=speed_current,json=speedCurrent,proto3" json:"speed_current,omitempty"` } func (x *GetTrafficResponse) Reset() { *x = GetTrafficResponse{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetTrafficResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetTrafficResponse) ProtoMessage() {} func (x *GetTrafficResponse) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetTrafficResponse.ProtoReflect.Descriptor instead. func (*GetTrafficResponse) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{5} } func (x *GetTrafficResponse) GetSuccess() bool { if x != nil { return x.Success } return false } func (x *GetTrafficResponse) GetInfo() string { if x != nil { return x.Info } return "" } func (x *GetTrafficResponse) GetTrafficTotal() *Traffic { if x != nil { return x.TrafficTotal } return nil } func (x *GetTrafficResponse) GetSpeedCurrent() *Speed { if x != nil { return x.SpeedCurrent } return nil } type ListUsersRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields } func (x *ListUsersRequest) Reset() { *x = ListUsersRequest{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ListUsersRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUsersRequest) ProtoMessage() {} func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead. func (*ListUsersRequest) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{6} } type ListUsersResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Status *UserStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` } func (x *ListUsersResponse) Reset() { *x = ListUsersResponse{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *ListUsersResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*ListUsersResponse) ProtoMessage() {} func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead. func (*ListUsersResponse) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{7} } func (x *ListUsersResponse) GetStatus() *UserStatus { if x != nil { return x.Status } return nil } type GetUsersRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` } func (x *GetUsersRequest) Reset() { *x = GetUsersRequest{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetUsersRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetUsersRequest) ProtoMessage() {} func (x *GetUsersRequest) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetUsersRequest.ProtoReflect.Descriptor instead. func (*GetUsersRequest) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{8} } func (x *GetUsersRequest) GetUser() *User { if x != nil { return x.User } return nil } type GetUsersResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` Info string `protobuf:"bytes,2,opt,name=info,proto3" json:"info,omitempty"` Status *UserStatus `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` } func (x *GetUsersResponse) Reset() { *x = GetUsersResponse{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *GetUsersResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*GetUsersResponse) ProtoMessage() {} func (x *GetUsersResponse) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use GetUsersResponse.ProtoReflect.Descriptor instead. func (*GetUsersResponse) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{9} } func (x *GetUsersResponse) GetSuccess() bool { if x != nil { return x.Success } return false } func (x *GetUsersResponse) GetInfo() string { if x != nil { return x.Info } return "" } func (x *GetUsersResponse) GetStatus() *UserStatus { if x != nil { return x.Status } return nil } type SetUsersRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Status *UserStatus `protobuf:"bytes,1,opt,name=status,proto3" json:"status,omitempty"` Operation SetUsersRequest_Operation `protobuf:"varint,2,opt,name=operation,proto3,enum=trojan.api.SetUsersRequest_Operation" json:"operation,omitempty"` } func (x *SetUsersRequest) Reset() { *x = SetUsersRequest{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SetUsersRequest) String() string { return protoimpl.X.MessageStringOf(x) } func (*SetUsersRequest) ProtoMessage() {} func (x *SetUsersRequest) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SetUsersRequest.ProtoReflect.Descriptor instead. func (*SetUsersRequest) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{10} } func (x *SetUsersRequest) GetStatus() *UserStatus { if x != nil { return x.Status } return nil } func (x *SetUsersRequest) GetOperation() SetUsersRequest_Operation { if x != nil { return x.Operation } return SetUsersRequest_Add } type SetUsersResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` Info string `protobuf:"bytes,2,opt,name=info,proto3" json:"info,omitempty"` } func (x *SetUsersResponse) Reset() { *x = SetUsersResponse{} if protoimpl.UnsafeEnabled { mi := &file_api_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } func (x *SetUsersResponse) String() string { return protoimpl.X.MessageStringOf(x) } func (*SetUsersResponse) ProtoMessage() {} func (x *SetUsersResponse) ProtoReflect() protoreflect.Message { mi := &file_api_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) } return ms } return mi.MessageOf(x) } // Deprecated: Use SetUsersResponse.ProtoReflect.Descriptor instead. func (*SetUsersResponse) Descriptor() ([]byte, []int) { return file_api_proto_rawDescGZIP(), []int{11} } func (x *SetUsersResponse) GetSuccess() bool { if x != nil { return x.Success } return false } func (x *SetUsersResponse) GetInfo() string { if x != nil { return x.Info } return "" } var File_api_proto protoreflect.FileDescriptor var file_api_proto_rawDesc = []byte{ 0x0a, 0x09, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x22, 0x5b, 0x0a, 0x07, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x12, 0x25, 0x0a, 0x0e, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x12, 0x29, 0x0a, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x22, 0x51, 0x0a, 0x05, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x75, 0x70, 0x6c, 0x6f, 0x61, 0x64, 0x53, 0x70, 0x65, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x73, 0x70, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x53, 0x70, 0x65, 0x65, 0x64, 0x22, 0x36, 0x0a, 0x04, 0x55, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x92, 0x02, 0x0a, 0x0a, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x24, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0d, 0x73, 0x70, 0x65, 0x65, 0x64, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x52, 0x0c, 0x73, 0x70, 0x65, 0x65, 0x64, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x0b, 0x73, 0x70, 0x65, 0x65, 0x64, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x52, 0x0a, 0x73, 0x70, 0x65, 0x65, 0x64, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x70, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x69, 0x70, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x69, 0x70, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x69, 0x70, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0x39, 0x0a, 0x11, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0xb4, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x38, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x36, 0x0a, 0x0d, 0x73, 0x70, 0x65, 0x65, 0x64, 0x5f, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x70, 0x65, 0x65, 0x64, 0x52, 0x0c, 0x73, 0x70, 0x65, 0x65, 0x64, 0x43, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x74, 0x22, 0x12, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x43, 0x0a, 0x11, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x37, 0x0a, 0x0f, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x22, 0x70, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xb4, 0x01, 0x0a, 0x0f, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2e, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x43, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x2c, 0x0a, 0x09, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x07, 0x0a, 0x03, 0x41, 0x64, 0x64, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x79, 0x10, 0x02, 0x22, 0x40, 0x0a, 0x10, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x69, 0x6e, 0x66, 0x6f, 0x32, 0x64, 0x0a, 0x13, 0x54, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4d, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x12, 0x1d, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x54, 0x72, 0x61, 0x66, 0x66, 0x69, 0x63, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x32, 0xfd, 0x01, 0x0a, 0x13, 0x54, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1c, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x30, 0x01, 0x12, 0x4b, 0x0a, 0x08, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x12, 0x4b, 0x0a, 0x08, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1c, 0x2e, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2c, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x34, 0x67, 0x65, 0x66, 0x61, 0x75, 0x31, 0x74, 0x2f, 0x74, 0x72, 0x6f, 0x6a, 0x61, 0x6e, 0x2d, 0x67, 0x6f, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( file_api_proto_rawDescOnce sync.Once file_api_proto_rawDescData = file_api_proto_rawDesc ) func file_api_proto_rawDescGZIP() []byte { file_api_proto_rawDescOnce.Do(func() { file_api_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_proto_rawDescData) }) return file_api_proto_rawDescData } var file_api_proto_enumTypes = make([]protoimpl.EnumInfo, 1) var file_api_proto_msgTypes = make([]protoimpl.MessageInfo, 12) var file_api_proto_goTypes = []interface{}{ (SetUsersRequest_Operation)(0), // 0: trojan.api.SetUsersRequest.Operation (*Traffic)(nil), // 1: trojan.api.Traffic (*Speed)(nil), // 2: trojan.api.Speed (*User)(nil), // 3: trojan.api.User (*UserStatus)(nil), // 4: trojan.api.UserStatus (*GetTrafficRequest)(nil), // 5: trojan.api.GetTrafficRequest (*GetTrafficResponse)(nil), // 6: trojan.api.GetTrafficResponse (*ListUsersRequest)(nil), // 7: trojan.api.ListUsersRequest (*ListUsersResponse)(nil), // 8: trojan.api.ListUsersResponse (*GetUsersRequest)(nil), // 9: trojan.api.GetUsersRequest (*GetUsersResponse)(nil), // 10: trojan.api.GetUsersResponse (*SetUsersRequest)(nil), // 11: trojan.api.SetUsersRequest (*SetUsersResponse)(nil), // 12: trojan.api.SetUsersResponse } var file_api_proto_depIdxs = []int32{ 3, // 0: trojan.api.UserStatus.user:type_name -> trojan.api.User 1, // 1: trojan.api.UserStatus.traffic_total:type_name -> trojan.api.Traffic 2, // 2: trojan.api.UserStatus.speed_current:type_name -> trojan.api.Speed 2, // 3: trojan.api.UserStatus.speed_limit:type_name -> trojan.api.Speed 3, // 4: trojan.api.GetTrafficRequest.user:type_name -> trojan.api.User 1, // 5: trojan.api.GetTrafficResponse.traffic_total:type_name -> trojan.api.Traffic 2, // 6: trojan.api.GetTrafficResponse.speed_current:type_name -> trojan.api.Speed 4, // 7: trojan.api.ListUsersResponse.status:type_name -> trojan.api.UserStatus 3, // 8: trojan.api.GetUsersRequest.user:type_name -> trojan.api.User 4, // 9: trojan.api.GetUsersResponse.status:type_name -> trojan.api.UserStatus 4, // 10: trojan.api.SetUsersRequest.status:type_name -> trojan.api.UserStatus 0, // 11: trojan.api.SetUsersRequest.operation:type_name -> trojan.api.SetUsersRequest.Operation 5, // 12: trojan.api.TrojanClientService.GetTraffic:input_type -> trojan.api.GetTrafficRequest 7, // 13: trojan.api.TrojanServerService.ListUsers:input_type -> trojan.api.ListUsersRequest 9, // 14: trojan.api.TrojanServerService.GetUsers:input_type -> trojan.api.GetUsersRequest 11, // 15: trojan.api.TrojanServerService.SetUsers:input_type -> trojan.api.SetUsersRequest 6, // 16: trojan.api.TrojanClientService.GetTraffic:output_type -> trojan.api.GetTrafficResponse 8, // 17: trojan.api.TrojanServerService.ListUsers:output_type -> trojan.api.ListUsersResponse 10, // 18: trojan.api.TrojanServerService.GetUsers:output_type -> trojan.api.GetUsersResponse 12, // 19: trojan.api.TrojanServerService.SetUsers:output_type -> trojan.api.SetUsersResponse 16, // [16:20] is the sub-list for method output_type 12, // [12:16] is the sub-list for method input_type 12, // [12:12] is the sub-list for extension type_name 12, // [12:12] is the sub-list for extension extendee 0, // [0:12] is the sub-list for field type_name } func init() { file_api_proto_init() } func file_api_proto_init() { if File_api_proto != nil { return } if !protoimpl.UnsafeEnabled { file_api_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Traffic); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*Speed); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*User); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*UserStatus); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetTrafficRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetTrafficResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ListUsersRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ListUsersResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetUsersRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*GetUsersResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SetUsersRequest); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } file_api_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*SetUsersResponse); i { case 0: return &v.state case 1: return &v.sizeCache case 2: return &v.unknownFields default: return nil } } } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_api_proto_rawDesc, NumEnums: 1, NumMessages: 12, NumExtensions: 0, NumServices: 2, }, GoTypes: file_api_proto_goTypes, DependencyIndexes: file_api_proto_depIdxs, EnumInfos: file_api_proto_enumTypes, MessageInfos: file_api_proto_msgTypes, }.Build() File_api_proto = out.File file_api_proto_rawDesc = nil file_api_proto_goTypes = nil file_api_proto_depIdxs = nil } ================================================ FILE: api/service/api.proto ================================================ syntax = "proto3"; package trojan.api; option go_package = "github.com/p4gefau1t/trojan-go/api/service"; message Traffic { uint64 upload_traffic = 1; uint64 download_traffic = 2; } message Speed { uint64 upload_speed = 1; uint64 download_speed = 2; } message User { string password = 1; string hash = 2; } message UserStatus { User user = 1; Traffic traffic_total = 2; Speed speed_current = 3; Speed speed_limit = 4; int32 ip_current = 5; int32 ip_limit = 6; } message GetTrafficRequest { User user = 1; } message GetTrafficResponse { bool success = 1; string info = 2; Traffic traffic_total = 3; Speed speed_current = 4; } message ListUsersRequest { } message ListUsersResponse { UserStatus status = 1; } message GetUsersRequest { User user = 1; } message GetUsersResponse { bool success = 1; string info = 2; UserStatus status = 3; } message SetUsersRequest { enum Operation { Add = 0; Delete = 1; Modify = 2; } UserStatus status = 1; Operation operation = 2; } message SetUsersResponse { bool success = 1; string info = 2; } service TrojanClientService { rpc GetTraffic(GetTrafficRequest) returns(GetTrafficResponse){} } service TrojanServerService { // list all users rpc ListUsers(ListUsersRequest) returns(stream ListUsersResponse){} // obtain specified user's info rpc GetUsers(stream GetUsersRequest) returns(stream GetUsersResponse){} // setup existing users' config rpc SetUsers(stream SetUsersRequest) returns(stream SetUsersResponse){} } ================================================ FILE: api/service/api_grpc.pb.go ================================================ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. package service import ( context "context" grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" ) // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 // TrojanClientServiceClient is the client API for TrojanClientService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type TrojanClientServiceClient interface { GetTraffic(ctx context.Context, in *GetTrafficRequest, opts ...grpc.CallOption) (*GetTrafficResponse, error) } type trojanClientServiceClient struct { cc grpc.ClientConnInterface } func NewTrojanClientServiceClient(cc grpc.ClientConnInterface) TrojanClientServiceClient { return &trojanClientServiceClient{cc} } func (c *trojanClientServiceClient) GetTraffic(ctx context.Context, in *GetTrafficRequest, opts ...grpc.CallOption) (*GetTrafficResponse, error) { out := new(GetTrafficResponse) err := c.cc.Invoke(ctx, "/trojan.api.TrojanClientService/GetTraffic", in, out, opts...) if err != nil { return nil, err } return out, nil } // TrojanClientServiceServer is the server API for TrojanClientService service. // All implementations must embed UnimplementedTrojanClientServiceServer // for forward compatibility type TrojanClientServiceServer interface { GetTraffic(context.Context, *GetTrafficRequest) (*GetTrafficResponse, error) mustEmbedUnimplementedTrojanClientServiceServer() } // UnimplementedTrojanClientServiceServer must be embedded to have forward compatible implementations. type UnimplementedTrojanClientServiceServer struct { } func (UnimplementedTrojanClientServiceServer) GetTraffic(context.Context, *GetTrafficRequest) (*GetTrafficResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method GetTraffic not implemented") } func (UnimplementedTrojanClientServiceServer) mustEmbedUnimplementedTrojanClientServiceServer() {} // UnsafeTrojanClientServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to TrojanClientServiceServer will // result in compilation errors. type UnsafeTrojanClientServiceServer interface { mustEmbedUnimplementedTrojanClientServiceServer() } func RegisterTrojanClientServiceServer(s grpc.ServiceRegistrar, srv TrojanClientServiceServer) { s.RegisterService(&TrojanClientService_ServiceDesc, srv) } func _TrojanClientService_GetTraffic_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetTrafficRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(TrojanClientServiceServer).GetTraffic(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/trojan.api.TrojanClientService/GetTraffic", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(TrojanClientServiceServer).GetTraffic(ctx, req.(*GetTrafficRequest)) } return interceptor(ctx, in, info, handler) } // TrojanClientService_ServiceDesc is the grpc.ServiceDesc for TrojanClientService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var TrojanClientService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "trojan.api.TrojanClientService", HandlerType: (*TrojanClientServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "GetTraffic", Handler: _TrojanClientService_GetTraffic_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "api.proto", } // TrojanServerServiceClient is the client API for TrojanServerService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. type TrojanServerServiceClient interface { // list all users ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (TrojanServerService_ListUsersClient, error) // obtain specified user's info GetUsers(ctx context.Context, opts ...grpc.CallOption) (TrojanServerService_GetUsersClient, error) // setup existing users' config SetUsers(ctx context.Context, opts ...grpc.CallOption) (TrojanServerService_SetUsersClient, error) } type trojanServerServiceClient struct { cc grpc.ClientConnInterface } func NewTrojanServerServiceClient(cc grpc.ClientConnInterface) TrojanServerServiceClient { return &trojanServerServiceClient{cc} } func (c *trojanServerServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (TrojanServerService_ListUsersClient, error) { stream, err := c.cc.NewStream(ctx, &TrojanServerService_ServiceDesc.Streams[0], "/trojan.api.TrojanServerService/ListUsers", opts...) if err != nil { return nil, err } x := &trojanServerServiceListUsersClient{stream} if err := x.ClientStream.SendMsg(in); err != nil { return nil, err } if err := x.ClientStream.CloseSend(); err != nil { return nil, err } return x, nil } type TrojanServerService_ListUsersClient interface { Recv() (*ListUsersResponse, error) grpc.ClientStream } type trojanServerServiceListUsersClient struct { grpc.ClientStream } func (x *trojanServerServiceListUsersClient) Recv() (*ListUsersResponse, error) { m := new(ListUsersResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *trojanServerServiceClient) GetUsers(ctx context.Context, opts ...grpc.CallOption) (TrojanServerService_GetUsersClient, error) { stream, err := c.cc.NewStream(ctx, &TrojanServerService_ServiceDesc.Streams[1], "/trojan.api.TrojanServerService/GetUsers", opts...) if err != nil { return nil, err } x := &trojanServerServiceGetUsersClient{stream} return x, nil } type TrojanServerService_GetUsersClient interface { Send(*GetUsersRequest) error Recv() (*GetUsersResponse, error) grpc.ClientStream } type trojanServerServiceGetUsersClient struct { grpc.ClientStream } func (x *trojanServerServiceGetUsersClient) Send(m *GetUsersRequest) error { return x.ClientStream.SendMsg(m) } func (x *trojanServerServiceGetUsersClient) Recv() (*GetUsersResponse, error) { m := new(GetUsersResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func (c *trojanServerServiceClient) SetUsers(ctx context.Context, opts ...grpc.CallOption) (TrojanServerService_SetUsersClient, error) { stream, err := c.cc.NewStream(ctx, &TrojanServerService_ServiceDesc.Streams[2], "/trojan.api.TrojanServerService/SetUsers", opts...) if err != nil { return nil, err } x := &trojanServerServiceSetUsersClient{stream} return x, nil } type TrojanServerService_SetUsersClient interface { Send(*SetUsersRequest) error Recv() (*SetUsersResponse, error) grpc.ClientStream } type trojanServerServiceSetUsersClient struct { grpc.ClientStream } func (x *trojanServerServiceSetUsersClient) Send(m *SetUsersRequest) error { return x.ClientStream.SendMsg(m) } func (x *trojanServerServiceSetUsersClient) Recv() (*SetUsersResponse, error) { m := new(SetUsersResponse) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // TrojanServerServiceServer is the server API for TrojanServerService service. // All implementations must embed UnimplementedTrojanServerServiceServer // for forward compatibility type TrojanServerServiceServer interface { // list all users ListUsers(*ListUsersRequest, TrojanServerService_ListUsersServer) error // obtain specified user's info GetUsers(TrojanServerService_GetUsersServer) error // setup existing users' config SetUsers(TrojanServerService_SetUsersServer) error mustEmbedUnimplementedTrojanServerServiceServer() } // UnimplementedTrojanServerServiceServer must be embedded to have forward compatible implementations. type UnimplementedTrojanServerServiceServer struct { } func (UnimplementedTrojanServerServiceServer) ListUsers(*ListUsersRequest, TrojanServerService_ListUsersServer) error { return status.Errorf(codes.Unimplemented, "method ListUsers not implemented") } func (UnimplementedTrojanServerServiceServer) GetUsers(TrojanServerService_GetUsersServer) error { return status.Errorf(codes.Unimplemented, "method GetUsers not implemented") } func (UnimplementedTrojanServerServiceServer) SetUsers(TrojanServerService_SetUsersServer) error { return status.Errorf(codes.Unimplemented, "method SetUsers not implemented") } func (UnimplementedTrojanServerServiceServer) mustEmbedUnimplementedTrojanServerServiceServer() {} // UnsafeTrojanServerServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to TrojanServerServiceServer will // result in compilation errors. type UnsafeTrojanServerServiceServer interface { mustEmbedUnimplementedTrojanServerServiceServer() } func RegisterTrojanServerServiceServer(s grpc.ServiceRegistrar, srv TrojanServerServiceServer) { s.RegisterService(&TrojanServerService_ServiceDesc, srv) } func _TrojanServerService_ListUsers_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(ListUsersRequest) if err := stream.RecvMsg(m); err != nil { return err } return srv.(TrojanServerServiceServer).ListUsers(m, &trojanServerServiceListUsersServer{stream}) } type TrojanServerService_ListUsersServer interface { Send(*ListUsersResponse) error grpc.ServerStream } type trojanServerServiceListUsersServer struct { grpc.ServerStream } func (x *trojanServerServiceListUsersServer) Send(m *ListUsersResponse) error { return x.ServerStream.SendMsg(m) } func _TrojanServerService_GetUsers_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(TrojanServerServiceServer).GetUsers(&trojanServerServiceGetUsersServer{stream}) } type TrojanServerService_GetUsersServer interface { Send(*GetUsersResponse) error Recv() (*GetUsersRequest, error) grpc.ServerStream } type trojanServerServiceGetUsersServer struct { grpc.ServerStream } func (x *trojanServerServiceGetUsersServer) Send(m *GetUsersResponse) error { return x.ServerStream.SendMsg(m) } func (x *trojanServerServiceGetUsersServer) Recv() (*GetUsersRequest, error) { m := new(GetUsersRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } func _TrojanServerService_SetUsers_Handler(srv interface{}, stream grpc.ServerStream) error { return srv.(TrojanServerServiceServer).SetUsers(&trojanServerServiceSetUsersServer{stream}) } type TrojanServerService_SetUsersServer interface { Send(*SetUsersResponse) error Recv() (*SetUsersRequest, error) grpc.ServerStream } type trojanServerServiceSetUsersServer struct { grpc.ServerStream } func (x *trojanServerServiceSetUsersServer) Send(m *SetUsersResponse) error { return x.ServerStream.SendMsg(m) } func (x *trojanServerServiceSetUsersServer) Recv() (*SetUsersRequest, error) { m := new(SetUsersRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil } // TrojanServerService_ServiceDesc is the grpc.ServiceDesc for TrojanServerService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) var TrojanServerService_ServiceDesc = grpc.ServiceDesc{ ServiceName: "trojan.api.TrojanServerService", HandlerType: (*TrojanServerServiceServer)(nil), Methods: []grpc.MethodDesc{}, Streams: []grpc.StreamDesc{ { StreamName: "ListUsers", Handler: _TrojanServerService_ListUsers_Handler, ServerStreams: true, }, { StreamName: "GetUsers", Handler: _TrojanServerService_GetUsers_Handler, ServerStreams: true, ClientStreams: true, }, { StreamName: "SetUsers", Handler: _TrojanServerService_SetUsers_Handler, ServerStreams: true, ClientStreams: true, }, }, Metadata: "api.proto", } ================================================ FILE: api/service/client.go ================================================ package service import ( "context" "net" "github.com/p4gefau1t/trojan-go/api" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/statistic" "github.com/p4gefau1t/trojan-go/tunnel/trojan" ) type ClientAPI struct { TrojanClientServiceServer auth statistic.Authenticator ctx context.Context uploadSpeed uint64 downloadSpeed uint64 lastSent uint64 lastRecv uint64 } func (s *ClientAPI) GetTraffic(ctx context.Context, req *GetTrafficRequest) (*GetTrafficResponse, error) { log.Debug("API: GetTraffic") if req.User == nil { return nil, common.NewError("User is unspecified") } if req.User.Hash == "" { req.User.Hash = common.SHA224String(req.User.Password) } valid, user := s.auth.AuthUser(req.User.Hash) if !valid { return nil, common.NewError("User " + req.User.Hash + " not found") } sent, recv := user.GetTraffic() sentSpeed, recvSpeed := user.GetSpeed() resp := &GetTrafficResponse{ Success: true, TrafficTotal: &Traffic{ UploadTraffic: sent, DownloadTraffic: recv, }, SpeedCurrent: &Speed{ UploadSpeed: sentSpeed, DownloadSpeed: recvSpeed, }, } return resp, nil } func RunClientAPI(ctx context.Context, auth statistic.Authenticator) error { cfg := config.FromContext(ctx, Name).(*Config) if !cfg.API.Enabled { return nil } server, err := newAPIServer(cfg) if err != nil { return err } defer server.Stop() service := &ClientAPI{ ctx: ctx, auth: auth, } RegisterTrojanClientServiceServer(server, service) addr, err := net.ResolveIPAddr("ip", cfg.API.APIHost) if err != nil { return common.NewError("api found invalid addr").Base(err) } listener, err := net.Listen("tcp", (&net.TCPAddr{ IP: addr.IP, Port: cfg.API.APIPort, Zone: addr.Zone, }).String()) if err != nil { return common.NewError("client api failed to listen").Base(err) } defer listener.Close() log.Info("client-side api service is listening on", listener.Addr().String()) errChan := make(chan error, 1) go func() { errChan <- server.Serve(listener) }() select { case err := <-errChan: return err case <-ctx.Done(): log.Debug("closed") return nil } } func init() { api.RegisterHandler(trojan.Name+"_CLIENT", RunClientAPI) } ================================================ FILE: api/service/client_test.go ================================================ package service import ( "context" "fmt" "testing" "time" "google.golang.org/grpc" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/statistic/memory" ) func TestClientAPI(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) ctx = config.WithConfig(ctx, memory.Name, &memory.Config{ Passwords: []string{"useless"}, }) port := common.PickPort("tcp", "127.0.0.1") ctx = config.WithConfig(ctx, Name, &Config{ APIConfig{ Enabled: true, APIHost: "127.0.0.1", APIPort: port, }, }) auth, err := memory.NewAuthenticator(ctx) common.Must(err) go RunClientAPI(ctx, auth) time.Sleep(time.Second * 3) common.Must(auth.AddUser("hash1234")) valid, user := auth.AuthUser("hash1234") if !valid { t.Fail() } user.AddTraffic(1234, 5678) time.Sleep(time.Second) conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithInsecure()) common.Must(err) client := NewTrojanClientServiceClient(conn) resp, err := client.GetTraffic(ctx, &GetTrafficRequest{User: &User{ Hash: "hash1234", }}) common.Must(err) if resp.TrafficTotal.DownloadTraffic != 5678 || resp.TrafficTotal.UploadTraffic != 1234 { t.Fail() } _, err = client.GetTraffic(ctx, &GetTrafficRequest{}) if err == nil { t.Fail() } cancel() } ================================================ FILE: api/service/config.go ================================================ package service import "github.com/p4gefau1t/trojan-go/config" const Name = "API_SERVICE" type SSLConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` CertPath string `json:"cert" yaml:"cert"` KeyPath string `json:"key" yaml:"key"` VerifyClient bool `json:"verify_client" yaml:"verify-client"` ClientCertPath []string `json:"client_cert" yaml:"client-cert"` } type APIConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` APIHost string `json:"api_addr" yaml:"api-addr"` APIPort int `json:"api_port" yaml:"api-port"` SSL SSLConfig `json:"ssl" yaml:"ssl"` } type Config struct { API APIConfig `json:"api" yaml:"api"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(Config) }) } ================================================ FILE: api/service/gen.sh ================================================ #!/usr/bin/env bash echo "Processing..." GOPATH=${GOPATH:-$(go env GOPATH)} GOBIN=${GOBIN:-$(go env GOBIN)} if [[ $GOBIN == "" ]]; then GOBIN=${GOPATH}/bin fi go install -v google.golang.org/protobuf/cmd/protoc-gen-go@latest go install -v google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest echo "Use protoc-gen-go and protoc-gen-go-grpc in $GOBIN." protoc --go_out=. \ --go_opt=paths=source_relative \ --go-grpc_out=. \ --go-grpc_opt=paths=source_relative \ --plugin=protoc-gen-go=${GOBIN}/protoc-gen-go \ --plugin=protoc-gen-go-grpc=${GOBIN}/protoc-gen-go-grpc \ api.proto if [ $? -eq 0 ]; then echo "Generated successfully." fi ================================================ FILE: api/service/server.go ================================================ package service import ( "context" "crypto/tls" "crypto/x509" "io" "io/ioutil" "net" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "github.com/p4gefau1t/trojan-go/api" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/statistic" "github.com/p4gefau1t/trojan-go/tunnel/trojan" ) type ServerAPI struct { TrojanServerServiceServer auth statistic.Authenticator } func (s *ServerAPI) GetUsers(stream TrojanServerService_GetUsersServer) error { log.Debug("API: GetUsers") for { req, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } if req.User == nil { return common.NewError("user is unspecified") } if req.User.Hash == "" { req.User.Hash = common.SHA224String(req.User.Password) } valid, user := s.auth.AuthUser(req.User.Hash) if !valid { stream.Send(&GetUsersResponse{ Success: false, Info: "invalid user: " + req.User.Hash, }) continue } downloadTraffic, uploadTraffic := user.GetTraffic() downloadSpeed, uploadSpeed := user.GetSpeed() downloadSpeedLimit, uploadSpeedLimit := user.GetSpeedLimit() ipLimit := user.GetIPLimit() ipCurrent := user.GetIP() err = stream.Send(&GetUsersResponse{ Success: true, Status: &UserStatus{ User: req.User, TrafficTotal: &Traffic{ UploadTraffic: uploadTraffic, DownloadTraffic: downloadTraffic, }, SpeedCurrent: &Speed{ DownloadSpeed: downloadSpeed, UploadSpeed: uploadSpeed, }, SpeedLimit: &Speed{ DownloadSpeed: uint64(downloadSpeedLimit), UploadSpeed: uint64(uploadSpeedLimit), }, IpCurrent: int32(ipCurrent), IpLimit: int32(ipLimit), }, }) if err != nil { return err } } } func (s *ServerAPI) SetUsers(stream TrojanServerService_SetUsersServer) error { log.Debug("API: SetUsers") for { req, err := stream.Recv() if err == io.EOF { return nil } if err != nil { return err } if req.Status == nil { return common.NewError("status is unspecified") } if req.Status.User.Hash == "" { req.Status.User.Hash = common.SHA224String(req.Status.User.Password) } switch req.Operation { case SetUsersRequest_Add: if err = s.auth.AddUser(req.Status.User.Hash); err != nil { err = common.NewError("failed to add new user").Base(err) break } if req.Status.SpeedLimit != nil { valid, user := s.auth.AuthUser(req.Status.User.Hash) if !valid { err = common.NewError("failed to auth new user").Base(err) continue } if req.Status.SpeedLimit != nil { user.SetSpeedLimit(int(req.Status.SpeedLimit.DownloadSpeed), int(req.Status.SpeedLimit.UploadSpeed)) } if req.Status.TrafficTotal != nil { user.SetTraffic(req.Status.TrafficTotal.DownloadTraffic, req.Status.TrafficTotal.UploadTraffic) } user.SetIPLimit(int(req.Status.IpLimit)) } case SetUsersRequest_Delete: err = s.auth.DelUser(req.Status.User.Hash) case SetUsersRequest_Modify: valid, user := s.auth.AuthUser(req.Status.User.Hash) if !valid { err = common.NewError("invalid user " + req.Status.User.Hash) } else { if req.Status.SpeedLimit != nil { user.SetSpeedLimit(int(req.Status.SpeedLimit.DownloadSpeed), int(req.Status.SpeedLimit.UploadSpeed)) } if req.Status.TrafficTotal != nil { user.SetTraffic(req.Status.TrafficTotal.DownloadTraffic, req.Status.TrafficTotal.UploadTraffic) } user.SetIPLimit(int(req.Status.IpLimit)) } } if err != nil { stream.Send(&SetUsersResponse{ Success: false, Info: err.Error(), }) continue } stream.Send(&SetUsersResponse{ Success: true, }) } } func (s *ServerAPI) ListUsers(req *ListUsersRequest, stream TrojanServerService_ListUsersServer) error { log.Debug("API: ListUsers") users := s.auth.ListUsers() for _, user := range users { downloadTraffic, uploadTraffic := user.GetTraffic() downloadSpeed, uploadSpeed := user.GetSpeed() downloadSpeedLimit, uploadSpeedLimit := user.GetSpeedLimit() ipLimit := user.GetIPLimit() ipCurrent := user.GetIP() err := stream.Send(&ListUsersResponse{ Status: &UserStatus{ User: &User{ Hash: user.Hash(), }, TrafficTotal: &Traffic{ DownloadTraffic: downloadTraffic, UploadTraffic: uploadTraffic, }, SpeedCurrent: &Speed{ DownloadSpeed: downloadSpeed, UploadSpeed: uploadSpeed, }, SpeedLimit: &Speed{ DownloadSpeed: uint64(downloadSpeedLimit), UploadSpeed: uint64(uploadSpeedLimit), }, IpLimit: int32(ipLimit), IpCurrent: int32(ipCurrent), }, }) if err != nil { return err } } return nil } func newAPIServer(cfg *Config) (*grpc.Server, error) { var server *grpc.Server if cfg.API.SSL.Enabled { log.Info("api tls enabled") keyPair, err := tls.LoadX509KeyPair(cfg.API.SSL.CertPath, cfg.API.SSL.KeyPath) if err != nil { return nil, common.NewError("failed to load key pair").Base(err) } tlsConfig := &tls.Config{ Certificates: []tls.Certificate{keyPair}, } if cfg.API.SSL.VerifyClient { tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert tlsConfig.ClientCAs = x509.NewCertPool() for _, path := range cfg.API.SSL.ClientCertPath { log.Debug("loading client cert: " + path) certBytes, err := ioutil.ReadFile(path) if err != nil { return nil, common.NewError("failed to load cert file").Base(err) } ok := tlsConfig.ClientCAs.AppendCertsFromPEM(certBytes) if !ok { return nil, common.NewError("invalid client cert") } } } creds := credentials.NewTLS(tlsConfig) server = grpc.NewServer(grpc.Creds(creds)) } else { server = grpc.NewServer() } return server, nil } func RunServerAPI(ctx context.Context, auth statistic.Authenticator) error { cfg := config.FromContext(ctx, Name).(*Config) if !cfg.API.Enabled { return nil } service := &ServerAPI{ auth: auth, } server, err := newAPIServer(cfg) if err != nil { return err } defer server.Stop() RegisterTrojanServerServiceServer(server, service) addr, err := net.ResolveIPAddr("ip", cfg.API.APIHost) if err != nil { return common.NewError("api found invalid addr").Base(err) } listener, err := net.Listen("tcp", (&net.TCPAddr{ IP: addr.IP, Port: cfg.API.APIPort, Zone: addr.Zone, }).String()) if err != nil { return common.NewError("server api failed to listen").Base(err) } defer listener.Close() log.Info("server-side api service is listening on", listener.Addr().String()) errChan := make(chan error, 1) go func() { errChan <- server.Serve(listener) }() select { case err := <-errChan: return err case <-ctx.Done(): log.Debug("closed") return nil } } func init() { api.RegisterHandler(trojan.Name+"_SERVER", RunServerAPI) } ================================================ FILE: api/service/server_test.go ================================================ package service import ( "context" "crypto/tls" "crypto/x509" "fmt" "os" "testing" "time" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/statistic/memory" ) func TestServerAPI(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) ctx = config.WithConfig(ctx, memory.Name, &memory.Config{ Passwords: []string{}, }) port := common.PickPort("tcp", "127.0.0.1") ctx = config.WithConfig(ctx, Name, &Config{ APIConfig{ Enabled: true, APIHost: "127.0.0.1", APIPort: port, }, }) auth, err := memory.NewAuthenticator(ctx) common.Must(err) go RunServerAPI(ctx, auth) time.Sleep(time.Second * 3) common.Must(auth.AddUser("hash1234")) _, user := auth.AuthUser("hash1234") conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithInsecure()) common.Must(err) server := NewTrojanServerServiceClient(conn) stream1, err := server.ListUsers(ctx, &ListUsersRequest{}) common.Must(err) for { resp, err := stream1.Recv() if err != nil { break } fmt.Println(resp.Status.User.Hash) if resp.Status.User.Hash != "hash1234" { t.Fail() } fmt.Println(resp.Status.SpeedCurrent) fmt.Println(resp.Status.SpeedLimit) } stream1.CloseSend() user.AddTraffic(1234, 5678) time.Sleep(time.Second * 1) stream2, err := server.GetUsers(ctx) common.Must(err) stream2.Send(&GetUsersRequest{ User: &User{ Hash: "hash1234", }, }) resp2, err := stream2.Recv() common.Must(err) if resp2.Status.TrafficTotal.DownloadTraffic != 1234 || resp2.Status.TrafficTotal.UploadTraffic != 5678 { t.Fatal("wrong traffic") } stream3, err := server.SetUsers(ctx) common.Must(err) stream3.Send(&SetUsersRequest{ Status: &UserStatus{ User: &User{ Hash: "hash1234", }, }, Operation: SetUsersRequest_Delete, }) resp3, err := stream3.Recv() if err != nil || !resp3.Success { t.Fatal("user not exists") } valid, _ := auth.AuthUser("hash1234") if valid { t.Fatal("failed to auth") } stream3.Send(&SetUsersRequest{ Status: &UserStatus{ User: &User{ Hash: "newhash", }, }, Operation: SetUsersRequest_Add, }) resp3, err = stream3.Recv() if err != nil || !resp3.Success { t.Fatal("failed to read") } valid, user = auth.AuthUser("newhash") if !valid { t.Fatal("failed to auth 2") } stream3.Send(&SetUsersRequest{ Status: &UserStatus{ User: &User{ Hash: "newhash", }, SpeedLimit: &Speed{ DownloadSpeed: 5000, UploadSpeed: 3000, }, TrafficTotal: &Traffic{ DownloadTraffic: 1, UploadTraffic: 1, }, }, Operation: SetUsersRequest_Modify, }) go func() { for { select { case <-ctx.Done(): return default: } user.AddTraffic(200, 0) } }() go func() { for { select { case <-ctx.Done(): return default: } user.AddTraffic(0, 300) } }() time.Sleep(time.Second * 3) for i := 0; i < 3; i++ { stream2.Send(&GetUsersRequest{ User: &User{ Hash: "newhash", }, }) resp2, err = stream2.Recv() common.Must(err) fmt.Println(resp2.Status.SpeedCurrent) fmt.Println(resp2.Status.SpeedLimit) time.Sleep(time.Second) } stream2.CloseSend() cancel() } func TestTLSRSA(t *testing.T) { port := common.PickPort("tcp", "127.0.0.1") cfg := &Config{ API: APIConfig{ Enabled: true, APIHost: "127.0.0.1", APIPort: port, SSL: SSLConfig{ Enabled: true, CertPath: "server-rsa2048.crt", KeyPath: "server-rsa2048.key", VerifyClient: false, ClientCertPath: []string{"client-rsa2048.crt"}, }, }, } ctx := config.WithConfig(context.Background(), Name, cfg) ctx = config.WithConfig(ctx, memory.Name, &memory.Config{ Passwords: []string{}, }) auth, err := memory.NewAuthenticator(ctx) common.Must(err) go func() { common.Must(RunServerAPI(ctx, auth)) }() time.Sleep(time.Second) pool := x509.NewCertPool() certBytes, err := os.ReadFile("server-rsa2048.crt") common.Must(err) pool.AppendCertsFromPEM(certBytes) certificate, err := tls.LoadX509KeyPair("client-rsa2048.crt", "client-rsa2048.key") common.Must(err) creds := credentials.NewTLS(&tls.Config{ ServerName: "localhost", RootCAs: pool, Certificates: []tls.Certificate{certificate}, }) conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithTransportCredentials(creds)) common.Must(err) server := NewTrojanServerServiceClient(conn) stream, err := server.ListUsers(ctx, &ListUsersRequest{}) common.Must(err) stream.CloseSend() conn.Close() } func TestTLSECC(t *testing.T) { port := common.PickPort("tcp", "127.0.0.1") cfg := &Config{ API: APIConfig{ Enabled: true, APIHost: "127.0.0.1", APIPort: port, SSL: SSLConfig{ Enabled: true, CertPath: "server-ecc.crt", KeyPath: "server-ecc.key", VerifyClient: false, ClientCertPath: []string{"client-ecc.crt"}, }, }, } ctx := config.WithConfig(context.Background(), Name, cfg) ctx = config.WithConfig(ctx, memory.Name, &memory.Config{ Passwords: []string{}, }) auth, err := memory.NewAuthenticator(ctx) common.Must(err) go func() { common.Must(RunServerAPI(ctx, auth)) }() time.Sleep(time.Second) pool := x509.NewCertPool() certBytes, err := os.ReadFile("server-ecc.crt") common.Must(err) pool.AppendCertsFromPEM(certBytes) certificate, err := tls.LoadX509KeyPair("client-ecc.crt", "client-ecc.key") common.Must(err) creds := credentials.NewTLS(&tls.Config{ ServerName: "localhost", RootCAs: pool, Certificates: []tls.Certificate{certificate}, }) conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%d", port), grpc.WithTransportCredentials(creds)) common.Must(err) server := NewTrojanServerServiceClient(conn) stream, err := server.ListUsers(ctx, &ListUsersRequest{}) common.Must(err) stream.CloseSend() conn.Close() } var serverRSA2048Cert = ` -----BEGIN CERTIFICATE----- MIIC5TCCAc2gAwIBAgIJAJqNVe6g/10vMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV BAMMCWxvY2FsaG9zdDAeFw0yMTA5MTQwNjE1MTFaFw0yNjA5MTMwNjE1MTFaMBQx EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAK7bupJ8tmHM3shQ/7N730jzpRsXdNiBxq/Jxx8j+vB3AcxuP5bjXQZqS6YR 5W5vrfLlegtq1E/mmaI3Ht0RfIlzev04Dua9PWmIQJD801nEPknbfgCLXDh+pYr2 sfg8mUh3LjGtrxyH+nmbTjWg7iWSKohmZ8nUDcX94Llo5FxibMAz8OsAwOmUueCH jP3XswZYHEy+OOP3K0ZEiJy0f5T6ZXk9OWYuPN4VQKJx1qrc9KzZtSPHwqVdkGUi ase9tOPA4aMutzt0btgW7h7UrvG6C1c/Rr1BxdiYq1EQ+yypnAlyToVQSNbo67zz wGQk4GeruIkOgJOLdooN/HjhbHMCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B AQsFAAOCAQEASsBzHHYiWDDiBVWUEwVZAduTrslTLNOxG0QHBKsHWIlz/3QlhQil ywb3OhfMTUR1dMGY5Iq5432QiCHO4IMCOv7tDIkgb4Bc3v/3CRlBlnurtAmUfNJ6 pTRSlK4AjWpGHAEEd/8aCaOE86hMP8WDht8MkJTRrQqpJ1HeDISoKt9nepHOIsj+ I2zLZZtw0pg7FuR4MzWuqOt071iRS46Pupryb3ZEGIWNz5iLrDQod5Iz2ZGSRGqE rB8idX0mlj5AHRRanVR3PAes+eApsW9JvYG/ImuCOs+ZsukY614zQZdR+SyFm85G 4NICyeQsmiypNHHgw+xZmGqZg65bXNGoyg== -----END CERTIFICATE----- ` var serverRSA2048Key = ` -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu27qSfLZhzN7I UP+ze99I86UbF3TYgcavyccfI/rwdwHMbj+W410GakumEeVub63y5XoLatRP5pmi Nx7dEXyJc3r9OA7mvT1piECQ/NNZxD5J234Ai1w4fqWK9rH4PJlIdy4xra8ch/p5 m041oO4lkiqIZmfJ1A3F/eC5aORcYmzAM/DrAMDplLngh4z917MGWBxMvjjj9ytG RIictH+U+mV5PTlmLjzeFUCicdaq3PSs2bUjx8KlXZBlImrHvbTjwOGjLrc7dG7Y Fu4e1K7xugtXP0a9QcXYmKtREPssqZwJck6FUEjW6Ou888BkJOBnq7iJDoCTi3aK Dfx44WxzAgMBAAECggEBAKYhib/H0ZhWB4yWuHqUxG4RXtrAjHlvw5Acy5zgmHiC +Sh7ztrTJf0EXN9pvWwRm1ldgXj7hMBtPaaLbD1pccM9/qo66p17Sq/LjlyyeTOe affOHIbz4Sij2zCOdkR9fr0EztTQScF3yBhl4Aa/4cO8fcCeWxm86WEldq9x4xWJ s5WMR4CnrOJhDINLNPQPKX92KyxEQ/RfuBWovx3M0nl3fcUWfESY134t5g/UBFId In19tZ+pGIpCkxP0U1AZWrlZRA8Q/3sO2orUpoAOdCrGk/DcCTMh0c1pMzbYZ1/i cYXn38MpUo8QeG4FElUhAv6kzeBIl2tRBMVzIigo+AECgYEA3No1rHdFu6Ox9vC8 E93PTZevYVcL5J5yx6x7khCaOLKKuRXpjOX/h3Ll+hlN2DVAg5Jli/JVGCco4GeK kbFLSyxG1+E63JbgsVpaEOgvFT3bHHSPSRJDnIU+WkcNQ2u4Ky5ahZzbNdV+4fj2 NO2iMgkm7hoJANrm3IqqW8epenMCgYEAyq+qdNj5DiDzBcDvLwY+4/QmMOOgDqeh /TzhbDRyr+m4xNT7LLS4s/3wcbkQC33zhMUI3YvOHnYq5Ze/iL/TSloj0QCp1I7L J7sZeM1XimMBQIpCfOC7lf4tU76Fz0DTHAL+CmX1DgmRJdYO09843VsKkscC968R 4cwL5oGxxgECgYAM4TTsH/CTJtLEIfn19qOWVNhHhvoMlSkAeBCkzg8Qa2knrh12 uBsU3SCIW11s1H40rh758GICDJaXr7InGP3ZHnXrNRlnr+zeqvRBtCi6xma23B1X F5eV0zd1sFsXqXqOGh/xVtp54z+JEinZoForLNl2XVJVGG8KQZP50kUR/QKBgH4O 8zzpFT0sUPlrHVdp0wODfZ06dPmoWJ9flfPuSsYN3tTMgcs0Owv3C+wu5UPAegxB X1oq8W8Qn21cC8vJQmgj19LNTtLcXI3BV/5B+Aghu02gr+lq/EA1bYuAG0jjUGlD kyx0bQzl9lhJ4b70PjGtxc2z6KyTPdPpTB143FABAoGAQDoIUdc77/IWcjzcaXeJ 8abak5rAZA7cu2g2NVfs+Km+njsB0pbTwMnV1zGoFABdaHLdqbthLWtX7WOb1PDD MQ+kbiLw5uj8IY2HEqJhDGGEdXBqxbW7kyuIAN9Mw+mwKzkikNcFQdxgchWH1d1o lVkr92iEX+IhIeYb4DN1vQw= -----END PRIVATE KEY----- ` var clientRSA2048Cert = ` -----BEGIN CERTIFICATE----- MIIC5TCCAc2gAwIBAgIJAKD1wSl+Mnk7MA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV BAMMCWxvY2FsaG9zdDAeFw0yMTA5MTQwNjE2MDBaFw0yNjA5MTMwNjE2MDBaMBQx EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAJeDu630louuf2V4sw396cGiAnxmTseVRMG+m3PnZ831puAsApm3IWSEcOqI UMk6s1pgSLysg6GxRZhX4L/ljErjMO4+y8riZjqqR0wd0GnhuNxuXaSUsEmKdDZb cICqXkZeZRn4jw/7L0xgdAdM2w3LXR6aq6CwveFY3/JncEZFQHH5mnorZdpbheR6 rhvIL6AAI0YEY9uzuQBSrzOml3f7D+x5Xll14HoMN0kCysWt8jSP/An5yP8pL5RO pn5kNBc8Bx8lykuV1uS8ogncSM7JzmpP1SeAViOq8CqXlJtUbUqVPckMmdfMMtbI qIO7R5/8imrdhLMi25fAOnfmDzcCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B AQsFAAOCAQEAFu2QPE3x1Sm3SfnHzAhvdjviYkbWvM8rQziIlIevbvA9Nl+vxDBf N5aRR6Hpxq02J2G/w7tzrKB9IluWdMU1+tilph5bCnwx3QUh/GR4oTsFiTvTZ5br SNf3xfTyIsL+Hf6iLvEgSt15ziY/334wu9NmQrU0FNZ+Lcc7Mx0OgvuP9Zim+6oo /FW80R3pUSzUZcUQgsI4Sz7/6nJTxhsc+kqtnOXIQLPC9GA06kP8eN6XjTsavP7f eZq/yozddOk0dqx8uwmKUOb1Rg+pS8VIhQBRv3UPb4L/07AWSTZSMZLf1+CMgzMY Jtsxa1MLqPkB7fiAR6SFUFW7Q36gDp/Mdw== -----END CERTIFICATE----- ` var clientRSA2048Key = ` -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCXg7ut9JaLrn9l eLMN/enBogJ8Zk7HlUTBvptz52fN9abgLAKZtyFkhHDqiFDJOrNaYEi8rIOhsUWY V+C/5YxK4zDuPsvK4mY6qkdMHdBp4bjcbl2klLBJinQ2W3CAql5GXmUZ+I8P+y9M YHQHTNsNy10emqugsL3hWN/yZ3BGRUBx+Zp6K2XaW4Xkeq4byC+gACNGBGPbs7kA Uq8zppd3+w/seV5ZdeB6DDdJAsrFrfI0j/wJ+cj/KS+UTqZ+ZDQXPAcfJcpLldbk vKIJ3EjOyc5qT9UngFYjqvAql5SbVG1KlT3JDJnXzDLWyKiDu0ef/Ipq3YSzItuX wDp35g83AgMBAAECggEASOv4CjMrubKUUgwTcWqBdNY6iBDdXaVz4COSweffx/qx BDdqUP0Yrz4m8loFN7Ru2dJ5b4VAHTQqoLW6z+D08p4B0MicYNsyBI4rnnDC/BLN XBoqK6n8Zoiigf7kWKimkwufcS51/GUSUJojfdf5ndwAx1f9vmsSGEEkF5C9MrQo Sa4eyySuXuDS9swrXuTn9FcVcbUoIblgL8GIlmX4S1Xl9NaVS/VAVt42FgpNSYy7 2Qf9Medg3ApimjwkLiDSh0RElirlUwzSg9dx2U+hHwWQWimb2AhA85uK3bpFB/x9 b2agS1uxTar4mk4LFppQqVUuXlpj2hW7HxTdcxGHYQKBgQDGA+Wv3b8FMeszYqqx BbI5+FeXmQ5AoYNdHyPfCH8f2LX1FTnnQbUvFMJ3UQZl/GGHocgyFNvfDo6YyYsg 2XgcNO/JWMbKEw0HkfMgkaIa3Jfq/PTB386NhqBq5FHiBUrvSHzhaIzpuaokBrRk jFlcqONK+uj77iRgci4wR59POQKBgQDD4fDZzOGQCy/AWrMX/adk8ymoxhWQj6PN zhy2GZo9jGwyskQr5neIDQtxRbgokMspxpyPdQG2SxbG/zygyFEaCsp/Xb3iB3aA 2dcktkV9agx+g1WrflTpGG9quW5vQJgQv9FtlVXJdiihN9CQvXAcYRjUA/zkadIL GsrVF9rh7wKBgFoMLaiDU7neEJKGnQ7xgzI/kD29ebDEgkOXxK1JZN4ro9t3MqTK ycVGUIUIELvSQNv4I107BR3ztb8fcCiZHLjfDehnecctULCPm5vE/o3uoRtYu0lr KLhNb6gMenwpYgFc2oV7ERG8v/WwItrSxFSR7QMNBWSD0IEXi4+jEnxpAoGAMJTS 5VG5B76egzh7fpG8eH8Ob/tg0c+uMpbR7CABbw5qr1AjNDgeoTGLCvbdq8HtgVju 7213lTyeU5Bt+vpzkt/mRRx8wZhUPbTJdSN3rJkmrCHql3Pnn0AeMfv3dcQxcsYA LQuCkUqq3QE4yw0QxxkVzU+H4yaTn4lvkNYvxSUCgYEAgD85MM3DaiNcrevQv6Wb vSo7jBhd8Q/NawH53V32eIlF9Kkm2mqTmPIQ2OxOtdZ3xm+JmPd20jEVg7NcPL58 t6BoSH10pLaI0CP2NdcYfJTIiHAhuQe7PxPCA0nlHTXgSv97QR7y4qMhbf2j1umc hKym0tdk2QjR3qqglaD572A= -----END PRIVATE KEY----- ` var serverECCCert = ` -----BEGIN CERTIFICATE----- MIICTDCCAfKgAwIBAgIQDtCrO8cNST2eY2tA/AGrsDAKBggqhkjOPQQDAjBeMQsw CQYDVQQGEwJDTjEOMAwGA1UEChMFTXlTU0wxKzApBgNVBAsTIk15U1NMIFRlc3Qg RUNDIC0gRm9yIHRlc3QgdXNlIG9ubHkxEjAQBgNVBAMTCU15U1NMLmNvbTAeFw0y MTA5MTQwNjQ1MzNaFw0yNjA5MTMwNjQ1MzNaMCExCzAJBgNVBAYTAkNOMRIwEAYD VQQDEwlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASvYy/r7XR1 Y39lC2JpRJh582zR2CTNynbuolK9a1jsbXaZv+hpBlHkgzMHsWu7LY9Pnb/Dbp4i 1lRASOddD/rLo4HOMIHLMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEF BQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgwFoAUWxGyVxD0fBhTy3tH4eKznRFXFCYw YwYIKwYBBQUHAQEEVzBVMCEGCCsGAQUFBzABhhVodHRwOi8vb2NzcC5teXNzbC5j b20wMAYIKwYBBQUHMAKGJGh0dHA6Ly9jYS5teXNzbC5jb20vbXlzc2x0ZXN0ZWNj LmNydDAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIgDQUa GEdmKstLMHUmmPMGm/P9S4vvSZV2VHsb3+AEyIUCIQCdJpbyTCz+mEyskhwrGOw/ blh3WBONv6MBtqPpmgE1AQ== -----END CERTIFICATE----- ` var serverECCKey = ` -----BEGIN EC PRIVATE KEY----- MHcCAQEEIB8G2suYKuBLoodNIwRMp3JPN1fcZxCt3kcOYIx4nbcPoAoGCCqGSM49 AwEHoUQDQgAEr2Mv6+10dWN/ZQtiaUSYefNs0dgkzcp27qJSvWtY7G12mb/oaQZR 5IMzB7Fruy2PT52/w26eItZUQEjnXQ/6yw== -----END EC PRIVATE KEY----- ` var clientECCCert = ` -----BEGIN CERTIFICATE----- MIICTDCCAfKgAwIBAgIQb5FLuCggTiWFtt/dPh+2bTAKBggqhkjOPQQDAjBeMQsw CQYDVQQGEwJDTjEOMAwGA1UEChMFTXlTU0wxKzApBgNVBAsTIk15U1NMIFRlc3Qg RUNDIC0gRm9yIHRlc3QgdXNlIG9ubHkxEjAQBgNVBAMTCU15U1NMLmNvbTAeFw0y MTA5MTQwNjQ2MTFaFw0yNjA5MTMwNjQ2MTFaMCExCzAJBgNVBAYTAkNOMRIwEAYD VQQDEwlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQME+DnYtcK lbcZmc33oEtoeRWH61DYdl4ei/bM+vkv01MkBB+YTZl0yofJIFJYsfU5pMFK+uyw D4qdcklKPGIKo4HOMIHLMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEF BQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgwFoAUWxGyVxD0fBhTy3tH4eKznRFXFCYw YwYIKwYBBQUHAQEEVzBVMCEGCCsGAQUFBzABhhVodHRwOi8vb2NzcC5teXNzbC5j b20wMAYIKwYBBQUHMAKGJGh0dHA6Ly9jYS5teXNzbC5jb20vbXlzc2x0ZXN0ZWNj LmNydDAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIgfjaU sCfgklPsjHzs3fUtSRGfWoRLFsRBO66RtHJSzrYCIQDAxgx0FB0mAXbflj4mXJVA 9/SjaCI40D6MhMnJhQS7Zg== -----END CERTIFICATE----- ` var clientECCKey = ` -----BEGIN EC PRIVATE KEY----- MHcCAQEEINgbap6WOGXm8V+ghIyporGcGWnggbjjP9xNsxhn+0sqoAoGCCqGSM49 AwEHoUQDQgAEDBPg52LXCpW3GZnN96BLaHkVh+tQ2HZeHov2zPr5L9NTJAQfmE2Z dMqHySBSWLH1OaTBSvrssA+KnXJJSjxiCg== -----END EC PRIVATE KEY----- ` func init() { os.WriteFile("server-rsa2048.crt", []byte(serverRSA2048Cert), 0o777) os.WriteFile("server-rsa2048.key", []byte(serverRSA2048Key), 0o777) os.WriteFile("client-rsa2048.crt", []byte(clientRSA2048Cert), 0o777) os.WriteFile("client-rsa2048.key", []byte(clientRSA2048Key), 0o777) os.WriteFile("server-ecc.crt", []byte(serverECCCert), 0o777) os.WriteFile("server-ecc.key", []byte(serverECCKey), 0o777) os.WriteFile("client-ecc.crt", []byte(clientECCCert), 0o777) os.WriteFile("client-ecc.key", []byte(clientECCKey), 0o777) } ================================================ FILE: common/common.go ================================================ package common import ( "crypto/sha256" "fmt" "os" "path/filepath" "github.com/p4gefau1t/trojan-go/log" ) type Runnable interface { Run() error Close() error } func SHA224String(password string) string { hash := sha256.New224() hash.Write([]byte(password)) val := hash.Sum(nil) str := "" for _, v := range val { str += fmt.Sprintf("%02x", v) } return str } func GetProgramDir() string { dir, err := filepath.Abs(filepath.Dir(os.Args[0])) if err != nil { log.Fatal(err) } return dir } func GetAssetLocation(file string) string { if filepath.IsAbs(file) { return file } if loc := os.Getenv("TROJAN_GO_LOCATION_ASSET"); loc != "" { absPath, err := filepath.Abs(loc) if err != nil { log.Fatal(err) } log.Debugf("env set: TROJAN_GO_LOCATION_ASSET=%s", absPath) return filepath.Join(absPath, file) } return filepath.Join(GetProgramDir(), file) } ================================================ FILE: common/error.go ================================================ package common import ( "fmt" ) type Error struct { info string } func (e *Error) Error() string { return e.info } func (e *Error) Base(err error) *Error { if err != nil { e.info += " | " + err.Error() } return e } func NewError(info string) *Error { return &Error{ info: info, } } func Must(err error) { if err != nil { fmt.Println(err) panic(err) } } func Must2(_ interface{}, err error) { if err != nil { fmt.Println(err) panic(err) } } ================================================ FILE: common/geodata/cache.go ================================================ package geodata import ( "io/ioutil" "strings" v2router "github.com/v2fly/v2ray-core/v4/app/router" "google.golang.org/protobuf/proto" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" ) type geoipCache map[string]*v2router.GeoIP func (g geoipCache) Has(key string) bool { return !(g.Get(key) == nil) } func (g geoipCache) Get(key string) *v2router.GeoIP { if g == nil { return nil } return g[key] } func (g geoipCache) Set(key string, value *v2router.GeoIP) { if g == nil { g = make(map[string]*v2router.GeoIP) } g[key] = value } func (g geoipCache) Unmarshal(filename, code string) (*v2router.GeoIP, error) { asset := common.GetAssetLocation(filename) idx := strings.ToLower(asset + ":" + code) if g.Has(idx) { log.Debugf("geoip cache HIT: %s -> %s", code, idx) return g.Get(idx), nil } geoipBytes, err := Decode(asset, code) switch err { case nil: var geoip v2router.GeoIP if err := proto.Unmarshal(geoipBytes, &geoip); err != nil { return nil, err } g.Set(idx, &geoip) return &geoip, nil case ErrCodeNotFound: return nil, common.NewError("country code " + code + " not found in " + filename) case ErrFailedToReadBytes, ErrFailedToReadExpectedLenBytes, ErrInvalidGeodataFile, ErrInvalidGeodataVarintLength: log.Warnf("failed to decode geoip file: %s, fallback to the original ReadFile method", filename) geoipBytes, err = ioutil.ReadFile(asset) if err != nil { return nil, err } var geoipList v2router.GeoIPList if err := proto.Unmarshal(geoipBytes, &geoipList); err != nil { return nil, err } for _, geoip := range geoipList.GetEntry() { if strings.EqualFold(code, geoip.GetCountryCode()) { g.Set(idx, geoip) return geoip, nil } } default: return nil, err } return nil, common.NewError("country code " + code + " not found in " + filename) } type geositeCache map[string]*v2router.GeoSite func (g geositeCache) Has(key string) bool { return !(g.Get(key) == nil) } func (g geositeCache) Get(key string) *v2router.GeoSite { if g == nil { return nil } return g[key] } func (g geositeCache) Set(key string, value *v2router.GeoSite) { if g == nil { g = make(map[string]*v2router.GeoSite) } g[key] = value } func (g geositeCache) Unmarshal(filename, code string) (*v2router.GeoSite, error) { asset := common.GetAssetLocation(filename) idx := strings.ToLower(asset + ":" + code) if g.Has(idx) { log.Debugf("geosite cache HIT: %s -> %s", code, idx) return g.Get(idx), nil } geositeBytes, err := Decode(asset, code) switch err { case nil: var geosite v2router.GeoSite if err := proto.Unmarshal(geositeBytes, &geosite); err != nil { return nil, err } g.Set(idx, &geosite) return &geosite, nil case ErrCodeNotFound: return nil, common.NewError("list " + code + " not found in " + filename) case ErrFailedToReadBytes, ErrFailedToReadExpectedLenBytes, ErrInvalidGeodataFile, ErrInvalidGeodataVarintLength: log.Warnf("failed to decode geoip file: %s, fallback to the original ReadFile method", filename) geositeBytes, err = ioutil.ReadFile(asset) if err != nil { return nil, err } var geositeList v2router.GeoSiteList if err := proto.Unmarshal(geositeBytes, &geositeList); err != nil { return nil, err } for _, geosite := range geositeList.GetEntry() { if strings.EqualFold(code, geosite.GetCountryCode()) { g.Set(idx, geosite) return geosite, nil } } default: return nil, err } return nil, common.NewError("list " + code + " not found in " + filename) } ================================================ FILE: common/geodata/decode.go ================================================ // Package geodata includes utilities to decode and parse the geoip & geosite dat files. // // It relies on the proto structure of GeoIP, GeoIPList, GeoSite and GeoSiteList in // github.com/v2fly/v2ray-core/v4/app/router/config.proto to comply with following rules: // // 1. GeoIPList and GeoSiteList cannot be changed // 2. The country_code in GeoIP and GeoSite must be // a length-delimited `string`(wired type) and has field_number set to 1 // package geodata import ( "errors" "io" "os" "strings" "google.golang.org/protobuf/encoding/protowire" ) var ( ErrFailedToReadBytes = errors.New("failed to read bytes") ErrFailedToReadExpectedLenBytes = errors.New("failed to read expected length of bytes") ErrInvalidGeodataFile = errors.New("invalid geodata file") ErrInvalidGeodataVarintLength = errors.New("invalid geodata varint length") ErrCodeNotFound = errors.New("code not found") ) func EmitBytes(f io.ReadSeeker, code string) ([]byte, error) { count := 1 isInner := false tempContainer := make([]byte, 0, 5) var result []byte var advancedN uint64 = 1 var geoDataVarintLength, codeVarintLength, varintLenByteLen uint64 = 0, 0, 0 Loop: for { container := make([]byte, advancedN) bytesRead, err := f.Read(container) if err == io.EOF { return nil, ErrCodeNotFound } if err != nil { return nil, ErrFailedToReadBytes } if bytesRead != len(container) { return nil, ErrFailedToReadExpectedLenBytes } switch count { case 1, 3: // data type ((field_number << 3) | wire_type) if container[0] != 10 { // byte `0A` equals to `10` in decimal return nil, ErrInvalidGeodataFile } advancedN = 1 count++ case 2, 4: // data length tempContainer = append(tempContainer, container...) if container[0] > 127 { // max one-byte-length byte `7F`(0FFF FFFF) equals to `127` in decimal advancedN = 1 goto Loop } lenVarint, n := protowire.ConsumeVarint(tempContainer) if n < 0 { return nil, ErrInvalidGeodataVarintLength } tempContainer = nil if !isInner { isInner = true geoDataVarintLength = lenVarint advancedN = 1 } else { isInner = false codeVarintLength = lenVarint varintLenByteLen = uint64(n) advancedN = codeVarintLength } count++ case 5: // data value if strings.EqualFold(string(container), code) { count++ offset := -(1 + int64(varintLenByteLen) + int64(codeVarintLength)) f.Seek(offset, 1) // back to the start of GeoIP or GeoSite varint advancedN = geoDataVarintLength // the number of bytes to be read in next round } else { count = 1 offset := int64(geoDataVarintLength) - int64(codeVarintLength) - int64(varintLenByteLen) - 1 f.Seek(offset, 1) // skip the unmatched GeoIP or GeoSite varint advancedN = 1 // the next round will be the start of another GeoIPList or GeoSiteList } case 6: // matched GeoIP or GeoSite varint result = container break Loop } } return result, nil } func Decode(filename, code string) ([]byte, error) { f, err := os.Open(filename) if err != nil { return nil, err } defer f.Close() geoBytes, err := EmitBytes(f, code) if err != nil { return nil, err } return geoBytes, nil } ================================================ FILE: common/geodata/decode_test.go ================================================ package geodata_test import ( "bytes" "errors" "io/fs" "os" "path/filepath" "runtime" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/common/geodata" ) func init() { const ( geoipURL = "https://raw.githubusercontent.com/v2fly/geoip/release/geoip.dat" geositeURL = "https://raw.githubusercontent.com/v2fly/domain-list-community/release/dlc.dat" ) wd, err := os.Getwd() common.Must(err) tempPath := filepath.Join(wd, "..", "..", "test", "temp") os.Setenv("TROJAN_GO_LOCATION_ASSET", tempPath) geoipPath := common.GetAssetLocation("geoip.dat") geositePath := common.GetAssetLocation("geosite.dat") if _, err := os.Stat(geoipPath); err != nil && errors.Is(err, fs.ErrNotExist) { common.Must(os.MkdirAll(tempPath, 0o755)) geoipBytes, err := common.FetchHTTPContent(geoipURL) common.Must(err) common.Must(common.WriteFile(geoipPath, geoipBytes)) } if _, err := os.Stat(geositePath); err != nil && errors.Is(err, fs.ErrNotExist) { common.Must(os.MkdirAll(tempPath, 0o755)) geositeBytes, err := common.FetchHTTPContent(geositeURL) common.Must(err) common.Must(common.WriteFile(geositePath, geositeBytes)) } } func TestDecodeGeoIP(t *testing.T) { filename := common.GetAssetLocation("geoip.dat") result, err := geodata.Decode(filename, "test") if err != nil { t.Error(err) } expected := []byte{10, 4, 84, 69, 83, 84, 18, 8, 10, 4, 127, 0, 0, 0, 16, 8} if !bytes.Equal(result, expected) { t.Errorf("failed to load geoip:test, expected: %v, got: %v", expected, result) } } func TestDecodeGeoSite(t *testing.T) { filename := common.GetAssetLocation("geosite.dat") result, err := geodata.Decode(filename, "test") if err != nil { t.Error(err) } expected := []byte{10, 4, 84, 69, 83, 84, 18, 20, 8, 3, 18, 16, 116, 101, 115, 116, 46, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109} if !bytes.Equal(result, expected) { t.Errorf("failed to load geosite:test, expected: %v, got: %v", expected, result) } } func BenchmarkLoadGeoIP(b *testing.B) { m1 := runtime.MemStats{} m2 := runtime.MemStats{} loader := geodata.NewGeodataLoader() runtime.ReadMemStats(&m1) cn, _ := loader.LoadGeoIP("cn") private, _ := loader.LoadGeoIP("private") runtime.KeepAlive(cn) runtime.KeepAlive(private) runtime.ReadMemStats(&m2) b.ReportMetric(float64(m2.Alloc-m1.Alloc)/1024, "KiB(GeoIP-Alloc)") b.ReportMetric(float64(m2.TotalAlloc-m1.TotalAlloc)/1024/1024, "MiB(GeoIP-TotalAlloc)") } func BenchmarkLoadGeoSite(b *testing.B) { m3 := runtime.MemStats{} m4 := runtime.MemStats{} loader := geodata.NewGeodataLoader() runtime.ReadMemStats(&m3) cn, _ := loader.LoadGeoSite("cn") notcn, _ := loader.LoadGeoSite("geolocation-!cn") private, _ := loader.LoadGeoSite("private") runtime.KeepAlive(cn) runtime.KeepAlive(notcn) runtime.KeepAlive(private) runtime.ReadMemStats(&m4) b.ReportMetric(float64(m4.Alloc-m3.Alloc)/1024/1024, "MiB(GeoSite-Alloc)") b.ReportMetric(float64(m4.TotalAlloc-m3.TotalAlloc)/1024/1024, "MiB(GeoSite-TotalAlloc)") } ================================================ FILE: common/geodata/interface.go ================================================ package geodata import v2router "github.com/v2fly/v2ray-core/v4/app/router" type GeodataLoader interface { LoadIP(filename, country string) ([]*v2router.CIDR, error) LoadSite(filename, list string) ([]*v2router.Domain, error) LoadGeoIP(country string) ([]*v2router.CIDR, error) LoadGeoSite(list string) ([]*v2router.Domain, error) } ================================================ FILE: common/geodata/loader.go ================================================ package geodata import ( "runtime" v2router "github.com/v2fly/v2ray-core/v4/app/router" ) type geodataCache struct { geoipCache geositeCache } func NewGeodataLoader() GeodataLoader { return &geodataCache{ make(map[string]*v2router.GeoIP), make(map[string]*v2router.GeoSite), } } func (g *geodataCache) LoadIP(filename, country string) ([]*v2router.CIDR, error) { geoip, err := g.geoipCache.Unmarshal(filename, country) if err != nil { return nil, err } runtime.GC() return geoip.Cidr, nil } func (g *geodataCache) LoadSite(filename, list string) ([]*v2router.Domain, error) { geosite, err := g.geositeCache.Unmarshal(filename, list) if err != nil { return nil, err } runtime.GC() return geosite.Domain, nil } func (g *geodataCache) LoadGeoIP(country string) ([]*v2router.CIDR, error) { return g.LoadIP("geoip.dat", country) } func (g *geodataCache) LoadGeoSite(list string) ([]*v2router.Domain, error) { return g.LoadSite("geosite.dat", list) } ================================================ FILE: common/io.go ================================================ package common import ( "io" "net" "sync" "github.com/p4gefau1t/trojan-go/log" ) type RewindReader struct { mu sync.Mutex rawReader io.Reader buf []byte bufReadIdx int rewound bool buffering bool bufferSize int } func (r *RewindReader) Read(p []byte) (int, error) { r.mu.Lock() defer r.mu.Unlock() if r.rewound { if len(r.buf) > r.bufReadIdx { n := copy(p, r.buf[r.bufReadIdx:]) r.bufReadIdx += n return n, nil } r.rewound = false // all buffering content has been read } n, err := r.rawReader.Read(p) if r.buffering { r.buf = append(r.buf, p[:n]...) if len(r.buf) > r.bufferSize*2 { log.Debug("read too many bytes!") } } return n, err } func (r *RewindReader) ReadByte() (byte, error) { buf := [1]byte{} _, err := r.Read(buf[:]) return buf[0], err } func (r *RewindReader) Discard(n int) (int, error) { buf := [128]byte{} if n < 128 { return r.Read(buf[:n]) } for discarded := 0; discarded+128 < n; discarded += 128 { _, err := r.Read(buf[:]) if err != nil { return discarded, err } } if rest := n % 128; rest != 0 { return r.Read(buf[:rest]) } return n, nil } func (r *RewindReader) Rewind() { r.mu.Lock() if r.bufferSize == 0 { panic("no buffer") } r.rewound = true r.bufReadIdx = 0 r.mu.Unlock() } func (r *RewindReader) StopBuffering() { r.mu.Lock() r.buffering = false r.mu.Unlock() } func (r *RewindReader) SetBufferSize(size int) { r.mu.Lock() if size == 0 { // disable buffering if !r.buffering { panic("reader is disabled") } r.buffering = false r.buf = nil r.bufReadIdx = 0 r.bufferSize = 0 } else { if r.buffering { panic("reader is buffering") } r.buffering = true r.bufReadIdx = 0 r.bufferSize = size r.buf = make([]byte, 0, size) } r.mu.Unlock() } type RewindConn struct { net.Conn *RewindReader } func (c *RewindConn) Read(p []byte) (int, error) { return c.RewindReader.Read(p) } func NewRewindConn(conn net.Conn) *RewindConn { return &RewindConn{ Conn: conn, RewindReader: &RewindReader{ rawReader: conn, }, } } type StickyWriter struct { rawWriter io.Writer writeBuffer []byte MaxBuffered int } func (w *StickyWriter) Write(p []byte) (int, error) { if w.MaxBuffered > 0 { w.MaxBuffered-- w.writeBuffer = append(w.writeBuffer, p...) if w.MaxBuffered != 0 { return len(p), nil } w.MaxBuffered = 0 _, err := w.rawWriter.Write(w.writeBuffer) w.writeBuffer = nil return len(p), err } return w.rawWriter.Write(p) } ================================================ FILE: common/io_test.go ================================================ package common import ( "bytes" "crypto/rand" "testing" "github.com/v2fly/v2ray-core/v4/common" ) func TestBufferedReader(t *testing.T) { payload := [1024]byte{} rand.Reader.Read(payload[:]) rawReader := bytes.NewBuffer(payload[:]) r := RewindReader{ rawReader: rawReader, } r.SetBufferSize(2048) buf1 := make([]byte, 512) buf2 := make([]byte, 512) common.Must2(r.Read(buf1)) r.Rewind() common.Must2(r.Read(buf2)) if !bytes.Equal(buf1, buf2) { t.Fail() } buf3 := make([]byte, 512) common.Must2(r.Read(buf3)) if !bytes.Equal(buf3, payload[512:]) { t.Fail() } r.Rewind() buf4 := make([]byte, 1024) common.Must2(r.Read(buf4)) if !bytes.Equal(payload[:], buf4) { t.Fail() } } ================================================ FILE: common/net.go ================================================ package common import ( "fmt" "io" "io/ioutil" "net" "net/http" "net/url" "os" "strconv" "strings" "time" ) const ( KiB = 1024 MiB = KiB * 1024 GiB = MiB * 1024 ) func HumanFriendlyTraffic(bytes uint64) string { if bytes <= KiB { return fmt.Sprintf("%d B", bytes) } if bytes <= MiB { return fmt.Sprintf("%.2f KiB", float32(bytes)/KiB) } if bytes <= GiB { return fmt.Sprintf("%.2f MiB", float32(bytes)/MiB) } return fmt.Sprintf("%.2f GiB", float32(bytes)/GiB) } func PickPort(network string, host string) int { switch network { case "tcp": for retry := 0; retry < 16; retry++ { l, err := net.Listen("tcp", host+":0") if err != nil { continue } defer l.Close() _, port, err := net.SplitHostPort(l.Addr().String()) Must(err) p, err := strconv.ParseInt(port, 10, 32) Must(err) return int(p) } case "udp": for retry := 0; retry < 16; retry++ { conn, err := net.ListenPacket("udp", host+":0") if err != nil { continue } defer conn.Close() _, port, err := net.SplitHostPort(conn.LocalAddr().String()) Must(err) p, err := strconv.ParseInt(port, 10, 32) Must(err) return int(p) } default: return 0 } return 0 } func WriteAllBytes(writer io.Writer, payload []byte) error { for len(payload) > 0 { n, err := writer.Write(payload) if err != nil { return err } payload = payload[n:] } return nil } func WriteFile(path string, payload []byte) error { writer, err := os.Create(path) if err != nil { return err } defer writer.Close() return WriteAllBytes(writer, payload) } func FetchHTTPContent(target string) ([]byte, error) { parsedTarget, err := url.Parse(target) if err != nil { return nil, fmt.Errorf("invalid URL: %s", target) } if s := strings.ToLower(parsedTarget.Scheme); s != "http" && s != "https" { return nil, fmt.Errorf("invalid scheme: %s", parsedTarget.Scheme) } client := &http.Client{ Timeout: 30 * time.Second, } resp, err := client.Do(&http.Request{ Method: "GET", URL: parsedTarget, Close: true, }) if err != nil { return nil, fmt.Errorf("failed to dial to %s", target) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("unexpected HTTP status code: %d", resp.StatusCode) } content, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("failed to read HTTP response") } return content, nil } ================================================ FILE: common/sync.go ================================================ package common // Notifier is a utility for notifying changes. The change producer may notify changes multiple time, and the consumer may get notified asynchronously. type Notifier struct { c chan struct{} } // NewNotifier creates a new Notifier. func NewNotifier() *Notifier { return &Notifier{ c: make(chan struct{}, 1), } } // Signal signals a change, usually by producer. This method never blocks. func (n *Notifier) Signal() { select { case n.c <- struct{}{}: default: } } // Wait returns a channel for waiting for changes. The returned channel never gets closed. func (n *Notifier) Wait() <-chan struct{} { return n.c } ================================================ FILE: component/api.go ================================================ //go:build api || full // +build api full package build import ( _ "github.com/p4gefau1t/trojan-go/api/control" _ "github.com/p4gefau1t/trojan-go/api/service" ) ================================================ FILE: component/base.go ================================================ package build import ( _ "github.com/p4gefau1t/trojan-go/log/golog" _ "github.com/p4gefau1t/trojan-go/statistic/memory" _ "github.com/p4gefau1t/trojan-go/version" ) ================================================ FILE: component/client.go ================================================ //go:build client || full || mini // +build client full mini package build import ( _ "github.com/p4gefau1t/trojan-go/proxy/client" ) ================================================ FILE: component/custom.go ================================================ //go:build custom || full // +build custom full package build import ( _ "github.com/p4gefau1t/trojan-go/proxy/custom" _ "github.com/p4gefau1t/trojan-go/tunnel/adapter" _ "github.com/p4gefau1t/trojan-go/tunnel/dokodemo" _ "github.com/p4gefau1t/trojan-go/tunnel/freedom" _ "github.com/p4gefau1t/trojan-go/tunnel/http" _ "github.com/p4gefau1t/trojan-go/tunnel/mux" _ "github.com/p4gefau1t/trojan-go/tunnel/router" _ "github.com/p4gefau1t/trojan-go/tunnel/shadowsocks" _ "github.com/p4gefau1t/trojan-go/tunnel/simplesocks" _ "github.com/p4gefau1t/trojan-go/tunnel/socks" _ "github.com/p4gefau1t/trojan-go/tunnel/tls" _ "github.com/p4gefau1t/trojan-go/tunnel/tproxy" _ "github.com/p4gefau1t/trojan-go/tunnel/transport" _ "github.com/p4gefau1t/trojan-go/tunnel/trojan" _ "github.com/p4gefau1t/trojan-go/tunnel/websocket" ) ================================================ FILE: component/forward.go ================================================ //go:build forward || full || mini // +build forward full mini package build import ( _ "github.com/p4gefau1t/trojan-go/proxy/forward" ) ================================================ FILE: component/mysql.go ================================================ //go:build mysql || full || mini // +build mysql full mini package build import ( _ "github.com/p4gefau1t/trojan-go/statistic/mysql" ) ================================================ FILE: component/nat.go ================================================ //go:build nat || full || mini // +build nat full mini package build import ( _ "github.com/p4gefau1t/trojan-go/proxy/nat" ) ================================================ FILE: component/other.go ================================================ //go:build other || full // +build other full package build import ( _ "github.com/p4gefau1t/trojan-go/easy" _ "github.com/p4gefau1t/trojan-go/url" ) ================================================ FILE: component/server.go ================================================ //go:build server || full || mini // +build server full mini package build import ( _ "github.com/p4gefau1t/trojan-go/proxy/server" ) ================================================ FILE: config/config.go ================================================ package config import ( "context" "encoding/json" "gopkg.in/yaml.v3" ) var creators = make(map[string]Creator) // Creator creates default config struct for a module type Creator func() interface{} // RegisterConfigCreator registers a config struct for parsing func RegisterConfigCreator(name string, creator Creator) { name += "_CONFIG" creators[name] = creator } func parseJSON(data []byte) (map[string]interface{}, error) { result := make(map[string]interface{}) for name, creator := range creators { config := creator() if err := json.Unmarshal(data, config); err != nil { return nil, err } result[name] = config } return result, nil } func parseYAML(data []byte) (map[string]interface{}, error) { result := make(map[string]interface{}) for name, creator := range creators { config := creator() if err := yaml.Unmarshal(data, config); err != nil { return nil, err } result[name] = config } return result, nil } func WithJSONConfig(ctx context.Context, data []byte) (context.Context, error) { var configs map[string]interface{} var err error configs, err = parseJSON(data) if err != nil { return ctx, err } for name, config := range configs { ctx = context.WithValue(ctx, name, config) } return ctx, nil } func WithYAMLConfig(ctx context.Context, data []byte) (context.Context, error) { var configs map[string]interface{} var err error configs, err = parseYAML(data) if err != nil { return ctx, err } for name, config := range configs { ctx = context.WithValue(ctx, name, config) } return ctx, nil } func WithConfig(ctx context.Context, name string, cfg interface{}) context.Context { name += "_CONFIG" return context.WithValue(ctx, name, cfg) } // FromContext extracts config from a context func FromContext(ctx context.Context, name string) interface{} { return ctx.Value(name + "_CONFIG") } ================================================ FILE: config/config_test.go ================================================ package config import ( "context" "testing" "github.com/p4gefau1t/trojan-go/common" ) type Foo struct { Field1 string `json,yaml:"field1"` Field2 bool `json:"field2" yaml:"field2"` } type TestStruct struct { Field1 string `json,yaml:"field1"` Field2 bool `json,yaml:"field2"` Field3 []Foo `json,yaml:"field3"` } func creator() interface{} { return &TestStruct{} } func TestJSONConfig(t *testing.T) { RegisterConfigCreator("test", creator) data := []byte(` { "field1": "test1", "field2": true, "field3": [ { "field1": "aaaa", "field2": true } ] } `) ctx, err := WithJSONConfig(context.Background(), data) common.Must(err) c := FromContext(ctx, "test").(*TestStruct) if c.Field1 != "test1" || c.Field2 != true { t.Fail() } } func TestYAMLConfig(t *testing.T) { RegisterConfigCreator("test", creator) data := []byte(` field1: 012345678 field2: true field3: - field1: test field2: true `) ctx, err := WithYAMLConfig(context.Background(), data) common.Must(err) c := FromContext(ctx, "test").(*TestStruct) if c.Field1 != "012345678" || c.Field2 != true || c.Field3[0].Field1 != "test" { t.Fail() } } ================================================ FILE: constant/constant.go ================================================ package constant var ( Version = "Custom Version" Commit = "Unknown Git Commit ID" ) ================================================ FILE: docs/.gitignore ================================================ public/ themes/ ================================================ FILE: docs/Makefile ================================================ .PHONY: default clean hugo hugo-build default: hugo clean: rm -rf public/ hugo-build: clean hugo-themes hugo --enableGitInfo --source . hugo: hugo server --disableFastRender --enableGitInfo --watch --source . # hugo server -D hugo-themes: rm -rf themes mkdir themes git clone --depth=1 https://github.com/llc1123/hugo-theme-techdoc.git themes/hugo-theme-techdoc rm -rf themes/hugo-theme-techdoc/.git ================================================ FILE: docs/archetypes/default.md ================================================ --- title: "{{ replace .Name "-" " " | title }}" date: {{ .Date }} draft: true --- ================================================ FILE: docs/config.toml ================================================ baseURL = "https://p4gefau1t.github.io/trojan-go" languageCode = "zh-CN" title = "Trojan-Go Docs" theme = "hugo-theme-techdoc" hasCJKLanguage = true metaDataFormat = "yaml" defaultContentLanguage = "zh" defaultContentLanguageInSubdir= true enableMissingTranslationPlaceholders = false [params] # Source Code repository section description = "An unidentifiable mechanism that helps you bypass GFW." github_repository = "https://github.com/p4gefau1t/trojan-go" # Documentation repository section # documentation repository (set edit link to documentation repository) github_doc_repository = "https://github.com/p4gefau1t/trojan-go/docs" # Analytic section google_analytics_id = "" # Your Google Analytics tracking id tag_manager_container_id = "" # Your Google Tag Manager container id google_site_verification = "" # Your Google Site Verification for Search Console # Open Graph and Twitter Cards settings section # Open Graph settings for each page are set on the front matter. # See https://gohugo.io/templates/internal/#open-graph # See https://gohugo.io/templates/internal/#twitter-cards title = "Trojan-Go Docs" images = ["images/og-image.png"] # Open graph images are placed in `static/images` # Theme settings section # Theme color # See color value reference https://developer.mozilla.org/en-US/docs/Web/CSS/color custom_font_color = "" custom_background_color = "" # Documentation Menu section # Menu style settings menu_style = "open-menu" # "open-menu" or "slide-menu" # Date format dateformat = "" # default "2 Jan 2006" # See the format reference https://gohugo.io/functions/format/#hugo-date-and-time-templating-reference # path name excluded from documentation menu menu_exclusion = [ "archives", "archive", "blog", "entry", "post", "posts", ] # Algolia site search section # See https://www.algolia.com/doc/ algolia_search_enable = true algolia_indexName = "hugo-demo-techdoc" algolia_appId = "7W4SAN4PLK" algolia_apiKey = "cbf12a63ff72d9c5dc0c10c195cf9128" # Search-Only API Key # Global menu section # See https://gohugo.io/content-management/menus/ [menu] [[menu.main]] name = "Home" url = "/" weight = 1 [[menu.main]] name = "GitHub" url = "https://github.com/p4gefau1t" weight = 2 # Markup configure section # See https://gohugo.io/getting-started/configuration-markup/ [markup] defaultMarkdownHandler = "goldmark" [markup.goldmark.renderer] unsafe= true [markup.tableOfContents] startLevel = 2 endLevel = 4 ordered = false # Algolia Search configure section [outputFormats.Algolia] baseName = "algolia" isPlainText = true mediaType = "application/json" notAlternative = true [params.algolia] vars = [ "title", "summary", "content", "date", "publishdate", "description", "permalink", "keywords", "lastmod", ] params = [ "tags", "categories", ] ================================================ FILE: docs/content/_index.md ================================================ --- title: "简介" draft: false weight: 10 --- # Trojan-Go 这里是Trojan-Go的文档,你可以在左侧的导航栏中找到一些使用技巧,以及完整的配置文件说明。 Trojan-Go是使用Go语言实现的完整的Trojan代理,和Trojan协议以及原版的配置文件格式兼容。支持并且兼容Trojan-GFW版本的绝大多数功能,并扩展了更多的实用功能。 Trojan-Go的的首要目标是保障传输安全性和隐蔽性。在此前提下,尽可能提升传输性能和易用性。 如果你遇到配置和使用方面的问题,发现了软件Bug,或是有更好的想法,欢迎加入Trojan-Go的[Telegram交流反馈群](https://t.me/trojan_go_chat)。 ---- > Across the Great Wall, we can reach every corner in the world. > > (越过长城,走向世界。) ================================================ FILE: docs/content/advance/_index.md ================================================ --- title: "高级配置" draft: false weight: 30 --- 这一部分内容将介绍更复杂的Trojan-Go配置方法。对于与Trojan原版不兼容的特性,将在小节中明确标明。 ================================================ FILE: docs/content/advance/aead.md ================================================ --- title: "使用Shadowsocks AEAD进行二次加密" draft: false weight: 8 --- ### 注意,Trojan不支持这个特性 Trojan协议本身无加密,其安全性依赖于下层的TLS。在一般情况下,TLS安全性很好,并不需要再次加密Trojan流量。但是,某些场景下,你可能无法保证TLS隧道的安全性: - 你使用了Websocket,经过不可信的CDN进行中转(如国内CDN) - 你与服务器的连接遭到了GFW针对TLS的中间人攻击 - 你的证书失效,无法验证证书有效性 - 你使用了无法保证密码学安全的可插拔传输层 等等。 Trojan-Go支持使用Shadowsocks AEAD对Trojan-Go进行加密。其本质是在Trojan协议下方加上一层Shadowsocks AEAD加密。服务端和客户端必须同时开启,且密码和加密方式必须一致,否则无法进行通讯。 要开启AEAD加密,只需添加一个```shadowsocks```选项: ```json ... "shadowsocks": { "enabled": true, "method": "AES-128-GCM", "password": "1234567890" } ``` ```method```如果省略,则默认使用AES-128-GCM。更多信息,参见“完整的配置文件”一节。 ================================================ FILE: docs/content/advance/api.md ================================================ --- title: "使用API动态管理用户" draft: false weight: 10 --- ### 注意,Trojan不支持这个特性 Trojan-Go使用gRPC提供了一组API,API支持以下功能: - 用户信息增删改查 - 流量统计 - 速度统计 - IP连接数统计 Trojan-Go本身集成了API控制功能,也即可以使用一个Trojan-Go实例控制另一个Trojan-Go服务器。 你需要在你需要被控制的服务端配置添加API设置,例如: ```json { ... "api": { "enabled": true, "api_addr": "127.0.0.1", "api_port": 10000, } } ``` 然后启动Trojan-Go服务器 ```shell ./trojan-go -config ./server.json ``` 然后可以使用另一个Trojan-Go连接该服务器进行管理,基本命令格式为 ```shell ./trojan-go -api-addr SERVER_API_ADDRESS -api COMMAND ``` 其中```SERVER_API_ADDRESS```为API地址和端口,如127.0.0.1:10000 ```COMMAND```为API命令,合法的命令有 - list 列出所有用户 - get 获取某个用户信息 - set 设置某个用户信息(添加/删除/修改) 下面是一些例子 1. 列出所有用户信息 ```shell ./trojan-go -api-addr 127.0.0.1:10000 -api list ``` 所有的用户信息将以json的形式导出,信息包括在线IP数量,实时速度,总上传和下载流量等。下面是一个返回的结果的例子 ```json [{"user":{"hash":"d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01"},"status":{"traffic_total":{"upload_traffic":36393,"download_traffic":186478},"speed_current":{"upload_speed":25210,"download_speed":72384},"speed_limit":{"upload_speed":5242880,"download_speed":5242880},"ip_limit":50}}] ``` 流量单位均为字节。 2. 获取一个用户信息 可以使用 -target-password 指定密码,也可以使用 -target-hash 指定目标用户密码的SHA224散列值。格式和list命令相同 ```shell ./trojan-go -api-addr 127.0.0.1:10000 -api get -target-password password ``` 或者 ```shell ./trojan-go -api-addr 127.0.0.1:10000 -api get -target-hash d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01 ``` 以上两条命令等价,下面的例子统一使用明文密码的方式,散列值指定某个用户的方式以此类推。 该用户信息将以json的形式导出,格式与list命令类似。下面是一个返回的结果的例子 ```json {"user":{"hash":"d63dc919e201d7bc4c825630d2cf25fdc93d4b2f0d46706d29038d01"},"status":{"traffic_total":{"upload_traffic":36393,"download_traffic":186478},"speed_current":{"upload_speed":25210,"download_speed":72384},"speed_limit":{"upload_speed":5242880,"download_speed":5242880},"ip_limit":50}} ``` 3. 添加一个用户信息 ```shell ./trojan-go -api-addr 127.0.0.1:10000 -api set -add-profile -target-password password ``` 4. 删除一个用户信息 ```shell ./trojan-go -api-addr 127.0.0.1:10000 -api set -delete-profile -target-password password ``` 5. 修改一个用户信息 ```shell ./trojan-go -api-addr 127.0.0.1:10000 -api set -modify-profile -target-password password \ -ip-limit 3 \ -upload-speed-limit 5242880 \ -download-speed-limit 5242880 ``` 这个命令将密码为password的用户上传和下载速度限制为5MiB/s,同时连接的IP数量限制为3个,注意这里5242880的单位是字节。如果填写0或者负数,则表示不进行限制。 ================================================ FILE: docs/content/advance/customize-protocol-stack.md ================================================ --- title: "自定义协议栈" draft: false weight: 8 --- ### 注意,Trojan不支持这个特性 Trojan-Go允许高级用户自定义协议栈。在自定义模式下,Trojan-Go将放弃对协议栈的控制,允许用户操作底层协议栈组合。例如 - 在一层TLS上再建立一层或更多层TLS加密 - 使用TLS传输Websocket流量,在Websocket层上再建立一层TLS,在第二层TLS上再使用Shadowsocks AEAD进行加密传输 - 在TCP连接上,使用Shadowsocks的AEAD加密传输Trojan协议 - 将一个入站Trojan的TLS流量解包后重新用TLS包装为新的出站Trojan流量 等等。 **如果你不了解网络相关知识,请不要尝试使用这个功能。不正确的配置可能导致Trojan-Go无法正常工作,或是导致性能和安全性方面的问题。** Trojan-Go将所有协议抽象为隧道,每个隧道可能提供客户端,负责发送;也可能提供服务端,负责接受;或者两者皆提供。自定义协议栈即自定义隧道的堆叠方式。 ### 在继续配置之前,请先阅读开发指南中“基本介绍”一节,确保已经理解Trojan-Go运作方式 下面是Trojan-Go支持的隧道和他们的属性: | 隧道 | 需要下层提供流 | 需要下层提供包 | 向上层提供流 | 向上层提供包 | 可以作为入站 | 可以作为出站 | | ----------- | -------------- | -------------- | ------------ | ------------ | ------------ | ------------ | | transport | n | n | y | y | y | y | | dokodemo | n | n | y | y | y | n | | tproxy | n | n | y | y | y | n | | tls | y | n | y | n | y | y | | trojan | y | n | y | y | y | y | | mux | y | n | y | n | y | y | | simplesocks | y | n | y | y | y | y | | shadowsocks | y | n | y | n | y | y | | websocket | y | n | y | n | y | y | | freedom | n | n | y | y | n | y | | socks | y | y | y | y | y | n | | http | y | n | y | n | y | n | | router | y | y | y | y | n | y | | adapter | n | n | y | y | y | n | 自定义协议栈的工作方式是,定义树/链上节点并分别它们起名(tag)并添加配置,然后使用tag组成的有向路径,描述这棵树/链。例如,对于一个典型的Trojan-Go服务器,可以如此描述: 入站,一共两条路径,tls节点将自动识别trojan和websocket流量并进行分发 - transport->tls->trojan - transport->tls->websocket->trojan 出站,只能有一条路径 - router->freedom 对于入站,从根开始描述多条路径,组成一棵**多叉树**(也可以退化为一条链),不满足树性质的图将导致未定义的行为;对于出站,必须描述一条**链**。 每条路径必须满足这样的条件: 1. 必须以**不需要下层提供流或包**的隧道开始(transport/adapter/tproxy/dokodemo等) 2. 必须以**能向上层提供包和流**的隧道终止(trojan/simplesocks/freedom等) 3. 出站单链上,隧道必须都可作为出站。入站的所有路径上,隧道必须都可作为入站。 要启用自定义协议栈,将```run_type```指定为custom,此时除```inbound```和```outbound```之外的其他选项将被忽略。 下面是一个例子,你可以在此基础上插入或减少协议节点。配置文件为简明起见,使用YAML进行配置,你也可以使用JSON来配置,除格式不同之外,效果是等价的。 客户端 client.yaml ```yaml run-type: custom inbound: node: - protocol: adapter tag: adapter config: local-addr: 127.0.0.1 local-port: 1080 - protocol: socks tag: socks config: local-addr: 127.0.0.1 local-port: 1080 path: - - adapter - socks outbound: node: - protocol: transport tag: transport config: remote-addr: you_server remote-port: 443 - protocol: tls tag: tls config: ssl: sni: localhost key: server.key cert: server.crt - protocol: trojan tag: trojan config: password: - 12345678 path: - - transport - tls - trojan ``` 服务端 server.yaml ```yaml run-type: custom inbound: node: - protocol: websocket tag: websocket config: websocket: enabled: true hostname: example.com path: /ws - protocol: transport tag: transport config: local-addr: 0.0.0.0 local-port: 443 remote-addr: 127.0.0.1 remote-port: 80 - protocol: tls tag: tls config: remote-addr: 127.0.0.1 remote-port: 80 ssl: sni: localhost key: server.key cert: server.crt - protocol: trojan tag: trojan1 config: remote-addr: 127.0.0.1 remote-port: 80 password: - 12345678 - protocol: trojan tag: trojan2 config: remote-addr: 127.0.0.1 remote-port: 80 password: - 87654321 path: - - transport - tls - trojan1 - - transport - tls - websocket - trojan2 outbound: node: - protocol: freedom tag: freedom path: - - freedom ``` ================================================ FILE: docs/content/advance/forward.md ================================================ --- title: "隧道和反向代理" draft: false weight: 5 --- 你可以使用Trojan-Go建立隧道。一个典型的应用是,使用Trojan-Go在本地建立一个无污染的DNS服务器,下面是一个配置的例子 ```json { "run_type": "forward", "local_addr": "127.0.0.1", "local_port": 53, "remote_addr": "your_awesome_server", "remote_port": 443, "target_addr": "8.8.8.8", "target_port": 53, "password": [ "your_awesome_password" ] } ``` forward本质上是一个客户端,不过你需要填入```target_addr```和```target_port```字段,指明反向代理的目标。 使用这份配置文件后,本地53的TCP和UDP端口将被监听,所有的向本地53端口发送的TCP或者UDP数据,都会通过TLS隧道转发给远端服务器your_awesome_server,远端服务器得到回应后,数据会通过隧道返回到本地53端口。 也就是说,你可以将127.0.0.1当作一个DNS服务器,本地查询的结果和远端服务器查询的结果是一致的。你可以使用这个配置避开DNS污染。 同样的原理,你可以在本地搭建一个Google的镜像 ```json { "run_type": "forward", "local_addr": "127.0.0.1", "local_port": 443, "remote_addr": "your_awesome_server", "remote_port": 443, "target_addr": "www.google.com", "target_port": 443, "password": [ "your_awesome_password" ] } ``` 访问```https://127.0.0.1```即可访问谷歌主页,但是注意这里由于谷歌服务器提供的https证书是google.com的证书,而当前域名为127.0.0.1,因此浏览器会引发一个证书错误的警告。 类似的,可以使用forward传输其他代理协议。例如,使用Trojan-Go传输shadowsocks的流量,远端主机开启ss服务器,监听127.0.0.1:12345,并且远端服务器在443端口开启了正常的Trojan-Go服务器。你可以如此指定配置 ```json { "run_type": "forward", "local_addr": "0.0.0.0", "local_port": 54321, "remote_addr": "your_awesome_server", "remote_port": 443, "target_addr": "www.google.com", "target_port": 12345, "password": [ "your_awesome_password" ] } ``` 此后,任何连接本机54321端口的TCP/UDP连接,等同于连接远端12345端口。你可以使用shadowsocks客户端连接本地的54321端口,ss流量将使用trojan的隧道连接传输至远端12345端口的ss服务器。 ================================================ FILE: docs/content/advance/mux.md ================================================ --- title: "启用多路复用提升网络并发性能" draft: false weight: 1 --- ### 注意,Trojan不支持这个特性 Trojan-Go支持使用多路复用提升网络并发性能。 Trojan协议基于TLS。在一个TLS安全连接建立之前,连接双方需要进行密钥协商和交换等步骤确保后续通讯的安全性。这个过程即为TLS握手。 目前GFW对于TLS握手存在审查和干扰,同时由于出口网络拥塞的原因,普通的线路完成TLS握手通常需要将近一秒甚至更长的时间。这可能导致浏览网页和观看视频的延迟提高。 Trojan-Go使用多路复用的方式解决这一问题。每个建立的TLS连接将承载多个TCP连接。当新的代理请求到来时,不需要和服务器握手发起一个新的TLS连接,而是尽可能重复使用已有的TLS连接。以此减少频繁TLS握手和TCP握手的带来的延迟。 启用多路复用不会增加你的链路速度(甚至会有所减少),而且可能会增加服务器和客户端的计算负担。可以粗略地理解为,多路复用牺牲网络吞吐和CPU功耗,换取更低的延迟。在高并发的情景下,如浏览含有大量图片的网页时,或者发送大量UDP请求时,可以提升使用体验。 激活```mux```模块,只需要将```mux```选项中```enabled```字段设为true即可,下面是一个客户端的例子 ```json ... "mux" :{ "enabled": true } ``` 只需要配置客户端即可,服务端可以自动适配,无需配置```mux```选项。 完整的mux配置如下 ```json "mux": { "enabled": false, "concurrency": 8, "idle_timeout": 60 } ``` ```concurrency```是每个TLS连接最多可以承载的TCP连接数。这个数值越大,每个TLS连接被复用的程度就更高,握手导致的延迟越低。但服务器和客户端的计算负担也会越大,这有可能使你的网络吞吐量降低。如果你的线路的TLS握手极端缓慢,你可以将这个数值设置为-1,Trojan-Go将只进行一次TLS握手,只使用唯一的一条TLS连接进行传输。 ```idle_timeout```指的是每个TLS连接空闲多长时间后关闭。设置超时时间,**可能**有助于减少不必要的长连接存活确认(Keep Alive)流量传输引发GFW的探测。你可以将这个数值设置为-1,TLS连接在空闲时将被立即关闭。 ================================================ FILE: docs/content/advance/nat.md ================================================ --- title: "透明代理" draft: false weight: 11 --- ### 注意,Trojan不完全支持这个特性(UDP) Trojan-Go支持基于tproxy的透明TCP/UDP代理。 要开启透明代理模式,将一份正确的客户端配置(配置方式参见基本配置部分)其中的```run_type```修改为```nat```,并按照需求修改本地监听端口即可。 之后需要添加iptables规则。这里假定你的网关具有两个网卡,下面这份配置将其中一个网卡(局域网)的入站包转交给Trojan-Go,由Trojan-Go通过隧道,通过另一个网卡(互联网)发送到远端Trojan-Go服务器。你需要将下面的```$SERVER_IP```,```$TROJAN_GO_PORT```,```$INTERFACE```替换为自己的配置。 ```shell # 新建TROJAN_GO链 iptables -t mangle -N TROJAN_GO # 绕过Trojan-Go服务器地址 iptables -t mangle -A TROJAN_GO -d $SERVER_IP -j RETURN # 绕过私有地址 iptables -t mangle -A TROJAN_GO -d 0.0.0.0/8 -j RETURN iptables -t mangle -A TROJAN_GO -d 10.0.0.0/8 -j RETURN iptables -t mangle -A TROJAN_GO -d 127.0.0.0/8 -j RETURN iptables -t mangle -A TROJAN_GO -d 169.254.0.0/16 -j RETURN iptables -t mangle -A TROJAN_GO -d 172.16.0.0/12 -j RETURN iptables -t mangle -A TROJAN_GO -d 192.168.0.0/16 -j RETURN iptables -t mangle -A TROJAN_GO -d 224.0.0.0/4 -j RETURN iptables -t mangle -A TROJAN_GO -d 240.0.0.0/4 -j RETURN # 未命中上文的规则的包,打上标记 iptables -t mangle -A TROJAN_GO -j TPROXY -p tcp --on-port $TROJAN_GO_PORT --tproxy-mark 0x01/0x01 iptables -t mangle -A TROJAN_GO -j TPROXY -p udp --on-port $TROJAN_GO_PORT --tproxy-mark 0x01/0x01 # 从$INTERFACE网卡流入的所有TCP/UDP包,跳转TROJAN_GO链 iptables -t mangle -A PREROUTING -p tcp -i $INTERFACE -j TROJAN_GO iptables -t mangle -A PREROUTING -p udp -i $INTERFACE -j TROJAN_GO # 添加路由,打上标记的包重新进入本地回环 ip route add local default dev lo table 100 ip rule add fwmark 1 lookup 100 ``` 配置完成后**以root权限启动**Trojan-Go客户端: ```shell sudo trojan-go ``` ================================================ FILE: docs/content/advance/nginx-relay.md ================================================ --- title: "一种基于SNI代理的多路径分流中继方案" draft: false weight: 6 --- ## 前言 Trojan 是一种通过 TLS 封装后进行加密数据传输的工具,利用其 TLS 的特性,我们可以通过 SNI 代理实现在同一主机端口上实现不同路径的分流中继。 ## 所需工具及其他准备 - 中继机:nginx 1.11.5 及以上版本 - 落地机:trojan 服务端(无版本要求) ## 配置方法 为了便于说明,这里使用了两台中继主机和两台落地主机。 四台主机所绑定的域名分别为 (a/b/c/d).example.com。如图所示。 相互连接一共4条路径。分别为 a-c、a-d、b-c、b-d 。 ```text +-----------------+ +--------------------+ | +---------->+ | | VPS RELAY A | | VPS ENDPOINT C | +---->+ | +------>+ | | | a.example.com | | | c.example.com | | | +------+ | | +----------+ | +-----------------+ | | +--------------------+ | | | | | | client +----+ | | | | | | | +----------+ | +-----------------+ | | +--------------------+ | | | | | | | | | VPS RELAY B | | +--->+ VPS ENDPOINT D | +---->+ +---+ | | | b.example.com | | d.example.com | | +---------->+ | +-----------------+ +--------------------+ ``` ### 配置路径域名和相应的证书 首先我们需要将每条路径分别分配一个域名,并使其解析到分别的入口主机上。 ```text a-c.example.com CNAME a.example.com a-d.example.com CNAME a.example.com b-c.example.com CNAME b.example.com b-d.example.com CNAME b.example.com ``` 然后我们需要在落地主机上部署所有目标路径的证书 由于解析记录和主机 IP 不符,HTTP 验证无法通过。这里建议使用 DNS 验证方式签发证书。 具体 DNS 验证插件需要根据您的域名 DNS 解析托管商选择,这里使用了 AWS Route 53。 ```shell certbot certonly --dns-route53 -d a-c.example.com -d b-c.example.com // 主机 C 上 certbot certonly --dns-route53 -d a-d.example.com -d b-d.example.com // 主机 D 上 ``` ### 配置 SNI 代理 这里我们使用 nginx 的 ssl_preread 模块实现 SNI 代理。 请安装 nginx 后按如下方法修过 nginx.conf 文件。 注意这里不是 HTTP 服务,请不要写在虚拟主机的配置中。 这里给出主机 A 上的对应配置,主机 B 同理。 ```nginx stream { map $ssl_preread_server_name $name { a-c.example.com c.example.com; # 将 a-c 路径流量转发至主机 C a-d.example.com d.example.com; # 将 a-d 路径流量转发至主机 D # 如果此主机上需要配置其他占用 443 端口的服务 (例如 web 服务和 Trojan 服务) # 请使那些服务监听在其他本地端口(这里使用了 4000) # 所有不匹配上方 SNI 的 TLS 请求都会转发至此端口,如不需要可以删除此行 default localhost:4000; } server { listen 443; # 监听 443 端口 proxy_pass $name; ssl_preread on; } } ``` ### 配置落地 Trojan 服务 在之前的配置中我们使用了一个证书签发了所有目标路径的域名,所以这里我们可以使用一个 Trojan 服务端处理所有目标路径的请求。 Trojan 的配置和通常配置方法无异,这里还是提供一份例子。无关的配置已省略。 ```json { "run_type": "server", "local_addr": "0.0.0.0", "local_port": 443, "ssl": { "cert": "/path/to/certificate.crt", "key": "/path/to/private.key", } ... } ``` 小提示:如果需要在落地主机上对不同路径分别使用独立的 Trojan 服务端(比如需要分别接入各自的计费服务),可以在落地机上再配置一个 SNI 代理,并分别转发至本地不同的 Trojan 服务端监听端口。由于配置与前面所提到的过程基本相同,这里便不再赘述。 ## 总结 通过以上介绍的配置方法,我们可以在单一端口上实现多入口多出口多级中继的 Trojan 流量转发。 对于多级中继,只需在中间节点上按相同思路配置 SNI 代理即可。 ================================================ FILE: docs/content/advance/plugin.md ================================================ --- title: "使用Shadowsocks插件/可插拔传输层" draft: false weight: 7 --- ### 注意,Trojan不支持这个特性 Trojan-Go支持可插拔的传输层。原则上,Trojan-Go可以使用任何有TCP隧道功能的软件作为传输层,如v2ray、shadowsocks、kcp等。同时,Trojan-Go也兼容Shadowsocks的SIP003插件标准,如GoQuiet,v2ray-plugin等。也可以使用Tor的传输层插件,如obfs4,meek等。 你可以使用这些插件,替换Trojan-Go的TLS传输层。 开启可插拔传输层插件后,Trojan-Go客户端将会把**流量明文**直接传输给客户端本地的插件处理。由客户端插件负责进行加密和混淆,并将流量传输给服务端的插件。服务端的插件接收到流量,进行解密和解析,将**流量明文**传输给服务端本地的Trojan-Go服务端。 你可以使用任何插件对流量进行加密和混淆,只需添加"transport_plugin"选项,并指定插件的可执行文件的路径,并做好配置即可。 我们更建议**自行设计协议并开发相应插件**。因为目前现有的所有插件无法对接Trojan-Go的对抗主动探测的特性,而且部分插件并无加密能力。如果你对开发插件有兴趣,欢迎在"实现细节和开发指南"一节中查看插件设计的指南。 例如,可以使用符合SIP003标准的v2ray-plugin,下面是一个例子: **这个配置中使用了websocket明文传输未经加密的trojan协议,存在安全隐患。这个配置仅作为演示使用。** **不要在任何情况下使用这个配置穿透GFW。** 服务端配置: ```json ...(省略) "transport_plugin": { "enabled": true, "type": "shadowsocks", "command": "./v2ray-plugin", "arg": ["-server", "-host", "www.baidu.com"] } ``` 客户端配置: ```json ...(省略) "transport_plugin": { "enabled": true, "type": "shadowsocks", "command": "./v2ray-plugin", "arg": ["-host", "www.baidu.com"] } ``` 注意,v2ray-plugin插件需要指定```-server```参数来区分客户端和服务端。更多关于该插件详细的说明,参考v2ray-plugin的文档。 启动Trojan-Go后,你可以看到v2ray-plugin启动的输出。插件将把流量伪装为Websocket流量并传输。 非SIP003标准的插件可能需要不同的配置,你可以指定```type```为"other",并自行指定插件地址,插件启动参数、环境变量。 ================================================ FILE: docs/content/advance/router.md ================================================ --- title: "国内直连和广告屏蔽" draft: false weight: 3 --- ### 注意,Trojan不支持这个特性 Trojan-Go内建的路由模块可以帮助你实现国内直连,即客户端对于国内网站不经过代理,直接连接。 路由模块在客户端可以配置三种策略(```bypass```, ```proxy```, ```block```),在服务端只可使用```block```策略。 下面是一个例子 ```json { "run_type": "client", "local_addr": "127.0.0.1", "local_port": 1080, "remote_addr": "your_server", "remote_port": 443, "password": [ "your_password" ], "ssl": { "sni": "your-domain-name.com" }, "mux" :{ "enabled": true }, "router":{ "enabled": true, "bypass": [ "geoip:cn", "geoip:private", "geosite:cn", "geosite:geolocation-cn" ], "block": [ "geosite:category-ads" ], "proxy": [ "geosite:geolocation-!cn" ] } } ``` 这个配置文件激活了router模块,使用的是白名单的模式,当匹配到中国大陆或者局域网的IP/域名时,直接连接。如果是广告运营商的域名,则直接断开连接。 所需要的数据库```geoip.dat```和```geosite.dat```已经包含在release的压缩包中,直接使用即可。它们来自V2Ray的[domain-list-community](https://github.com/v2fly/domain-list-community)和[geoip](https://github.com/v2fly/geoip)。 你可以使用如```geosite:cn```、```geosite:geolocation-!cn```、```geosite:category-ads-all```、```geosite:bilibili```的形式来指定某一类域名,所有可用的tag可以在[domain-list-community](https://github.com/v2fly/domain-list-community)仓库的[```data```](https://github.com/v2fly/domain-list-community/tree/master/data)目录中找到。```geosite.dat``` 更详细使用说明,参考[V2Ray/Routing路由#预定义域名列表](https://www.v2fly.org/config/routing.html#预定义域名列表)。 你可以使用如```geoip:cn```、```geoip:hk```、```geoip:us```、```geoip:private```的形式来指定某一类IP。`geoip:private`为特殊项,囊括了内网IP和保留IP,其余类别囊括了各个国家/地区的IP地址段。各国家/地区的代号参考[维基百科](https://zh.wikipedia.org/wiki/%E5%9C%8B%E5%AE%B6%E5%9C%B0%E5%8D%80%E4%BB%A3%E7%A2%BC)。 你也可以配置自己的路由规则。例如,想要屏蔽所有example.com域名以及其子域名,以及192.168.1.0/24,添加下面的规则。 ```json "block": [ "domain:example.com", "cidr:192.168.1.0/24" ] ``` 支持的格式有 - "domain:",子域名匹配 - "full:",完全域名匹配 - "regexp:",正则表达式匹配 - "cidr:",CIDR匹配 更详细的说明参考"完整的配置文件"一节。 ================================================ FILE: docs/content/advance/websocket.md ================================================ --- title: "使用Websocket进行CDN转发和抵抗中间人攻击" draft: false weight: 2 --- ### 注意,Trojan不支持这个特性 Trojan-Go支持使用TLS+Websocket承载Trojan协议,使得利用CDN进行流量中转成为可能。 Trojan协议本身不带加密,安全性依赖外层的TLS。但流量一旦经过CDN,TLS对CDN是透明的。其服务提供者可以对TLS的明文内容进行审查。**如果你使用的是不可信任的CDN(任何在中国大陆注册备案的CDN服务均应被视为不可信任),请务必开启Shadowsocks AEAD对Webosocket流量进行加密,以避免遭到识别和审查。** 服务器和客户端配置文件中同时添加websocket选项,并将其```enabled```字段设置为true,并填写```path```字段和```host```字段即可启用Websocket支持。下面是一个完整的Websocket选项: ```json "websocket": { "enabled": true, "path": "/your-websocket-path", "host": "example.com" } ``` ```host```是主机名,一般填写域名。客户端```host```是可选的,填写你的域名。如果留空,将会使用```remote_addr```填充。 ```path```指的是websocket所在的URL路径,必须以斜杠("/")开始。路径并无特别要求,满足URL基本格式即可,但要保证客户端和服务端的```path```一致。```path```应当选择较长的字符串,以避免遭到GFW直接的主动探测。 客户端的```host```将包含在Websocket的握手HTTP请求中,发送给CDN服务器,必须有效;服务端和客户端```path```必须一致,否则Websocket握手无法进行。 下面是一个客户端配置文件的例子 ```json { "run_type": "client", "local_addr": "127.0.0.1", "local_port": 1080, "remote_addr": "www.your_awesome_domain_name.com", "remote_port": 443, "password": [ "your_password" ], "websocket": { "enabled": true, "path": "/your-websocket-path", "host": "example.com" }, "shadowsocks": { "enabled": true, "password": "12345678" } } ``` ================================================ FILE: docs/content/basic/_index.md ================================================ --- title: "基本配置" draft: false weight: 20 --- 这一部分内容将介绍如何配置基本的Trojan-Go代理服务器和客户端。 ================================================ FILE: docs/content/basic/config.md ================================================ --- title: "正确配置Trojan-Go" draft: false weight: 22 --- 下面将介绍如何正确配置Trojan-Go以完全隐藏你的代理节点特征。 在开始之前,你需要 - 一个服务器,且未被GFW封锁 - 一个域名,可以使用免费的域名服务,如.tk等 - Trojan-Go,可以从release页面下载 - 证书和密钥,可以从letsencrypt等机构免费申请签发 ### 服务端配置 我们的目标是,使得你的服务器和正常的HTTPS网站表现相同。 首先你需要一个HTTP服务器,可以使用nginx,apache,caddy等配置一个本地HTTP服务器,也可以使用别人的HTTP服务器。HTTP服务器的作用是,当GFW主动探测时,向它展示一个完全正常的Web页面。 **你需要在```remote_addr```和```remote_port```指定这个HTTP服务器的地址。```remote_addr```可以是IP或者域名。Trojan-Go将会测试这个HTTP服务器是否工作正常,如果不正常,Trojan-Go会拒绝启动。** 下面是一份比较安全的服务器配置server.json,需要你在本地80端口配置一个HTTP服务(必要,你也可以使用其他的网站HTTP服务器,如"remote_addr": "example.com"),在1234端口配置一个HTTPS服务,或是一个展示"400 Bad Request"的静态HTTP网页服务。(可选,可以删除```fallback_port```字段,跳过这个步骤) ```json { "run_type": "server", "local_addr": "0.0.0.0", "local_port": 443, "remote_addr": "127.0.0.1", "remote_port": 80, "password": [ "your_awesome_password" ], "ssl": { "cert": "server.crt", "key": "server.key", "fallback_port": 1234 } } ``` 这个配置文件使Trojan-Go在服务器的所有IP地址上(0.0.0.0)监听443端口,分别使用server.crt和server.key作为证书和密钥进行TLS握手。你应该使用尽可能复杂的密码,同时确保客户端和服务端```password```是一致的。注意,**Trojan-Go会检测你的HTTP服务器```http://remote_addr:remote_port```是否正常工作。如果你的HTTP服务器工作不正常,Trojan-Go将拒绝启动。** 当一个客户端试图连接Trojan-Go的监听端口时,会发生下面的事情: - 如果TLS握手成功,检测到TLS的内容非Trojan协议(有可能是HTTP请求,或者来自GFW的主动探测)。Trojan-Go将TLS连接代理到本地127.0.0.1:80上的HTTP服务。这时在远端看来,Trojan-Go服务就是一个HTTPS网站。 - 如果TLS握手成功,并且被确认是Trojan协议头部,并且其中的密码正确,那么服务器将解析来自客户端的请求并进行代理,否则和上一步的处理方法相同。 - 如果TLS握手失败,说明对方使用的不是TLS协议进行连接。此时Trojan-Go将这个TCP连接代理到本地127.0.0.1:1234上运行的HTTPS服务(或者HTTP服务),返回一个展示400 Bad Reqeust的HTTP页面。```fallback_port```是一个可选选项,如果没有填写,Trojan-Go会直接终止连接。虽然是可选的,但是还是强烈建议填写。 你可以通过使用浏览器访问你的域名```https://your-domain-name.com```来验证。如果工作正常,你的浏览器会显示一个正常的HTTPS保护的Web页面,页面内容与服务器本机80端口上的页面一致。你还可以使用```http://your-domain-name.com:443```验证```fallback_port```工作是否正常。 事实上,你甚至可以将Trojan-Go当作你的HTTPS服务器,用来给你的网站提供HTTPS服务。访客可以正常地通过Trojan-Go浏览你的网站,而和代理流量互不影响。但是注意,不要在```remote_port```和```fallback_port```搭建有高实时性需求的服务,Trojan-Go识别到非Trojan协议流量时会有意增加少许延迟以抵抗GFW基于时间的检测。 配置完成后,可以使用 ```shell ./trojan-go -config ./server.json ``` 启动服务端。 ### 客户端配置 对应的客户端配置client.json ```json { "run_type": "client", "local_addr": "127.0.0.1", "local_port": 1080, "remote_addr": "your_awesome_server", "remote_port": 443, "password": [ "your_awesome_password" ], "ssl": { "sni": "your-domain-name.com" } } ``` 这个客户端配置使Trojan-Go开启一个监听在本地1080端口的socks5/http代理(自动识别),远端服务器为your_awesome_server:443,your_awesome_server可以是IP或者域名。 如果你在```remote_addr```中填写的是域名,```sni```可以省略。如果你在```remote_addr```填写的是IP地址,```sni```字段应当填写你申请证书的对应域名,或者你自己签发的证书的Common Name,而且必须一致。注意,```sni```字段目前的在TLS协议中是**明文传送**的(目的是使服务器提供相应证书)。GFW已经被证实具有SNI探测和阻断能力,所以不要填写类似```google.com```等已经被封锁的域名,否则很有可能导致你的服务器也被遭到封锁。 配置完成后,可以使用 ```shell ./trojan-go -config ./client.json ``` 启动客户端。 更多关于配置文件的信息,可以在左侧导航栏中找到相应介绍。 ================================================ FILE: docs/content/basic/full-config.md ================================================ --- title: "完整的配置文件" draft: false weight: 30 --- 下面是一个完整的配置文件,其中的必填选项有 - ```run_type``` - ```local_addr``` - ```local_port``` - ```remote_addr``` - ```remote_port``` 对于服务器```server```,```key```和```cert```为必填。 对于客户端```client```,反向代理隧道```forward```,以及透明代理```nat```,```password```必填 其余未填的选项,用下面给出的值进行填充。 *Trojan-Go支持对人类更友好的YAML语法,配置文件的基本结构与JSON相同,效果等价。但是为了遵守YAML的命名习惯,你需要把下划线("_")转换为横杠("-"),如```remote_addr```在YAML文件中为```remote-addr```* ```json { "run_type": *required*, "local_addr": *required*, "local_port": *required*, "remote_addr": *required*, "remote_port": *required*, "log_level": 1, "log_file": "", "password": [], "disable_http_check": false, "udp_timeout": 60, "ssl": { "verify": true, "verify_hostname": true, "cert": *required*, "key": *required*, "key_password": "", "cipher": "", "curves": "", "prefer_server_cipher": false, "sni": "", "alpn": [ "http/1.1" ], "session_ticket": true, "reuse_session": true, "plain_http_response": "", "fallback_addr": "", "fallback_port": 0, "fingerprint": "" }, "tcp": { "no_delay": true, "keep_alive": true, "prefer_ipv4": false }, "mux": { "enabled": false, "concurrency": 8, "idle_timeout": 60 }, "router": { "enabled": false, "bypass": [], "proxy": [], "block": [], "default_policy": "proxy", "domain_strategy": "as_is", "geoip": "$PROGRAM_DIR$/geoip.dat", "geosite": "$PROGRAM_DIR$/geosite.dat" }, "websocket": { "enabled": false, "path": "", "host": "" }, "shadowsocks": { "enabled": false, "method": "AES-128-GCM", "password": "" }, "transport_plugin": { "enabled": false, "type": "", "command": "", "option": "", "arg": [], "env": [] }, "forward_proxy": { "enabled": false, "proxy_addr": "", "proxy_port": 0, "username": "", "password": "" }, "mysql": { "enabled": false, "server_addr": "localhost", "server_port": 3306, "database": "", "username": "", "password": "", "check_rate": 60 }, "api": { "enabled": false, "api_addr": "", "api_port": 0, "ssl": { "enabled": false, "key": "", "cert": "", "verify_client": false, "client_cert": [] } } } ``` ## 说明 ### 一般选项 对于client/nat/forward,```remote_xxxx```应当填写你的trojan服务器地址和端口号,```local_xxxx```对应本地开放的socks5/http代理地址(自动适配) 对于server,```local_xxxx```对应trojan服务器监听地址(强烈建议使用443端口),```remote_xxxx```填写识别到非trojan流量时代理到的HTTP服务地址,通常填写本地80端口。 ```log_level```指定日志等级。等级越高,输出的信息越少。合法的值有 - 0 输出Debug以上日志(所有日志) - 1 输出Info及以上日志 - 2 输出Warning及以上日志 - 3 输出Error及以上日志 - 4 输出Fatal及以上日志 - 5 完全不输出日志 ```log_file```指定日志输出文件路径。如果未指定则使用标准输出。 ```password```可以填入多个密码。除了使用配置文件配置密码之外,trojan-go还支持使用mysql配置密码,参见下文。客户端的密码,只有与服务端配置文件中或者在数据库中的密码记录一致,才能通过服务端的校验,正常使用代理服务。 ```disable_http_check```是否禁用HTTP伪装服务器可用性检查。 ```udp_timeout``` UDP会话超时时间。 ### ```ssl```选项 ```verify```表示客户端(client/nat/forward)是否校验服务端提供的证书合法性,默认开启。出于安全性考虑,这个选项不应该在实际场景中选择false,否则可能遭受中间人攻击。如果使用自签名或者自签发的证书,开启```verify```会导致校验失败。这种情况下,应当保持```verify```开启,然后在```cert```中填写服务端的证书,即可正常连接。 ```verify_hostname```表示服务端是否校验客户端提供的SNI与服务端设置的一致性。如果服务端SNI字段留空,认证将被强制关闭。 服务端必须填入```cert```和```key```,对应服务器的证书和私钥文件,请注意证书是否有效/过期。如果使用权威CA签发的证书,客户端(client/nat/forward)可以不填写```cert```。如果使用自签名或者自签发的证书,应当在的```cert```处填入服务器证书文件,否则可能导致校验失败。 ```sni```指的是TLS客户端请求中的服务器名字段,一般和证书的Common Name相同。如果你使用let'sencrypt等机构签发的证书,这里填入你的域名。对于客户端,如果这一项未填,将使用```remote_addr```填充。你应当指定一个有效的SNI(和远端证书CN一致),否则客户端可能无法验证远端证书有效性从而无法连接;对于服务端,若此项不填,则使用证书中Common Name作为SNI校验依据,支持通配符如*.example.com。 ```fingerprint```用于指定客户端TLS Client Hello指纹伪造类型,以抵抗GFW对于TLS Client Hello指纹的特征识别和阻断。trojan-go使用[utls](https://github.com/refraction-networking/utls)进行指纹伪造,默认伪造Firefox的指纹。合法的值有 - "",不使用指纹伪造(默认) - "firefox",伪造Firefox指纹 - "chrome",伪造Chrome指纹 - "ios",伪造iOS指纹 一旦指纹的值被设置,客户端的```cipher```,```curves```,```alpn```,```session_ticket```等有可能影响指纹的字段将使用该指纹的特定设置覆写。 ```alpn```为TLS的应用层协议协商指定协议。在TLS Client/Server Hello中传输,协商应用层使用的协议,仅用作指纹伪造,并无实际作用。**如果使用了CDN,错误的alpn字段可能导致与CDN协商得到错误的应用层协议**。 ```prefer_server_cipher```客户端是否偏好选择服务端在协商中提供的密码学套件。 ```cipher```TLS使用的密码学套件。```cipher13``字段与此字段合并。只有在你明确知道自己在做什么的情况下,才应该去填写此项以修改trojan-go使用的TLS密码学套件。**正常情况下,你应该将其留空或者不填**,trojan-go会根据当前硬件平台以及远端的情况,自动选择最合适的加密算法以提升性能和安全性。如果需要填写,密码学套件名用分号(":")分隔,按优先顺序排列。Go的TLS库中弃用了TLS1.2中部分不安全的密码学套件,并完全支持TLS1.3。默认情况下,trojan-go将优先使用更安全的TLS1.3。 ```curves```指定TLS在ECDHE中偏好使用的椭圆曲线。只有你明确知道自己在做什么的情况下,才应该填写此项。曲线名称用分号(":")分隔,按优先顺序排列。 ```plain_http_response```指服务端TLS握手失败时,明文发送的原始数据(原始TCP数据)。这个字段填入该文件路径。推荐使用```fallback_port```而不是该字段。 ```fallback_addr```和```fallback_port```指服务端TLS握手失败时,trojan-go将该连接重定向到该地址。这是trojan-go的特性,以便更好地隐蔽服务器,抵抗GFW的主动检测,使得服务器的443端口在遭遇非TLS协议的探测时,行为与正常服务器完全一致。当服务器接受了一个连接但无法进行TLS握手时,如果```fallback_port```不为空,则流量将会被代理至fallback_addr:fallback_port。如果```fallback_addr```为空,则用```remote_addr```填充。例如,你可以在本地使用nginx开启一个https服务,当你的服务器443端口被非TLS协议请求时(比如http请求),trojan-go将代理至本地https服务器,nginx将使用http协议明文返回一个400 Bad Request页面。你可以通过使用浏览器访问```http://your-domain-name.com:443```进行验证。 ```key_log```TLS密钥日志的文件路径。如果填写则开启密钥日志。**记录密钥将破坏TLS的安全性,此项不应该用于除调试以外的其他任何用途。** ### ```mux```多路复用选项 多路复用是trojan-go的特性。如果服务器和客户端都是trojan-go,可以开启mux多路复用以减少高并发情景下的延迟(只需要客户端开启此选项即可,服务端自动适配)。 注意,多路复用的意义在于降低握手延迟,而不是提升链路速度。相反,它会增加客户端和服务端的CPU和内存消耗,从而可能造成速度下降。 ```enabled```是否开启多路复用。 ```concurrency```指单个TLS隧道可以承载的最大连接数,默认为8。这个数值越大,多连接并发时TLS由于握手产生的延迟就越低,但网络吞吐量可能会有所降低,填入负数或者0表示所有连接只使用一个TLS隧道承载。 ```idle_timeout```空闲超时时间。指TLS隧道在空闲多长时间之后关闭,单位为秒。如果数值为负值或0,则一旦TLS隧道空闲,则立即关闭。 ### ```router```路由选项 路由功能是trojan-go的特性。trojan-go的路由策略有三种。 - Proxy 代理。将请求通过TLS隧道进行代理,由trojan服务器和目的地址进行连接。 - Bypass 绕过。直接在本地和目的地址进行连接。 - Block 封锁。不代理请求,直接关闭连接。 在```proxy```, ```bypass```, ```block```字段中填入对应列表geoip/geosite或路由规则,trojan-go即根据列表中的IP(CIDR)或域名执行相应路由策略。客户端(client)可以配置三种策略,服务端(server)只可配置block策略。 ```enabled```是否开启路由模块。 ```default_policy```指的是三个列表匹配均失败后,使用的默认策略,默认为"proxy",即进行代理。合法的值有 - "proxy" - "bypass" - "block" 含义同上。 ```domain_strategy```域名解析策略,默认"as_is"。合法的值有: - "as_is",只在各列表中的域名规则内进行匹配。 - "ip_if_non_match",先在各列表中的域名规则内进行匹配;如果不匹配,则解析为IP后,在各列表中的IP地址规则内进行匹配。该策略可能导致DNS泄漏或遭到污染。 - "ip_on_demand",先解析为IP,在各列表中的IP地址规则内进行匹配;如果不匹配,则在各列表中的域名规则内进行匹配。该策略可能导致DNS泄漏或遭到污染。 ```geoip```和```geosite```字段指geoip和geosite数据库文件路径,默认使用程序所在目录的geoip.dat和geosite.dat。也可以通过指定环境变量TROJAN_GO_LOCATION_ASSET指定工作目录。 ### ```websocket```选项 Websocket传输是trojan-go的特性。在**正常的直接连接代理节点**的情况下,开启这个选项不会改善你的链路速度(甚至有可能下降),也不会提升你的连接安全性。你只应该在需要利用CDN进行中转,或利用nginx等服务器根据路径分发的情况下,使用websocket。 ```enabled```表示是否启用Websocket承载流量,服务端开启后同时支持一般Trojan协议和基于websocket的Trojan协议,客户端开启后将只使用websocket承载所有Trojan协议流量。 ```path```指的是Websocket使用的URL路径,必须以斜杠("/")开头,如"/longlongwebsocketpath",并且服务器和客户端必须一致。 ```host```Websocket握手时,HTTP请求中使用的主机名。客户端如果留空则使用```remote_addr```填充。如果使用了CDN,这个选项一般填入域名。不正确的```host```可能导致CDN无法转发请求。 ### ``shadowsocks`` AEAD加密选项 此选项用于替代弃用的混淆加密和双重TLS。如果此选项被设置启用,Trojan协议层下将插入一层Shadowsocks AEAD加密层。也即(已经加密的)TLS隧道内,所有的Trojan协议将再使用AEAD方法进行加密。注意,此选项和Websocket是否开启无关。无论Websocket是否开启,所有Trojan流量都会被再进行一次加密。 注意,开启这个选项将有可能降低传输性能,你只应该在不信任承载Trojan协议的传输信道的情况下,启用这个选项。例如: - 你使用了Websocket,经过不可信的CDN进行中转(如国内CDN) - 你与服务器的连接遭到了GFW针对TLS的中间人攻击 - 你的证书失效,无法验证证书有效性 - 你使用了无法保证密码学安全的可插拔传输层 等等。 由于使用的是AEAD,trojan-go可以正确判断请求是否有效,是否遭到主动探测,并作出相应的响应。 ```enabled```是否启用Shadowsocks AEAD加密Trojan协议层。 ```method```加密方式。合法的值有: - "CHACHA20-IETF-POLY1305" - "AES-128-GCM" (默认) - "AES-256-GCM" ```password```用于生成主密钥的密码。如果启用AEAD加密,必须确保客户端和服务端一致。 ### ```transport_plugin```传输层插件选项 ```enabled```是否启用传输层插件替代TLS传输。一旦启用传输层插件支持,trojan-go将会把**未经TLS加密的trojan协议流量明文传输给插件**,以允许用户对流量进行自定义的混淆和加密。 ```type```插件类型。目前支持的类型有 - "shadowsocks",支持符合[SIP003](https://shadowsocks.org/en/spec/Plugin.html)标准的shadowsocks混淆插件。trojan-go将在启动时按照SIP003标准替换环境变量并修改自身配置(```remote_addr/remote_port/local_addr/local_port```),使插件与远端直接通讯,而trojan-go仅监听/连接插件。 - "plaintext",使用明文传输。选择此项,trojan-go不会修改任何地址配置(```remote_addr/remote_port/local_addr/local_port```),也不会启动```command```中插件,仅移除最底层的TLS传输层并使用TCP明文传输。此选项目的为支持nginx等接管TLS并进行分流,以及高级用户进行调试测试。**请勿直接使用明文传输模式穿透防火墙。** - "other",其他插件。选择此项,trojan-go不会修改任何地址配置(```remote_addr/remote_port/local_addr/local_port```),但会启动```command```中插件并传入参数和环境变量。 ```command```传输层插件可执行文件的路径。trojan-go将在启动时一并执行它。 ```arg```传输层插件启动参数。这是一个列表,例如```["-config", "test.json"]```。 ```env```传输层插件环境变量。这是一个列表,例如```["VAR1=foo", "VAR2=bar"]```。 ```option```传输层插件配置(SIP003)。例如```"obfs=http;obfs-host=www.baidu.com"```。 ### ```tcp```选项 ```no_delay```TCP封包是否直接发出而不等待缓冲区填满。 ```keep_alive```是否启用TCP心跳存活检测。 ```prefer_ipv4```是否优先使用IPv4地址。 ### ```mysql```数据库选项 trojan-go兼容trojan的基于mysql的用户管理方式,但更推荐的方式是使用API。 ```enabled```表示是否启用mysql数据库进行用户验证。 ```check_rate```是trojan-go从MySQL获取用户数据并更新缓存的间隔时间,单位为秒。 其他选项可以顾名思义,不再赘述。 users表结构和trojan版本定义一致,下面是一个创建users表的例子。注意这里的password指的是密码经过SHA224散列之后的值(字符串),流量download, upload, quota的单位是字节。你可以通过修改数据库users表中的用户记录的方式,添加和删除用户,或者指定用户的流量配额。trojan-go会根据所有的用户流量配额,自动更新当前有效的用户列表。如果download+upload>quota,trojan-go服务器将拒绝该用户的连接。 ```mysql CREATE TABLE users ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, password CHAR(56) NOT NULL, quota BIGINT NOT NULL DEFAULT 0, download BIGINT UNSIGNED NOT NULL DEFAULT 0, upload BIGINT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (id), INDEX (password) ); ``` ### ```forward_proxy```前置代理选项 前置代理选项允许使用其他代理承载trojan-go的流量 ```enabled```是否启用前置代理(socks5)。 ```proxy_addr```前置代理的主机地址。 ```proxy_port```前置代理的端口号。 ```username``` ```password```代理的用户和密码,如果留空则不使用认证。 ### ```api```选项 trojan-go基于gRPC提供了API,以支持服务端和客户端的管理和统计。可以实现客户端的流量和速度统计,服务端各用户的流量和速度统计,用户的动态增删和限速等。 ```enabled```是否启用API功能。 ```api_addr```gRPC监听的地址。 ```api_port```gRPC监听的端口。 ```ssl``` TLS相关设置。 - ```enabled```是否使用TLS传输gRPC流量。 - ```key```,```cert```服务器私钥和证书。 - ```verify_client```是否认证客户端证书。 - ```client_cert```如果开启客户端认证,此处填入认证的客户端证书列表。 警告:**不要将未开启TLS双向认证的API服务直接暴露在互联网上,否则可能导致各类安全问题。** ================================================ FILE: docs/content/basic/trojan.md ================================================ --- title: "Trojan基本原理" draft: false weight: 21 --- 这个页面将会简单讲述Trojan协议的基本工作原理。如果你对于GFW和Trojan的工作方式不感兴趣,可以跳过这一小节。但为了更好地保护你的通讯安全性和节点的隐蔽性,我还是建议你阅读。 ## 为什么(使用流密码的)Shadowsocks容易遭到封锁 防火墙在早期仅仅只是对出境流量进行截获和审查,也即**被动检测**。Shadowsocks的加密协议设计使得传输的数据包本身几乎没有任何特征,看起来类似于完全随机的比特流,这在早期的确能有效绕过GFW。 目前的GFW已经开始采用**主动探测**的方式。具体来说,当GFW发现一个可疑的无法识别的连接时(大流量,随机字节流,高位端口等特征),将会**主动连接**这个服务器端口,重放之前捕获到的流量(或者经过一些精心修改后重放)。Shadowsocks服务器检测到不正常的连接,将连接断开。这种不正常的流量和断开连接的行为被视作可疑的Shadowsocks服务器的特征,于是该服务器被加入GFW的可疑名单中。这个名单不一定立即生效,而是在某些特殊的敏感时期,可疑名单中的服务器会遭到暂时或者永久的封锁。该可疑名单是否封锁,可能由人为因素决定。 如果你想了解更多,可以参考[这篇文章](https://gfw.report/blog/gfw_shadowsocks/)。 ## Trojan如何绕过GFW 与Shadowsocks相反,Trojan不使用自定义的加密协议来隐藏自身。相反,使用特征明显的TLS协议(TLS/SSL),使得流量看起来与正常的HTTPS网站相同。TLS是一个成熟的加密体系,HTTPS即使用TLS承载HTTP流量。使用**正确配置**的加密TLS隧道,可以保证传输的 - 保密性(GFW无法得知传输的内容) - 完整性(一旦GFW试图篡改传输的密文,通讯双方都会发现) - 不可抵赖(GFW无法伪造身份冒充服务端或者客户端) - 前向安全(即使密钥泄露,GFW也无法解密先前的加密流量) 对于被动检测,Trojan协议的流量与HTTPS流量的特征和行为完全一致。而HTTPS流量占据了目前互联网流量的一半以上,且TLS握手成功后流量均为密文,几乎不存在可行方法从其中分辨出Trojan协议流量。 对于主动检测,当防火墙主动连接Trojan服务器进行检测时,Trojan可以正确识别非Trojan协议的流量。与Shadowsocks等代理不同的是,此时Trojan不会断开连接,而是将这个连接代理到一个正常的Web服务器。在GFW看来,该服务器的行为和一个普通的HTTPS网站行为完全相同,无法判断是否是一个Trojan代理节点。这也是Trojan推荐使用合法的域名、使用权威CA签名的HTTPS证书的原因: 这让你的服务器完全无法被GFW使用主动检测判定是一个Trojan服务器。 因此,就目前的情况来看,若要识别并阻断Trojan的连接,只能使用无差别封锁(封锁某个IP段,某一类证书,某一类域名,甚至阻断全国所有出境HTTPS连接)或发动大规模的中间人攻击(劫持所有TLS流量并劫持证书,审查内容)。对于中间人攻击,可以使用Websocket的双重TLS应对,高级配置中有详细讲解。 ================================================ FILE: docs/content/developer/_index.md ================================================ --- title: "实现细节和开发指南" draft: false weight: 40 --- 这一部分介绍Trojan-Go底层实现的细节,主要面向开发者。 ================================================ FILE: docs/content/developer/api.md ================================================ --- title: "API开发" draft: false weight: 100 --- Trojan-Go基于gRPC实现了API,使用protobuf交换数据。客户端可获取流量和速度信息;服务端可获取各用户流量,速度,在线情况,并动态增删用户和限制速度。可以通过在配置文件中添加```api```选项激活API模块。下面是一个例子,各字段含义参见“完整的配置文件”一节。 ```json ... "api": { "enabled": true, "api_addr": "0.0.0.0", "api_port": 10000, "ssl": { "enabled": true, "cert": "api_cert.crt", "key": "api_key.key", "verify_client": true, "client_cert": [ "api_client_cert1.crt", "api_client_cert2.crt" ] }, } ``` 如果需要实现API客户端进行对接,请参考api/service/api.proto文件。 ================================================ FILE: docs/content/developer/build.md ================================================ --- title: "编译和自定义Trojan-Go" draft: false weight: 10 --- 编译需要Go版本号高于1.14.x,请在编译前确认你的编译器版本。推荐使用snap安装和更新go。 编译方式非常简单,可以使用Makefile预设步骤进行编译: ```shell make make install #安装systemd服务等,可选 ``` 或者直接使用Go进行编译: ```shell go build -tags "full" #编译完整版本 ``` 可以通过指定GOOS和GOARCH环境变量,指定交叉编译的目标操作系统和架构,例如 ```shell GOOS=windows GOARCH=386 go build -tags "full" #windows x86 GOOS=linux GOARCH=arm64 go build -tags "full" #linux arm64 ``` 你可以使用release.sh进行批量的多个平台的交叉编译,release版本使用了这个脚本进行构建。 Trojan-Go的大多数模块是可插拔的。在build文件夹下可以找到各个模块的导入声明。如果你不需要其中某些功能,或者需要缩小可执行文件的体积,可以使用构建标签(tags)进行模块的自定义,例如 ```shell go build -tags "full" #编译所有模块 go build -tags "client" -trimpath -ldflags="-s -w -buildid=" #只有客户端功能,且去除符号表缩小体积 go build -tags "server mysql" #只有服务端和mysql支持 ``` 使用full标签等价于 ```shell go build -tags "api client server forward nat other" ``` ================================================ FILE: docs/content/developer/mux.md ================================================ --- title: "多路复用" draft: false weight: 30 --- Trojan-Go使用[smux](https://github.com/xtaci/smux)实现多路复用。同时实现了simplesocks协议用于进行代理传输。 当启用多路复用时,客户端首先发起TLS连接,使用正常trojan协议格式,但协议Command部分填入0x7f(protocol.Mux),标识此连接为复用连接(类似于http的upgrade),之后连接交由smux客户端管理。服务器收到请求头部后,交由smux服务器解析该连接的所有流量。在每条分离出的smux连接上,使用simplesocks协议(去除认证部分的trojan协议)标明代理目的地。自顶向下的协议栈如下: | 协议 | 备注 | | ----------- | -------- | | 真实流量 | | SimpleSocks | | smux | | Trojan | 用于鉴权 | | 底层协议 | | ================================================ FILE: docs/content/developer/overview.md ================================================ --- title: "基本介绍" draft: false weight: 1 --- Trojan-Go的核心部分有 - tunnel 各个协议具体实现 - proxy 代理核心 - config 配置注册和解析模块 - redirector 主动检测欺骗模块 - statistics 用户认证和统计模块 可以在对应文件夹中找到相关源代码。 ## tunnel.Tunnel隧道 Trojan-Go将所有协议(包括路由功能等)抽象为隧道(tunnel.Tunnel接口),每个隧道可开启服务端(tunnel.Server接口)和客户端(tunnel.Client)。每个服务端可以从其底层隧道中,剥离并接受流(tunnel.Conn)和包(tunnel.PacketConn)。客户端可以向底层隧道,创建流和包。 每个隧道并不关心其下方的隧道是什么,但是每个隧道清楚知道这个它上方的其他隧道的相关信息。 所有隧道需要下层提供流或包传输支持,或两者都要求提供。所有隧道必须向上层隧道提供流传输支持,但不一定提供包传输。 隧道可能只有服务端,也可能只有客户端,也可能两者皆有。两者皆有的隧道,可被用于作为Trojan-Go客户端和服务端间的传输隧道。 注意,请区分Trojan-Go的服务端/客户端,和隧道的服务端/客户端的区别。下面是一个方便理解的图例。 ```text 入站 GFW 出站 -------->隧道A服务端->隧道B客户端 ----------------> 隧道B服务端->隧道C客户端-----------> (Trojan-Go客户端) (Trojan-Go服务端) ``` 最底层的隧道为传输层,即不从其他隧道获取或者创建流和包的隧道,充当上图中隧道A或者C的角色。 - transport,可插拔传输层 - socks,socks5代理,仅隧道服务端 - tproxy,透明代理,仅隧道服务端 - dokodemo,反向代理,仅隧道服务端 - freedom,自由出站,仅隧道客户端 这几个隧道直接从TCP/UDP Socket创建流和包,不接受为其底层添加的任何隧道。 其他隧道,只要下层能满足上层对包和流传输的需求,则原则上可以任何方式,任何数量进行组合和堆叠。这些隧道在上图中充当隧道B的角色,他们有 - trojan - websocket - mux - simplesocks - tls - router,路由功能,仅隧道客户端 他们都不关心其下层隧道实现。但可以根据到来的流和包,将其分发给上层隧道。 例如,在这张图中,是一个典型的Trojan-Go客户端和服务端,各个隧道自下往上堆叠的顺序是: - 隧道A: transport->socks - 隧道B: transport->tls->trojan - 隧道C: freedom 实际上的隧道堆叠的情况会比这个更复杂一些。通常的入站的隧道是一棵多叉树的形式,而非一条链。具体解释参考下文。 ## proxy.Proxy代理核心 代理核心的作用,是监听上述隧道进行组合堆叠并形成的协议栈,将所有的入站协议栈(多个隧道Server的终端节点,见下)中抽取的流和包,以及对应元信息,转送给出站协议栈(一个隧道Client)。 注意,这里的入站协议栈可以有多个,如客户端可以同时从Socks5和HTTP协议栈中抽取流和包,服务端可以同时从Websocket承载的Trojan协议,和TLS承载的Trojan协议中抽取流和包等。但是出站协议栈只能有一个,如只使用TLS承载的Trojan协议出站。 为了描述入站协议栈(隧道服务端)的组合和堆叠方式,使用一棵多叉树对所有协议栈进行描述。你可以在proxy文件夹中各组件,看到构建树的过程。 而出站协议栈则比较简单,使用一个简单列表即可描述。 所以实际上,对于一个典型的开启了Websocket和Mux的客户端/服务端,上图的隧道堆叠模型为: 客户端 - 入站(树) - transport (根) - adapter 能够识别HTTP和Socks流量并分发给上层协议 - http (终端节点) - socks(终端节点) - 出站(链) - transport (根) - tls - websocket - trojan - mux - simplesocks 服务端 - 入站(树) - transport (根) - tls 能够识别HTTP和非HTTP流量并分发 - websocket - trojan(终端节点) - mux - simplesocks (终端节点) - trojan 能够识别mux和普通trojan流量并分发(终端节点) - mux - simplesocks (终端节点) - 出站(链) - freedom 注意,代理核心只从隧道构成的树的终端节点抽取流和包,并转送到唯一的出站上。多个终端节点的设计的目的,是使Trojan-Go同时兼容Websocket和Trojan协议入站连接,开启/未开启Mux的入站连接,以及HTTP/Socks5自动识别的功能。每个拥有多个儿子的树上节点,具有精确识别和分发流和包给不同的儿子节点的能力。这符合我们假定每个协议了解其上层承载协议的假设。 ================================================ FILE: docs/content/developer/plugin.md ================================================ --- title: "可插拔传输层插件开发" draft: false weight: 150 --- Trojan-Go鼓励开发传输层插件,以丰富协议类型,增加与GFW对抗的战略纵深。 传输层插件的作用,是替代tansport隧道的TLS进行传输加密和混淆。 插件与Trojan-Go基于TCP Socket通讯,与Trojan-Go本身不存在任何耦合关系,你可以使用任何你喜欢的语言和设计模式进行开发。我们建议的参照[SIP003](https://shadowsocks.org/en/spec/Plugin.html)标准进行开发。如此开发的插件可以同时用于Trojan-Go和Shadowsocks。 Trojan-Go开启插件功能后,仅使用TCP进行传输(明文)。你的插件只需要处理入站的TCP请求即可。你可以将这些TCP流量转换成任何你喜欢的流量格式,如QUIC,HTTP,甚至是ICMP。 Trojan-Go插件设计原则,与Shadowsocks略有不同: 1. 插件本身可以对传输内容进行加密,混淆和完整性校验,以及可以抵抗重放攻击。 2. 插件应该伪造一种已有的、常见的服务(记做X服务),及其流量,在此基础上嵌入自己的加密内容。 3. 服务端的插件,在检验到内容被篡改/遭到重放时,**必须将此连接交由Trojan-Go处理**。具体步骤是,将已读入和未读入的内容一并发送给Trojan-Go,并建立双向连接,而不是直接断开。Trojan-Go将与一个真实的X服务器建立连接,使攻击者直接与真实的X服务器进行交互。 解释如下: 第一点原则,是由于Trojan协议本身并不加密。将TLS替换为传输层插件后,将**完全信任插件的安全性**。 第二点原则,是继承Trojan的精神。最适合隐藏一棵树的地方,是森林。 第三点原则,是为了充分利用Trojan-Go的抗主动探测特性。即使GFW对你的服务器进行主动探测,你的服务器也可以表现得与X服务一致,而不存在其他特征。 为了方便理解,举一个例子。 1. 假设你的插件伪装的是MySQL流量。防火墙通过流量嗅探,发现你的MySQL流量大得异常,决定主动连接你的服务器进行主动探测。 2. 防火墙连接到你的服务器并发送探测载荷,你的Trojan-Go服务端插件,经过校验,发现这个异常连接不是代理流量,于是将这个连接交由Trojan-Go处理。 3. Trojan-Go发现这个连接异常,将这个连接重定向到一个真正的MySQL服务器上。于是,防火墙开始与一个真正的MySQL服务器进行交互,发现其行为与真实MySQL服务器无异,无法对服务器进行封禁。 另外,即使你的协议协议和插件不能满足原则2和原则3,甚至不能很好满足原则1,我们同样鼓励开发。因为GFW仅仅针对流行的协议进行审计和封锁,此类协议(土制密码学/土制协议)只要不公开发表,同样能保持非常强健的生命力。 ================================================ FILE: docs/content/developer/simplesocks.md ================================================ --- title: "SimpleSocks协议" draft: false weight: 50 --- SimpleSocks协议是无鉴权机制的简单代理协议,本质上是去除了sha224的Trojan协议。使用该协议的目的是减少多路复用时的overhead。 只有启用多路复用之后,被复用的连接才会使用这个协议。也即SimpleSocks总是被SMux承载。 SimpleSocks甚至比Socks5更简单,下面是头部结构。 ```text +-----+------+----------+----------+-----------+ | CMD | ATYP | DST.ADDR | DST.PORT | Payload | +-----+------+----------+----------+-----------+ | 1 | 1 | Variable | 2 | Variable | +-----+------+----------+----------+-----------+ ``` 各字段定义与Trojan协议相同,不再赘述。 ================================================ FILE: docs/content/developer/trojan.md ================================================ --- title: "Trojan协议" draft: false weight: 20 --- Trojan-Go遵循原始的trojan协议,具体格式可以参考[Trojan文档](https://trojan-gfw.github.io/trojan/protocol),这里不再赘述。 默认情况下,trojan协议使用TLS来承载,协议栈如下: | 协议 | | -------- | | 真实流量 | | Trojan | | TLS | | TCP | ================================================ FILE: docs/content/developer/url.md ================================================ --- title: "URL方案(草案)" draft: false weight: 200 --- ## Changelog - encryption 格式修改为 ss;method:password ## 概述 感谢 @DuckSoft @StudentMain @phlinhng 对 Trojan-Go URL 方案的讨论和贡献。**目前 URL 方案为草案,需要更多的实践和讨论。** Trojan-Go**客户端**可以接受URL,以定位服务器资源。原则如下: - 遵守 URL 格式规范 - 保证人类可读,对机器友好 - URL 的作用,是定位 Trojan-Go 节点资源,方便资源分享 需要注意,基于人类可读性的考虑,禁止将 base64 等编码数据嵌入 URL 中。首先, base64 编码不能保证传输安全,其意义在于在 ASCII 信道传输非 ASCII 数据。其次,如果需要保证分享 URL 时的传输安全,请对明文 URL 进行加密,而不是修改 URL 格式。 ## 格式 基本格式如下,`$()` 代表此处需要 `encodeURIComponent`。 ```text trojan-go:// $(trojan-password) @ trojan-host : port /? sni=$(tls-sni.com)& type=$(original|ws|h2|h2+ws)& host=$(websocket-host.com)& path=$(/websocket/path)& encryption=$(ss;aes-256-gcm;ss-password)& plugin=$(...) #$(descriptive-text) ``` 例如 ```text trojan-go://password1234@google.com/?sni=microsoft.com&type=ws&host=youtube.com&path=%2Fgo&encryption=ss%3Baes-256-gcm%3Afuckgfw ``` 由于 Trojan-Go 兼容 Trojan,所以对于 Trojan 的 URL 方案 ```text trojan://password@remote_host:remote_port ``` 可以兼容接受。它等价于 ```text trojan-go://password@remote_host:remote_port ``` 需要注意的是,一旦服务器使用了非Trojan兼容的功能,必须使用```trojan-go://```定位服务器。这样设计的目的是使得 Trojan-Go 的 URL 不会被 Trojan 错误接受,避免污染 Trojan 用户的 URL 分享。同时,Trojan-Go 确保可以兼容接受 Trojan 的 URL。 ## 详述 注意:所有参数名和常数字符串均区分大小写。 ### `trojan-password` Trojan 的密码。 不可省略,不能为空字符串,不建议含有非 ASCII 可打印字符。 必须使用 `encodeURIComponent` 编码。 ### `trojan-host` 节点 IP / 域名。 不可省略,不能为空字符串。 IPv6 地址必须扩方括号。 IDN 域名(如“百度.cn”)必须使用 `xn--xxxxxx` 格式。 ### `port` 节点端口。 省略时默认为 `443`。 必须取 `[1,65535]` 中的整数。 ### `tls`或`allowInsecure` 没有这个字段。 TLS 默认一直启用,除非有传输插件禁用它。 TLS 认证必须开启。无法使用根CA校验服务器身份的节点,不适合分享。 ### `sni` 自定义 TLS 的 SNI。 省略时默认与 `trojan-host` 同值。不得为空字符串。 必须使用 `encodeURIComponent` 编码。 ### `type` 传输类型。 省略时默认为 `original`,但不可为空字符串。 目前可选值只有 `original` 和 `ws`,未来可能会有 `h2`、`h2+ws` 等取值。 当取值为 `original` 时,使用原始 Trojan 传输方式,无法方便通过 CDN。 当取值为 `ws` 时,使用 Websocket over TLS 传输。 ### `host` 自定义 HTTP `Host` 头。 可以省略,省略时值同 `trojan-host`。 可以为空字符串,但可能带来非预期情形。 警告:若你的端口非标准端口(不是 80 / 443),RFC 标准规定 `Host` 应在主机名后附上端口号,例如 `example.com:44333`。至于是否遵守,请自行斟酌。 必须使用 `encodeURIComponent` 编码。 ### `path` 当传输类型 `type` 取 `ws`、`h2`、`h2+ws` 时,此项有效。 不可省略,不可为空。 必须以 `/` 开头。 可以使用 URL 中的 `&` `#` `?` 等字符,但应当是合法的 URL 路径。 必须使用 `encodeURIComponent` 编码。 ### `mux` 没有这个字段。 当前服务器默认一直支持 `mux`。 启用 `mux` 与否各有利弊,应由客户端决定自己是否启用。URL的作用,是定位服务器资源,而不是规定用户使用偏好。 ### `encryption` 用于保证 Trojan 流量密码学安全的加密层。 可省略,默认为 `none`,即不使用加密。 不可以为空字符串。 必须使用 encodeURIComponent 编码。 使用 Shadowsocks 算法进行流量加密时,其格式为: ```text ss;method:password ``` 其中 ss 是固定内容,method 是加密方法,必须为下列之一: - `aes-128-gcm` - `aes-256-gcm` - `chacha20-ietf-poly1305` 其中的 `password` 是 Shadowsocks 的密码,不得为空字符串。 `password` 中若包含分号,不需要进行转义。 `password` 应为英文可打印 ASCII 字符。 其他加密方案待定。 ### `plugin` 额外的插件选项。本字段保留。 可省略,但不可以为空字符串。 ### URL Fragment (# 后内容) 节点说明。 不建议省略,不建议为空字符串。 必须使用 `encodeURIComponent` 编码。 ================================================ FILE: docs/content/developer/websocket.md ================================================ --- title: "Websocket" draft: false weight: 40 --- 由于使用CDN中转时,HTTPS对CDN透明,CDN可以审查Websocket传输内容。而Trojan协议本身是明文传输,因此为保证安全性,可添加一层Shadowsocks AEAD加密层以混淆流量特征并保证安全性。 **如果你使用的是中国境内运营商提供的CDN,请务必开启AEAD加密** 开启AEAD加密后,Websocket承载的流量将被Shadowsocks AEAD加密,头部具体格式参见Shadowsocks白皮书。 开启Websocket支持后,协议栈如下: | 协议 | 备注 | | ----------- | ---------------- | | 真实流量 | | | SimpleSocks | 如果开启多路复用 | | smux | 如果开启多路复用 | | Trojan | | | Shadowsocks | 如果开启加密 | | Websocket | | | 传输层协议 | | ================================================ FILE: easy/easy.go ================================================ package easy import ( "encoding/json" "flag" "net" "strconv" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/option" "github.com/p4gefau1t/trojan-go/proxy" ) type easy struct { server *bool client *bool password *string local *string remote *string cert *string key *string } type ClientConfig struct { RunType string `json:"run_type"` LocalAddr string `json:"local_addr"` LocalPort int `json:"local_port"` RemoteAddr string `json:"remote_addr"` RemotePort int `json:"remote_port"` Password []string `json:"password"` } type TLS struct { SNI string `json:"sni"` Cert string `json:"cert"` Key string `json:"key"` } type ServerConfig struct { RunType string `json:"run_type"` LocalAddr string `json:"local_addr"` LocalPort int `json:"local_port"` RemoteAddr string `json:"remote_addr"` RemotePort int `json:"remote_port"` Password []string `json:"password"` TLS `json:"ssl"` } func (o *easy) Name() string { return "easy" } func (o *easy) Handle() error { if !*o.server && !*o.client { return common.NewError("empty") } if *o.password == "" { log.Fatal("empty password is not allowed") } log.Info("easy mode enabled, trojan-go will NOT use the config file") if *o.client { if *o.local == "" { log.Warn("client local addr is unspecified, using 127.0.0.1:1080") *o.local = "127.0.0.1:1080" } localHost, localPortStr, err := net.SplitHostPort(*o.local) if err != nil { log.Fatal(common.NewError("invalid local addr format:" + *o.local).Base(err)) } remoteHost, remotePortStr, err := net.SplitHostPort(*o.remote) if err != nil { log.Fatal(common.NewError("invalid remote addr format:" + *o.remote).Base(err)) } localPort, err := strconv.Atoi(localPortStr) if err != nil { log.Fatal(err) } remotePort, err := strconv.Atoi(remotePortStr) if err != nil { log.Fatal(err) } clientConfig := ClientConfig{ RunType: "client", LocalAddr: localHost, LocalPort: localPort, RemoteAddr: remoteHost, RemotePort: remotePort, Password: []string{ *o.password, }, } clientConfigJSON, err := json.Marshal(&clientConfig) common.Must(err) log.Info("generated config:") log.Info(string(clientConfigJSON)) proxy, err := proxy.NewProxyFromConfigData(clientConfigJSON, true) if err != nil { log.Fatal(err) } if err := proxy.Run(); err != nil { log.Fatal(err) } } else if *o.server { if *o.remote == "" { log.Warn("server remote addr is unspecified, using 127.0.0.1:80") *o.remote = "127.0.0.1:80" } if *o.local == "" { log.Warn("server local addr is unspecified, using 0.0.0.0:443") *o.local = "0.0.0.0:443" } localHost, localPortStr, err := net.SplitHostPort(*o.local) if err != nil { log.Fatal(common.NewError("invalid local addr format:" + *o.local).Base(err)) } remoteHost, remotePortStr, err := net.SplitHostPort(*o.remote) if err != nil { log.Fatal(common.NewError("invalid remote addr format:" + *o.remote).Base(err)) } localPort, err := strconv.Atoi(localPortStr) if err != nil { log.Fatal(err) } remotePort, err := strconv.Atoi(remotePortStr) if err != nil { log.Fatal(err) } serverConfig := ServerConfig{ RunType: "server", LocalAddr: localHost, LocalPort: localPort, RemoteAddr: remoteHost, RemotePort: remotePort, Password: []string{ *o.password, }, TLS: TLS{ Cert: *o.cert, Key: *o.key, }, } serverConfigJSON, err := json.Marshal(&serverConfig) common.Must(err) log.Info("generated json config:") log.Info(string(serverConfigJSON)) proxy, err := proxy.NewProxyFromConfigData(serverConfigJSON, true) if err != nil { log.Fatal(err) } if err := proxy.Run(); err != nil { log.Fatal(err) } } return nil } func (o *easy) Priority() int { return 50 } func init() { option.RegisterHandler(&easy{ server: flag.Bool("server", false, "Run a trojan-go server"), client: flag.Bool("client", false, "Run a trojan-go client"), password: flag.String("password", "", "Password for authentication"), remote: flag.String("remote", "", "Remote address, e.g. 127.0.0.1:12345"), local: flag.String("local", "", "Local address, e.g. 127.0.0.1:12345"), key: flag.String("key", "server.key", "Key of the server"), cert: flag.String("cert", "server.crt", "Certificates of the server"), }) } ================================================ FILE: example/client.json ================================================ { "run_type": "client", "local_addr": "127.0.0.1", "local_port": 1080, "remote_addr": "your_server", "remote_port": 443, "password": [ "your_password" ], "ssl": { "sni": "your-domain-name.com" }, "mux": { "enabled": true }, "router": { "enabled": true, "bypass": [ "geoip:cn", "geoip:private", "geosite:cn", "geosite:private" ], "block": [ "geosite:category-ads" ], "proxy": [ "geosite:geolocation-!cn" ], "default_policy": "proxy", "geoip": "/usr/share/trojan-go/geoip.dat", "geosite": "/usr/share/trojan-go/geosite.dat" } } ================================================ FILE: example/client.yaml ================================================ run-type: client local-addr: 127.0.0.1 local-port: 1080 remote-addr: your_server remote-port: 443 password: - your_password ssl: sni: your-domain-name.com mux: enabled: true router: enabled: true bypass: ['geoip:cn', 'geoip:private', 'geosite:cn', 'geosite:private'] block: ['geosite:category-ads'] proxy: ['geosite:geolocation-!cn'] default_policy: proxy geoip: /usr/share/trojan-go/geoip.dat geosite: /usr/share/trojan-go/geosite.dat ================================================ FILE: example/server.json ================================================ { "run_type": "server", "local_addr": "0.0.0.0", "local_port": 443, "remote_addr": "127.0.0.1", "remote_port": 80, "password": [ "your_password" ], "ssl": { "cert": "your_cert.crt", "key": "your_key.key", "sni": "your-domain-name.com" }, "router": { "enabled": true, "block": [ "geoip:private" ], "geoip": "/usr/share/trojan-go/geoip.dat", "geosite": "/usr/share/trojan-go/geosite.dat" } } ================================================ FILE: example/server.yaml ================================================ run-type: server local-addr: 0.0.0.0 local-port: 443 remote-addr: 127.0.0.1 remote-port: 80 password: - your_password ssl: cert: your_cert.crt key: your_key.key sni: your-domain-name.com router: enabled: true block: - 'geoip:private' geoip: /usr/share/trojan-go/geoip.dat geosite: /usr/share/trojan-go/geosite.dat ================================================ FILE: example/trojan-go.service ================================================ [Unit] Description=Trojan-Go - An unidentifiable mechanism that helps you bypass GFW Documentation=https://p4gefau1t.github.io/trojan-go/ After=network.target nss-lookup.target [Service] User=nobody CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true ExecStart=/usr/bin/trojan-go -config /etc/trojan-go/config.json Restart=on-failure RestartSec=10s LimitNOFILE=infinity [Install] WantedBy=multi-user.target ================================================ FILE: example/trojan-go@.service ================================================ [Unit] Description=Trojan-Go - An unidentifiable mechanism that helps you bypass GFW Documentation=https://p4gefau1t.github.io/trojan-go/ After=network.target nss-lookup.target [Service] User=nobody CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true ExecStart=/usr/bin/trojan-go -config /etc/trojan-go/%i.json Restart=on-failure RestartSec=10s LimitNOFILE=infinity [Install] WantedBy=multi-user.target ================================================ FILE: go.mod ================================================ module github.com/p4gefau1t/trojan-go go 1.17 require ( github.com/go-sql-driver/mysql v1.6.0 github.com/refraction-networking/utls v0.0.0-20210713165636-0b2885c8c0d4 github.com/shadowsocks/go-shadowsocks2 v0.1.5 github.com/smartystreets/goconvey v1.6.4 github.com/stretchr/testify v1.7.0 github.com/txthinking/socks5 v0.0.0-20210716140126-fa1f52a8f2da github.com/v2fly/v2ray-core/v4 v4.42.1 github.com/xtaci/smux v1.5.15 golang.org/x/net v0.0.0-20210913180222-943fd674d43e golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac google.golang.org/grpc v1.40.0 google.golang.org/protobuf v1.27.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 // indirect github.com/jtolds/gls v4.20.0+incompatible // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pires/go-proxyproto v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf // indirect github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe // indirect go.starlark.net v0.0.0-20210602144842-1cdb82c9e17a // indirect golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 // indirect golang.org/x/text v0.3.6 // indirect google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect ) ================================================ FILE: go.sum ================================================ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-metro v0.0.0-20200812162917-85c65e2d0165 h1:BS21ZUJ/B5X2UVUbczfmdWH7GapPWAhxcMsDnjJTU1E= github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a h1:YtdtTUN1iH97s+6PUjLnaiKSQj4oG1/EZ3N9bx6g4kU= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/jhump/protoreflect v1.9.0 h1:npqHz788dryJiR/l6K/RUQAyh2SwV91+d1dnh4RjO9w= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/lucas-clemente/quic-go v0.23.0 h1:5vFnKtZ6nHDFsc/F3uuiF4T3y/AXaQdxjUqiVw26GZE= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/marten-seemann/qtls-go1-16 v0.1.4 h1:xbHbOGGhrenVtII6Co8akhLEdrawwB2iHl5yhJRpnco= github.com/marten-seemann/qtls-go1-17 v0.1.0 h1:P9ggrs5xtwiqXv/FHNwntmuLMNq3KaSIG93AtAZ48xk= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pires/go-proxyproto v0.6.0 h1:cLJUPnuQdiNf7P/wbeOKmM1khVdaMgTFDLj8h9ZrVYk= github.com/pires/go-proxyproto v0.6.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/refraction-networking/utls v0.0.0-20210713165636-0b2885c8c0d4 h1:n9NMHJusHylTmtaJ0Qe0VV9dkTZLiwAxHmrI/l98GeE= github.com/refraction-networking/utls v0.0.0-20210713165636-0b2885c8c0d4/go.mod h1:tz9gX959MEFfFN5whTIocCLUG57WiILqtdVxI8c6Wj0= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/seiflotfy/cuckoofilter v0.0.0-20201222105146-bc6005554a0c h1:pqy40B3MQWYrza7YZXOXgl0Nf0QGFqrOC0BKae1UNAA= github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28= github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf h1:7PflaKRtU4np/epFxRXlFhlzLXZzKFrH5/I4so5Ove0= github.com/txthinking/runnergroup v0.0.0-20210608031112-152c7c4432bf/go.mod h1:CLUSJbazqETbaR+i0YAhXBICV9TrKH93pziccMhmhpM= github.com/txthinking/socks5 v0.0.0-20210716140126-fa1f52a8f2da h1:7x8pJcBTdKTBpQbRjZZc9o6CDquXBbvm9UIrR6ZSRJ4= github.com/txthinking/socks5 v0.0.0-20210716140126-fa1f52a8f2da/go.mod h1:7NloQcrxaZYKURWph5HLxVDlIwMHJXCPkeWPtpftsIg= github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe h1:gMWxZxBFRAXqoGkwkYlPX2zvyyKNWJpxOxCrjqJkm5A= github.com/txthinking/x v0.0.0-20210326105829-476fab902fbe/go.mod h1:WgqbSEmUYSjEV3B1qmee/PpP2NYEz4bL9/+mF1ma+s4= github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 h1:4Yh46CVE3k/lPq6hUbEdbB1u1anRBXLewm3k+L0iOMc= github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848 h1:p1UzXK6VAutXFFQMnre66h7g1BjRKUnLv0HfmmRoz7w= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/v2ray-core/v4 v4.42.1 h1:oQwW7dSpzx+nY3SrWw25OXVOcXNtwLFZ6BanqMufYdk= github.com/v2fly/v2ray-core/v4 v4.42.1/go.mod h1:YrWpRau9RYPHZLJXJIoVHjSXwL5DhGuIlTnA1i9FG98= github.com/xtaci/smux v1.5.15 h1:6hMiXswcleXj5oNfcJc+DXS8Vj36XX2LaX98udog6Kc= github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.starlark.net v0.0.0-20210602144842-1cdb82c9e17a h1:wDtSCWGrX9tusypq2Qq9xzaA3Tf/+4D2KaWO+HQvGZE= go.starlark.net v0.0.0-20210602144842-1cdb82c9e17a/go.mod h1:t3mmBBPzAVvK0L0n1drDmrQsJ8FoIx4INCqVMTr/Zo0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20210913180222-943fd674d43e h1:+b/22bPvDYt4NPDcy4xAGCmON713ONAWFeY3Z7I3tR8= golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55 h1:rw6UNGRMfarCepjI8qOepea/SXwIBVfTKjztZ5gBbq4= golang.org/x/sys v0.0.0-20210820121016-41cdb8703e55/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.1.1 h1:wGiQel/hW0NnEkJUk8lbzkX2gFJU6PFxf1v5OlCfuOs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= ================================================ FILE: log/golog/buffer/buffer.go ================================================ // Buffer-like byte slice // Copyright (c) 2017 Fadhli Dzil Ikram package buffer // Buffer type wrap up byte slice built-in type type Buffer []byte // Reset buffer position to start func (b *Buffer) Reset() { *b = Buffer([]byte(*b)[:0]) } // Append byte slice to buffer func (b *Buffer) Append(data []byte) { *b = append(*b, data...) } // AppendByte to buffer func (b *Buffer) AppendByte(data byte) { *b = append(*b, data) } // AppendInt to buffer func (b *Buffer) AppendInt(val int, width int) { var repr [8]byte reprCount := len(repr) - 1 for val >= 10 || width > 1 { reminder := val / 10 repr[reprCount] = byte('0' + val - reminder*10) val = reminder reprCount-- width-- } repr[reprCount] = byte('0' + val) b.Append(repr[reprCount:]) } // Bytes return underlying slice data func (b Buffer) Bytes() []byte { return []byte(b) } ================================================ FILE: log/golog/buffer/buffer_test.go ================================================ // Buffer-like byte slice // Copyright (c) 2017 Fadhli Dzil Ikram // // Test file for buffer package buffer import ( "testing" . "github.com/smartystreets/goconvey/convey" ) func TestBufferAllocation(t *testing.T) { Convey("Given new unallocated buffer", t, func() { var buf Buffer Convey("When appended with data", func() { data := []byte("Hello") buf.Append(data) Convey("It should have same content as the original data", func() { So(buf.Bytes(), ShouldResemble, data) }) }) Convey("When appended with single byte", func() { data := byte('H') buf.AppendByte(data) Convey("It should have 1 byte length", func() { So(len(buf), ShouldEqual, 1) }) Convey("It should have same content", func() { So(buf.Bytes()[0], ShouldEqual, data) }) }) Convey("When appended with integer", func() { data := 12345 repr := []byte("012345") buf.AppendInt(data, len(repr)) Convey("Should have same content with the integer representation", func() { So(buf.Bytes(), ShouldResemble, repr) }) }) }) } func TestBufferReset(t *testing.T) { Convey("Given allocated buffer", t, func() { var buf Buffer data := []byte("Hello") replace := []byte("World") buf.Append(data) Convey("When buffer reset", func() { buf.Reset() Convey("It should have zero length", func() { So(len(buf), ShouldEqual, 0) }) }) Convey("When buffer reset and replaced with another append", func() { buf.Reset() buf.Append(replace) Convey("It should have same content with the replaced data", func() { So(buf.Bytes(), ShouldResemble, replace) }) }) }) } ================================================ FILE: log/golog/colorful/colorful.go ================================================ // The color engine for the go-log library // Copyright (c) 2017 Fadhli Dzil Ikram package colorful import ( "runtime" "github.com/p4gefau1t/trojan-go/log/golog/buffer" ) // ColorBuffer add color option to buffer append type ColorBuffer struct { buffer.Buffer } // color palette map var ( colorOff = []byte("\033[0m") colorRed = []byte("\033[0;31m") colorGreen = []byte("\033[0;32m") colorOrange = []byte("\033[0;33m") colorBlue = []byte("\033[0;34m") colorPurple = []byte("\033[0;35m") colorCyan = []byte("\033[0;36m") colorGray = []byte("\033[0;37m") ) func init() { if runtime.GOOS != "linux" { colorOff = []byte("") colorRed = []byte("") colorGreen = []byte("") colorOrange = []byte("") colorBlue = []byte("") colorPurple = []byte("") colorCyan = []byte("") colorGray = []byte("") } } // Off apply no color to the data func (cb *ColorBuffer) Off() { cb.Append(colorOff) } // Red apply red color to the data func (cb *ColorBuffer) Red() { cb.Append(colorRed) } // Green apply green color to the data func (cb *ColorBuffer) Green() { cb.Append(colorGreen) } // Orange apply orange color to the data func (cb *ColorBuffer) Orange() { cb.Append(colorOrange) } // Blue apply blue color to the data func (cb *ColorBuffer) Blue() { cb.Append(colorBlue) } // Purple apply purple color to the data func (cb *ColorBuffer) Purple() { cb.Append(colorPurple) } // Cyan apply cyan color to the data func (cb *ColorBuffer) Cyan() { cb.Append(colorCyan) } // Gray apply gray color to the data func (cb *ColorBuffer) Gray() { cb.Append(colorGray) } // mixer mix the color on and off byte with the actual data func mixer(data []byte, color []byte) []byte { var result []byte return append(append(append(result, color...), data...), colorOff...) } // Red apply red color to the data func Red(data []byte) []byte { return mixer(data, colorRed) } // Green apply green color to the data func Green(data []byte) []byte { return mixer(data, colorGreen) } // Orange apply orange color to the data func Orange(data []byte) []byte { return mixer(data, colorOrange) } // Blue apply blue color to the data func Blue(data []byte) []byte { return mixer(data, colorBlue) } // Purple apply purple color to the data func Purple(data []byte) []byte { return mixer(data, colorPurple) } // Cyan apply cyan color to the data func Cyan(data []byte) []byte { return mixer(data, colorCyan) } // Gray apply gray color to the data func Gray(data []byte) []byte { return mixer(data, colorGray) } ================================================ FILE: log/golog/colorful/colorful_test.go ================================================ // The color engine for the go-log library // Copyright (c) 2017 Fadhli Dzil Ikram // // Test file package colorful import ( "testing" . "github.com/smartystreets/goconvey/convey" "github.com/p4gefau1t/trojan-go/log/golog/buffer" ) func TestColorBuffer(t *testing.T) { Convey("Given empty color buffer and test data", t, func() { var cb ColorBuffer var result buffer.Buffer // Add color to the result buffer result.Append(colorRed) result.Append(colorGreen) result.Append(colorOrange) result.Append(colorBlue) result.Append(colorPurple) result.Append(colorCyan) result.Append(colorGray) result.Append(colorOff) Convey("When appended with color", func() { cb.Red() cb.Green() cb.Orange() cb.Blue() cb.Purple() cb.Cyan() cb.Gray() cb.Off() Convey("It should have same content with the test data", func() { So(result.Bytes(), ShouldResemble, cb.Bytes()) }) }) }) } func TestColorMixer(t *testing.T) { Convey("Given mixer test result data", t, func() { var ( data = []byte("Hello") resultRed buffer.Buffer resultGreen buffer.Buffer resultOrange buffer.Buffer resultBlue buffer.Buffer resultPurple buffer.Buffer resultCyan buffer.Buffer resultGray buffer.Buffer ) // Add result to buffer resultRed.Append(colorRed) resultRed.Append(data) resultRed.Append(colorOff) resultGreen.Append(colorGreen) resultGreen.Append(data) resultGreen.Append(colorOff) resultOrange.Append(colorOrange) resultOrange.Append(data) resultOrange.Append(colorOff) resultBlue.Append(colorBlue) resultBlue.Append(data) resultBlue.Append(colorOff) resultPurple.Append(colorPurple) resultPurple.Append(data) resultPurple.Append(colorOff) resultCyan.Append(colorCyan) resultCyan.Append(data) resultCyan.Append(colorOff) resultGray.Append(colorGray) resultGray.Append(data) resultGray.Append(colorOff) Convey("It should have same result when data appended with Red color", func() { So(Red(data), ShouldResemble, resultRed.Bytes()) }) Convey("It should have same result when data appended with Green color", func() { So(Green(data), ShouldResemble, resultGreen.Bytes()) }) Convey("It should have same result when data appended with Orange color", func() { So(Orange(data), ShouldResemble, resultOrange.Bytes()) }) Convey("It should have same result when data appended with Blue color", func() { So(Blue(data), ShouldResemble, resultBlue.Bytes()) }) Convey("It should have same result when data appended with Purple color", func() { So(Purple(data), ShouldResemble, resultPurple.Bytes()) }) Convey("It should have same result when data appended with Cyan color", func() { So(Cyan(data), ShouldResemble, resultCyan.Bytes()) }) Convey("It should have same result when data appended with Gray color", func() { So(Gray(data), ShouldResemble, resultGray.Bytes()) }) }) } ================================================ FILE: log/golog/golog.go ================================================ // The colorful and simple logging library // Copyright (c) 2017 Fadhli Dzil Ikram package golog import ( "fmt" "io" "os" "path/filepath" "runtime" "sync" "sync/atomic" "time" terminal "golang.org/x/term" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/log/golog/colorful" ) func init() { log.RegisterLogger(New(os.Stdout)) } // FdWriter interface extends existing io.Writer with file descriptor function // support type FdWriter interface { io.Writer Fd() uintptr } // Logger struct define the underlying storage for single logger type Logger struct { mu sync.RWMutex color bool out io.Writer debug bool timestamp bool quiet bool buf colorful.ColorBuffer logLevel int32 } // Prefix struct define plain and color byte type Prefix struct { Plain []byte Color []byte File bool } var ( // Plain prefix template plainFatal = []byte("[FATAL] ") plainError = []byte("[ERROR] ") plainWarn = []byte("[WARN] ") plainInfo = []byte("[INFO] ") plainDebug = []byte("[DEBUG] ") plainTrace = []byte("[TRACE] ") // FatalPrefix show fatal prefix FatalPrefix = Prefix{ Plain: plainFatal, Color: colorful.Red(plainFatal), File: true, } // ErrorPrefix show error prefix ErrorPrefix = Prefix{ Plain: plainError, Color: colorful.Red(plainError), File: true, } // WarnPrefix show warn prefix WarnPrefix = Prefix{ Plain: plainWarn, Color: colorful.Orange(plainWarn), } // InfoPrefix show info prefix InfoPrefix = Prefix{ Plain: plainInfo, Color: colorful.Green(plainInfo), } // DebugPrefix show info prefix DebugPrefix = Prefix{ Plain: plainDebug, Color: colorful.Purple(plainDebug), File: true, } // TracePrefix show info prefix TracePrefix = Prefix{ Plain: plainTrace, Color: colorful.Cyan(plainTrace), } ) // New returns new Logger instance with predefined writer output and // automatically detect terminal coloring support func New(out FdWriter) *Logger { return &Logger{ color: terminal.IsTerminal(int(out.Fd())), out: out, timestamp: true, } } func (l *Logger) SetLogLevel(level log.LogLevel) { l.mu.Lock() defer l.mu.Unlock() atomic.StoreInt32(&l.logLevel, int32(level)) } func (l *Logger) SetOutput(w io.Writer) { l.mu.Lock() defer l.mu.Unlock() l.color = false if fdw, ok := w.(FdWriter); ok { l.color = terminal.IsTerminal(int(fdw.Fd())) } l.out = w } // WithColor explicitly turn on colorful features on the log func (l *Logger) WithColor() *Logger { l.mu.Lock() defer l.mu.Unlock() l.color = true return l } // WithoutColor explicitly turn off colorful features on the log func (l *Logger) WithoutColor() *Logger { l.mu.Lock() defer l.mu.Unlock() l.color = false return l } // WithDebug turn on debugging output on the log to reveal debug and trace level func (l *Logger) WithDebug() *Logger { l.mu.Lock() defer l.mu.Unlock() l.debug = true return l } // WithoutDebug turn off debugging output on the log func (l *Logger) WithoutDebug() *Logger { l.mu.Lock() defer l.mu.Unlock() l.debug = false return l } // IsDebug check the state of debugging output func (l *Logger) IsDebug() bool { l.mu.RLock() defer l.mu.RUnlock() return l.debug } // WithTimestamp turn on timestamp output on the log func (l *Logger) WithTimestamp() *Logger { l.mu.Lock() defer l.mu.Unlock() l.timestamp = true return l } // WithoutTimestamp turn off timestamp output on the log func (l *Logger) WithoutTimestamp() *Logger { l.mu.Lock() defer l.mu.Unlock() l.timestamp = false return l } // Quiet turn off all log output func (l *Logger) Quiet() *Logger { l.mu.Lock() defer l.mu.Unlock() l.quiet = true return l } // NoQuiet turn on all log output func (l *Logger) NoQuiet() *Logger { l.mu.Lock() defer l.mu.Unlock() l.quiet = false return l } // IsQuiet check for quiet state func (l *Logger) IsQuiet() bool { l.mu.RLock() defer l.mu.RUnlock() return l.quiet } // Output print the actual value func (l *Logger) Output(depth int, prefix Prefix, data string) error { // Check if quiet is requested, and try to return no error and be quiet if l.IsQuiet() { return nil } // Get current time now := time.Now() // Temporary storage for file and line tracing var file string var line int var fn string // Check if the specified prefix needs to be included with file logging if prefix.File { var ok bool var pc uintptr // Get the caller filename and line if pc, file, line, ok = runtime.Caller(depth + 2); !ok { file = "" fn = "" line = 0 } else { file = filepath.Base(file) fn = runtime.FuncForPC(pc).Name() } } // Acquire exclusive access to the shared buffer l.mu.Lock() defer l.mu.Unlock() // Reset buffer so it start from the beginning l.buf.Reset() // Write prefix to the buffer if l.color { l.buf.Append(prefix.Color) } else { l.buf.Append(prefix.Plain) } // Check if the log require timestamping if l.timestamp { // Print timestamp color if color enabled if l.color { l.buf.Blue() } // Print date and time year, month, day := now.Date() l.buf.AppendInt(year, 4) l.buf.AppendByte('/') l.buf.AppendInt(int(month), 2) l.buf.AppendByte('/') l.buf.AppendInt(day, 2) l.buf.AppendByte(' ') hour, min, sec := now.Clock() l.buf.AppendInt(hour, 2) l.buf.AppendByte(':') l.buf.AppendInt(min, 2) l.buf.AppendByte(':') l.buf.AppendInt(sec, 2) l.buf.AppendByte(' ') // Print reset color if color enabled if l.color { l.buf.Off() } } // Add caller filename and line if enabled if prefix.File { // Print color start if enabled if l.color { l.buf.Orange() } // Print filename and line l.buf.Append([]byte(fn)) l.buf.AppendByte(':') l.buf.Append([]byte(file)) l.buf.AppendByte(':') l.buf.AppendInt(line, 0) l.buf.AppendByte(' ') // Print color stop if l.color { l.buf.Off() } } // Print the actual string data from caller l.buf.Append([]byte(data)) if len(data) == 0 || data[len(data)-1] != '\n' { l.buf.AppendByte('\n') } // Flush buffer to output _, err := l.out.Write(l.buf.Buffer) return err } // Fatal print fatal message to output and quit the application with status 1 func (l *Logger) Fatal(v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 4 { l.Output(1, FatalPrefix, fmt.Sprintln(v...)) } os.Exit(1) } // Fatalf print formatted fatal message to output and quit the application // with status 1 func (l *Logger) Fatalf(format string, v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 4 { l.Output(1, FatalPrefix, fmt.Sprintf(format, v...)) } os.Exit(1) } // Error print error message to output func (l *Logger) Error(v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 3 { l.Output(1, ErrorPrefix, fmt.Sprintln(v...)) } } // Errorf print formatted error message to output func (l *Logger) Errorf(format string, v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 3 { l.Output(1, ErrorPrefix, fmt.Sprintf(format, v...)) } } // Warn print warning message to output func (l *Logger) Warn(v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 2 { l.Output(1, WarnPrefix, fmt.Sprintln(v...)) } } // Warnf print formatted warning message to output func (l *Logger) Warnf(format string, v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 2 { l.Output(1, WarnPrefix, fmt.Sprintf(format, v...)) } } // Info print informational message to output func (l *Logger) Info(v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 1 { l.Output(1, InfoPrefix, fmt.Sprintln(v...)) } } // Infof print formatted informational message to output func (l *Logger) Infof(format string, v ...interface{}) { if atomic.LoadInt32(&l.logLevel) <= 1 { l.Output(1, InfoPrefix, fmt.Sprintf(format, v...)) } } // Debug print debug message to output if debug output enabled func (l *Logger) Debug(v ...interface{}) { if atomic.LoadInt32(&l.logLevel) == 0 { l.Output(1, DebugPrefix, fmt.Sprintln(v...)) } } // Debugf print formatted debug message to output if debug output enabled func (l *Logger) Debugf(format string, v ...interface{}) { if atomic.LoadInt32(&l.logLevel) == 0 { l.Output(1, DebugPrefix, fmt.Sprintf(format, v...)) } } // Trace print trace message to output if debug output enabled func (l *Logger) Trace(v ...interface{}) { if atomic.LoadInt32(&l.logLevel) == 0 { l.Output(1, TracePrefix, fmt.Sprintln(v...)) } } // Tracef print formatted trace message to output if debug output enabled func (l *Logger) Tracef(format string, v ...interface{}) { if atomic.LoadInt32(&l.logLevel) == 0 { l.Output(1, TracePrefix, fmt.Sprintf(format, v...)) } } ================================================ FILE: log/log.go ================================================ package log import ( "io" "os" ) // LogLevel how much log to dump // 0: ALL; 1: INFO; 2: WARN; 3: ERROR; 4: FATAL; 5: OFF type LogLevel int const ( AllLevel LogLevel = 0 InfoLevel LogLevel = 1 WarnLevel LogLevel = 2 ErrorLevel LogLevel = 3 FatalLevel LogLevel = 4 OffLevel LogLevel = 5 ) type Logger interface { Fatal(v ...interface{}) Fatalf(format string, v ...interface{}) Error(v ...interface{}) Errorf(format string, v ...interface{}) Warn(v ...interface{}) Warnf(format string, v ...interface{}) Info(v ...interface{}) Infof(format string, v ...interface{}) Debug(v ...interface{}) Debugf(format string, v ...interface{}) Trace(v ...interface{}) Tracef(format string, v ...interface{}) SetLogLevel(level LogLevel) SetOutput(io.Writer) } var logger Logger = &EmptyLogger{} type EmptyLogger struct{} func (l *EmptyLogger) SetLogLevel(LogLevel) {} func (l *EmptyLogger) Fatal(v ...interface{}) { os.Exit(1) } func (l *EmptyLogger) Fatalf(format string, v ...interface{}) { os.Exit(1) } func (l *EmptyLogger) Error(v ...interface{}) {} func (l *EmptyLogger) Errorf(format string, v ...interface{}) {} func (l *EmptyLogger) Warn(v ...interface{}) {} func (l *EmptyLogger) Warnf(format string, v ...interface{}) {} func (l *EmptyLogger) Info(v ...interface{}) {} func (l *EmptyLogger) Infof(format string, v ...interface{}) {} func (l *EmptyLogger) Debug(v ...interface{}) {} func (l *EmptyLogger) Debugf(format string, v ...interface{}) {} func (l *EmptyLogger) Trace(v ...interface{}) {} func (l *EmptyLogger) Tracef(format string, v ...interface{}) {} func (l *EmptyLogger) SetOutput(w io.Writer) {} func Error(v ...interface{}) { logger.Error(v...) } func Errorf(format string, v ...interface{}) { logger.Errorf(format, v...) } func Warn(v ...interface{}) { logger.Warn(v...) } func Warnf(format string, v ...interface{}) { logger.Warnf(format, v...) } func Info(v ...interface{}) { logger.Info(v...) } func Infof(format string, v ...interface{}) { logger.Infof(format, v...) } func Debug(v ...interface{}) { logger.Debug(v...) } func Debugf(format string, v ...interface{}) { logger.Debugf(format, v...) } func Trace(v ...interface{}) { logger.Trace(v...) } func Tracef(format string, v ...interface{}) { logger.Tracef(format, v...) } func Fatal(v ...interface{}) { logger.Fatal(v...) } func Fatalf(format string, v ...interface{}) { logger.Fatalf(format, v...) } func SetLogLevel(level LogLevel) { logger.SetLogLevel(level) } func SetOutput(w io.Writer) { logger.SetOutput(w) } func RegisterLogger(l Logger) { logger = l } ================================================ FILE: log/simplelog/simplelog.go ================================================ package simplelog import ( "io" golog "log" "os" "github.com/p4gefau1t/trojan-go/log" ) func init() { log.RegisterLogger(&SimpleLogger{}) } type SimpleLogger struct { logLevel log.LogLevel } func (l *SimpleLogger) SetLogLevel(level log.LogLevel) { l.logLevel = level } func (l *SimpleLogger) Fatal(v ...interface{}) { if l.logLevel <= log.FatalLevel { golog.Fatal(v...) } os.Exit(1) } func (l *SimpleLogger) Fatalf(format string, v ...interface{}) { if l.logLevel <= log.FatalLevel { golog.Fatalf(format, v...) } os.Exit(1) } func (l *SimpleLogger) Error(v ...interface{}) { if l.logLevel <= log.ErrorLevel { golog.Println(v...) } } func (l *SimpleLogger) Errorf(format string, v ...interface{}) { if l.logLevel <= log.ErrorLevel { golog.Printf(format, v...) } } func (l *SimpleLogger) Warn(v ...interface{}) { if l.logLevel <= log.WarnLevel { golog.Println(v...) } } func (l *SimpleLogger) Warnf(format string, v ...interface{}) { if l.logLevel <= log.WarnLevel { golog.Printf(format, v...) } } func (l *SimpleLogger) Info(v ...interface{}) { if l.logLevel <= log.InfoLevel { golog.Println(v...) } } func (l *SimpleLogger) Infof(format string, v ...interface{}) { if l.logLevel <= log.InfoLevel { golog.Printf(format, v...) } } func (l *SimpleLogger) Debug(v ...interface{}) { if l.logLevel <= log.AllLevel { golog.Println(v...) } } func (l *SimpleLogger) Debugf(format string, v ...interface{}) { if l.logLevel <= log.AllLevel { golog.Printf(format, v...) } } func (l *SimpleLogger) Trace(v ...interface{}) { if l.logLevel <= log.AllLevel { golog.Println(v...) } } func (l *SimpleLogger) Tracef(format string, v ...interface{}) { if l.logLevel <= log.AllLevel { golog.Printf(format, v...) } } func (l *SimpleLogger) SetOutput(io.Writer) { // do nothing } ================================================ FILE: main.go ================================================ package main import ( "flag" _ "github.com/p4gefau1t/trojan-go/component" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/option" ) func main() { flag.Parse() for { h, err := option.PopOptionHandler() if err != nil { log.Fatal("invalid options") } err = h.Handle() if err == nil { break } } } ================================================ FILE: option/option.go ================================================ package option import "github.com/p4gefau1t/trojan-go/common" type Handler interface { Name() string Handle() error Priority() int } var handlers = make(map[string]Handler) func RegisterHandler(h Handler) { handlers[h.Name()] = h } func PopOptionHandler() (Handler, error) { var maxHandler Handler = nil for _, h := range handlers { if maxHandler == nil || maxHandler.Priority() < h.Priority() { maxHandler = h } } if maxHandler == nil { return nil, common.NewError("no option left") } delete(handlers, maxHandler.Name()) return maxHandler, nil } ================================================ FILE: proxy/client/client.go ================================================ package client import ( "context" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/proxy" "github.com/p4gefau1t/trojan-go/tunnel/adapter" "github.com/p4gefau1t/trojan-go/tunnel/http" "github.com/p4gefau1t/trojan-go/tunnel/mux" "github.com/p4gefau1t/trojan-go/tunnel/router" "github.com/p4gefau1t/trojan-go/tunnel/shadowsocks" "github.com/p4gefau1t/trojan-go/tunnel/simplesocks" "github.com/p4gefau1t/trojan-go/tunnel/socks" "github.com/p4gefau1t/trojan-go/tunnel/tls" "github.com/p4gefau1t/trojan-go/tunnel/transport" "github.com/p4gefau1t/trojan-go/tunnel/trojan" "github.com/p4gefau1t/trojan-go/tunnel/websocket" ) const Name = "CLIENT" // GenerateClientTree generate general outbound protocol stack func GenerateClientTree(transportPlugin bool, muxEnabled bool, wsEnabled bool, ssEnabled bool, routerEnabled bool) []string { clientStack := []string{transport.Name} if !transportPlugin { clientStack = append(clientStack, tls.Name) } if wsEnabled { clientStack = append(clientStack, websocket.Name) } if ssEnabled { clientStack = append(clientStack, shadowsocks.Name) } clientStack = append(clientStack, trojan.Name) if muxEnabled { clientStack = append(clientStack, []string{mux.Name, simplesocks.Name}...) } if routerEnabled { clientStack = append(clientStack, router.Name) } return clientStack } func init() { proxy.RegisterProxyCreator(Name, func(ctx context.Context) (*proxy.Proxy, error) { cfg := config.FromContext(ctx, Name).(*Config) adapterServer, err := adapter.NewServer(ctx, nil) if err != nil { return nil, err } ctx, cancel := context.WithCancel(ctx) root := &proxy.Node{ Name: adapter.Name, Next: make(map[string]*proxy.Node), IsEndpoint: false, Context: ctx, Server: adapterServer, } root.BuildNext(http.Name).IsEndpoint = true root.BuildNext(socks.Name).IsEndpoint = true clientStack := GenerateClientTree(cfg.TransportPlugin.Enabled, cfg.Mux.Enabled, cfg.Websocket.Enabled, cfg.Shadowsocks.Enabled, cfg.Router.Enabled) c, err := proxy.CreateClientStack(ctx, clientStack) if err != nil { cancel() return nil, err } s := proxy.FindAllEndpoints(root) return proxy.NewProxy(ctx, cancel, s, c), nil }) } ================================================ FILE: proxy/client/config.go ================================================ package client import "github.com/p4gefau1t/trojan-go/config" type MuxConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } type WebsocketConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } type RouterConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } type ShadowsocksConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } type TransportPluginConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } type Config struct { Mux MuxConfig `json:"mux" yaml:"mux"` Websocket WebsocketConfig `json:"websocket" yaml:"websocket"` Router RouterConfig `json:"router" yaml:"router"` Shadowsocks ShadowsocksConfig `json:"shadowsocks" yaml:"shadowsocks"` TransportPlugin TransportPluginConfig `json:"transport_plugin" yaml:"transport-plugin"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(Config) }) } ================================================ FILE: proxy/config.go ================================================ package proxy import "github.com/p4gefau1t/trojan-go/config" type Config struct { RunType string `json:"run_type" yaml:"run-type"` LogLevel int `json:"log_level" yaml:"log-level"` LogFile string `json:"log_file" yaml:"log-file"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ LogLevel: 1, } }) } ================================================ FILE: proxy/custom/config.go ================================================ package custom import "github.com/p4gefau1t/trojan-go/config" const Name = "CUSTOM" type NodeConfig struct { Protocol string `json:"protocol" yaml:"protocol"` Tag string `json:"tag" yaml:"tag"` Config interface{} `json:"config" yaml:"config"` } type StackConfig struct { Path [][]string `json:"path" yaml:"path"` Node []NodeConfig `json:"node" yaml:"node"` } type Config struct { Inbound StackConfig `json:"inbound" yaml:"inbound"` Outbound StackConfig `json:"outbound" yaml:"outbound"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(Config) }) } ================================================ FILE: proxy/custom/custom.go ================================================ package custom import ( "context" "strings" "gopkg.in/yaml.v3" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/proxy" "github.com/p4gefau1t/trojan-go/tunnel" ) func convert(i interface{}) interface{} { switch x := i.(type) { case map[interface{}]interface{}: m2 := map[string]interface{}{} for k, v := range x { m2[k.(string)] = convert(v) } return m2 case []interface{}: for i, v := range x { x[i] = convert(v) } } return i } func buildNodes(ctx context.Context, nodeConfigList []NodeConfig) (map[string]*proxy.Node, error) { nodes := make(map[string]*proxy.Node) for _, nodeCfg := range nodeConfigList { nodeCfg.Protocol = strings.ToUpper(nodeCfg.Protocol) if _, err := tunnel.GetTunnel(nodeCfg.Protocol); err != nil { return nil, common.NewError("invalid protocol name:" + nodeCfg.Protocol) } data, err := yaml.Marshal(nodeCfg.Config) common.Must(err) nodeContext, err := config.WithYAMLConfig(ctx, data) if err != nil { return nil, common.NewError("failed to parse config data for " + nodeCfg.Tag + " with protocol" + nodeCfg.Protocol).Base(err) } node := &proxy.Node{ Name: nodeCfg.Protocol, Next: make(map[string]*proxy.Node), Context: nodeContext, } nodes[nodeCfg.Tag] = node } return nodes, nil } func init() { proxy.RegisterProxyCreator(Name, func(ctx context.Context) (*proxy.Proxy, error) { cfg := config.FromContext(ctx, Name).(*Config) ctx, cancel := context.WithCancel(ctx) success := false defer func() { if !success { cancel() } }() // inbound nodes, err := buildNodes(ctx, cfg.Inbound.Node) if err != nil { return nil, err } var root *proxy.Node // build server tree for _, path := range cfg.Inbound.Path { var lastNode *proxy.Node for _, tag := range path { if _, found := nodes[tag]; !found { return nil, common.NewError("invalid node tag: " + tag) } if lastNode == nil { if root == nil { lastNode = nodes[tag] root = lastNode t, err := tunnel.GetTunnel(root.Name) if err != nil { return nil, common.NewError("failed to find root tunnel").Base(err) } s, err := t.NewServer(root.Context, nil) if err != nil { return nil, common.NewError("failed to init root server").Base(err) } root.Server = s } else { lastNode = root } } else { lastNode = lastNode.LinkNextNode(nodes[tag]) } } lastNode.IsEndpoint = true } servers := proxy.FindAllEndpoints(root) if len(cfg.Outbound.Path) != 1 { return nil, common.NewError("there must be only 1 path for outbound protocol stack") } // outbound nodes, err = buildNodes(ctx, cfg.Outbound.Node) if err != nil { return nil, err } // build client stack var client tunnel.Client for _, tag := range cfg.Outbound.Path[0] { if _, found := nodes[tag]; !found { return nil, common.NewError("invalid node tag: " + tag) } t, err := tunnel.GetTunnel(nodes[tag].Name) if err != nil { return nil, common.NewError("invalid tunnel name").Base(err) } client, err = t.NewClient(nodes[tag].Context, client) if err != nil { return nil, common.NewError("failed to create client").Base(err) } } success = true return proxy.NewProxy(ctx, cancel, servers, client), nil }) } ================================================ FILE: proxy/forward/forward.go ================================================ package forward import ( "context" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/proxy" "github.com/p4gefau1t/trojan-go/proxy/client" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/dokodemo" ) const Name = "FORWARD" func init() { proxy.RegisterProxyCreator(Name, func(ctx context.Context) (*proxy.Proxy, error) { cfg := config.FromContext(ctx, Name).(*client.Config) ctx, cancel := context.WithCancel(ctx) serverStack := []string{dokodemo.Name} clientStack := client.GenerateClientTree(cfg.TransportPlugin.Enabled, cfg.Mux.Enabled, cfg.Websocket.Enabled, cfg.Shadowsocks.Enabled, cfg.Router.Enabled) c, err := proxy.CreateClientStack(ctx, clientStack) if err != nil { cancel() return nil, err } s, err := proxy.CreateServerStack(ctx, serverStack) if err != nil { cancel() return nil, err } return proxy.NewProxy(ctx, cancel, []tunnel.Server{s}, c), nil }) } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(client.Config) }) } ================================================ FILE: proxy/nat/nat.go ================================================ //go:build linux // +build linux package nat import ( "context" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/proxy" "github.com/p4gefau1t/trojan-go/proxy/client" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/tproxy" ) const Name = "NAT" func init() { proxy.RegisterProxyCreator(Name, func(ctx context.Context) (*proxy.Proxy, error) { cfg := config.FromContext(ctx, Name).(*client.Config) if cfg.Router.Enabled { return nil, common.NewError("router is not allowed in nat mode") } ctx, cancel := context.WithCancel(ctx) serverStack := []string{tproxy.Name} clientStack := client.GenerateClientTree(cfg.TransportPlugin.Enabled, cfg.Mux.Enabled, cfg.Websocket.Enabled, cfg.Shadowsocks.Enabled, false) c, err := proxy.CreateClientStack(ctx, clientStack) if err != nil { cancel() return nil, err } s, err := proxy.CreateServerStack(ctx, serverStack) if err != nil { cancel() return nil, err } return proxy.NewProxy(ctx, cancel, []tunnel.Server{s}, c), nil }) } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(client.Config) }) } ================================================ FILE: proxy/nat/nat_stub.go ================================================ package nat ================================================ FILE: proxy/option.go ================================================ package proxy import ( "bufio" "flag" "fmt" "io/ioutil" "os" "runtime" "strings" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/constant" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/option" ) type Option struct { path *string } func (o *Option) Name() string { return Name } func detectAndReadConfig(file string) ([]byte, bool, error) { isJSON := false switch { case strings.HasSuffix(file, ".json"): isJSON = true case strings.HasSuffix(file, ".yaml"), strings.HasSuffix(file, ".yml"): isJSON = false default: log.Fatalf("unsupported config format: %s. use .yaml or .json instead.", file) } data, err := ioutil.ReadFile(file) if err != nil { return nil, false, err } return data, isJSON, nil } func (o *Option) Handle() error { defaultConfigPath := []string{ "config.json", "config.yml", "config.yaml", } isJSON := false var data []byte var err error switch *o.path { case "": log.Warn("no specified config file, use default path to detect config file") for _, file := range defaultConfigPath { log.Warn("try to load config from default path:", file) data, isJSON, err = detectAndReadConfig(file) if err != nil { log.Warn(err) continue } break } default: data, isJSON, err = detectAndReadConfig(*o.path) if err != nil { log.Fatal(err) } } if data != nil { log.Info("trojan-go", constant.Version, "initializing") proxy, err := NewProxyFromConfigData(data, isJSON) if err != nil { log.Fatal(err) } err = proxy.Run() if err != nil { log.Fatal(err) } } log.Fatal("no valid config") return nil } func (o *Option) Priority() int { return -1 } func init() { option.RegisterHandler(&Option{ path: flag.String("config", "", "Trojan-Go config filename (.yaml/.yml/.json)"), }) option.RegisterHandler(&StdinOption{ format: flag.String("stdin-format", "disabled", "Read from standard input (yaml/json)"), suppressHint: flag.Bool("stdin-suppress-hint", false, "Suppress hint text"), }) } type StdinOption struct { format *string suppressHint *bool } func (o *StdinOption) Name() string { return Name + "_STDIN" } func (o *StdinOption) Handle() error { isJSON, e := o.isFormatJson() if e != nil { return e } if o.suppressHint == nil || !*o.suppressHint { fmt.Printf("Trojan-Go %s (%s/%s)\n", constant.Version, runtime.GOOS, runtime.GOARCH) if isJSON { fmt.Println("Reading JSON configuration from stdin.") } else { fmt.Println("Reading YAML configuration from stdin.") } } data, e := ioutil.ReadAll(bufio.NewReader(os.Stdin)) if e != nil { log.Fatalf("Failed to read from stdin: %s", e.Error()) } proxy, err := NewProxyFromConfigData(data, isJSON) if err != nil { log.Fatal(err) } err = proxy.Run() if err != nil { log.Fatal(err) } return nil } func (o *StdinOption) Priority() int { return 0 } func (o *StdinOption) isFormatJson() (isJson bool, e error) { if o.format == nil { return false, common.NewError("format specifier is nil") } if *o.format == "disabled" { return false, common.NewError("reading from stdin is disabled") } return strings.ToLower(*o.format) == "json", nil } ================================================ FILE: proxy/proxy.go ================================================ package proxy import ( "context" "io" "math/rand" "net" "os" "strings" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "PROXY" const ( MaxPacketSize = 1024 * 8 ) // Proxy relay connections and packets type Proxy struct { sources []tunnel.Server sink tunnel.Client ctx context.Context cancel context.CancelFunc } func (p *Proxy) Run() error { p.relayConnLoop() p.relayPacketLoop() <-p.ctx.Done() return nil } func (p *Proxy) Close() error { p.cancel() p.sink.Close() for _, source := range p.sources { source.Close() } return nil } func (p *Proxy) relayConnLoop() { for _, source := range p.sources { go func(source tunnel.Server) { for { inbound, err := source.AcceptConn(nil) if err != nil { select { case <-p.ctx.Done(): log.Debug("exiting") return default: } log.Error(common.NewError("failed to accept connection").Base(err)) continue } go func(inbound tunnel.Conn) { defer inbound.Close() outbound, err := p.sink.DialConn(inbound.Metadata().Address, nil) if err != nil { log.Error(common.NewError("proxy failed to dial connection").Base(err)) return } defer outbound.Close() errChan := make(chan error, 2) copyConn := func(a, b net.Conn) { _, err := io.Copy(a, b) errChan <- err } go copyConn(inbound, outbound) go copyConn(outbound, inbound) select { case err = <-errChan: if err != nil { log.Error(err) } case <-p.ctx.Done(): log.Debug("shutting down conn relay") return } log.Debug("conn relay ends") }(inbound) } }(source) } } func (p *Proxy) relayPacketLoop() { for _, source := range p.sources { go func(source tunnel.Server) { for { inbound, err := source.AcceptPacket(nil) if err != nil { select { case <-p.ctx.Done(): log.Debug("exiting") return default: } log.Error(common.NewError("failed to accept packet").Base(err)) continue } go func(inbound tunnel.PacketConn) { defer inbound.Close() outbound, err := p.sink.DialPacket(nil) if err != nil { log.Error(common.NewError("proxy failed to dial packet").Base(err)) return } defer outbound.Close() errChan := make(chan error, 2) copyPacket := func(a, b tunnel.PacketConn) { for { buf := make([]byte, MaxPacketSize) n, metadata, err := a.ReadWithMetadata(buf) if err != nil { errChan <- err return } if n == 0 { errChan <- nil return } _, err = b.WriteWithMetadata(buf[:n], metadata) if err != nil { errChan <- err return } } } go copyPacket(inbound, outbound) go copyPacket(outbound, inbound) select { case err = <-errChan: if err != nil { log.Error(err) } case <-p.ctx.Done(): log.Debug("shutting down packet relay") } log.Debug("packet relay ends") }(inbound) } }(source) } } func NewProxy(ctx context.Context, cancel context.CancelFunc, sources []tunnel.Server, sink tunnel.Client) *Proxy { return &Proxy{ sources: sources, sink: sink, ctx: ctx, cancel: cancel, } } type Creator func(ctx context.Context) (*Proxy, error) var creators = make(map[string]Creator) func RegisterProxyCreator(name string, creator Creator) { creators[name] = creator } func NewProxyFromConfigData(data []byte, isJSON bool) (*Proxy, error) { // create a unique context for each proxy instance to avoid duplicated authenticator ctx := context.WithValue(context.Background(), Name+"_ID", rand.Int()) var err error if isJSON { ctx, err = config.WithJSONConfig(ctx, data) if err != nil { return nil, err } } else { ctx, err = config.WithYAMLConfig(ctx, data) if err != nil { return nil, err } } cfg := config.FromContext(ctx, Name).(*Config) create, ok := creators[strings.ToUpper(cfg.RunType)] if !ok { return nil, common.NewError("unknown proxy type: " + cfg.RunType) } log.SetLogLevel(log.LogLevel(cfg.LogLevel)) if cfg.LogFile != "" { file, err := os.OpenFile(cfg.LogFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) if err != nil { return nil, common.NewError("failed to open log file").Base(err) } log.SetOutput(file) } return create(ctx) } ================================================ FILE: proxy/server/config.go ================================================ package server import ( "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/proxy/client" ) func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(client.Config) }) } ================================================ FILE: proxy/server/server.go ================================================ package server import ( "context" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/proxy" "github.com/p4gefau1t/trojan-go/proxy/client" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/mux" "github.com/p4gefau1t/trojan-go/tunnel/router" "github.com/p4gefau1t/trojan-go/tunnel/shadowsocks" "github.com/p4gefau1t/trojan-go/tunnel/simplesocks" "github.com/p4gefau1t/trojan-go/tunnel/tls" "github.com/p4gefau1t/trojan-go/tunnel/transport" "github.com/p4gefau1t/trojan-go/tunnel/trojan" "github.com/p4gefau1t/trojan-go/tunnel/websocket" ) const Name = "SERVER" func init() { proxy.RegisterProxyCreator(Name, func(ctx context.Context) (*proxy.Proxy, error) { cfg := config.FromContext(ctx, Name).(*client.Config) ctx, cancel := context.WithCancel(ctx) transportServer, err := transport.NewServer(ctx, nil) if err != nil { cancel() return nil, err } clientStack := []string{freedom.Name} if cfg.Router.Enabled { clientStack = []string{freedom.Name, router.Name} } root := &proxy.Node{ Name: transport.Name, Next: make(map[string]*proxy.Node), IsEndpoint: false, Context: ctx, Server: transportServer, } if !cfg.TransportPlugin.Enabled { root = root.BuildNext(tls.Name) } trojanSubTree := root if cfg.Shadowsocks.Enabled { trojanSubTree = trojanSubTree.BuildNext(shadowsocks.Name) } trojanSubTree.BuildNext(trojan.Name).BuildNext(mux.Name).BuildNext(simplesocks.Name).IsEndpoint = true trojanSubTree.BuildNext(trojan.Name).IsEndpoint = true wsSubTree := root.BuildNext(websocket.Name) if cfg.Shadowsocks.Enabled { wsSubTree = wsSubTree.BuildNext(shadowsocks.Name) } wsSubTree.BuildNext(trojan.Name).BuildNext(mux.Name).BuildNext(simplesocks.Name).IsEndpoint = true wsSubTree.BuildNext(trojan.Name).IsEndpoint = true serverList := proxy.FindAllEndpoints(root) clientList, err := proxy.CreateClientStack(ctx, clientStack) if err != nil { cancel() return nil, err } return proxy.NewProxy(ctx, cancel, serverList, clientList), nil }) } ================================================ FILE: proxy/stack.go ================================================ package proxy import ( "context" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type Node struct { Name string Next map[string]*Node IsEndpoint bool context.Context tunnel.Server tunnel.Client } func (n *Node) BuildNext(name string) *Node { if next, found := n.Next[name]; found { return next } t, err := tunnel.GetTunnel(name) if err != nil { log.Fatal(err) } s, err := t.NewServer(n.Context, n.Server) if err != nil { log.Fatal(err) } newNode := &Node{ Name: name, Next: make(map[string]*Node), Context: n.Context, Server: s, } n.Next[name] = newNode return newNode } func (n *Node) LinkNextNode(next *Node) *Node { if next, found := n.Next[next.Name]; found { return next } n.Next[next.Name] = next t, err := tunnel.GetTunnel(next.Name) if err != nil { log.Fatal(err) } s, err := t.NewServer(next.Context, n.Server) // context of the child nodes have been initialized if err != nil { log.Fatal(err) } next.Server = s return next } func FindAllEndpoints(root *Node) []tunnel.Server { list := make([]tunnel.Server, 0) if root.IsEndpoint || len(root.Next) == 0 { list = append(list, root.Server) } for _, next := range root.Next { list = append(list, FindAllEndpoints(next)...) } return list } // CreateClientStack create client tunnel stacks from lists func CreateClientStack(ctx context.Context, clientStack []string) (tunnel.Client, error) { var client tunnel.Client for _, name := range clientStack { t, err := tunnel.GetTunnel(name) if err != nil { return nil, err } client, err = t.NewClient(ctx, client) if err != nil { return nil, err } } return client, nil } // CreateServerStack create server tunnel stack from list func CreateServerStack(ctx context.Context, serverStack []string) (tunnel.Server, error) { var server tunnel.Server for _, name := range serverStack { t, err := tunnel.GetTunnel(name) if err != nil { return nil, err } server, err = t.NewServer(ctx, server) if err != nil { return nil, err } } return server, nil } ================================================ FILE: redirector/redirector.go ================================================ package redirector import ( "context" "io" "net" "reflect" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" ) type Dial func(net.Addr) (net.Conn, error) func defaultDial(addr net.Addr) (net.Conn, error) { return net.Dial("tcp", addr.String()) } type Redirection struct { Dial RedirectTo net.Addr InboundConn net.Conn } type Redirector struct { ctx context.Context redirectionChan chan *Redirection } func (r *Redirector) Redirect(redirection *Redirection) { select { case r.redirectionChan <- redirection: log.Debug("redirect request") case <-r.ctx.Done(): log.Debug("exiting") } } func (r *Redirector) worker() { for { select { case redirection := <-r.redirectionChan: handle := func(redirection *Redirection) { if redirection.InboundConn == nil || reflect.ValueOf(redirection.InboundConn).IsNil() { log.Error("nil inbound conn") return } defer redirection.InboundConn.Close() if redirection.RedirectTo == nil || reflect.ValueOf(redirection.RedirectTo).IsNil() { log.Error("nil redirection addr") return } if redirection.Dial == nil { redirection.Dial = defaultDial } log.Warn("redirecting connection from", redirection.InboundConn.RemoteAddr(), "to", redirection.RedirectTo.String()) outboundConn, err := redirection.Dial(redirection.RedirectTo) if err != nil { log.Error(common.NewError("failed to redirect to target address").Base(err)) return } defer outboundConn.Close() errChan := make(chan error, 2) copyConn := func(a, b net.Conn) { _, err := io.Copy(a, b) errChan <- err } go copyConn(outboundConn, redirection.InboundConn) go copyConn(redirection.InboundConn, outboundConn) select { case err := <-errChan: if err != nil { log.Error(common.NewError("failed to redirect").Base(err)) } log.Info("redirection done") case <-r.ctx.Done(): log.Debug("exiting") return } } go handle(redirection) case <-r.ctx.Done(): log.Debug("shutting down redirector") return } } } func NewRedirector(ctx context.Context) *Redirector { r := &Redirector{ ctx: ctx, redirectionChan: make(chan *Redirection, 64), } go r.worker() return r } ================================================ FILE: redirector/redirector_test.go ================================================ package redirector import ( "context" "fmt" "net" "net/http" "strings" "testing" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/test/util" ) func TestRedirector(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) redir := NewRedirector(ctx) redir.Redirect(&Redirection{ Dial: nil, RedirectTo: nil, InboundConn: nil, }) var fakeAddr net.Addr var fakeConn net.Conn redir.Redirect(&Redirection{ Dial: nil, RedirectTo: fakeAddr, InboundConn: fakeConn, }) redir.Redirect(&Redirection{ Dial: nil, RedirectTo: nil, InboundConn: fakeConn, }) redir.Redirect(&Redirection{ Dial: nil, RedirectTo: fakeAddr, InboundConn: nil, }) l, err := net.Listen("tcp", "127.0.0.1:0") common.Must(err) conn1, err := net.Dial("tcp", l.Addr().String()) common.Must(err) conn2, err := l.Accept() common.Must(err) redirAddr, err := net.ResolveTCPAddr("tcp", util.HTTPAddr) common.Must(err) redir.Redirect(&Redirection{ Dial: nil, RedirectTo: redirAddr, InboundConn: conn2, }) time.Sleep(time.Second) req, err := http.NewRequest("GET", "http://localhost/", nil) common.Must(err) req.Write(conn1) buf := make([]byte, 1024) conn1.Read(buf) fmt.Println(string(buf)) if !strings.HasPrefix(string(buf), "HTTP/1.1 200 OK") { t.Fail() } cancel() conn1.Close() conn2.Close() } ================================================ FILE: statistic/memory/config.go ================================================ package memory import ( "github.com/p4gefau1t/trojan-go/config" ) type Config struct { Passwords []string `json:"password" yaml:"password"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{} }) } ================================================ FILE: statistic/memory/memory.go ================================================ package memory import ( "context" "sync" "sync/atomic" "time" "golang.org/x/time/rate" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/statistic" ) const Name = "MEMORY" type User struct { // WARNING: do not change the order of these fields. // 64-bit fields that use `sync/atomic` package functions // must be 64-bit aligned on 32-bit systems. // Reference: https://github.com/golang/go/issues/599 // Solution: https://github.com/golang/go/issues/11891#issuecomment-433623786 sent uint64 recv uint64 lastSent uint64 lastRecv uint64 sendSpeed uint64 recvSpeed uint64 hash string ipTable sync.Map ipNum int32 maxIPNum int limiterLock sync.RWMutex sendLimiter *rate.Limiter recvLimiter *rate.Limiter ctx context.Context cancel context.CancelFunc } func (u *User) Close() error { u.ResetTraffic() u.cancel() return nil } func (u *User) AddIP(ip string) bool { if u.maxIPNum <= 0 { return true } _, found := u.ipTable.Load(ip) if found { return true } if int(u.ipNum)+1 > u.maxIPNum { return false } u.ipTable.Store(ip, true) atomic.AddInt32(&u.ipNum, 1) return true } func (u *User) DelIP(ip string) bool { if u.maxIPNum <= 0 { return true } _, found := u.ipTable.Load(ip) if !found { return false } u.ipTable.Delete(ip) atomic.AddInt32(&u.ipNum, -1) return true } func (u *User) GetIP() int { return int(u.ipNum) } func (u *User) SetIPLimit(n int) { u.maxIPNum = n } func (u *User) GetIPLimit() int { return u.maxIPNum } func (u *User) AddTraffic(sent, recv int) { u.limiterLock.RLock() defer u.limiterLock.RUnlock() if u.sendLimiter != nil && sent >= 0 { u.sendLimiter.WaitN(u.ctx, sent) } else if u.recvLimiter != nil && recv >= 0 { u.recvLimiter.WaitN(u.ctx, recv) } atomic.AddUint64(&u.sent, uint64(sent)) atomic.AddUint64(&u.recv, uint64(recv)) } func (u *User) SetSpeedLimit(send, recv int) { u.limiterLock.Lock() defer u.limiterLock.Unlock() if send <= 0 { u.sendLimiter = nil } else { u.sendLimiter = rate.NewLimiter(rate.Limit(send), send*2) } if recv <= 0 { u.recvLimiter = nil } else { u.recvLimiter = rate.NewLimiter(rate.Limit(recv), recv*2) } } func (u *User) GetSpeedLimit() (send, recv int) { u.limiterLock.RLock() defer u.limiterLock.RUnlock() if u.sendLimiter != nil { send = int(u.sendLimiter.Limit()) } if u.recvLimiter != nil { recv = int(u.recvLimiter.Limit()) } return } func (u *User) Hash() string { return u.hash } func (u *User) SetTraffic(send, recv uint64) { atomic.StoreUint64(&u.sent, send) atomic.StoreUint64(&u.recv, recv) } func (u *User) GetTraffic() (uint64, uint64) { return atomic.LoadUint64(&u.sent), atomic.LoadUint64(&u.recv) } func (u *User) ResetTraffic() (uint64, uint64) { sent := atomic.SwapUint64(&u.sent, 0) recv := atomic.SwapUint64(&u.recv, 0) atomic.StoreUint64(&u.lastSent, 0) atomic.StoreUint64(&u.lastRecv, 0) return sent, recv } func (u *User) speedUpdater() { ticker := time.NewTicker(time.Second) for { select { case <-u.ctx.Done(): return case <-ticker.C: sent, recv := u.GetTraffic() atomic.StoreUint64(&u.sendSpeed, sent-u.lastSent) atomic.StoreUint64(&u.recvSpeed, recv-u.lastRecv) atomic.StoreUint64(&u.lastSent, sent) atomic.StoreUint64(&u.lastRecv, recv) } } } func (u *User) GetSpeed() (uint64, uint64) { return atomic.LoadUint64(&u.sendSpeed), atomic.LoadUint64(&u.recvSpeed) } type Authenticator struct { users sync.Map ctx context.Context } func (a *Authenticator) AuthUser(hash string) (bool, statistic.User) { if user, found := a.users.Load(hash); found { return true, user.(*User) } return false, nil } func (a *Authenticator) AddUser(hash string) error { if _, found := a.users.Load(hash); found { return common.NewError("hash " + hash + " is already exist") } ctx, cancel := context.WithCancel(a.ctx) meter := &User{ hash: hash, ctx: ctx, cancel: cancel, } go meter.speedUpdater() a.users.Store(hash, meter) return nil } func (a *Authenticator) DelUser(hash string) error { meter, found := a.users.Load(hash) if !found { return common.NewError("hash " + hash + " not found") } meter.(*User).Close() a.users.Delete(hash) return nil } func (a *Authenticator) ListUsers() []statistic.User { result := make([]statistic.User, 0) a.users.Range(func(k, v interface{}) bool { result = append(result, v.(*User)) return true }) return result } func (a *Authenticator) Close() error { return nil } func NewAuthenticator(ctx context.Context) (statistic.Authenticator, error) { cfg := config.FromContext(ctx, Name).(*Config) u := &Authenticator{ ctx: ctx, } for _, password := range cfg.Passwords { hash := common.SHA224String(password) u.AddUser(hash) } log.Debug("memory authenticator created") return u, nil } func init() { statistic.RegisterAuthenticatorCreator(Name, NewAuthenticator) } ================================================ FILE: statistic/memory/memory_test.go ================================================ package memory import ( "context" "runtime" "strconv" "testing" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" ) func TestMemoryAuth(t *testing.T) { cfg := &Config{ Passwords: nil, } ctx := config.WithConfig(context.Background(), Name, cfg) auth, err := NewAuthenticator(ctx) common.Must(err) auth.AddUser("user1") valid, user := auth.AuthUser("user1") if !valid { t.Fatal("add, auth") } if user.Hash() != "user1" { t.Fatal("Hash") } user.AddTraffic(100, 200) sent, recv := user.GetTraffic() if sent != 100 || recv != 200 { t.Fatal("traffic") } sent, recv = user.ResetTraffic() if sent != 100 || recv != 200 { t.Fatal("ResetTraffic") } sent, recv = user.GetTraffic() if sent != 0 || recv != 0 { t.Fatal("ResetTraffic") } user.AddIP("1234") user.AddIP("5678") if user.GetIP() != 0 { t.Fatal("GetIP") } user.SetIPLimit(2) user.AddIP("1234") user.AddIP("5678") user.DelIP("1234") if user.GetIP() != 1 { t.Fatal("DelIP") } user.DelIP("5678") user.SetIPLimit(2) if !user.AddIP("1") || !user.AddIP("2") { t.Fatal("AddIP") } if user.AddIP("3") { t.Fatal("AddIP") } if !user.AddIP("2") { t.Fatal("AddIP") } user.SetTraffic(1234, 4321) if a, b := user.GetTraffic(); a != 1234 || b != 4321 { t.Fatal("SetTraffic") } user.ResetTraffic() go func() { for { k := 100 time.Sleep(time.Second / time.Duration(k)) user.AddTraffic(2000/k, 1000/k) } }() time.Sleep(time.Second * 4) if sent, recv := user.GetSpeed(); sent > 3000 || sent < 1000 || recv > 1500 || recv < 500 { t.Error("GetSpeed", sent, recv) } else { t.Log("GetSpeed", sent, recv) } user.SetSpeedLimit(30, 20) time.Sleep(time.Second * 4) if sent, recv := user.GetSpeed(); sent > 60 || recv > 40 { t.Error("SetSpeedLimit", sent, recv) } else { t.Log("SetSpeedLimit", sent, recv) } user.SetSpeedLimit(0, 0) time.Sleep(time.Second * 4) if sent, recv := user.GetSpeed(); sent < 30 || recv < 20 { t.Error("SetSpeedLimit", sent, recv) } else { t.Log("SetSpeedLimit", sent, recv) } auth.AddUser("user2") valid, _ = auth.AuthUser("user2") if !valid { t.Fatal() } auth.DelUser("user2") valid, _ = auth.AuthUser("user2") if valid { t.Fatal() } auth.AddUser("user3") users := auth.ListUsers() if len(users) != 2 { t.Fatal() } user.Close() auth.Close() } func BenchmarkMemoryUsage(b *testing.B) { cfg := &Config{ Passwords: nil, } ctx := config.WithConfig(context.Background(), Name, cfg) auth, err := NewAuthenticator(ctx) common.Must(err) m1 := runtime.MemStats{} m2 := runtime.MemStats{} runtime.ReadMemStats(&m1) for i := 0; i < b.N; i++ { common.Must(auth.AddUser(common.SHA224String("hash" + strconv.Itoa(i)))) } runtime.ReadMemStats(&m2) b.ReportMetric(float64(m2.Alloc-m1.Alloc)/1024/1024, "MiB(Alloc)") b.ReportMetric(float64(m2.TotalAlloc-m1.TotalAlloc)/1024/1024, "MiB(TotalAlloc)") } ================================================ FILE: statistic/mysql/config.go ================================================ package mysql import ( "github.com/p4gefau1t/trojan-go/config" ) type MySQLConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` ServerHost string `json:"server_addr" yaml:"server-addr"` ServerPort int `json:"server_port" yaml:"server-port"` Database string `json:"database" yaml:"database"` Username string `json:"username" yaml:"username"` Password string `json:"password" yaml:"password"` CheckRate int `json:"check_rate" yaml:"check-rate"` } type Config struct { MySQL MySQLConfig `json:"mysql" yaml:"mysql"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ MySQL: MySQLConfig{ ServerPort: 3306, CheckRate: 30, }, } }) } ================================================ FILE: statistic/mysql/mysql.go ================================================ package mysql import ( "context" "database/sql" "fmt" "strings" "time" // MySQL Driver _ "github.com/go-sql-driver/mysql" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/statistic" "github.com/p4gefau1t/trojan-go/statistic/memory" ) const Name = "MYSQL" type Authenticator struct { *memory.Authenticator db *sql.DB updateDuration time.Duration ctx context.Context } func (a *Authenticator) updater() { for { for _, user := range a.ListUsers() { // swap upload and download for users hash := user.Hash() sent, recv := user.ResetTraffic() s, err := a.db.Exec("UPDATE `users` SET `upload`=`upload`+?, `download`=`download`+? WHERE `password`=?;", recv, sent, hash) if err != nil { log.Error(common.NewError("failed to update data to user table").Base(err)) continue } if r, err := s.RowsAffected(); err != nil { if r == 0 { a.DelUser(hash) } } } log.Info("buffered data has been written into the database") // update memory rows, err := a.db.Query("SELECT password,quota,download,upload FROM users") if err != nil || rows.Err() != nil { log.Error(common.NewError("failed to pull data from the database").Base(err)) time.Sleep(a.updateDuration) continue } for rows.Next() { var hash string var quota, download, upload int64 err := rows.Scan(&hash, "a, &download, &upload) if err != nil { log.Error(common.NewError("failed to obtain data from the query result").Base(err)) break } if download+upload < quota || quota < 0 { a.AddUser(hash) } else { a.DelUser(hash) } } select { case <-time.After(a.updateDuration): case <-a.ctx.Done(): log.Debug("MySQL daemon exiting...") return } } } func connectDatabase(driverName, username, password, ip string, port int, dbName string) (*sql.DB, error) { path := strings.Join([]string{username, ":", password, "@tcp(", ip, ":", fmt.Sprintf("%d", port), ")/", dbName, "?charset=utf8"}, "") return sql.Open(driverName, path) } func NewAuthenticator(ctx context.Context) (statistic.Authenticator, error) { cfg := config.FromContext(ctx, Name).(*Config) db, err := connectDatabase( "mysql", cfg.MySQL.Username, cfg.MySQL.Password, cfg.MySQL.ServerHost, cfg.MySQL.ServerPort, cfg.MySQL.Database, ) if err != nil { return nil, common.NewError("Failed to connect to database server").Base(err) } memoryAuth, err := memory.NewAuthenticator(ctx) if err != nil { return nil, err } a := &Authenticator{ db: db, ctx: ctx, updateDuration: time.Duration(cfg.MySQL.CheckRate) * time.Second, Authenticator: memoryAuth.(*memory.Authenticator), } go a.updater() log.Debug("mysql authenticator created") return a, nil } func init() { statistic.RegisterAuthenticatorCreator(Name, NewAuthenticator) } ================================================ FILE: statistic/statistics.go ================================================ package statistic import ( "context" "io" "strings" "sync" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" ) type TrafficMeter interface { io.Closer Hash() string AddTraffic(sent, recv int) GetTraffic() (sent, recv uint64) SetTraffic(sent, recv uint64) ResetTraffic() (sent, recv uint64) GetSpeed() (sent, recv uint64) GetSpeedLimit() (sent, recv int) SetSpeedLimit(sent, recv int) } type IPRecorder interface { AddIP(string) bool DelIP(string) bool GetIP() int SetIPLimit(int) GetIPLimit() int } type User interface { TrafficMeter IPRecorder } type Authenticator interface { io.Closer AuthUser(hash string) (valid bool, user User) AddUser(hash string) error DelUser(hash string) error ListUsers() []User } type Creator func(ctx context.Context) (Authenticator, error) var ( createdAuthLock sync.Mutex authCreators = make(map[string]Creator) createdAuth = make(map[context.Context]Authenticator) ) func RegisterAuthenticatorCreator(name string, creator Creator) { authCreators[name] = creator } func NewAuthenticator(ctx context.Context, name string) (Authenticator, error) { // allocate a unique authenticator for each context createdAuthLock.Lock() // avoid concurrent map read/write defer createdAuthLock.Unlock() if auth, found := createdAuth[ctx]; found { log.Debug("authenticator has been created:", name) return auth, nil } creator, found := authCreators[strings.ToUpper(name)] if !found { return nil, common.NewError("auth driver name " + name + " not found") } auth, err := creator(ctx) if err != nil { return nil, err } createdAuth[ctx] = auth return auth, err } ================================================ FILE: test/scenario/custom_test.go ================================================ package scenario import ( "fmt" "testing" "github.com/p4gefau1t/trojan-go/common" _ "github.com/p4gefau1t/trojan-go/proxy/custom" "github.com/p4gefau1t/trojan-go/test/util" ) func TestCustom1(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: custom inbound: node: - protocol: adapter tag: adapter config: local-addr: 127.0.0.1 local-port: %d - protocol: socks tag: socks config: local-addr: 127.0.0.1 local-port: %d path: - - adapter - socks outbound: node: - protocol: transport tag: transport config: remote-addr: 127.0.0.1 remote-port: %d - protocol: tls tag: tls config: ssl: sni: localhost key: server.key cert: server.crt - protocol: trojan tag: trojan config: password: - "12345678" path: - - transport - tls - trojan `, socksPort, socksPort, serverPort) serverData := fmt.Sprintf(` run-type: custom inbound: node: - protocol: transport tag: transport config: local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s - protocol: tls tag: tls config: ssl: sni: localhost key: server.key cert: server.crt - protocol: trojan tag: trojan config: disable-http-check: true password: - "12345678" - protocol: mux tag: mux - protocol: simplesocks tag: simplesocks path: - - transport - tls - trojan - - transport - tls - trojan - mux - simplesocks outbound: node: - protocol: freedom tag: freedom path: - - freedom `, serverPort, util.HTTPPort) if !CheckClientServer(clientData, serverData, socksPort) { t.Fail() } } func TestCustom2(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: custom log-level: 0 inbound: node: - protocol: adapter tag: adapter config: local-addr: 127.0.0.1 local-port: %d - protocol: socks tag: socks config: local-addr: 127.0.0.1 local-port: %d path: - - adapter - socks outbound: node: - protocol: transport tag: transport config: remote-addr: 127.0.0.1 remote-port: %d - protocol: tls tag: tls config: ssl: sni: localhost key: server.key cert: server.crt - protocol: trojan tag: trojan config: password: - "12345678" - protocol: shadowsocks tag: shadowsocks config: remote-addr: 127.0.0.1 remote-port: 80 shadowsocks: enabled: true password: "12345678" - protocol: websocket tag: websocket config: websocket: host: localhost path: /ws path: - - transport - tls - websocket - shadowsocks - trojan `, socksPort, socksPort, serverPort) serverData := fmt.Sprintf(` run-type: custom log-level: 0 inbound: node: - protocol: transport tag: transport config: local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s - protocol: tls tag: tls config: ssl: sni: localhost key: server.key cert: server.crt - protocol: trojan tag: trojan config: disable-http-check: true password: - "12345678" - protocol: trojan tag: trojan2 config: disable-http-check: true password: - "12345678" - protocol: websocket tag: websocket config: websocket: enabled: true host: localhost path: /ws - protocol: mux tag: mux - protocol: simplesocks tag: simplesocks - protocol: shadowsocks tag: shadowsocks config: remote-addr: 127.0.0.1 remote-port: 80 shadowsocks: enabled: true password: "12345678" - protocol: shadowsocks tag: shadowsocks2 config: remote-addr: 127.0.0.1 remote-port: 80 shadowsocks: enabled: true password: "12345678" path: - - transport - tls - shadowsocks - trojan - - transport - tls - websocket - shadowsocks2 - trojan2 - - transport - tls - shadowsocks - trojan - mux - simplesocks outbound: node: - protocol: freedom tag: freedom path: - - freedom `, serverPort, util.HTTPPort) if !CheckClientServer(clientData, serverData, socksPort) { t.Fail() } } ================================================ FILE: test/scenario/proxy_test.go ================================================ package scenario import ( "bytes" "fmt" "net" "net/http" _ "net/http/pprof" "os" "sync" "testing" "time" netproxy "golang.org/x/net/proxy" _ "github.com/p4gefau1t/trojan-go/api" _ "github.com/p4gefau1t/trojan-go/api/service" "github.com/p4gefau1t/trojan-go/common" _ "github.com/p4gefau1t/trojan-go/log/golog" "github.com/p4gefau1t/trojan-go/proxy" _ "github.com/p4gefau1t/trojan-go/proxy/client" _ "github.com/p4gefau1t/trojan-go/proxy/forward" _ "github.com/p4gefau1t/trojan-go/proxy/nat" _ "github.com/p4gefau1t/trojan-go/proxy/server" _ "github.com/p4gefau1t/trojan-go/statistic/memory" "github.com/p4gefau1t/trojan-go/test/util" ) // test key and cert var cert = ` -----BEGIN CERTIFICATE----- MIIC5TCCAc2gAwIBAgIJAJqNVe6g/10vMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV BAMMCWxvY2FsaG9zdDAeFw0yMTA5MTQwNjE1MTFaFw0yNjA5MTMwNjE1MTFaMBQx EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAK7bupJ8tmHM3shQ/7N730jzpRsXdNiBxq/Jxx8j+vB3AcxuP5bjXQZqS6YR 5W5vrfLlegtq1E/mmaI3Ht0RfIlzev04Dua9PWmIQJD801nEPknbfgCLXDh+pYr2 sfg8mUh3LjGtrxyH+nmbTjWg7iWSKohmZ8nUDcX94Llo5FxibMAz8OsAwOmUueCH jP3XswZYHEy+OOP3K0ZEiJy0f5T6ZXk9OWYuPN4VQKJx1qrc9KzZtSPHwqVdkGUi ase9tOPA4aMutzt0btgW7h7UrvG6C1c/Rr1BxdiYq1EQ+yypnAlyToVQSNbo67zz wGQk4GeruIkOgJOLdooN/HjhbHMCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B AQsFAAOCAQEASsBzHHYiWDDiBVWUEwVZAduTrslTLNOxG0QHBKsHWIlz/3QlhQil ywb3OhfMTUR1dMGY5Iq5432QiCHO4IMCOv7tDIkgb4Bc3v/3CRlBlnurtAmUfNJ6 pTRSlK4AjWpGHAEEd/8aCaOE86hMP8WDht8MkJTRrQqpJ1HeDISoKt9nepHOIsj+ I2zLZZtw0pg7FuR4MzWuqOt071iRS46Pupryb3ZEGIWNz5iLrDQod5Iz2ZGSRGqE rB8idX0mlj5AHRRanVR3PAes+eApsW9JvYG/ImuCOs+ZsukY614zQZdR+SyFm85G 4NICyeQsmiypNHHgw+xZmGqZg65bXNGoyg== -----END CERTIFICATE----- ` var key = ` -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu27qSfLZhzN7I UP+ze99I86UbF3TYgcavyccfI/rwdwHMbj+W410GakumEeVub63y5XoLatRP5pmi Nx7dEXyJc3r9OA7mvT1piECQ/NNZxD5J234Ai1w4fqWK9rH4PJlIdy4xra8ch/p5 m041oO4lkiqIZmfJ1A3F/eC5aORcYmzAM/DrAMDplLngh4z917MGWBxMvjjj9ytG RIictH+U+mV5PTlmLjzeFUCicdaq3PSs2bUjx8KlXZBlImrHvbTjwOGjLrc7dG7Y Fu4e1K7xugtXP0a9QcXYmKtREPssqZwJck6FUEjW6Ou888BkJOBnq7iJDoCTi3aK Dfx44WxzAgMBAAECggEBAKYhib/H0ZhWB4yWuHqUxG4RXtrAjHlvw5Acy5zgmHiC +Sh7ztrTJf0EXN9pvWwRm1ldgXj7hMBtPaaLbD1pccM9/qo66p17Sq/LjlyyeTOe affOHIbz4Sij2zCOdkR9fr0EztTQScF3yBhl4Aa/4cO8fcCeWxm86WEldq9x4xWJ s5WMR4CnrOJhDINLNPQPKX92KyxEQ/RfuBWovx3M0nl3fcUWfESY134t5g/UBFId In19tZ+pGIpCkxP0U1AZWrlZRA8Q/3sO2orUpoAOdCrGk/DcCTMh0c1pMzbYZ1/i cYXn38MpUo8QeG4FElUhAv6kzeBIl2tRBMVzIigo+AECgYEA3No1rHdFu6Ox9vC8 E93PTZevYVcL5J5yx6x7khCaOLKKuRXpjOX/h3Ll+hlN2DVAg5Jli/JVGCco4GeK kbFLSyxG1+E63JbgsVpaEOgvFT3bHHSPSRJDnIU+WkcNQ2u4Ky5ahZzbNdV+4fj2 NO2iMgkm7hoJANrm3IqqW8epenMCgYEAyq+qdNj5DiDzBcDvLwY+4/QmMOOgDqeh /TzhbDRyr+m4xNT7LLS4s/3wcbkQC33zhMUI3YvOHnYq5Ze/iL/TSloj0QCp1I7L J7sZeM1XimMBQIpCfOC7lf4tU76Fz0DTHAL+CmX1DgmRJdYO09843VsKkscC968R 4cwL5oGxxgECgYAM4TTsH/CTJtLEIfn19qOWVNhHhvoMlSkAeBCkzg8Qa2knrh12 uBsU3SCIW11s1H40rh758GICDJaXr7InGP3ZHnXrNRlnr+zeqvRBtCi6xma23B1X F5eV0zd1sFsXqXqOGh/xVtp54z+JEinZoForLNl2XVJVGG8KQZP50kUR/QKBgH4O 8zzpFT0sUPlrHVdp0wODfZ06dPmoWJ9flfPuSsYN3tTMgcs0Owv3C+wu5UPAegxB X1oq8W8Qn21cC8vJQmgj19LNTtLcXI3BV/5B+Aghu02gr+lq/EA1bYuAG0jjUGlD kyx0bQzl9lhJ4b70PjGtxc2z6KyTPdPpTB143FABAoGAQDoIUdc77/IWcjzcaXeJ 8abak5rAZA7cu2g2NVfs+Km+njsB0pbTwMnV1zGoFABdaHLdqbthLWtX7WOb1PDD MQ+kbiLw5uj8IY2HEqJhDGGEdXBqxbW7kyuIAN9Mw+mwKzkikNcFQdxgchWH1d1o lVkr92iEX+IhIeYb4DN1vQw= -----END PRIVATE KEY----- ` func init() { os.WriteFile("server.crt", []byte(cert), 0o777) os.WriteFile("server.key", []byte(key), 0o777) } func CheckClientServer(clientData, serverData string, socksPort int) (ok bool) { server, err := proxy.NewProxyFromConfigData([]byte(serverData), false) common.Must(err) go server.Run() client, err := proxy.NewProxyFromConfigData([]byte(clientData), false) common.Must(err) go client.Run() time.Sleep(time.Second * 2) dialer, err := netproxy.SOCKS5("tcp", fmt.Sprintf("127.0.0.1:%d", socksPort), nil, netproxy.Direct) common.Must(err) ok = true const num = 100 wg := sync.WaitGroup{} wg.Add(num) for i := 0; i < num; i++ { go func() { const payloadSize = 1024 payload := util.GeneratePayload(payloadSize) buf := [payloadSize]byte{} conn, err := dialer.Dial("tcp", util.EchoAddr) common.Must(err) common.Must2(conn.Write(payload)) common.Must2(conn.Read(buf[:])) if !bytes.Equal(payload, buf[:]) { ok = false } conn.Close() wg.Done() }() } wg.Wait() client.Close() server.Close() return } func TestClientServerWebsocketSubTree(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: client local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %d password: - password ssl: verify: false fingerprint: firefox sni: localhost websocket: enabled: true path: /ws host: somedomainname.com shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 mux: enabled: true `, socksPort, serverPort) serverData := fmt.Sprintf(` run-type: server local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s disable-http-check: true password: - password ssl: verify-hostname: false key: server.key cert: server.crt sni: localhost shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 websocket: enabled: true path: /ws host: 127.0.0.1 `, serverPort, util.HTTPPort) if !CheckClientServer(clientData, serverData, socksPort) { t.Fail() } } func TestClientServerTrojanSubTree(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: client local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %d password: - password ssl: verify: false fingerprint: firefox sni: localhost shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 mux: enabled: true `, socksPort, serverPort) serverData := fmt.Sprintf(` run-type: server local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s disable-http-check: true password: - password ssl: verify-hostname: false key: server.key cert: server.crt sni: localhost shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 `, serverPort, util.HTTPPort) if !CheckClientServer(clientData, serverData, socksPort) { t.Fail() } } func TestWebsocketDetection(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: client local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %d password: - password ssl: verify: false fingerprint: firefox sni: localhost shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 mux: enabled: true `, socksPort, serverPort) serverData := fmt.Sprintf(` run-type: server local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s disable-http-check: true password: - password ssl: verify-hostname: false key: server.key cert: server.crt sni: localhost shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 websocket: enabled: true path: /ws hostname: 127.0.0.1 `, serverPort, util.HTTPPort) if !CheckClientServer(clientData, serverData, socksPort) { t.Fail() } } func TestPluginWebsocket(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: client local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %d password: - password transport-plugin: enabled: true type: plaintext shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 mux: enabled: true websocket: enabled: true path: /ws hostname: 127.0.0.1 `, socksPort, serverPort) serverData := fmt.Sprintf(` run-type: server local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s disable-http-check: true password: - password transport-plugin: enabled: true type: plaintext shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 websocket: enabled: true path: /ws hostname: 127.0.0.1 `, serverPort, util.HTTPPort) if !CheckClientServer(clientData, serverData, socksPort) { t.Fail() } } func TestForward(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") clientPort := common.PickPort("tcp", "127.0.0.1") _, targetPort, _ := net.SplitHostPort(util.EchoAddr) clientData := fmt.Sprintf(` run-type: forward local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %d target-addr: 127.0.0.1 target-port: %s password: - password ssl: verify: false fingerprint: firefox sni: localhost websocket: enabled: true path: /ws hostname: 127.0.0.1 shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 mux: enabled: true `, clientPort, serverPort, targetPort) go func() { proxy, err := proxy.NewProxyFromConfigData([]byte(clientData), false) common.Must(err) common.Must(proxy.Run()) }() serverData := fmt.Sprintf(` run-type: server local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s disable-http-check: true password: - password ssl: verify-hostname: false key: server.key cert: server.crt sni: "localhost" websocket: enabled: true path: /ws hostname: 127.0.0.1 shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 `, serverPort, util.HTTPPort) go func() { proxy, err := proxy.NewProxyFromConfigData([]byte(serverData), false) common.Must(err) common.Must(proxy.Run()) }() time.Sleep(time.Second * 2) payload := util.GeneratePayload(1024) buf := [1024]byte{} conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", clientPort)) common.Must(err) common.Must2(conn.Write(payload)) common.Must2(conn.Read(buf[:])) if !bytes.Equal(payload, buf[:]) { t.Fail() } packet, err := net.ListenPacket("udp", "") common.Must(err) common.Must2(packet.WriteTo(payload, &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: clientPort, })) _, _, err = packet.ReadFrom(buf[:]) common.Must(err) if !bytes.Equal(payload, buf[:]) { t.Fail() } } func TestLeak(t *testing.T) { serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: client local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %d log-level: 0 password: - password ssl: verify: false fingerprint: firefox sni: localhost shadowsocks: enabled: true method: AEAD_CHACHA20_POLY1305 password: 12345678 mux: enabled: true api: enabled: true api-port: 0 `, socksPort, serverPort) client, err := proxy.NewProxyFromConfigData([]byte(clientData), false) common.Must(err) go client.Run() time.Sleep(time.Second * 3) client.Close() time.Sleep(time.Second * 3) // http.ListenAndServe("localhost:6060", nil) } func SingleThreadBenchmark(clientData, serverData string, socksPort int) { server, err := proxy.NewProxyFromConfigData([]byte(clientData), false) common.Must(err) go server.Run() client, err := proxy.NewProxyFromConfigData([]byte(serverData), false) common.Must(err) go client.Run() time.Sleep(time.Second * 2) dialer, err := netproxy.SOCKS5("tcp", fmt.Sprintf("127.0.0.1:%d", socksPort), nil, netproxy.Direct) common.Must(err) const num = 100 wg := sync.WaitGroup{} wg.Add(num) const payloadSize = 1024 * 1024 * 1024 payload := util.GeneratePayload(payloadSize) for i := 0; i < 100; i++ { conn, err := dialer.Dial("tcp", util.BlackHoleAddr) common.Must(err) t1 := time.Now() common.Must2(conn.Write(payload)) t2 := time.Now() speed := float64(payloadSize) / (float64(t2.Sub(t1).Nanoseconds()) / float64(time.Second)) fmt.Printf("speed: %f Gbps\n", speed/1024/1024/1024) conn.Close() } client.Close() server.Close() } func BenchmarkClientServer(b *testing.B) { go func() { fmt.Println(http.ListenAndServe("localhost:6060", nil)) }() serverPort := common.PickPort("tcp", "127.0.0.1") socksPort := common.PickPort("tcp", "127.0.0.1") clientData := fmt.Sprintf(` run-type: client local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %d log-level: 0 password: - password ssl: verify: false fingerprint: firefox sni: localhost `, socksPort, serverPort) serverData := fmt.Sprintf(` run-type: server local-addr: 127.0.0.1 local-port: %d remote-addr: 127.0.0.1 remote-port: %s log-level: 0 disable-http-check: true password: - password ssl: verify-hostname: false key: server.key cert: server.crt sni: localhost `, serverPort, util.HTTPPort) SingleThreadBenchmark(clientData, serverData, socksPort) } ================================================ FILE: test/util/target.go ================================================ package util import ( "crypto/rand" "fmt" "io" "io/ioutil" "net" "net/http" "sync" "time" "golang.org/x/net/websocket" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" ) var ( HTTPAddr string HTTPPort string ) func runHelloHTTPServer() { httpHello := func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("HelloWorld")) } wsConfig, err := websocket.NewConfig("wss://127.0.0.1/websocket", "https://127.0.0.1") common.Must(err) wsServer := websocket.Server{ Config: *wsConfig, Handler: func(conn *websocket.Conn) { conn.Write([]byte("HelloWorld")) }, Handshake: func(wsConfig *websocket.Config, httpRequest *http.Request) error { log.Debug("websocket url", httpRequest.URL, "origin", httpRequest.Header.Get("Origin")) return nil }, } mux := &http.ServeMux{} mux.HandleFunc("/", httpHello) mux.HandleFunc("/websocket", wsServer.ServeHTTP) HTTPAddr = GetTestAddr() _, HTTPPort, _ = net.SplitHostPort(HTTPAddr) server := http.Server{ Addr: HTTPAddr, Handler: mux, } go server.ListenAndServe() time.Sleep(time.Second * 1) // wait for http server fmt.Println("http test server listening on", HTTPAddr) wg.Done() } var ( EchoAddr string EchoPort int ) func runTCPEchoServer() { listener, err := net.Listen("tcp", EchoAddr) common.Must(err) wg.Done() go func() { defer listener.Close() for { conn, err := listener.Accept() if err != nil { return } go func(conn net.Conn) { defer conn.Close() for { buf := make([]byte, 2048) conn.SetDeadline(time.Now().Add(time.Second * 5)) n, err := conn.Read(buf) conn.SetDeadline(time.Time{}) if err != nil { return } _, err = conn.Write(buf[0:n]) if err != nil { return } } }(conn) } }() } func runUDPEchoServer() { conn, err := net.ListenPacket("udp", EchoAddr) common.Must(err) wg.Done() go func() { for { buf := make([]byte, 1024*8) n, addr, err := conn.ReadFrom(buf) if err != nil { return } log.Info("Echo from", addr) conn.WriteTo(buf[0:n], addr) } }() } func GeneratePayload(length int) []byte { buf := make([]byte, length) io.ReadFull(rand.Reader, buf) return buf } var ( BlackHoleAddr string BlackHolePort int ) func runTCPBlackHoleServer() { listener, err := net.Listen("tcp", BlackHoleAddr) common.Must(err) wg.Done() go func() { defer listener.Close() for { conn, err := listener.Accept() if err != nil { return } go func(conn net.Conn) { io.Copy(ioutil.Discard, conn) conn.Close() }(conn) } }() } func runUDPBlackHoleServer() { conn, err := net.ListenPacket("udp", BlackHoleAddr) common.Must(err) wg.Done() go func() { defer conn.Close() buf := make([]byte, 1024*8) for { _, _, err := conn.ReadFrom(buf) if err != nil { return } } }() } var wg = sync.WaitGroup{} func init() { wg.Add(5) runHelloHTTPServer() EchoPort = common.PickPort("tcp", "127.0.0.1") EchoAddr = fmt.Sprintf("127.0.0.1:%d", EchoPort) BlackHolePort = common.PickPort("tcp", "127.0.0.1") BlackHoleAddr = fmt.Sprintf("127.0.0.1:%d", BlackHolePort) runTCPEchoServer() runUDPEchoServer() runTCPBlackHoleServer() runUDPBlackHoleServer() wg.Wait() } ================================================ FILE: test/util/util.go ================================================ package util import ( "bytes" "crypto/rand" "fmt" "net" "sync" "github.com/p4gefau1t/trojan-go/common" ) // CheckConn checks if two netConn were connected and work properly func CheckConn(a net.Conn, b net.Conn) bool { payload1 := make([]byte, 1024) payload2 := make([]byte, 1024) result1 := make([]byte, 1024) result2 := make([]byte, 1024) rand.Reader.Read(payload1) rand.Reader.Read(payload2) wg := sync.WaitGroup{} wg.Add(2) go func() { a.Write(payload1) a.Read(result2) wg.Done() }() go func() { b.Read(result1) b.Write(payload2) wg.Done() }() wg.Wait() return bytes.Equal(payload1, result1) && bytes.Equal(payload2, result2) } // CheckPacketOverConn checks if two PacketConn streaming over a connection work properly func CheckPacketOverConn(a, b net.PacketConn) bool { port := common.PickPort("tcp", "127.0.0.1") addr := &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: port, } payload1 := make([]byte, 1024) payload2 := make([]byte, 1024) result1 := make([]byte, 1024) result2 := make([]byte, 1024) rand.Reader.Read(payload1) rand.Reader.Read(payload2) common.Must2(a.WriteTo(payload1, addr)) _, addr1, err := b.ReadFrom(result1) common.Must(err) if addr1.String() != addr.String() { return false } common.Must2(a.WriteTo(payload2, addr)) _, addr2, err := b.ReadFrom(result2) common.Must(err) if addr2.String() != addr.String() { return false } return bytes.Equal(payload1, result1) && bytes.Equal(payload2, result2) } func CheckPacket(a, b net.PacketConn) bool { payload1 := make([]byte, 1024) payload2 := make([]byte, 1024) result1 := make([]byte, 1024) result2 := make([]byte, 1024) rand.Reader.Read(payload1) rand.Reader.Read(payload2) _, err := a.WriteTo(payload1, b.LocalAddr()) common.Must(err) _, _, err = b.ReadFrom(result1) common.Must(err) _, err = b.WriteTo(payload2, a.LocalAddr()) common.Must(err) _, _, err = a.ReadFrom(result2) common.Must(err) return bytes.Equal(payload1, result1) && bytes.Equal(payload2, result2) } func GetTestAddr() string { port := common.PickPort("tcp", "127.0.0.1") return fmt.Sprintf("127.0.0.1:%d", port) } ================================================ FILE: tunnel/adapter/config.go ================================================ package adapter import "github.com/p4gefau1t/trojan-go/config" type Config struct { LocalHost string `json:"local_addr" yaml:"local-addr"` LocalPort int `json:"local_port" yaml:"local-port"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(Config) }) } ================================================ FILE: tunnel/adapter/server.go ================================================ package adapter import ( "context" "net" "sync" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/http" "github.com/p4gefau1t/trojan-go/tunnel/socks" ) type Server struct { tcpListener net.Listener udpListener net.PacketConn socksConn chan tunnel.Conn httpConn chan tunnel.Conn socksLock sync.RWMutex nextSocks bool ctx context.Context cancel context.CancelFunc } func (s *Server) acceptConnLoop() { for { conn, err := s.tcpListener.Accept() if err != nil { select { case <-s.ctx.Done(): log.Debug("exiting") return default: continue } } rewindConn := common.NewRewindConn(conn) rewindConn.SetBufferSize(16) buf := [3]byte{} _, err = rewindConn.Read(buf[:]) rewindConn.Rewind() rewindConn.StopBuffering() if err != nil { log.Error(common.NewError("failed to detect proxy protocol type").Base(err)) continue } s.socksLock.RLock() if buf[0] == 5 && s.nextSocks { s.socksLock.RUnlock() log.Debug("socks5 connection") s.socksConn <- &freedom.Conn{ Conn: rewindConn, } } else { s.socksLock.RUnlock() log.Debug("http connection") s.httpConn <- &freedom.Conn{ Conn: rewindConn, } } } } func (s *Server) AcceptConn(overlay tunnel.Tunnel) (tunnel.Conn, error) { if _, ok := overlay.(*http.Tunnel); ok { select { case conn := <-s.httpConn: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("adapter closed") } } else if _, ok := overlay.(*socks.Tunnel); ok { s.socksLock.Lock() s.nextSocks = true s.socksLock.Unlock() select { case conn := <-s.socksConn: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("adapter closed") } } else { panic("invalid overlay") } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { return &freedom.PacketConn{ UDPConn: s.udpListener.(*net.UDPConn), }, nil } func (s *Server) Close() error { s.cancel() s.tcpListener.Close() return s.udpListener.Close() } func NewServer(ctx context.Context, _ tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) var cancel context.CancelFunc ctx, cancel = context.WithCancel(ctx) addr := tunnel.NewAddressFromHostPort("tcp", cfg.LocalHost, cfg.LocalPort) tcpListener, err := net.Listen("tcp", addr.String()) if err != nil { cancel() return nil, common.NewError("adapter failed to create tcp listener").Base(err) } udpListener, err := net.ListenPacket("udp", addr.String()) if err != nil { cancel() return nil, common.NewError("adapter failed to create tcp listener").Base(err) } server := &Server{ tcpListener: tcpListener, udpListener: udpListener, socksConn: make(chan tunnel.Conn, 32), httpConn: make(chan tunnel.Conn, 32), ctx: ctx, cancel: cancel, } log.Info("adapter listening on tcp/udp:", addr) go server.acceptConnLoop() return server, nil } ================================================ FILE: tunnel/adapter/tunnel.go ================================================ package adapter import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "ADAPTER" type Tunnel struct{} func (t *Tunnel) Name() string { return Name } func (t *Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { panic("not supported") } func (t *Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/dokodemo/config.go ================================================ package dokodemo import "github.com/p4gefau1t/trojan-go/config" type Config struct { LocalHost string `json:"local_addr" yaml:"local-addr"` LocalPort int `json:"local_port" yaml:"local-port"` TargetHost string `json:"target_addr" yaml:"target-addr"` TargetPort int `json:"target_port" yaml:"target-port"` UDPTimeout int `json:"udp_timeout" yaml:"udp-timeout"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ UDPTimeout: 60, } }) } ================================================ FILE: tunnel/dokodemo/conn.go ================================================ package dokodemo import ( "context" "net" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/tunnel" ) const MaxPacketSize = 1024 * 8 type Conn struct { net.Conn src *tunnel.Address targetMetadata *tunnel.Metadata } func (c *Conn) Metadata() *tunnel.Metadata { return c.targetMetadata } // PacketConn receive packet info from the packet dispatcher type PacketConn struct { net.PacketConn metadata *tunnel.Metadata input chan []byte output chan []byte src net.Addr ctx context.Context cancel context.CancelFunc } func (c *PacketConn) Close() error { c.cancel() // don't close the underlying udp socket return nil } func (c *PacketConn) ReadFrom(p []byte) (int, net.Addr, error) { return c.ReadWithMetadata(p) } func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { address, err := tunnel.NewAddressFromAddr("udp", addr.String()) if err != nil { return 0, err } return c.WriteWithMetadata(p, &tunnel.Metadata{ Address: address, }) } func (c *PacketConn) ReadWithMetadata(p []byte) (int, *tunnel.Metadata, error) { select { case payload := <-c.input: n := copy(p, payload) return n, c.metadata, nil case <-c.ctx.Done(): return 0, nil, common.NewError("dokodemo packet conn closed") } } func (c *PacketConn) WriteWithMetadata(p []byte, m *tunnel.Metadata) (int, error) { select { case c.output <- p: return len(p), nil case <-c.ctx.Done(): return 0, common.NewError("dokodemo packet conn failed to write") } } ================================================ FILE: tunnel/dokodemo/dokodemo_test.go ================================================ package dokodemo import ( "context" "fmt" "net" "sync" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" ) func TestDokodemo(t *testing.T) { cfg := &Config{ LocalHost: "127.0.0.1", LocalPort: common.PickPort("tcp", "127.0.0.1"), TargetHost: "127.0.0.1", TargetPort: common.PickPort("tcp", "127.0.0.1"), UDPTimeout: 30, } ctx := config.WithConfig(context.Background(), Name, cfg) s, err := NewServer(ctx, nil) common.Must(err) conn1, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", cfg.LocalPort)) common.Must(err) conn2, err := s.AcceptConn(nil) common.Must(err) if !util.CheckConn(conn1, conn2) { t.Fail() } conn1.Close() conn2.Close() wg := sync.WaitGroup{} wg.Add(1) packet1, err := net.ListenPacket("udp", "") common.Must(err) common.Must2(packet1.(*net.UDPConn).WriteToUDP([]byte("hello1"), &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: cfg.LocalPort, })) packet2, err := s.AcceptPacket(nil) common.Must(err) buf := [100]byte{} n, m, err := packet2.ReadWithMetadata(buf[:]) common.Must(err) if m.Address.Port != cfg.TargetPort { t.Fail() } if string(buf[:n]) != "hello1" { t.Fail() } fmt.Println(n, m, string(buf[:n])) if !util.CheckPacket(packet1, packet2) { t.Fail() } packet3, err := net.ListenPacket("udp", "") common.Must(err) common.Must2(packet3.(*net.UDPConn).WriteToUDP([]byte("hello2"), &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: cfg.LocalPort, })) packet4, err := s.AcceptPacket(nil) common.Must(err) n, m, err = packet4.ReadWithMetadata(buf[:]) common.Must(err) if m.Address.Port != cfg.TargetPort { t.Fail() } if string(buf[:n]) != "hello2" { t.Fail() } fmt.Println(n, m, string(buf[:n])) wg = sync.WaitGroup{} wg.Add(2) go func() { if !util.CheckPacket(packet3, packet4) { t.Fail() } wg.Done() }() go func() { if !util.CheckPacket(packet1, packet2) { t.Fail() } wg.Done() }() wg.Wait() s.Close() } ================================================ FILE: tunnel/dokodemo/server.go ================================================ package dokodemo import ( "context" "net" "sync" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type Server struct { tunnel.Server tcpListener net.Listener udpListener net.PacketConn packetChan chan tunnel.PacketConn timeout time.Duration targetAddr *tunnel.Address mappingLock sync.Mutex mapping map[string]*PacketConn ctx context.Context cancel context.CancelFunc } func (s *Server) dispatchLoop() { fixedMetadata := &tunnel.Metadata{ Address: s.targetAddr, } for { buf := make([]byte, MaxPacketSize) n, addr, err := s.udpListener.ReadFrom(buf) if err != nil { select { case <-s.ctx.Done(): default: log.Fatal(common.NewError("dokodemo failed to read from udp socket").Base(err)) } return } log.Debug("udp packet from", addr) s.mappingLock.Lock() if conn, found := s.mapping[addr.String()]; found { conn.input <- buf[:n] s.mappingLock.Unlock() continue } ctx, cancel := context.WithCancel(s.ctx) conn := &PacketConn{ input: make(chan []byte, 16), output: make(chan []byte, 16), metadata: fixedMetadata, src: addr, PacketConn: s.udpListener, ctx: ctx, cancel: cancel, } s.mapping[addr.String()] = conn s.mappingLock.Unlock() conn.input <- buf[:n] s.packetChan <- conn go func(conn *PacketConn) { for { select { case payload := <-conn.output: // "Multiple goroutines may invoke methods on a Conn simultaneously." _, err := s.udpListener.WriteTo(payload, conn.src) if err != nil { log.Error(common.NewError("dokodemo udp write error").Base(err)) return } case <-s.ctx.Done(): return case <-time.After(s.timeout): s.mappingLock.Lock() delete(s.mapping, conn.src.String()) s.mappingLock.Unlock() conn.Close() log.Debug("closing timeout packetConn") return } } }(conn) } } func (s *Server) AcceptConn(tunnel.Tunnel) (tunnel.Conn, error) { conn, err := s.tcpListener.Accept() if err != nil { log.Fatal(common.NewError("dokodemo failed to accept connection").Base(err)) } return &Conn{ Conn: conn, targetMetadata: &tunnel.Metadata{ Address: s.targetAddr, }, }, nil } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { select { case conn := <-s.packetChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("dokodemo server closed") } } func (s *Server) Close() error { s.cancel() s.tcpListener.Close() s.udpListener.Close() return nil } func NewServer(ctx context.Context, _ tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) targetAddr := tunnel.NewAddressFromHostPort("tcp", cfg.TargetHost, cfg.TargetPort) listenAddr := tunnel.NewAddressFromHostPort("tcp", cfg.LocalHost, cfg.LocalPort) tcpListener, err := net.Listen("tcp", listenAddr.String()) if err != nil { return nil, common.NewError("failed to listen tcp").Base(err) } udpListener, err := net.ListenPacket("udp", listenAddr.String()) if err != nil { return nil, common.NewError("failed to listen udp").Base(err) } ctx, cancel := context.WithCancel(ctx) server := &Server{ tcpListener: tcpListener, udpListener: udpListener, targetAddr: targetAddr, mapping: make(map[string]*PacketConn), packetChan: make(chan tunnel.PacketConn, 32), timeout: time.Second * time.Duration(cfg.UDPTimeout), ctx: ctx, cancel: cancel, } go server.dispatchLoop() return server, nil } ================================================ FILE: tunnel/dokodemo/tunnel.go ================================================ package dokodemo import ( "context" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "DOKODEMO" type Tunnel struct{ tunnel.Tunnel } func (*Tunnel) Name() string { return Name } func (*Tunnel) NewServer(ctx context.Context, underlay tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, underlay) } func (*Tunnel) NewClient(ctx context.Context, underlay tunnel.Client) (tunnel.Client, error) { return nil, common.NewError("not supported") } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/freedom/client.go ================================================ package freedom import ( "context" "net" "github.com/txthinking/socks5" "golang.org/x/net/proxy" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/tunnel" ) type Client struct { preferIPv4 bool noDelay bool keepAlive bool ctx context.Context cancel context.CancelFunc forwardProxy bool proxyAddr *tunnel.Address username string password string } func (c *Client) DialConn(addr *tunnel.Address, _ tunnel.Tunnel) (tunnel.Conn, error) { // forward proxy if c.forwardProxy { var auth *proxy.Auth if c.username != "" { auth = &proxy.Auth{ User: c.username, Password: c.password, } } dialer, err := proxy.SOCKS5("tcp", c.proxyAddr.String(), auth, proxy.Direct) if err != nil { return nil, common.NewError("freedom failed to init socks dialer") } conn, err := dialer.Dial("tcp", addr.String()) if err != nil { return nil, common.NewError("freedom failed to dial target address via socks proxy " + addr.String()).Base(err) } return &Conn{ Conn: conn, }, nil } network := "tcp" if c.preferIPv4 { network = "tcp4" } dialer := new(net.Dialer) tcpConn, err := dialer.DialContext(c.ctx, network, addr.String()) if err != nil { return nil, common.NewError("freedom failed to dial " + addr.String()).Base(err) } tcpConn.(*net.TCPConn).SetKeepAlive(c.keepAlive) tcpConn.(*net.TCPConn).SetNoDelay(c.noDelay) return &Conn{ Conn: tcpConn, }, nil } func (c *Client) DialPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { if c.forwardProxy { socksClient, err := socks5.NewClient(c.proxyAddr.String(), c.username, c.password, 0, 0) common.Must(err) if err := socksClient.Negotiate(&net.TCPAddr{}); err != nil { return nil, common.NewError("freedom failed to negotiate socks").Base(err) } a, addr, port, err := socks5.ParseAddress("1.1.1.1:53") // useless address common.Must(err) resp, err := socksClient.Request(socks5.NewRequest(socks5.CmdUDP, a, addr, port)) if err != nil { return nil, common.NewError("freedom failed to dial udp to socks").Base(err) } // TODO fix hardcoded localhost packetConn, err := net.ListenPacket("udp", "127.0.0.1:0") if err != nil { return nil, common.NewError("freedom failed to listen udp").Base(err) } socksAddr, err := net.ResolveUDPAddr("udp", resp.Address()) if err != nil { return nil, common.NewError("freedom recv invalid socks bind addr").Base(err) } return &SocksPacketConn{ PacketConn: packetConn, socksAddr: socksAddr, socksClient: socksClient, }, nil } network := "udp" if c.preferIPv4 { network = "udp4" } udpConn, err := net.ListenPacket(network, "") if err != nil { return nil, common.NewError("freedom failed to listen udp socket").Base(err) } return &PacketConn{ UDPConn: udpConn.(*net.UDPConn), }, nil } func (c *Client) Close() error { c.cancel() return nil } func NewClient(ctx context.Context, _ tunnel.Client) (*Client, error) { cfg := config.FromContext(ctx, Name).(*Config) addr := tunnel.NewAddressFromHostPort("tcp", cfg.ForwardProxy.ProxyHost, cfg.ForwardProxy.ProxyPort) ctx, cancel := context.WithCancel(ctx) return &Client{ ctx: ctx, cancel: cancel, noDelay: cfg.TCP.NoDelay, keepAlive: cfg.TCP.KeepAlive, preferIPv4: cfg.TCP.PreferIPV4, forwardProxy: cfg.ForwardProxy.Enabled, proxyAddr: addr, username: cfg.ForwardProxy.Username, password: cfg.ForwardProxy.Password, }, nil } ================================================ FILE: tunnel/freedom/config.go ================================================ package freedom import "github.com/p4gefau1t/trojan-go/config" type Config struct { LocalHost string `json:"local_addr" yaml:"local-addr"` LocalPort int `json:"local_port" yaml:"local-port"` TCP TCPConfig `json:"tcp" yaml:"tcp"` ForwardProxy ForwardProxyConfig `json:"forward_proxy" yaml:"forward-proxy"` } type TCPConfig struct { PreferIPV4 bool `json:"prefer_ipv4" yaml:"prefer-ipv4"` KeepAlive bool `json:"keep_alive" yaml:"keep-alive"` NoDelay bool `json:"no_delay" yaml:"no-delay"` } type ForwardProxyConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` ProxyHost string `json:"proxy_addr" yaml:"proxy-addr"` ProxyPort int `json:"proxy_port" yaml:"proxy-port"` Username string `json:"username" yaml:"username"` Password string `json:"password" yaml:"password"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ TCP: TCPConfig{ PreferIPV4: false, NoDelay: true, KeepAlive: true, }, } }) } ================================================ FILE: tunnel/freedom/conn.go ================================================ package freedom import ( "bytes" "net" "github.com/txthinking/socks5" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) const MaxPacketSize = 1024 * 8 type Conn struct { net.Conn } func (c *Conn) Metadata() *tunnel.Metadata { return nil } type PacketConn struct { *net.UDPConn } func (c *PacketConn) WriteWithMetadata(p []byte, m *tunnel.Metadata) (int, error) { return c.WriteTo(p, m.Address) } func (c *PacketConn) ReadWithMetadata(p []byte) (int, *tunnel.Metadata, error) { n, addr, err := c.ReadFrom(p) if err != nil { return 0, nil, err } address, err := tunnel.NewAddressFromAddr("udp", addr.String()) common.Must(err) metadata := &tunnel.Metadata{ Address: address, } return n, metadata, nil } func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (int, error) { if udpAddr, ok := addr.(*net.UDPAddr); ok { return c.WriteToUDP(p, udpAddr) } ip, err := addr.(*tunnel.Address).ResolveIP() if err != nil { return 0, err } udpAddr := &net.UDPAddr{ IP: ip, Port: addr.(*tunnel.Address).Port, } return c.WriteToUDP(p, udpAddr) } type SocksPacketConn struct { net.PacketConn socksAddr *net.UDPAddr socksClient *socks5.Client } func (c *SocksPacketConn) WriteWithMetadata(payload []byte, metadata *tunnel.Metadata) (int, error) { buf := bytes.NewBuffer(make([]byte, 0, MaxPacketSize)) buf.Write([]byte{0, 0, 0}) // RSV, FRAG common.Must(metadata.Address.WriteTo(buf)) buf.Write(payload) _, err := c.PacketConn.WriteTo(buf.Bytes(), c.socksAddr) if err != nil { return 0, err } log.Debug("sent udp packet to " + c.socksAddr.String() + " with metadata " + metadata.String()) return len(payload), nil } func (c *SocksPacketConn) ReadWithMetadata(payload []byte) (int, *tunnel.Metadata, error) { buf := make([]byte, MaxPacketSize) n, from, err := c.PacketConn.ReadFrom(buf) if err != nil { return 0, nil, err } log.Debug("recv udp packet from " + from.String()) addr := new(tunnel.Address) r := bytes.NewBuffer(buf[3:n]) if err := addr.ReadFrom(r); err != nil { return 0, nil, common.NewError("socks5 failed to parse addr in the packet").Base(err) } length, err := r.Read(payload) if err != nil { return 0, nil, err } return length, &tunnel.Metadata{ Address: addr, }, nil } func (c *SocksPacketConn) Close() error { c.socksClient.Close() return c.PacketConn.Close() } ================================================ FILE: tunnel/freedom/freedom_test.go ================================================ package freedom import ( "bytes" "context" "fmt" "testing" "time" "github.com/txthinking/socks5" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel" ) func TestConn(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) client := &Client{ ctx: ctx, cancel: cancel, } addr, err := tunnel.NewAddressFromAddr("tcp", util.EchoAddr) common.Must(err) conn1, err := client.DialConn(addr, nil) common.Must(err) sendBuf := util.GeneratePayload(1024) recvBuf := [1024]byte{} common.Must2(conn1.Write(sendBuf)) common.Must2(conn1.Read(recvBuf[:])) if !bytes.Equal(sendBuf, recvBuf[:]) { t.Fail() } client.Close() } func TestPacket(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) client := &Client{ ctx: ctx, cancel: cancel, } addr, err := tunnel.NewAddressFromAddr("udp", util.EchoAddr) common.Must(err) conn1, err := client.DialPacket(nil) common.Must(err) sendBuf := util.GeneratePayload(1024) recvBuf := [1024]byte{} common.Must2(conn1.WriteTo(sendBuf, addr)) _, _, err = conn1.ReadFrom(recvBuf[:]) common.Must(err) if !bytes.Equal(sendBuf, recvBuf[:]) { t.Fail() } } func TestSocks(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) socksAddr := tunnel.NewAddressFromHostPort("udp", "127.0.0.1", common.PickPort("udp", "127.0.0.1")) client := &Client{ ctx: ctx, cancel: cancel, proxyAddr: socksAddr, forwardProxy: true, noDelay: true, } target, err := tunnel.NewAddressFromAddr("tcp", util.EchoAddr) common.Must(err) s, _ := socks5.NewClassicServer(socksAddr.String(), "127.0.0.1", "", "", 0, 0) s.Handle = &socks5.DefaultHandle{} go s.RunTCPServer() go s.RunUDPServer() time.Sleep(time.Second * 2) conn, err := client.DialConn(target, nil) common.Must(err) payload := util.GeneratePayload(1024) common.Must2(conn.Write(payload)) recvBuf := [1024]byte{} conn.Read(recvBuf[:]) if !bytes.Equal(recvBuf[:], payload) { t.Fail() } conn.Close() packet, err := client.DialPacket(nil) common.Must(err) common.Must2(packet.WriteWithMetadata(payload, &tunnel.Metadata{ Address: target, })) recvBuf = [1024]byte{} n, m, err := packet.ReadWithMetadata(recvBuf[:]) common.Must(err) if n != 1024 || !bytes.Equal(recvBuf[:], payload) { t.Fail() } fmt.Println(m) packet.Close() client.Close() } ================================================ FILE: tunnel/freedom/tunnel.go ================================================ package freedom import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "FREEDOM" type Tunnel struct{} func (*Tunnel) Name() string { return Name } func (*Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, client) } func (*Tunnel) NewServer(ctx context.Context, client tunnel.Server) (tunnel.Server, error) { panic("not supported") } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/http/http_test.go ================================================ package http import ( "bufio" "context" "fmt" "io/ioutil" "net" "net/http" "testing" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) func TestHTTP(t *testing.T) { port := common.PickPort("tcp", "127.0.0.1") ctx := config.WithConfig(context.Background(), transport.Name, &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, }) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) s, err := NewServer(ctx, tcpServer) common.Must(err) for i := 0; i < 10; i++ { go func() { resp, err := http.Get(fmt.Sprintf("http://127.0.0.1:%d", port)) common.Must(err) defer resp.Body.Close() }() time.Sleep(time.Microsecond * 10) conn, err := s.AcceptConn(nil) common.Must(err) bufReader := bufio.NewReader(bufio.NewReader(conn)) req, err := http.ReadRequest(bufReader) common.Must(err) fmt.Println(req) ioutil.ReadAll(req.Body) req.Body.Close() resp, err := http.Get("http://127.0.0.1:" + util.HTTPPort) common.Must(err) defer resp.Body.Close() err = resp.Write(conn) common.Must(err) buf := [100]byte{} _, err = conn.Read(buf[:]) if err == nil { t.Fail() } conn.Close() } req, err := http.NewRequest(http.MethodConnect, "https://google.com:443", nil) common.Must(err) conn1, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) common.Must(err) go func() { common.Must(req.Write(conn1)) }() conn2, err := s.AcceptConn(nil) common.Must(err) if conn2.Metadata().Port != 443 || conn2.Metadata().DomainName != "google.com" { t.Fail() } connResp := "HTTP/1.1 200 Connection established\r\n\r\n" buf := make([]byte, len(connResp)) _, err = conn1.Read(buf) common.Must(err) if string(buf) != connResp { t.Fail() } if !util.CheckConn(conn1, conn2) { t.Fail() } conn1.Close() conn2.Close() s.Close() } ================================================ FILE: tunnel/http/server.go ================================================ package http import ( "bufio" "context" "fmt" "io" "io/ioutil" "net" "net/http" "strings" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type ConnectConn struct { net.Conn metadata *tunnel.Metadata } func (c *ConnectConn) Metadata() *tunnel.Metadata { return c.metadata } type OtherConn struct { net.Conn metadata *tunnel.Metadata // fixed reqReader *io.PipeReader respWriter *io.PipeWriter ctx context.Context cancel context.CancelFunc } func (c *OtherConn) Metadata() *tunnel.Metadata { return c.metadata } func (c *OtherConn) Read(p []byte) (int, error) { n, err := c.reqReader.Read(p) if err == io.EOF { if n != 0 { panic("non zero") } for range c.ctx.Done() { return 0, common.NewError("http conn closed") } } return n, err } func (c *OtherConn) Write(p []byte) (int, error) { return c.respWriter.Write(p) } func (c *OtherConn) Close() error { c.cancel() c.reqReader.Close() c.respWriter.Close() return nil } type Server struct { underlay tunnel.Server connChan chan tunnel.Conn ctx context.Context cancel context.CancelFunc } func (s *Server) acceptLoop() { for { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { select { case <-s.ctx.Done(): log.Error(common.NewError("http closed")) return default: log.Error(common.NewError("http failed to accept connection").Base(err)) continue } } go func(conn net.Conn) { reqBufReader := bufio.NewReader(ioutil.NopCloser(conn)) req, err := http.ReadRequest(reqBufReader) if err != nil { log.Error(common.NewError("not a valid http request").Base(err)) return } if strings.ToUpper(req.Method) == "CONNECT" { // CONNECT addr, err := tunnel.NewAddressFromAddr("tcp", req.Host) if err != nil { log.Error(common.NewError("invalid http dest address").Base(err)) conn.Close() return } resp := fmt.Sprintf("HTTP/%d.%d 200 Connection established\r\n\r\n", req.ProtoMajor, req.ProtoMinor) _, err = conn.Write([]byte(resp)) if err != nil { log.Error("http failed to respond connect request") conn.Close() return } s.connChan <- &ConnectConn{ Conn: conn, metadata: &tunnel.Metadata{ Address: addr, }, } } else { // GET, POST, PUT... defer conn.Close() for { reqReader, reqWriter := io.Pipe() respReader, respWriter := io.Pipe() var addr *tunnel.Address if addr, err = tunnel.NewAddressFromAddr("tcp", req.Host); err != nil { addr = tunnel.NewAddressFromHostPort("tcp", req.Host, 80) } log.Debug("http dest", addr) ctx, cancel := context.WithCancel(s.ctx) newConn := &OtherConn{ Conn: conn, metadata: &tunnel.Metadata{ Address: addr, }, ctx: ctx, cancel: cancel, reqReader: reqReader, respWriter: respWriter, } s.connChan <- newConn // pass this http session connection to proxy.RelayConn err = req.Write(reqWriter) // write request to the remote if err != nil { log.Error(common.NewError("http failed to write http request").Base(err)) return } respBufReader := bufio.NewReader(ioutil.NopCloser(respReader)) // read response from the remote resp, err := http.ReadResponse(respBufReader, req) if err != nil { log.Error(common.NewError("http failed to read http response").Base(err)) return } err = resp.Write(conn) // send the response back to the local if err != nil { log.Error(common.NewError("http failed to write the response back").Base(err)) return } newConn.Close() req.Body.Close() resp.Body.Close() req, err = http.ReadRequest(reqBufReader) // read the next http request from local if err != nil { log.Error(common.NewError("http failed to the read request from local").Base(err)) return } } } }(conn) } } func (s *Server) AcceptConn(tunnel.Tunnel) (tunnel.Conn, error) { select { case conn := <-s.connChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("http server closed") } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { <-s.ctx.Done() return nil, common.NewError("http server closed") } func (s *Server) Close() error { s.cancel() return s.underlay.Close() } func NewServer(ctx context.Context, underlay tunnel.Server) (*Server, error) { ctx, cancel := context.WithCancel(ctx) server := &Server{ underlay: underlay, connChan: make(chan tunnel.Conn, 32), ctx: ctx, cancel: cancel, } go server.acceptLoop() return server, nil } ================================================ FILE: tunnel/http/tunnel.go ================================================ package http import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "HTTP" type Tunnel struct{} func (t *Tunnel) Name() string { return Name } func (t *Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { panic("not supported") } func (t *Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/metadata.go ================================================ package tunnel import ( "bytes" "encoding/binary" "fmt" "io" "net" "strconv" "github.com/p4gefau1t/trojan-go/common" ) type Command byte type Metadata struct { Command *Address } func (r *Metadata) ReadFrom(rr io.Reader) error { byteBuf := [1]byte{} _, err := io.ReadFull(rr, byteBuf[:]) if err != nil { return err } r.Command = Command(byteBuf[0]) r.Address = new(Address) err = r.Address.ReadFrom(rr) if err != nil { return common.NewError("failed to marshal address").Base(err) } return nil } func (r *Metadata) WriteTo(w io.Writer) error { buf := bytes.NewBuffer(make([]byte, 0, 64)) buf.WriteByte(byte(r.Command)) if err := r.Address.WriteTo(buf); err != nil { return err } // use tcp by default r.Address.NetworkType = "tcp" _, err := w.Write(buf.Bytes()) return err } func (r *Metadata) Network() string { return r.Address.Network() } func (r *Metadata) String() string { return r.Address.String() } type AddressType byte const ( IPv4 AddressType = 1 DomainName AddressType = 3 IPv6 AddressType = 4 ) type Address struct { DomainName string Port int NetworkType string net.IP AddressType } func (a *Address) String() string { switch a.AddressType { case IPv4: return fmt.Sprintf("%s:%d", a.IP.String(), a.Port) case IPv6: return fmt.Sprintf("[%s]:%d", a.IP.String(), a.Port) case DomainName: return fmt.Sprintf("%s:%d", a.DomainName, a.Port) default: return "INVALID_ADDRESS_TYPE" } } func (a *Address) Network() string { return a.NetworkType } func (a *Address) ResolveIP() (net.IP, error) { if a.AddressType == IPv4 || a.AddressType == IPv6 { return a.IP, nil } if a.IP != nil { return a.IP, nil } addr, err := net.ResolveIPAddr("ip", a.DomainName) if err != nil { return nil, err } a.IP = addr.IP return addr.IP, nil } func NewAddressFromAddr(network string, addr string) (*Address, error) { host, portStr, err := net.SplitHostPort(addr) if err != nil { return nil, err } port, err := strconv.ParseInt(portStr, 10, 32) common.Must(err) return NewAddressFromHostPort(network, host, int(port)), nil } func NewAddressFromHostPort(network string, host string, port int) *Address { if ip := net.ParseIP(host); ip != nil { if ip.To4() != nil { return &Address{ IP: ip, Port: port, AddressType: IPv4, NetworkType: network, } } return &Address{ IP: ip, Port: port, AddressType: IPv6, NetworkType: network, } } return &Address{ DomainName: host, Port: port, AddressType: DomainName, NetworkType: network, } } func (a *Address) ReadFrom(r io.Reader) error { byteBuf := [1]byte{} _, err := io.ReadFull(r, byteBuf[:]) if err != nil { return common.NewError("unable to read ATYP").Base(err) } a.AddressType = AddressType(byteBuf[0]) switch a.AddressType { case IPv4: var buf [6]byte _, err := io.ReadFull(r, buf[:]) if err != nil { return common.NewError("failed to read IPv4").Base(err) } a.IP = buf[0:4] a.Port = int(binary.BigEndian.Uint16(buf[4:6])) case IPv6: var buf [18]byte _, err := io.ReadFull(r, buf[:]) if err != nil { return common.NewError("failed to read IPv6").Base(err) } a.IP = buf[0:16] a.Port = int(binary.BigEndian.Uint16(buf[16:18])) case DomainName: _, err := io.ReadFull(r, byteBuf[:]) length := byteBuf[0] if err != nil { return common.NewError("failed to read domain name length") } buf := make([]byte, length+2) _, err = io.ReadFull(r, buf) if err != nil { return common.NewError("failed to read domain name") } // the fucking browser uses IP as a domain name sometimes host := buf[0:length] if ip := net.ParseIP(string(host)); ip != nil { a.IP = ip if ip.To4() != nil { a.AddressType = IPv4 } else { a.AddressType = IPv6 } } else { a.DomainName = string(host) } a.Port = int(binary.BigEndian.Uint16(buf[length : length+2])) default: return common.NewError("invalid ATYP " + strconv.FormatInt(int64(a.AddressType), 10)) } return nil } func (a *Address) WriteTo(w io.Writer) error { _, err := w.Write([]byte{byte(a.AddressType)}) if err != nil { return err } switch a.AddressType { case DomainName: w.Write([]byte{byte(len(a.DomainName))}) _, err = w.Write([]byte(a.DomainName)) case IPv4: _, err = w.Write(a.IP.To4()) case IPv6: _, err = w.Write(a.IP.To16()) default: return common.NewError("invalid ATYP " + strconv.FormatInt(int64(a.AddressType), 10)) } if err != nil { return err } port := [2]byte{} binary.BigEndian.PutUint16(port[:], uint16(a.Port)) _, err = w.Write(port[:]) return err } ================================================ FILE: tunnel/mux/client.go ================================================ package mux import ( "context" "fmt" "math/rand" "sync" "time" "github.com/xtaci/smux" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type muxID uint32 func generateMuxID() muxID { return muxID(rand.Uint32()) } type smuxClientInfo struct { id muxID client *smux.Session lastActiveTime time.Time underlayConn tunnel.Conn } // Client is a smux client type Client struct { clientPoolLock sync.Mutex clientPool map[muxID]*smuxClientInfo underlay tunnel.Client concurrency int timeout time.Duration ctx context.Context cancel context.CancelFunc } func (c *Client) Close() error { c.cancel() c.clientPoolLock.Lock() defer c.clientPoolLock.Unlock() for id, info := range c.clientPool { info.client.Close() log.Debug("mux client", id, "closed") } return nil } func (c *Client) cleanLoop() { var checkDuration time.Duration if c.timeout <= 0 { checkDuration = time.Second * 10 log.Warn("negative mux timeout") } else { checkDuration = c.timeout / 4 } log.Debug("check duration:", checkDuration.Seconds(), "s") for { select { case <-time.After(checkDuration): c.clientPoolLock.Lock() for id, info := range c.clientPool { if info.client.IsClosed() { info.client.Close() info.underlayConn.Close() delete(c.clientPool, id) log.Info("mux client", id, "is dead") } else if info.client.NumStreams() == 0 && time.Since(info.lastActiveTime) > c.timeout { info.client.Close() info.underlayConn.Close() delete(c.clientPool, id) log.Info("mux client", id, "is closed due to inactivity") } } log.Debug("current mux clients: ", len(c.clientPool)) for id, info := range c.clientPool { log.Debug(fmt.Sprintf(" - %x: %d/%d", id, info.client.NumStreams(), c.concurrency)) } c.clientPoolLock.Unlock() case <-c.ctx.Done(): log.Debug("shutting down mux cleaner..") c.clientPoolLock.Lock() for id, info := range c.clientPool { info.client.Close() info.underlayConn.Close() delete(c.clientPool, id) log.Debug("mux client", id, "closed") } c.clientPoolLock.Unlock() return } } } func (c *Client) newMuxClient() (*smuxClientInfo, error) { // The mutex should be locked when this function is called id := generateMuxID() if _, found := c.clientPool[id]; found { return nil, common.NewError("duplicated id") } fakeAddr := &tunnel.Address{ DomainName: "MUX_CONN", AddressType: tunnel.DomainName, } conn, err := c.underlay.DialConn(fakeAddr, &Tunnel{}) if err != nil { return nil, common.NewError("mux failed to dial").Base(err) } conn = newStickyConn(conn) smuxConfig := smux.DefaultConfig() // smuxConfig.KeepAliveDisabled = true client, _ := smux.Client(conn, smuxConfig) info := &smuxClientInfo{ client: client, underlayConn: conn, id: id, lastActiveTime: time.Now(), } c.clientPool[id] = info return info, nil } func (c *Client) DialConn(*tunnel.Address, tunnel.Tunnel) (tunnel.Conn, error) { createNewConn := func(info *smuxClientInfo) (tunnel.Conn, error) { rwc, err := info.client.Open() info.lastActiveTime = time.Now() if err != nil { info.underlayConn.Close() info.client.Close() delete(c.clientPool, info.id) return nil, common.NewError("mux failed to open stream from client").Base(err) } return &Conn{ rwc: rwc, Conn: info.underlayConn, }, nil } c.clientPoolLock.Lock() defer c.clientPoolLock.Unlock() for _, info := range c.clientPool { if info.client.IsClosed() { delete(c.clientPool, info.id) log.Info(fmt.Sprintf("Mux client %x is closed", info.id)) continue } if info.client.NumStreams() < c.concurrency || c.concurrency <= 0 { return createNewConn(info) } } info, err := c.newMuxClient() if err != nil { return nil, common.NewError("no available mux client found").Base(err) } return createNewConn(info) } func (c *Client) DialPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } func NewClient(ctx context.Context, underlay tunnel.Client) (*Client, error) { clientConfig := config.FromContext(ctx, Name).(*Config) ctx, cancel := context.WithCancel(ctx) client := &Client{ underlay: underlay, concurrency: clientConfig.Mux.Concurrency, timeout: time.Duration(clientConfig.Mux.IdleTimeout) * time.Second, ctx: ctx, cancel: cancel, clientPool: make(map[muxID]*smuxClientInfo), } go client.cleanLoop() log.Debug("mux client created") return client, nil } ================================================ FILE: tunnel/mux/config.go ================================================ package mux import "github.com/p4gefau1t/trojan-go/config" type MuxConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` IdleTimeout int `json:"idle_timeout" yaml:"idle-timeout"` Concurrency int `json:"concurrency" yaml:"concurrency"` } type Config struct { Mux MuxConfig `json:"mux" yaml:"mux"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ Mux: MuxConfig{ Enabled: false, IdleTimeout: 30, Concurrency: 8, }, } }) } ================================================ FILE: tunnel/mux/conn.go ================================================ package mux import ( "io" "math/rand" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type stickyConn struct { tunnel.Conn synQueue chan []byte finQueue chan []byte } func (c *stickyConn) stickToPayload(p []byte) []byte { buf := make([]byte, 0, len(p)+16) for { select { case header := <-c.synQueue: buf = append(buf, header...) default: goto stick1 } } stick1: buf = append(buf, p...) for { select { case header := <-c.finQueue: buf = append(buf, header...) default: goto stick2 } } stick2: return buf } func (c *stickyConn) Close() error { const maxPaddingLength = 512 padding := [maxPaddingLength + 8]byte{'A', 'B', 'C', 'D', 'E', 'F'} // for debugging buf := c.stickToPayload(nil) c.Write(append(buf, padding[:rand.Intn(maxPaddingLength)]...)) return c.Conn.Close() } func (c *stickyConn) Write(p []byte) (int, error) { if len(p) == 8 { if p[0] == 1 || p[0] == 2 { // smux 8 bytes header switch p[1] { // THE CONTENT OF THE BUFFER MIGHT CHANGE // NEVER STORE THE POINTER TO HEADER, COPY THE HEADER INSTEAD case 0: // cmdSYN header := make([]byte, 8) copy(header, p) c.synQueue <- header return 8, nil case 1: // cmdFIN header := make([]byte, 8) copy(header, p) c.finQueue <- header return 8, nil } } else { log.Debug("other 8 bytes header") } } _, err := c.Conn.Write(c.stickToPayload(p)) return len(p), err } func newStickyConn(conn tunnel.Conn) *stickyConn { return &stickyConn{ Conn: conn, synQueue: make(chan []byte, 128), finQueue: make(chan []byte, 128), } } type Conn struct { rwc io.ReadWriteCloser tunnel.Conn } func (c *Conn) Read(p []byte) (int, error) { return c.rwc.Read(p) } func (c *Conn) Write(p []byte) (int, error) { return c.rwc.Write(p) } func (c *Conn) Close() error { return c.rwc.Close() } ================================================ FILE: tunnel/mux/mux_test.go ================================================ package mux import ( "context" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) func TestMux(t *testing.T) { muxCfg := &Config{ Mux: MuxConfig{ Enabled: true, Concurrency: 8, IdleTimeout: 60, }, } ctx := config.WithConfig(context.Background(), Name, muxCfg) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx = config.WithConfig(ctx, transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) common.Must(err) muxTunnel := Tunnel{} muxClient, _ := muxTunnel.NewClient(ctx, tcpClient) muxServer, _ := muxTunnel.NewServer(ctx, tcpServer) conn1, err := muxClient.DialConn(nil, nil) common.Must2(conn1.Write(util.GeneratePayload(1024))) common.Must(err) buf := [1024]byte{} conn2, err := muxServer.AcceptConn(nil) common.Must(err) common.Must2(conn2.Read(buf[:])) if !util.CheckConn(conn1, conn2) { t.Fail() } conn1.Close() conn2.Close() muxClient.Close() muxServer.Close() } ================================================ FILE: tunnel/mux/server.go ================================================ package mux import ( "context" "github.com/xtaci/smux" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) // Server is a smux server type Server struct { underlay tunnel.Server connChan chan tunnel.Conn ctx context.Context cancel context.CancelFunc } func (s *Server) acceptConnWorker() { for { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { log.Debug(err) select { case <-s.ctx.Done(): return default: } continue } go func(conn tunnel.Conn) { smuxConfig := smux.DefaultConfig() // smuxConfig.KeepAliveDisabled = true smuxSession, err := smux.Server(conn, smuxConfig) if err != nil { log.Error(err) return } go func(session *smux.Session, conn tunnel.Conn) { defer session.Close() defer conn.Close() for { stream, err := session.AcceptStream() if err != nil { log.Error(err) return } select { case s.connChan <- &Conn{ rwc: stream, Conn: conn, }: case <-s.ctx.Done(): log.Debug("exiting") return } } }(smuxSession, conn) }(conn) } } func (s *Server) AcceptConn(tunnel.Tunnel) (tunnel.Conn, error) { select { case conn := <-s.connChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("mux server closed") } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } func (s *Server) Close() error { s.cancel() return s.underlay.Close() } func NewServer(ctx context.Context, underlay tunnel.Server) (*Server, error) { ctx, cancel := context.WithCancel(ctx) server := &Server{ underlay: underlay, ctx: ctx, cancel: cancel, connChan: make(chan tunnel.Conn, 32), } go server.acceptConnWorker() log.Debug("mux server created") return server, nil } ================================================ FILE: tunnel/mux/tunnel.go ================================================ package mux import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "MUX" type Tunnel struct{} func (*Tunnel) Name() string { return Name } func (*Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, client) } func (*Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/router/client.go ================================================ package router import ( "context" "net" "regexp" "runtime" "strconv" "strings" v2router "github.com/v2fly/v2ray-core/v4/app/router" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/common/geodata" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) const ( Block = 0 Bypass = 1 Proxy = 2 ) const ( AsIs = 0 IPIfNonMatch = 1 IPOnDemand = 2 ) const MaxPacketSize = 1024 * 8 func matchDomain(list []*v2router.Domain, target string) bool { for _, d := range list { switch d.GetType() { case v2router.Domain_Full: domain := d.GetValue() if domain == target { log.Tracef("domain %s hit domain(full) rule: %s", target, domain) return true } case v2router.Domain_Domain: domain := d.GetValue() if strings.HasSuffix(target, domain) { idx := strings.Index(target, domain) if idx == 0 || target[idx-1] == '.' { log.Tracef("domain %s hit domain rule: %s", target, domain) return true } } case v2router.Domain_Plain: // keyword if strings.Contains(target, d.GetValue()) { log.Tracef("domain %s hit keyword rule: %s", target, d.GetValue()) return true } case v2router.Domain_Regex: matched, err := regexp.Match(d.GetValue(), []byte(target)) if err != nil { log.Error("invalid regex", d.GetValue()) return false } if matched { log.Tracef("domain %s hit regex rule: %s", target, d.GetValue()) return true } default: log.Debug("unknown rule type:", d.GetType().String()) } } return false } func matchIP(list []*v2router.CIDR, target net.IP) bool { isIPv6 := true len := net.IPv6len if target.To4() != nil { len = net.IPv4len isIPv6 = false } for _, c := range list { n := int(c.GetPrefix()) mask := net.CIDRMask(n, 8*len) cidrIP := net.IP(c.GetIp()) if cidrIP.To4() != nil { // IPv4 CIDR if isIPv6 { continue } } else { // IPv6 CIDR if !isIPv6 { continue } } subnet := &net.IPNet{IP: cidrIP.Mask(mask), Mask: mask} if subnet.Contains(target) { return true } } return false } func newIPAddress(address *tunnel.Address) (*tunnel.Address, error) { ip, err := address.ResolveIP() if err != nil { return nil, common.NewError("router failed to resolve ip").Base(err) } newAddress := &tunnel.Address{ IP: ip, Port: address.Port, } if ip.To4() != nil { newAddress.AddressType = tunnel.IPv4 } else { newAddress.AddressType = tunnel.IPv6 } return newAddress, nil } type Client struct { domains [3][]*v2router.Domain cidrs [3][]*v2router.CIDR defaultPolicy int domainStrategy int underlay tunnel.Client direct *freedom.Client ctx context.Context cancel context.CancelFunc } func (c *Client) Route(address *tunnel.Address) int { if address.AddressType == tunnel.DomainName { if c.domainStrategy == IPOnDemand { resolvedIP, err := newIPAddress(address) if err == nil { for i := Block; i <= Proxy; i++ { if matchIP(c.cidrs[i], resolvedIP.IP) { return i } } } } for i := Block; i <= Proxy; i++ { if matchDomain(c.domains[i], address.DomainName) { return i } } if c.domainStrategy == IPIfNonMatch { resolvedIP, err := newIPAddress(address) if err == nil { for i := Block; i <= Proxy; i++ { if matchIP(c.cidrs[i], resolvedIP.IP) { return i } } } } } else { for i := Block; i <= Proxy; i++ { if matchIP(c.cidrs[i], address.IP) { return i } } } return c.defaultPolicy } func (c *Client) DialConn(address *tunnel.Address, overlay tunnel.Tunnel) (tunnel.Conn, error) { policy := c.Route(address) switch policy { case Proxy: return c.underlay.DialConn(address, overlay) case Block: return nil, common.NewError("router blocked address: " + address.String()) case Bypass: conn, err := c.direct.DialConn(address, &Tunnel{}) if err != nil { return nil, common.NewError("router dial error").Base(err) } return &transport.Conn{ Conn: conn, }, nil } panic("unknown policy") } func (c *Client) DialPacket(overlay tunnel.Tunnel) (tunnel.PacketConn, error) { directConn, err := net.ListenPacket("udp", "") if err != nil { return nil, common.NewError("router failed to dial udp (direct)").Base(err) } proxy, err := c.underlay.DialPacket(overlay) if err != nil { return nil, common.NewError("router failed to dial udp (proxy)").Base(err) } ctx, cancel := context.WithCancel(c.ctx) conn := &PacketConn{ Client: c, PacketConn: directConn, proxy: proxy, cancel: cancel, ctx: ctx, packetChan: make(chan *packetInfo, 16), } go conn.packetLoop() return conn, nil } func (c *Client) Close() error { c.cancel() return c.underlay.Close() } type codeInfo struct { code string strategy int } func loadCode(cfg *Config, prefix string) []codeInfo { codes := []codeInfo{} for _, s := range cfg.Router.Proxy { if strings.HasPrefix(s, prefix) { if left := s[len(prefix):]; len(left) > 0 { codes = append(codes, codeInfo{ code: left, strategy: Proxy, }) } else { log.Warn("invalid empty rule:", s) } } } for _, s := range cfg.Router.Bypass { if strings.HasPrefix(s, prefix) { if left := s[len(prefix):]; len(left) > 0 { codes = append(codes, codeInfo{ code: left, strategy: Bypass, }) } else { log.Warn("invalid empty rule:", s) } } } for _, s := range cfg.Router.Block { if strings.HasPrefix(s, prefix) { if left := s[len(prefix):]; len(left) > 0 { codes = append(codes, codeInfo{ code: left, strategy: Block, }) } else { log.Warn("invalid empty rule:", s) } } } return codes } func NewClient(ctx context.Context, underlay tunnel.Client) (*Client, error) { m1 := runtime.MemStats{} m2 := runtime.MemStats{} m3 := runtime.MemStats{} m4 := runtime.MemStats{} cfg := config.FromContext(ctx, Name).(*Config) var cancel context.CancelFunc ctx, cancel = context.WithCancel(ctx) direct, err := freedom.NewClient(ctx, nil) if err != nil { cancel() return nil, common.NewError("router failed to initialize raw client").Base(err) } client := &Client{ domains: [3][]*v2router.Domain{}, cidrs: [3][]*v2router.CIDR{}, underlay: underlay, direct: direct, ctx: ctx, cancel: cancel, } switch strings.ToLower(cfg.Router.DomainStrategy) { case "as_is", "as-is", "asis": client.domainStrategy = AsIs case "ip_if_non_match", "ip-if-non-match", "ipifnonmatch": client.domainStrategy = IPIfNonMatch case "ip_on_demand", "ip-on-demand", "ipondemand": client.domainStrategy = IPOnDemand default: return nil, common.NewError("unknown strategy: " + cfg.Router.DomainStrategy) } switch strings.ToLower(cfg.Router.DefaultPolicy) { case "proxy": client.defaultPolicy = Proxy case "bypass": client.defaultPolicy = Bypass case "block": client.defaultPolicy = Block default: return nil, common.NewError("unknown strategy: " + cfg.Router.DomainStrategy) } runtime.ReadMemStats(&m1) geodataLoader := geodata.NewGeodataLoader() ipCode := loadCode(cfg, "geoip:") for _, c := range ipCode { code := c.code cidrs, err := geodataLoader.LoadIP(cfg.Router.GeoIPFilename, code) if err != nil { log.Error(err) } else { log.Infof("geoip:%s loaded", code) client.cidrs[c.strategy] = append(client.cidrs[c.strategy], cidrs...) } } runtime.ReadMemStats(&m2) siteCode := loadCode(cfg, "geosite:") for _, c := range siteCode { code := c.code attrWanted := "" // Test if user wants domains that have an attribute if attrIdx := strings.Index(code, "@"); attrIdx > 0 { if !strings.HasSuffix(code, "@") { code = c.code[:attrIdx] attrWanted = c.code[attrIdx+1:] } else { // "geosite:google@" is invalid log.Warnf("geosite:%s invalid", code) continue } } else if attrIdx == 0 { // "geosite:@cn" is invalid log.Warnf("geosite:%s invalid", code) continue } domainList, err := geodataLoader.LoadSite(cfg.Router.GeoSiteFilename, code) if err != nil { log.Error(err) } else { found := false if attrWanted != "" { for _, domain := range domainList { for _, attr := range domain.GetAttribute() { if strings.EqualFold(attrWanted, attr.GetKey()) { client.domains[c.strategy] = append(client.domains[c.strategy], domain) found = true } } } } else { client.domains[c.strategy] = append(client.domains[c.strategy], domainList...) found = true } if found { log.Infof("geosite:%s loaded", c.code) } else { log.Errorf("geosite:%s not found", c.code) } } } runtime.ReadMemStats(&m3) domainInfo := loadCode(cfg, "domain:") for _, info := range domainInfo { client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{ Type: v2router.Domain_Domain, Value: strings.ToLower(info.code), Attribute: nil, }) } keywordInfo := loadCode(cfg, "keyword:") for _, info := range keywordInfo { client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{ Type: v2router.Domain_Plain, Value: strings.ToLower(info.code), Attribute: nil, }) } regexInfo := loadCode(cfg, "regex:") for _, info := range regexInfo { if _, err := regexp.Compile(info.code); err != nil { return nil, common.NewError("invalid regular expression: " + info.code).Base(err) } client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{ Type: v2router.Domain_Regex, Value: info.code, Attribute: nil, }) } // Just for compatibility with V2Ray rule type `regexp` regexpInfo := loadCode(cfg, "regexp:") for _, info := range regexpInfo { if _, err := regexp.Compile(info.code); err != nil { return nil, common.NewError("invalid regular expression: " + info.code).Base(err) } client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{ Type: v2router.Domain_Regex, Value: info.code, Attribute: nil, }) } fullInfo := loadCode(cfg, "full:") for _, info := range fullInfo { client.domains[info.strategy] = append(client.domains[info.strategy], &v2router.Domain{ Type: v2router.Domain_Full, Value: strings.ToLower(info.code), Attribute: nil, }) } cidrInfo := loadCode(cfg, "cidr:") for _, info := range cidrInfo { tmp := strings.Split(info.code, "/") if len(tmp) != 2 { return nil, common.NewError("invalid cidr: " + info.code) } ip := net.ParseIP(tmp[0]) if ip == nil { return nil, common.NewError("invalid cidr ip: " + info.code) } prefix, err := strconv.ParseInt(tmp[1], 10, 32) if err != nil { return nil, common.NewError("invalid prefix").Base(err) } client.cidrs[info.strategy] = append(client.cidrs[info.strategy], &v2router.CIDR{ Ip: ip, Prefix: uint32(prefix), }) } log.Info("router client created") runtime.ReadMemStats(&m4) log.Debugf("GeoIP rules -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m2.Alloc-m1.Alloc), common.HumanFriendlyTraffic(m2.TotalAlloc-m1.TotalAlloc)) log.Debugf("GeoSite rules -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m3.Alloc-m2.Alloc), common.HumanFriendlyTraffic(m3.TotalAlloc-m2.TotalAlloc)) log.Debugf("Plaintext rules -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m4.Alloc-m3.Alloc), common.HumanFriendlyTraffic(m4.TotalAlloc-m3.TotalAlloc)) log.Debugf("Total(router) -> Alloc: %s; TotalAlloc: %s", common.HumanFriendlyTraffic(m4.Alloc-m1.Alloc), common.HumanFriendlyTraffic(m4.TotalAlloc-m1.TotalAlloc)) return client, nil } ================================================ FILE: tunnel/router/config.go ================================================ package router import ( "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" ) type Config struct { Router RouterConfig `json:"router" yaml:"router"` } type RouterConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` Bypass []string `json:"bypass" yaml:"bypass"` Proxy []string `json:"proxy" yaml:"proxy"` Block []string `json:"block" yaml:"block"` DomainStrategy string `json:"domain_strategy" yaml:"domain-strategy"` DefaultPolicy string `json:"default_policy" yaml:"default-policy"` GeoIPFilename string `json:"geoip" yaml:"geoip"` GeoSiteFilename string `json:"geosite" yaml:"geosite"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { cfg := &Config{ Router: RouterConfig{ DefaultPolicy: "proxy", DomainStrategy: "as_is", GeoIPFilename: common.GetAssetLocation("geoip.dat"), GeoSiteFilename: common.GetAssetLocation("geosite.dat"), }, } return cfg }) } ================================================ FILE: tunnel/router/conn.go ================================================ package router import ( "context" "io" "net" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type packetInfo struct { src *tunnel.Metadata payload []byte } type PacketConn struct { proxy tunnel.PacketConn net.PacketConn packetChan chan *packetInfo *Client ctx context.Context cancel context.CancelFunc } func (c *PacketConn) packetLoop() { go func() { for { buf := make([]byte, MaxPacketSize) n, addr, err := c.proxy.ReadWithMetadata(buf) if err != nil { select { case <-c.ctx.Done(): return default: log.Error("router packetConn error", err) continue } } c.packetChan <- &packetInfo{ src: addr, payload: buf[:n], } } }() for { buf := make([]byte, MaxPacketSize) n, addr, err := c.PacketConn.ReadFrom(buf) if err != nil { select { case <-c.ctx.Done(): return default: log.Error("router packetConn error", err) continue } } address, _ := tunnel.NewAddressFromAddr("udp", addr.String()) c.packetChan <- &packetInfo{ src: &tunnel.Metadata{ Address: address, }, payload: buf[:n], } } } func (c *PacketConn) Close() error { c.cancel() c.proxy.Close() return c.PacketConn.Close() } func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { panic("implement me") } func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { panic("implement me") } func (c *PacketConn) WriteWithMetadata(p []byte, m *tunnel.Metadata) (int, error) { policy := c.Route(m.Address) switch policy { case Proxy: return c.proxy.WriteWithMetadata(p, m) case Block: return 0, common.NewError("router blocked address (udp): " + m.Address.String()) case Bypass: ip, err := m.Address.ResolveIP() if err != nil { return 0, common.NewError("router failed to resolve udp address").Base(err) } return c.PacketConn.WriteTo(p, &net.UDPAddr{ IP: ip, Port: m.Address.Port, }) default: panic("unknown policy") } } func (c *PacketConn) ReadWithMetadata(p []byte) (int, *tunnel.Metadata, error) { select { case info := <-c.packetChan: n := copy(p, info.payload) return n, info.src, nil case <-c.ctx.Done(): return 0, nil, io.EOF } } ================================================ FILE: tunnel/router/data.go ================================================ package router ================================================ FILE: tunnel/router/router_test.go ================================================ package router import ( "context" "net" "strconv" "strings" "testing" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel" ) type MockClient struct{} func (m *MockClient) DialConn(address *tunnel.Address, t tunnel.Tunnel) (tunnel.Conn, error) { return nil, common.NewError("mockproxy") } func (m *MockClient) DialPacket(t tunnel.Tunnel) (tunnel.PacketConn, error) { return MockPacketConn{}, nil } func (m MockClient) Close() error { return nil } type MockPacketConn struct{} func (m MockPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { panic("implement me") } func (m MockPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { panic("implement me") } func (m MockPacketConn) Close() error { panic("implement me") } func (m MockPacketConn) LocalAddr() net.Addr { panic("implement me") } func (m MockPacketConn) SetDeadline(t time.Time) error { panic("implement me") } func (m MockPacketConn) SetReadDeadline(t time.Time) error { panic("implement me") } func (m MockPacketConn) SetWriteDeadline(t time.Time) error { panic("implement me") } func (m MockPacketConn) WriteWithMetadata(bytes []byte, metadata *tunnel.Metadata) (int, error) { return 0, common.NewError("mockproxy") } func (m MockPacketConn) ReadWithMetadata(bytes []byte) (int, *tunnel.Metadata, error) { return 0, nil, common.NewError("mockproxy") } func TestRouter(t *testing.T) { data := ` router: enabled: true bypass: - "regex:bypassreg(.*)" - "full:bypassfull" - "full:localhost" - "domain:bypass.com" block: - "regexp:blockreg(.*)" - "full:blockfull" - "domain:block.com" proxy: - "regexp:proxyreg(.*)" - "full:proxyfull" - "domain:proxy.com" - "cidr:192.168.1.1/16" ` ctx, err := config.WithYAMLConfig(context.Background(), []byte(data)) common.Must(err) client, err := NewClient(ctx, &MockClient{}) common.Must(err) _, err = client.DialConn(&tunnel.Address{ AddressType: tunnel.DomainName, DomainName: "proxy.com", Port: 80, }, nil) if err.Error() != "mockproxy" { t.Fatal(err) } _, err = client.DialConn(&tunnel.Address{ AddressType: tunnel.DomainName, DomainName: "proxyreg123456", Port: 80, }, nil) if err.Error() != "mockproxy" { t.Fatal(err) } _, err = client.DialConn(&tunnel.Address{ AddressType: tunnel.DomainName, DomainName: "proxyfull", Port: 80, }, nil) if err.Error() != "mockproxy" { t.Fatal(err) } _, err = client.DialConn(&tunnel.Address{ AddressType: tunnel.IPv4, IP: net.ParseIP("192.168.123.123"), Port: 80, }, nil) if err.Error() != "mockproxy" { t.Fatal(err) } _, err = client.DialConn(&tunnel.Address{ AddressType: tunnel.DomainName, DomainName: "block.com", Port: 80, }, nil) if !strings.Contains(err.Error(), "block") { t.Fatal("block??") } port, err := strconv.Atoi(util.HTTPPort) common.Must(err) _, err = client.DialConn(&tunnel.Address{ AddressType: tunnel.DomainName, DomainName: "localhost", Port: port, }, nil) if err != nil { t.Fatal("dial http failed", err) } packet, err := client.DialPacket(nil) common.Must(err) buf := [10]byte{} _, err = packet.WriteWithMetadata(buf[:], &tunnel.Metadata{ Address: &tunnel.Address{ AddressType: tunnel.DomainName, DomainName: "proxyfull", Port: port, }, }) if err.Error() != "mockproxy" { t.Fail() } } ================================================ FILE: tunnel/router/tunnel.go ================================================ package router import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "ROUTER" type Tunnel struct{} func (t *Tunnel) Name() string { return Name } func (t *Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, client) } func (t *Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { panic("not supported") } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/shadowsocks/client.go ================================================ package shadowsocks import ( "context" "github.com/shadowsocks/go-shadowsocks2/core" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type Client struct { underlay tunnel.Client core.Cipher } func (c *Client) DialConn(address *tunnel.Address, tunnel tunnel.Tunnel) (tunnel.Conn, error) { conn, err := c.underlay.DialConn(address, &Tunnel{}) if err != nil { return nil, err } return &Conn{ aeadConn: c.Cipher.StreamConn(conn), Conn: conn, }, nil } func (c *Client) DialPacket(tunnel tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } func (c *Client) Close() error { return c.underlay.Close() } func NewClient(ctx context.Context, underlay tunnel.Client) (*Client, error) { cfg := config.FromContext(ctx, Name).(*Config) cipher, err := core.PickCipher(cfg.Shadowsocks.Method, nil, cfg.Shadowsocks.Password) if err != nil { return nil, common.NewError("invalid shadowsocks cipher").Base(err) } log.Debug("shadowsocks client created") return &Client{ underlay: underlay, Cipher: cipher, }, nil } ================================================ FILE: tunnel/shadowsocks/config.go ================================================ package shadowsocks import "github.com/p4gefau1t/trojan-go/config" type ShadowsocksConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` Method string `json:"method" yaml:"method"` Password string `json:"password" yaml:"password"` } type Config struct { RemoteHost string `json:"remote_addr" yaml:"remote-addr"` RemotePort int `json:"remote_port" yaml:"remote-port"` Shadowsocks ShadowsocksConfig `json:"shadowsocks" yaml:"shadowsocks"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ Shadowsocks: ShadowsocksConfig{ Method: "AES-128-GCM", }, } }) } ================================================ FILE: tunnel/shadowsocks/conn.go ================================================ package shadowsocks import ( "net" "github.com/p4gefau1t/trojan-go/tunnel" ) type Conn struct { aeadConn net.Conn tunnel.Conn } func (c *Conn) Read(p []byte) (n int, err error) { return c.aeadConn.Read(p) } func (c *Conn) Write(p []byte) (n int, err error) { return c.aeadConn.Write(p) } func (c *Conn) Close() error { c.Conn.Close() return c.aeadConn.Close() } func (c *Conn) Metadata() *tunnel.Metadata { return c.Conn.Metadata() } ================================================ FILE: tunnel/shadowsocks/server.go ================================================ package shadowsocks import ( "context" "net" "github.com/shadowsocks/go-shadowsocks2/core" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/redirector" "github.com/p4gefau1t/trojan-go/tunnel" ) type Server struct { core.Cipher *redirector.Redirector underlay tunnel.Server redirAddr net.Addr } func (s *Server) AcceptConn(overlay tunnel.Tunnel) (tunnel.Conn, error) { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { return nil, common.NewError("shadowsocks failed to accept connection from underlying tunnel").Base(err) } rewindConn := common.NewRewindConn(conn) rewindConn.SetBufferSize(1024) defer rewindConn.StopBuffering() // try to read something from this connection buf := [1024]byte{} testConn := s.Cipher.StreamConn(rewindConn) if _, err := testConn.Read(buf[:]); err != nil { // we are under attack log.Error(common.NewError("shadowsocks failed to decrypt").Base(err)) rewindConn.Rewind() rewindConn.StopBuffering() s.Redirect(&redirector.Redirection{ RedirectTo: s.redirAddr, InboundConn: rewindConn, }) return nil, common.NewError("invalid aead payload") } rewindConn.Rewind() rewindConn.StopBuffering() return &Conn{ aeadConn: s.Cipher.StreamConn(rewindConn), Conn: conn, }, nil } func (s *Server) AcceptPacket(t tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } func (s *Server) Close() error { return s.underlay.Close() } func NewServer(ctx context.Context, underlay tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) cipher, err := core.PickCipher(cfg.Shadowsocks.Method, nil, cfg.Shadowsocks.Password) if err != nil { return nil, common.NewError("invalid shadowsocks cipher").Base(err) } if cfg.RemoteHost == "" { return nil, common.NewError("invalid shadowsocks redirection address") } if cfg.RemotePort == 0 { return nil, common.NewError("invalid shadowsocks redirection port") } log.Debug("shadowsocks client created") return &Server{ underlay: underlay, Cipher: cipher, Redirector: redirector.NewRedirector(ctx), redirAddr: tunnel.NewAddressFromHostPort("tcp", cfg.RemoteHost, cfg.RemotePort), }, nil } ================================================ FILE: tunnel/shadowsocks/shadowsocks_test.go ================================================ package shadowsocks import ( "context" "fmt" "net" "strconv" "strings" "sync" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) func TestShadowsocks(t *testing.T) { p, err := strconv.ParseInt(util.HTTPPort, 10, 32) common.Must(err) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx := config.WithConfig(context.Background(), transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) cfg := &Config{ RemoteHost: "127.0.0.1", RemotePort: int(p), Shadowsocks: ShadowsocksConfig{ Enabled: true, Method: "AES-128-GCM", Password: "password", }, } ctx = config.WithConfig(ctx, Name, cfg) c, err := NewClient(ctx, tcpClient) common.Must(err) s, err := NewServer(ctx, tcpServer) common.Must(err) wg := sync.WaitGroup{} wg.Add(2) var conn1, conn2 net.Conn go func() { var err error conn1, err = c.DialConn(nil, nil) common.Must(err) conn1.Write(util.GeneratePayload(1024)) wg.Done() }() go func() { var err error conn2, err = s.AcceptConn(nil) common.Must(err) buf := [1024]byte{} conn2.Read(buf[:]) wg.Done() }() wg.Wait() if !util.CheckConn(conn1, conn2) { t.Fail() } go func() { var err error conn2, err = s.AcceptConn(nil) if err == nil { t.Fail() } }() // test redirection conn3, err := tcpClient.DialConn(nil, nil) common.Must(err) n, err := conn3.Write(util.GeneratePayload(1024)) common.Must(err) fmt.Println("write:", n) buf := [1024]byte{} n, err = conn3.Read(buf[:]) common.Must(err) fmt.Println("read:", n) if !strings.Contains(string(buf[:n]), "Bad Request") { t.Fail() } conn1.Close() conn3.Close() c.Close() s.Close() } ================================================ FILE: tunnel/shadowsocks/tunnel.go ================================================ package shadowsocks import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "SHADOWSOCKS" type Tunnel struct{} func (t *Tunnel) Name() string { return Name } func (t *Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, client) } func (t *Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/simplesocks/client.go ================================================ package simplesocks import ( "context" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/trojan" ) const ( Connect tunnel.Command = 1 Associate tunnel.Command = 3 ) type Client struct { underlay tunnel.Client } func (c *Client) DialConn(addr *tunnel.Address, t tunnel.Tunnel) (tunnel.Conn, error) { conn, err := c.underlay.DialConn(nil, &Tunnel{}) if err != nil { return nil, common.NewError("simplesocks failed to dial using underlying tunnel").Base(err) } return &Conn{ Conn: conn, isOutbound: true, metadata: &tunnel.Metadata{ Command: Connect, Address: addr, }, }, nil } func (c *Client) DialPacket(t tunnel.Tunnel) (tunnel.PacketConn, error) { conn, err := c.underlay.DialConn(nil, &Tunnel{}) if err != nil { return nil, common.NewError("simplesocks failed to dial using underlying tunnel").Base(err) } metadata := &tunnel.Metadata{ Command: Associate, Address: &tunnel.Address{ DomainName: "UDP_CONN", AddressType: tunnel.DomainName, }, } if err := metadata.WriteTo(conn); err != nil { return nil, common.NewError("simplesocks failed to write udp associate").Base(err) } return &PacketConn{ PacketConn: trojan.PacketConn{ Conn: conn, }, }, nil } func (c *Client) Close() error { return c.underlay.Close() } func NewClient(ctx context.Context, underlay tunnel.Client) (*Client, error) { log.Debug("simplesocks client created") return &Client{ underlay: underlay, }, nil } ================================================ FILE: tunnel/simplesocks/conn.go ================================================ package simplesocks import ( "bytes" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/trojan" ) // Conn is a simplesocks connection type Conn struct { tunnel.Conn metadata *tunnel.Metadata isOutbound bool headerWritten bool } func (c *Conn) Metadata() *tunnel.Metadata { return c.metadata } func (c *Conn) Write(payload []byte) (int, error) { if c.isOutbound && !c.headerWritten { buf := bytes.NewBuffer(make([]byte, 0, 4096)) c.metadata.WriteTo(buf) buf.Write(payload) _, err := c.Conn.Write(buf.Bytes()) if err != nil { return 0, common.NewError("failed to write simplesocks header").Base(err) } c.headerWritten = true return len(payload), nil } return c.Conn.Write(payload) } // PacketConn is a simplesocks packet connection // The header syntax is the same as trojan's type PacketConn struct { trojan.PacketConn } ================================================ FILE: tunnel/simplesocks/server.go ================================================ package simplesocks import ( "context" "fmt" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/trojan" ) // Server is a simplesocks server type Server struct { underlay tunnel.Server connChan chan tunnel.Conn packetChan chan tunnel.PacketConn ctx context.Context cancel context.CancelFunc } func (s *Server) Close() error { s.cancel() return s.underlay.Close() } func (s *Server) acceptLoop() { for { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { log.Error(common.NewError("simplesocks failed to accept connection from underlying tunnel").Base(err)) select { case <-s.ctx.Done(): return default: } continue } metadata := new(tunnel.Metadata) if err := metadata.ReadFrom(conn); err != nil { log.Error(common.NewError("simplesocks server faield to read header").Base(err)) conn.Close() continue } switch metadata.Command { case Connect: s.connChan <- &Conn{ metadata: metadata, Conn: conn, } case Associate: s.packetChan <- &PacketConn{ PacketConn: trojan.PacketConn{ Conn: conn, }, } default: log.Error(common.NewError(fmt.Sprintf("simplesocks unknown command %d", metadata.Command))) conn.Close() } } } func (s *Server) AcceptConn(tunnel.Tunnel) (tunnel.Conn, error) { select { case conn := <-s.connChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("simplesocks server closed") } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { select { case packetConn := <-s.packetChan: return packetConn, nil case <-s.ctx.Done(): return nil, common.NewError("simplesocks server closed") } } func NewServer(ctx context.Context, underlay tunnel.Server) (*Server, error) { ctx, cancel := context.WithCancel(ctx) server := &Server{ underlay: underlay, ctx: ctx, connChan: make(chan tunnel.Conn, 32), packetChan: make(chan tunnel.PacketConn, 32), cancel: cancel, } go server.acceptLoop() log.Debug("simplesocks server created") return server, nil } ================================================ FILE: tunnel/simplesocks/simplesocks_test.go ================================================ package simplesocks import ( "context" "fmt" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) func TestSimpleSocks(t *testing.T) { port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx := config.WithConfig(context.Background(), transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) c, err := NewClient(ctx, tcpClient) common.Must(err) s, err := NewServer(ctx, tcpServer) common.Must(err) conn1, err := c.DialConn(&tunnel.Address{ DomainName: "www.baidu.com", AddressType: tunnel.DomainName, Port: 443, }, nil) common.Must(err) defer conn1.Close() conn1.Write(util.GeneratePayload(1024)) conn2, err := s.AcceptConn(nil) common.Must(err) defer conn2.Close() buf := [1024]byte{} common.Must2(conn2.Read(buf[:])) if !util.CheckConn(conn1, conn2) { t.Fail() } packet1, err := c.DialPacket(nil) common.Must(err) packet1.WriteWithMetadata([]byte("12345678"), &tunnel.Metadata{ Address: &tunnel.Address{ DomainName: "test.com", AddressType: tunnel.DomainName, Port: 443, }, }) defer packet1.Close() packet2, err := s.AcceptPacket(nil) common.Must(err) defer packet2.Close() _, m, err := packet2.ReadWithMetadata(buf[:]) common.Must(err) fmt.Println(m) if !util.CheckPacketOverConn(packet1, packet2) { t.Fail() } s.Close() c.Close() } ================================================ FILE: tunnel/simplesocks/tunnel.go ================================================ package simplesocks import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "SIMPLESOCKS" type Tunnel struct{} func (*Tunnel) Name() string { return Name } func (*Tunnel) NewServer(ctx context.Context, underlay tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, underlay) } func (*Tunnel) NewClient(ctx context.Context, underlay tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, underlay) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/socks/config.go ================================================ package socks import "github.com/p4gefau1t/trojan-go/config" type Config struct { LocalHost string `json:"local_addr" yaml:"local-addr"` LocalPort int `json:"local_port" yaml:"local-port"` UDPTimeout int `json:"udp_timeout" yaml:"udp-timeout"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ UDPTimeout: 60, } }) } ================================================ FILE: tunnel/socks/conn.go ================================================ package socks import ( "context" "net" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/tunnel" ) type Conn struct { net.Conn metadata *tunnel.Metadata } func (c *Conn) Metadata() *tunnel.Metadata { return c.metadata } type packetInfo struct { metadata *tunnel.Metadata payload []byte } type PacketConn struct { net.PacketConn input chan *packetInfo output chan *packetInfo src net.Addr ctx context.Context cancel context.CancelFunc } func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { panic("implement me") } func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { panic("implement me") } func (c *PacketConn) Close() error { c.cancel() return nil } func (c *PacketConn) WriteWithMetadata(p []byte, m *tunnel.Metadata) (int, error) { select { case c.output <- &packetInfo{ metadata: m, payload: p, }: return len(p), nil case <-c.ctx.Done(): return 0, common.NewError("socks packet conn closed") } } func (c *PacketConn) ReadWithMetadata(p []byte) (int, *tunnel.Metadata, error) { select { case info := <-c.input: n := copy(p, info.payload) return n, info.metadata, nil case <-c.ctx.Done(): return 0, nil, common.NewError("socks packet conn closed") } } ================================================ FILE: tunnel/socks/server.go ================================================ package socks import ( "bytes" "context" "fmt" "io" "io/ioutil" "net" "sync" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) const ( Connect tunnel.Command = 1 Associate tunnel.Command = 3 ) const ( MaxPacketSize = 1024 * 8 ) type Server struct { connChan chan tunnel.Conn packetChan chan tunnel.PacketConn underlay tunnel.Server localHost string localPort int timeout time.Duration listenPacketConn tunnel.PacketConn mapping map[string]*PacketConn mappingLock sync.RWMutex ctx context.Context cancel context.CancelFunc } func (s *Server) AcceptConn(tunnel.Tunnel) (tunnel.Conn, error) { select { case conn := <-s.connChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("socks server closed") } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { select { case conn := <-s.packetChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("socks server closed") } } func (s *Server) Close() error { s.cancel() return s.underlay.Close() } func (s *Server) handshake(conn net.Conn) (*Conn, error) { version := [1]byte{} if _, err := conn.Read(version[:]); err != nil { return nil, common.NewError("failed to read socks version").Base(err) } if version[0] != 5 { return nil, common.NewError(fmt.Sprintf("invalid socks version %d", version[0])) } nmethods := [1]byte{} if _, err := conn.Read(nmethods[:]); err != nil { return nil, common.NewError("failed to read NMETHODS") } if _, err := io.CopyN(ioutil.Discard, conn, int64(nmethods[0])); err != nil { return nil, common.NewError("socks failed to read methods").Base(err) } if _, err := conn.Write([]byte{0x5, 0x0}); err != nil { return nil, common.NewError("failed to respond auth").Base(err) } buf := [3]byte{} if _, err := conn.Read(buf[:]); err != nil { return nil, common.NewError("failed to read command") } addr := new(tunnel.Address) if err := addr.ReadFrom(conn); err != nil { return nil, err } return &Conn{ metadata: &tunnel.Metadata{ Command: tunnel.Command(buf[1]), Address: addr, }, Conn: conn, }, nil } func (s *Server) connect(conn net.Conn) error { _, err := conn.Write([]byte{0x05, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}) return err } func (s *Server) associate(conn net.Conn, addr *tunnel.Address) error { buf := bytes.NewBuffer([]byte{0x05, 0x00, 0x00}) common.Must(addr.WriteTo(buf)) _, err := conn.Write(buf.Bytes()) return err } func (s *Server) packetDispatchLoop() { for { buf := make([]byte, MaxPacketSize) n, src, err := s.listenPacketConn.ReadFrom(buf) if err != nil { select { case <-s.ctx.Done(): log.Debug("exiting") return default: continue } } log.Debug("socks recv udp packet from", src) s.mappingLock.RLock() conn, found := s.mapping[src.String()] s.mappingLock.RUnlock() if !found { ctx, cancel := context.WithCancel(s.ctx) conn = &PacketConn{ input: make(chan *packetInfo, 128), output: make(chan *packetInfo, 128), ctx: ctx, cancel: cancel, PacketConn: s.listenPacketConn, src: src, } go func(conn *PacketConn) { defer conn.Close() for { select { case info := <-conn.output: buf := bytes.NewBuffer(make([]byte, 0, MaxPacketSize)) buf.Write([]byte{0, 0, 0}) // RSV, FRAG common.Must(info.metadata.Address.WriteTo(buf)) buf.Write(info.payload) _, err := s.listenPacketConn.WriteTo(buf.Bytes(), conn.src) if err != nil { log.Error("socks failed to respond packet to", src) return } log.Debug("socks respond udp packet to", src, "metadata", info.metadata) case <-time.After(time.Second * 5): log.Info("socks udp session timeout, closed") s.mappingLock.Lock() delete(s.mapping, src.String()) s.mappingLock.Unlock() return case <-conn.ctx.Done(): log.Info("socks udp session closed") return } } }(conn) s.mappingLock.Lock() s.mapping[src.String()] = conn s.mappingLock.Unlock() s.packetChan <- conn log.Info("socks new udp session from", src) } r := bytes.NewBuffer(buf[3:n]) address := new(tunnel.Address) if err := address.ReadFrom(r); err != nil { log.Error(common.NewError("socks failed to parse incoming packet").Base(err)) continue } payload := make([]byte, MaxPacketSize) length, _ := r.Read(payload) select { case conn.input <- &packetInfo{ metadata: &tunnel.Metadata{ Address: address, }, payload: payload[:length], }: default: log.Warn("socks udp queue full") } } } func (s *Server) acceptLoop() { for { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { log.Error(common.NewError("socks accept err").Base(err)) return } go func(conn net.Conn) { newConn, err := s.handshake(conn) if err != nil { log.Error(common.NewError("socks failed to handshake with client").Base(err)) return } log.Info("socks connection from", conn.RemoteAddr(), "metadata", newConn.metadata.String()) switch newConn.metadata.Command { case Connect: if err := s.connect(newConn); err != nil { log.Error(common.NewError("socks failed to respond CONNECT").Base(err)) newConn.Close() return } s.connChan <- newConn return case Associate: defer newConn.Close() associateAddr := tunnel.NewAddressFromHostPort("udp", s.localHost, s.localPort) if err := s.associate(newConn, associateAddr); err != nil { log.Error(common.NewError("socks failed to respond to associate request").Base(err)) return } buf := [16]byte{} newConn.Read(buf[:]) log.Debug("socks udp session ends") default: log.Error(common.NewError(fmt.Sprintf("unknown socks command %d", newConn.metadata.Command))) newConn.Close() } }(conn) } } // NewServer create a socks server func NewServer(ctx context.Context, underlay tunnel.Server) (tunnel.Server, error) { cfg := config.FromContext(ctx, Name).(*Config) listenPacketConn, err := underlay.AcceptPacket(&Tunnel{}) if err != nil { return nil, common.NewError("socks failed to listen packet from underlying server") } ctx, cancel := context.WithCancel(ctx) server := &Server{ underlay: underlay, ctx: ctx, cancel: cancel, connChan: make(chan tunnel.Conn, 32), packetChan: make(chan tunnel.PacketConn, 32), localHost: cfg.LocalHost, localPort: cfg.LocalPort, timeout: time.Duration(cfg.UDPTimeout) * time.Second, listenPacketConn: listenPacketConn, mapping: make(map[string]*PacketConn), } go server.acceptLoop() go server.packetDispatchLoop() log.Debug("socks server created") return server, nil } ================================================ FILE: tunnel/socks/socks_test.go ================================================ package socks_test import ( "bytes" "context" "fmt" "io/ioutil" "net" "sync" "testing" "time" "github.com/txthinking/socks5" "golang.org/x/net/proxy" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/adapter" "github.com/p4gefau1t/trojan-go/tunnel/socks" ) func TestSocks(t *testing.T) { port := common.PickPort("tcp", "127.0.0.1") ctx := config.WithConfig(context.Background(), adapter.Name, &adapter.Config{ LocalHost: "127.0.0.1", LocalPort: port, }) ctx = config.WithConfig(ctx, socks.Name, &socks.Config{ LocalHost: "127.0.0.1", LocalPort: port, }) tcpServer, err := adapter.NewServer(ctx, nil) common.Must(err) addr := tunnel.NewAddressFromHostPort("tcp", "127.0.0.1", port) s, err := socks.NewServer(ctx, tcpServer) common.Must(err) socksClient, err := proxy.SOCKS5("tcp", addr.String(), nil, proxy.Direct) common.Must(err) var conn1, conn2 net.Conn wg := sync.WaitGroup{} wg.Add(2) time.Sleep(time.Second * 2) go func() { conn2, err = s.AcceptConn(nil) common.Must(err) wg.Done() }() time.Sleep(time.Second * 1) go func() { conn1, err = socksClient.Dial("tcp", util.EchoAddr) common.Must(err) wg.Done() }() wg.Wait() if !util.CheckConn(conn1, conn2) { t.Fail() } fmt.Println(conn2.(tunnel.Conn).Metadata()) udpConn, err := net.ListenPacket("udp", ":0") common.Must(err) addr = &tunnel.Address{ AddressType: tunnel.DomainName, DomainName: "google.com", Port: 12345, } payload := util.GeneratePayload(1024) buf := bytes.NewBuffer(make([]byte, 0, 4096)) buf.Write([]byte{0, 0, 0}) // RSV, FRAG common.Must(addr.WriteTo(buf)) buf.Write(payload) udpConn.WriteTo(buf.Bytes(), &net.UDPAddr{ IP: net.ParseIP("127.0.0.1"), Port: port, }) packet, err := s.AcceptPacket(nil) common.Must(err) recvBuf := make([]byte, 4096) n, m, err := packet.ReadWithMetadata(recvBuf) common.Must(err) if m.DomainName != "google.com" || m.Port != 12345 || n != 1024 || !(bytes.Equal(recvBuf[:n], payload)) { t.Fail() } payload = util.GeneratePayload(1024) _, err = packet.WriteWithMetadata(payload, &tunnel.Metadata{ Address: &tunnel.Address{ AddressType: tunnel.IPv4, IP: net.ParseIP("123.123.234.234"), Port: 12345, }, }) common.Must(err) _, _, err = udpConn.ReadFrom(recvBuf) common.Must(err) r := bytes.NewReader(recvBuf) header := [3]byte{} r.Read(header[:]) addr = new(tunnel.Address) common.Must(addr.ReadFrom(r)) if addr.IP.String() != "123.123.234.234" || addr.Port != 12345 { t.Fail() } recvBuf, err = ioutil.ReadAll(r) common.Must(err) if bytes.Equal(recvBuf, payload) { t.Fail() } packet.Close() udpConn.Close() c, _ := socks5.NewClient(fmt.Sprintf("127.0.0.1:%d", port), "", "", 0, 0) conn, err := c.Dial("udp", util.EchoAddr) common.Must(err) payload = util.GeneratePayload(4096) recvBuf = make([]byte, 4096) conn.Write(payload) newPacket, err := s.AcceptPacket(nil) common.Must(err) _, m, err = newPacket.ReadWithMetadata(recvBuf) common.Must(err) if m.String() != util.EchoAddr || !bytes.Equal(recvBuf, payload) { t.Fail() } s.Close() } ================================================ FILE: tunnel/socks/tunnel.go ================================================ package socks import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "SOCKS" type Tunnel struct{} func (*Tunnel) Name() string { return Name } func (*Tunnel) NewClient(context.Context, tunnel.Client) (tunnel.Client, error) { panic("not supported") } func (*Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/tls/client.go ================================================ package tls import ( "context" "crypto/tls" "crypto/x509" "encoding/pem" "io" "io/ioutil" "strings" utls "github.com/refraction-networking/utls" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/tls/fingerprint" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) // Client is a tls client type Client struct { verify bool sni string ca *x509.CertPool cipher []uint16 sessionTicket bool reuseSession bool fingerprint string helloID utls.ClientHelloID keyLogger io.WriteCloser underlay tunnel.Client } func (c *Client) Close() error { if c.keyLogger != nil { c.keyLogger.Close() } return c.underlay.Close() } func (c *Client) DialPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } func (c *Client) DialConn(_ *tunnel.Address, overlay tunnel.Tunnel) (tunnel.Conn, error) { conn, err := c.underlay.DialConn(nil, &Tunnel{}) if err != nil { return nil, common.NewError("tls failed to dial conn").Base(err) } if c.fingerprint != "" { // utls fingerprint tlsConn := utls.UClient(conn, &utls.Config{ RootCAs: c.ca, ServerName: c.sni, InsecureSkipVerify: !c.verify, KeyLogWriter: c.keyLogger, }, c.helloID) if err := tlsConn.Handshake(); err != nil { return nil, common.NewError("tls failed to handshake with remote server").Base(err) } return &transport.Conn{ Conn: tlsConn, }, nil } // golang default tls library tlsConn := tls.Client(conn, &tls.Config{ InsecureSkipVerify: !c.verify, ServerName: c.sni, RootCAs: c.ca, KeyLogWriter: c.keyLogger, CipherSuites: c.cipher, SessionTicketsDisabled: !c.sessionTicket, }) err = tlsConn.Handshake() if err != nil { return nil, common.NewError("tls failed to handshake with remote server").Base(err) } return &transport.Conn{ Conn: tlsConn, }, nil } // NewClient creates a tls client func NewClient(ctx context.Context, underlay tunnel.Client) (*Client, error) { cfg := config.FromContext(ctx, Name).(*Config) helloID := utls.ClientHelloID{} if cfg.TLS.Fingerprint != "" { switch cfg.TLS.Fingerprint { case "firefox": helloID = utls.HelloFirefox_Auto case "chrome": helloID = utls.HelloChrome_Auto case "ios": helloID = utls.HelloIOS_Auto default: return nil, common.NewError("invalid fingerprint " + cfg.TLS.Fingerprint) } log.Info("tls fingerprint", cfg.TLS.Fingerprint, "applied") } if cfg.TLS.SNI == "" { cfg.TLS.SNI = cfg.RemoteHost log.Warn("tls sni is unspecified") } client := &Client{ underlay: underlay, verify: cfg.TLS.Verify, sni: cfg.TLS.SNI, cipher: fingerprint.ParseCipher(strings.Split(cfg.TLS.Cipher, ":")), sessionTicket: cfg.TLS.ReuseSession, fingerprint: cfg.TLS.Fingerprint, helloID: helloID, } if cfg.TLS.CertPath != "" { caCertByte, err := ioutil.ReadFile(cfg.TLS.CertPath) if err != nil { return nil, common.NewError("failed to load cert file").Base(err) } client.ca = x509.NewCertPool() ok := client.ca.AppendCertsFromPEM(caCertByte) if !ok { log.Warn("invalid cert list") } log.Info("using custom cert") // print cert info pemCerts := caCertByte for len(pemCerts) > 0 { var block *pem.Block block, pemCerts = pem.Decode(pemCerts) if block == nil { break } if block.Type != "CERTIFICATE" || len(block.Headers) != 0 { continue } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { continue } log.Trace("issuer:", cert.Issuer, "subject:", cert.Subject) } } if cfg.TLS.CertPath == "" { log.Info("cert is unspecified, using default ca list") } log.Debug("tls client created") return client, nil } ================================================ FILE: tunnel/tls/config.go ================================================ package tls import ( "github.com/p4gefau1t/trojan-go/config" ) type Config struct { RemoteHost string `json:"remote_addr" yaml:"remote-addr"` RemotePort int `json:"remote_port" yaml:"remote-port"` TLS TLSConfig `json:"ssl" yaml:"ssl"` Websocket WebsocketConfig `json:"websocket" yaml:"websocket"` } type WebsocketConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } type TLSConfig struct { Verify bool `json:"verify" yaml:"verify"` VerifyHostName bool `json:"verify_hostname" yaml:"verify-hostname"` CertPath string `json:"cert" yaml:"cert"` KeyPath string `json:"key" yaml:"key"` KeyPassword string `json:"key_password" yaml:"key-password"` Cipher string `json:"cipher" yaml:"cipher"` PreferServerCipher bool `json:"prefer_server_cipher" yaml:"prefer-server-cipher"` SNI string `json:"sni" yaml:"sni"` HTTPResponseFileName string `json:"plain_http_response" yaml:"plain-http-response"` FallbackHost string `json:"fallback_addr" yaml:"fallback-addr"` FallbackPort int `json:"fallback_port" yaml:"fallback-port"` ReuseSession bool `json:"reuse_session" yaml:"reuse-session"` ALPN []string `json:"alpn" yaml:"alpn"` Curves string `json:"curves" yaml:"curves"` Fingerprint string `json:"fingerprint" yaml:"fingerprint"` KeyLogPath string `json:"key_log" yaml:"key-log"` CertCheckRate int `json:"cert_check_rate" yaml:"cert-check-rate"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ TLS: TLSConfig{ Verify: true, VerifyHostName: true, Fingerprint: "", ALPN: []string{"http/1.1"}, }, } }) } ================================================ FILE: tunnel/tls/fingerprint/tls.go ================================================ package fingerprint import ( "crypto/tls" "github.com/p4gefau1t/trojan-go/log" ) func ParseCipher(s []string) []uint16 { all := tls.CipherSuites() var result []uint16 for _, p := range s { found := true for _, q := range all { if q.Name == p { result = append(result, q.ID) break } if !found { log.Warn("invalid cipher suite", p, "skipped") } } } return result } ================================================ FILE: tunnel/tls/server.go ================================================ package tls import ( "bufio" "bytes" "context" "crypto/tls" "crypto/x509" "encoding/pem" "io" "io/ioutil" "net" "net/http" "os" "strings" "sync" "sync/atomic" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/redirector" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/tls/fingerprint" "github.com/p4gefau1t/trojan-go/tunnel/transport" "github.com/p4gefau1t/trojan-go/tunnel/websocket" ) // Server is a tls server type Server struct { fallbackAddress *tunnel.Address verifySNI bool sni string alpn []string PreferServerCipher bool keyPair []tls.Certificate keyPairLock sync.RWMutex httpResp []byte cipherSuite []uint16 sessionTicket bool curve []tls.CurveID keyLogger io.WriteCloser connChan chan tunnel.Conn wsChan chan tunnel.Conn redir *redirector.Redirector ctx context.Context cancel context.CancelFunc underlay tunnel.Server nextHTTP int32 portOverrider map[string]int } func (s *Server) Close() error { s.cancel() if s.keyLogger != nil { s.keyLogger.Close() } return s.underlay.Close() } func isDomainNameMatched(pattern string, domainName string) bool { if strings.HasPrefix(pattern, "*.") { suffix := pattern[2:] domainPrefixLen := len(domainName) - len(suffix) - 1 return strings.HasSuffix(domainName, suffix) && domainPrefixLen > 0 && !strings.Contains(domainName[:domainPrefixLen], ".") } return pattern == domainName } func (s *Server) acceptLoop() { for { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { select { case <-s.ctx.Done(): default: log.Fatal(common.NewError("transport accept error" + err.Error())) } return } go func(conn net.Conn) { tlsConfig := &tls.Config{ CipherSuites: s.cipherSuite, PreferServerCipherSuites: s.PreferServerCipher, SessionTicketsDisabled: !s.sessionTicket, NextProtos: s.alpn, KeyLogWriter: s.keyLogger, GetCertificate: func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { s.keyPairLock.RLock() defer s.keyPairLock.RUnlock() sni := s.keyPair[0].Leaf.Subject.CommonName dnsNames := s.keyPair[0].Leaf.DNSNames if s.sni != "" { sni = s.sni } matched := isDomainNameMatched(sni, hello.ServerName) for _, name := range dnsNames { if isDomainNameMatched(name, hello.ServerName) { matched = true break } } if s.verifySNI && !matched { return nil, common.NewError("sni mismatched: " + hello.ServerName + ", expected: " + s.sni) } return &s.keyPair[0], nil }, } // ------------------------ WAR ZONE ---------------------------- handshakeRewindConn := common.NewRewindConn(conn) handshakeRewindConn.SetBufferSize(2048) tlsConn := tls.Server(handshakeRewindConn, tlsConfig) err = tlsConn.Handshake() handshakeRewindConn.StopBuffering() if err != nil { if strings.Contains(err.Error(), "first record does not look like a TLS handshake") { // not a valid tls client hello handshakeRewindConn.Rewind() log.Error(common.NewError("failed to perform tls handshake with " + tlsConn.RemoteAddr().String() + ", redirecting").Base(err)) switch { case s.fallbackAddress != nil: s.redir.Redirect(&redirector.Redirection{ InboundConn: handshakeRewindConn, RedirectTo: s.fallbackAddress, }) case s.httpResp != nil: handshakeRewindConn.Write(s.httpResp) handshakeRewindConn.Close() default: handshakeRewindConn.Close() } } else { // in other cases, simply close it tlsConn.Close() log.Error(common.NewError("tls handshake failed").Base(err)) } return } log.Info("tls connection from", conn.RemoteAddr()) state := tlsConn.ConnectionState() log.Trace("tls handshake", tls.CipherSuiteName(state.CipherSuite), state.DidResume, state.NegotiatedProtocol) // we use a real http header parser to mimic a real http server rewindConn := common.NewRewindConn(tlsConn) rewindConn.SetBufferSize(1024) r := bufio.NewReader(rewindConn) httpReq, err := http.ReadRequest(r) rewindConn.Rewind() rewindConn.StopBuffering() if err != nil { // this is not a http request. pass it to trojan protocol layer for further inspection s.connChan <- &transport.Conn{ Conn: rewindConn, } } else { if atomic.LoadInt32(&s.nextHTTP) != 1 { // there is no websocket layer waiting for connections, redirect it log.Error("incoming http request, but no websocket server is listening") s.redir.Redirect(&redirector.Redirection{ InboundConn: rewindConn, RedirectTo: s.fallbackAddress, }) return } // this is a http request, pass it to websocket protocol layer log.Debug("http req: ", httpReq) s.wsChan <- &transport.Conn{ Conn: rewindConn, } } }(conn) } } func (s *Server) AcceptConn(overlay tunnel.Tunnel) (tunnel.Conn, error) { if _, ok := overlay.(*websocket.Tunnel); ok { atomic.StoreInt32(&s.nextHTTP, 1) log.Debug("next proto http") // websocket overlay select { case conn := <-s.wsChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("transport server closed") } } // trojan overlay select { case conn := <-s.connChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("transport server closed") } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } func (s *Server) checkKeyPairLoop(checkRate time.Duration, keyPath string, certPath string, password string) { var lastKeyBytes, lastCertBytes []byte ticker := time.NewTicker(checkRate) for { log.Debug("checking cert...") keyBytes, err := ioutil.ReadFile(keyPath) if err != nil { log.Error(common.NewError("tls failed to check key").Base(err)) continue } certBytes, err := ioutil.ReadFile(certPath) if err != nil { log.Error(common.NewError("tls failed to check cert").Base(err)) continue } if !bytes.Equal(keyBytes, lastKeyBytes) || !bytes.Equal(lastCertBytes, certBytes) { log.Info("new key pair detected") keyPair, err := loadKeyPair(keyPath, certPath, password) if err != nil { log.Error(common.NewError("tls failed to load new key pair").Base(err)) continue } s.keyPairLock.Lock() s.keyPair = []tls.Certificate{*keyPair} s.keyPairLock.Unlock() lastKeyBytes = keyBytes lastCertBytes = certBytes } select { case <-ticker.C: continue case <-s.ctx.Done(): log.Debug("exiting") ticker.Stop() return } } } func loadKeyPair(keyPath string, certPath string, password string) (*tls.Certificate, error) { if password != "" { keyFile, err := ioutil.ReadFile(keyPath) if err != nil { return nil, common.NewError("failed to load key file").Base(err) } keyBlock, _ := pem.Decode(keyFile) if keyBlock == nil { return nil, common.NewError("failed to decode key file").Base(err) } decryptedKey, err := x509.DecryptPEMBlock(keyBlock, []byte(password)) if err == nil { return nil, common.NewError("failed to decrypt key").Base(err) } certFile, err := ioutil.ReadFile(certPath) certBlock, _ := pem.Decode(certFile) if certBlock == nil { return nil, common.NewError("failed to decode cert file").Base(err) } keyPair, err := tls.X509KeyPair(certBlock.Bytes, decryptedKey) if err != nil { return nil, err } keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { return nil, common.NewError("failed to parse leaf certificate").Base(err) } return &keyPair, nil } keyPair, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { return nil, common.NewError("failed to load key pair").Base(err) } keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0]) if err != nil { return nil, common.NewError("failed to parse leaf certificate").Base(err) } return &keyPair, nil } // NewServer creates a tls layer server func NewServer(ctx context.Context, underlay tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) var fallbackAddress *tunnel.Address var httpResp []byte if cfg.TLS.FallbackPort != 0 { if cfg.TLS.FallbackHost == "" { cfg.TLS.FallbackHost = cfg.RemoteHost log.Warn("empty tls fallback address") } fallbackAddress = tunnel.NewAddressFromHostPort("tcp", cfg.TLS.FallbackHost, cfg.TLS.FallbackPort) fallbackConn, err := net.Dial("tcp", fallbackAddress.String()) if err != nil { return nil, common.NewError("invalid fallback address").Base(err) } fallbackConn.Close() } else { log.Warn("empty tls fallback port") if cfg.TLS.HTTPResponseFileName != "" { httpRespBody, err := ioutil.ReadFile(cfg.TLS.HTTPResponseFileName) if err != nil { return nil, common.NewError("invalid response file").Base(err) } httpResp = httpRespBody } else { log.Warn("empty tls http response") } } keyPair, err := loadKeyPair(cfg.TLS.KeyPath, cfg.TLS.CertPath, cfg.TLS.KeyPassword) if err != nil { return nil, common.NewError("tls failed to load key pair") } var keyLogger io.WriteCloser if cfg.TLS.KeyLogPath != "" { log.Warn("tls key logging activated. USE OF KEY LOGGING COMPROMISES SECURITY. IT SHOULD ONLY BE USED FOR DEBUGGING.") file, err := os.OpenFile(cfg.TLS.KeyLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600) if err != nil { return nil, common.NewError("failed to open key log file").Base(err) } keyLogger = file } var cipherSuite []uint16 if len(cfg.TLS.Cipher) != 0 { cipherSuite = fingerprint.ParseCipher(strings.Split(cfg.TLS.Cipher, ":")) } ctx, cancel := context.WithCancel(ctx) server := &Server{ underlay: underlay, fallbackAddress: fallbackAddress, httpResp: httpResp, verifySNI: cfg.TLS.VerifyHostName, sni: cfg.TLS.SNI, alpn: cfg.TLS.ALPN, PreferServerCipher: cfg.TLS.PreferServerCipher, sessionTicket: cfg.TLS.ReuseSession, connChan: make(chan tunnel.Conn, 32), wsChan: make(chan tunnel.Conn, 32), redir: redirector.NewRedirector(ctx), keyPair: []tls.Certificate{*keyPair}, keyLogger: keyLogger, cipherSuite: cipherSuite, ctx: ctx, cancel: cancel, } go server.acceptLoop() if cfg.TLS.CertCheckRate > 0 { go server.checkKeyPairLoop( time.Second*time.Duration(cfg.TLS.CertCheckRate), cfg.TLS.KeyPath, cfg.TLS.CertPath, cfg.TLS.KeyPassword, ) } log.Debug("tls server created") return server, nil } ================================================ FILE: tunnel/tls/tls_test.go ================================================ package tls import ( "context" "net" "os" "sync" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) var rsa2048Cert = ` -----BEGIN CERTIFICATE----- MIIC5TCCAc2gAwIBAgIJAJqNVe6g/10vMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNV BAMMCWxvY2FsaG9zdDAeFw0yMTA5MTQwNjE1MTFaFw0yNjA5MTMwNjE1MTFaMBQx EjAQBgNVBAMMCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAK7bupJ8tmHM3shQ/7N730jzpRsXdNiBxq/Jxx8j+vB3AcxuP5bjXQZqS6YR 5W5vrfLlegtq1E/mmaI3Ht0RfIlzev04Dua9PWmIQJD801nEPknbfgCLXDh+pYr2 sfg8mUh3LjGtrxyH+nmbTjWg7iWSKohmZ8nUDcX94Llo5FxibMAz8OsAwOmUueCH jP3XswZYHEy+OOP3K0ZEiJy0f5T6ZXk9OWYuPN4VQKJx1qrc9KzZtSPHwqVdkGUi ase9tOPA4aMutzt0btgW7h7UrvG6C1c/Rr1BxdiYq1EQ+yypnAlyToVQSNbo67zz wGQk4GeruIkOgJOLdooN/HjhbHMCAwEAAaM6MDgwFAYDVR0RBA0wC4IJbG9jYWxo b3N0MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0B AQsFAAOCAQEASsBzHHYiWDDiBVWUEwVZAduTrslTLNOxG0QHBKsHWIlz/3QlhQil ywb3OhfMTUR1dMGY5Iq5432QiCHO4IMCOv7tDIkgb4Bc3v/3CRlBlnurtAmUfNJ6 pTRSlK4AjWpGHAEEd/8aCaOE86hMP8WDht8MkJTRrQqpJ1HeDISoKt9nepHOIsj+ I2zLZZtw0pg7FuR4MzWuqOt071iRS46Pupryb3ZEGIWNz5iLrDQod5Iz2ZGSRGqE rB8idX0mlj5AHRRanVR3PAes+eApsW9JvYG/ImuCOs+ZsukY614zQZdR+SyFm85G 4NICyeQsmiypNHHgw+xZmGqZg65bXNGoyg== -----END CERTIFICATE----- ` var rsa2048Key = ` -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCu27qSfLZhzN7I UP+ze99I86UbF3TYgcavyccfI/rwdwHMbj+W410GakumEeVub63y5XoLatRP5pmi Nx7dEXyJc3r9OA7mvT1piECQ/NNZxD5J234Ai1w4fqWK9rH4PJlIdy4xra8ch/p5 m041oO4lkiqIZmfJ1A3F/eC5aORcYmzAM/DrAMDplLngh4z917MGWBxMvjjj9ytG RIictH+U+mV5PTlmLjzeFUCicdaq3PSs2bUjx8KlXZBlImrHvbTjwOGjLrc7dG7Y Fu4e1K7xugtXP0a9QcXYmKtREPssqZwJck6FUEjW6Ou888BkJOBnq7iJDoCTi3aK Dfx44WxzAgMBAAECggEBAKYhib/H0ZhWB4yWuHqUxG4RXtrAjHlvw5Acy5zgmHiC +Sh7ztrTJf0EXN9pvWwRm1ldgXj7hMBtPaaLbD1pccM9/qo66p17Sq/LjlyyeTOe affOHIbz4Sij2zCOdkR9fr0EztTQScF3yBhl4Aa/4cO8fcCeWxm86WEldq9x4xWJ s5WMR4CnrOJhDINLNPQPKX92KyxEQ/RfuBWovx3M0nl3fcUWfESY134t5g/UBFId In19tZ+pGIpCkxP0U1AZWrlZRA8Q/3sO2orUpoAOdCrGk/DcCTMh0c1pMzbYZ1/i cYXn38MpUo8QeG4FElUhAv6kzeBIl2tRBMVzIigo+AECgYEA3No1rHdFu6Ox9vC8 E93PTZevYVcL5J5yx6x7khCaOLKKuRXpjOX/h3Ll+hlN2DVAg5Jli/JVGCco4GeK kbFLSyxG1+E63JbgsVpaEOgvFT3bHHSPSRJDnIU+WkcNQ2u4Ky5ahZzbNdV+4fj2 NO2iMgkm7hoJANrm3IqqW8epenMCgYEAyq+qdNj5DiDzBcDvLwY+4/QmMOOgDqeh /TzhbDRyr+m4xNT7LLS4s/3wcbkQC33zhMUI3YvOHnYq5Ze/iL/TSloj0QCp1I7L J7sZeM1XimMBQIpCfOC7lf4tU76Fz0DTHAL+CmX1DgmRJdYO09843VsKkscC968R 4cwL5oGxxgECgYAM4TTsH/CTJtLEIfn19qOWVNhHhvoMlSkAeBCkzg8Qa2knrh12 uBsU3SCIW11s1H40rh758GICDJaXr7InGP3ZHnXrNRlnr+zeqvRBtCi6xma23B1X F5eV0zd1sFsXqXqOGh/xVtp54z+JEinZoForLNl2XVJVGG8KQZP50kUR/QKBgH4O 8zzpFT0sUPlrHVdp0wODfZ06dPmoWJ9flfPuSsYN3tTMgcs0Owv3C+wu5UPAegxB X1oq8W8Qn21cC8vJQmgj19LNTtLcXI3BV/5B+Aghu02gr+lq/EA1bYuAG0jjUGlD kyx0bQzl9lhJ4b70PjGtxc2z6KyTPdPpTB143FABAoGAQDoIUdc77/IWcjzcaXeJ 8abak5rAZA7cu2g2NVfs+Km+njsB0pbTwMnV1zGoFABdaHLdqbthLWtX7WOb1PDD MQ+kbiLw5uj8IY2HEqJhDGGEdXBqxbW7kyuIAN9Mw+mwKzkikNcFQdxgchWH1d1o lVkr92iEX+IhIeYb4DN1vQw= -----END PRIVATE KEY----- ` var eccCert = ` -----BEGIN CERTIFICATE----- MIICTDCCAfKgAwIBAgIQDtCrO8cNST2eY2tA/AGrsDAKBggqhkjOPQQDAjBeMQsw CQYDVQQGEwJDTjEOMAwGA1UEChMFTXlTU0wxKzApBgNVBAsTIk15U1NMIFRlc3Qg RUNDIC0gRm9yIHRlc3QgdXNlIG9ubHkxEjAQBgNVBAMTCU15U1NMLmNvbTAeFw0y MTA5MTQwNjQ1MzNaFw0yNjA5MTMwNjQ1MzNaMCExCzAJBgNVBAYTAkNOMRIwEAYD VQQDEwlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASvYy/r7XR1 Y39lC2JpRJh582zR2CTNynbuolK9a1jsbXaZv+hpBlHkgzMHsWu7LY9Pnb/Dbp4i 1lRASOddD/rLo4HOMIHLMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEF BQcDAQYIKwYBBQUHAwIwHwYDVR0jBBgwFoAUWxGyVxD0fBhTy3tH4eKznRFXFCYw YwYIKwYBBQUHAQEEVzBVMCEGCCsGAQUFBzABhhVodHRwOi8vb2NzcC5teXNzbC5j b20wMAYIKwYBBQUHMAKGJGh0dHA6Ly9jYS5teXNzbC5jb20vbXlzc2x0ZXN0ZWNj LmNydDAUBgNVHREEDTALgglsb2NhbGhvc3QwCgYIKoZIzj0EAwIDSAAwRQIgDQUa GEdmKstLMHUmmPMGm/P9S4vvSZV2VHsb3+AEyIUCIQCdJpbyTCz+mEyskhwrGOw/ blh3WBONv6MBtqPpmgE1AQ== -----END CERTIFICATE----- ` var eccKey = ` -----BEGIN EC PRIVATE KEY----- MHcCAQEEIB8G2suYKuBLoodNIwRMp3JPN1fcZxCt3kcOYIx4nbcPoAoGCCqGSM49 AwEHoUQDQgAEr2Mv6+10dWN/ZQtiaUSYefNs0dgkzcp27qJSvWtY7G12mb/oaQZR 5IMzB7Fruy2PT52/w26eItZUQEjnXQ/6yw== -----END EC PRIVATE KEY----- ` func TestDefaultTLSRSA2048(t *testing.T) { os.WriteFile("server-rsa2048.crt", []byte(rsa2048Cert), 0o777) os.WriteFile("server-rsa2048.key", []byte(rsa2048Key), 0o777) serverCfg := &Config{ TLS: TLSConfig{ VerifyHostName: true, CertCheckRate: 1, KeyPath: "server-rsa2048.key", CertPath: "server-rsa2048.crt", }, } clientCfg := &Config{ TLS: TLSConfig{ Verify: false, SNI: "localhost", Fingerprint: "", }, } sctx := config.WithConfig(context.Background(), Name, serverCfg) cctx := config.WithConfig(context.Background(), Name, clientCfg) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx := config.WithConfig(context.Background(), transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) common.Must(err) s, err := NewServer(sctx, tcpServer) common.Must(err) c, err := NewClient(cctx, tcpClient) common.Must(err) wg := sync.WaitGroup{} wg.Add(1) var conn1, conn2 net.Conn go func() { conn2, err = s.AcceptConn(nil) common.Must(err) wg.Done() }() conn1, err = c.DialConn(nil, nil) common.Must(err) common.Must2(conn1.Write([]byte("12345678\r\n"))) wg.Wait() buf := [10]byte{} conn2.Read(buf[:]) if !util.CheckConn(conn1, conn2) { t.Fail() } conn1.Close() conn2.Close() } func TestDefaultTLSECC(t *testing.T) { os.WriteFile("server-ecc.crt", []byte(eccCert), 0o777) os.WriteFile("server-ecc.key", []byte(eccKey), 0o777) serverCfg := &Config{ TLS: TLSConfig{ VerifyHostName: true, CertCheckRate: 1, KeyPath: "server-ecc.key", CertPath: "server-ecc.crt", }, } clientCfg := &Config{ TLS: TLSConfig{ Verify: false, SNI: "localhost", Fingerprint: "", }, } sctx := config.WithConfig(context.Background(), Name, serverCfg) cctx := config.WithConfig(context.Background(), Name, clientCfg) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx := config.WithConfig(context.Background(), transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) common.Must(err) s, err := NewServer(sctx, tcpServer) common.Must(err) c, err := NewClient(cctx, tcpClient) common.Must(err) wg := sync.WaitGroup{} wg.Add(1) var conn1, conn2 net.Conn go func() { conn2, err = s.AcceptConn(nil) common.Must(err) wg.Done() }() conn1, err = c.DialConn(nil, nil) common.Must(err) common.Must2(conn1.Write([]byte("12345678\r\n"))) wg.Wait() buf := [10]byte{} conn2.Read(buf[:]) if !util.CheckConn(conn1, conn2) { t.Fail() } conn1.Close() conn2.Close() } func TestUTLSRSA2048(t *testing.T) { os.WriteFile("server-rsa2048.crt", []byte(rsa2048Cert), 0o777) os.WriteFile("server-rsa2048.key", []byte(rsa2048Key), 0o777) fingerprints := []string{ "chrome", "firefox", "ios", } for _, s := range fingerprints { serverCfg := &Config{ TLS: TLSConfig{ CertCheckRate: 1, KeyPath: "server-rsa2048.key", CertPath: "server-rsa2048.crt", }, } clientCfg := &Config{ TLS: TLSConfig{ Verify: false, SNI: "localhost", Fingerprint: s, }, } sctx := config.WithConfig(context.Background(), Name, serverCfg) cctx := config.WithConfig(context.Background(), Name, clientCfg) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx := config.WithConfig(context.Background(), transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) s, err := NewServer(sctx, tcpServer) common.Must(err) c, err := NewClient(cctx, tcpClient) common.Must(err) wg := sync.WaitGroup{} wg.Add(1) var conn1, conn2 net.Conn go func() { conn2, err = s.AcceptConn(nil) common.Must(err) wg.Done() }() conn1, err = c.DialConn(nil, nil) common.Must(err) common.Must2(conn1.Write([]byte("12345678\r\n"))) wg.Wait() buf := [10]byte{} conn2.Read(buf[:]) if !util.CheckConn(conn1, conn2) { t.Fail() } conn1.Close() conn2.Close() s.Close() c.Close() } } func TestUTLSECC(t *testing.T) { os.WriteFile("server-ecc.crt", []byte(eccCert), 0o777) os.WriteFile("server-ecc.key", []byte(eccKey), 0o777) fingerprints := []string{ "chrome", "firefox", "ios", } for _, s := range fingerprints { serverCfg := &Config{ TLS: TLSConfig{ CertCheckRate: 1, KeyPath: "server-ecc.key", CertPath: "server-ecc.crt", }, } clientCfg := &Config{ TLS: TLSConfig{ Verify: false, SNI: "localhost", Fingerprint: s, }, } sctx := config.WithConfig(context.Background(), Name, serverCfg) cctx := config.WithConfig(context.Background(), Name, clientCfg) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx := config.WithConfig(context.Background(), transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) s, err := NewServer(sctx, tcpServer) common.Must(err) c, err := NewClient(cctx, tcpClient) common.Must(err) wg := sync.WaitGroup{} wg.Add(1) var conn1, conn2 net.Conn go func() { conn2, err = s.AcceptConn(nil) common.Must(err) wg.Done() }() conn1, err = c.DialConn(nil, nil) common.Must(err) common.Must2(conn1.Write([]byte("12345678\r\n"))) wg.Wait() buf := [10]byte{} conn2.Read(buf[:]) if !util.CheckConn(conn1, conn2) { t.Fail() } conn1.Close() conn2.Close() s.Close() c.Close() } } func TestMatch(t *testing.T) { if !isDomainNameMatched("*.google.com", "www.google.com") { t.Fail() } if isDomainNameMatched("*.google.com", "google.com") { t.Fail() } if !isDomainNameMatched("localhost", "localhost") { t.Fail() } } ================================================ FILE: tunnel/tls/tunnel.go ================================================ package tls import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "TLS" type Tunnel struct{} func (t *Tunnel) Name() string { return Name } func (t *Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, client) } func (t *Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/tproxy/config.go ================================================ //go:build linux // +build linux package tproxy import "github.com/p4gefau1t/trojan-go/config" type Config struct { LocalHost string `json:"local_addr" yaml:"local-addr"` LocalPort int `json:"local_port" yaml:"local-port"` UDPTimeout int `json:"udp_timeout" yaml:"udp-timeout"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{ UDPTimeout: 60, } }) } ================================================ FILE: tunnel/tproxy/conn.go ================================================ //go:build linux // +build linux package tproxy import ( "context" "net" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/tunnel" ) type Conn struct { net.Conn metadata *tunnel.Metadata } func (c *Conn) Metadata() *tunnel.Metadata { return c.metadata } type packetInfo struct { metadata *tunnel.Metadata payload []byte } type PacketConn struct { net.PacketConn input chan *packetInfo output chan *packetInfo src net.Addr ctx context.Context cancel context.CancelFunc } func (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { panic("implement me") } func (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { panic("implement me") } func (c *PacketConn) Close() error { c.cancel() return nil } func (c *PacketConn) WriteWithMetadata(p []byte, m *tunnel.Metadata) (int, error) { select { case c.output <- &packetInfo{ metadata: m, payload: p, }: return len(p), nil case <-c.ctx.Done(): return 0, common.NewError("socks packet conn closed") } } func (c *PacketConn) ReadWithMetadata(p []byte) (int, *tunnel.Metadata, error) { select { case info := <-c.input: n := copy(p, info.payload) return n, info.metadata, nil case <-c.ctx.Done(): return 0, nil, common.NewError("socks packet conn closed") } } ================================================ FILE: tunnel/tproxy/getsockopt.go ================================================ //go:build linux && !386 // +build linux,!386 package tproxy import ( "syscall" "unsafe" ) func getsockopt(fd int, level int, optname int, optval unsafe.Pointer, optlen *uint32) (err error) { _, _, e := syscall.Syscall6( syscall.SYS_GETSOCKOPT, uintptr(fd), uintptr(level), uintptr(optname), uintptr(optval), uintptr(unsafe.Pointer(optlen)), 0) if e != 0 { return e } return } ================================================ FILE: tunnel/tproxy/getsockopt_i386.go ================================================ //go:build linux && 386 // +build linux,386 package tproxy import ( "syscall" "unsafe" ) const GETSOCKOPT = 15 func getsockopt(fd int, level int, optname int, optval unsafe.Pointer, optlen *uint32) (err error) { _, _, e := syscall.Syscall6( GETSOCKOPT, uintptr(fd), uintptr(level), uintptr(optname), uintptr(optval), uintptr(unsafe.Pointer(optlen)), 0) if e != 0 { return e } return } ================================================ FILE: tunnel/tproxy/server.go ================================================ //go:build linux // +build linux package tproxy import ( "context" "io" "net" "sync" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) const MaxPacketSize = 1024 * 8 type Server struct { tcpListener net.Listener udpListener *net.UDPConn packetChan chan tunnel.PacketConn timeout time.Duration mappingLock sync.RWMutex mapping map[string]*PacketConn ctx context.Context cancel context.CancelFunc } func (s *Server) Close() error { s.cancel() s.tcpListener.Close() return s.udpListener.Close() } func (s *Server) AcceptConn(tunnel.Tunnel) (tunnel.Conn, error) { conn, err := s.tcpListener.Accept() if err != nil { select { case <-s.ctx.Done(): default: log.Fatal(common.NewError("tproxy failed to accept connection").Base(err)) } return nil, common.NewError("tproxy failed to accept conn") } dst, err := getOriginalTCPDest(conn.(*net.TCPConn)) if err != nil { return nil, common.NewError("tproxy failed to obtain original address of tcp socket").Base(err) } address, err := tunnel.NewAddressFromAddr("tcp", dst.String()) common.Must(err) log.Info("tproxy connection from", conn.RemoteAddr().String(), "metadata", dst.String()) return &Conn{ metadata: &tunnel.Metadata{ Address: address, }, Conn: conn, }, nil } func (s *Server) packetDispatchLoop() { type tproxyPacketInfo struct { src *net.UDPAddr dst *net.UDPAddr payload []byte } packetQueue := make(chan *tproxyPacketInfo, 1024) go func() { for { buf := make([]byte, MaxPacketSize) n, src, dst, err := ReadFromUDP(s.udpListener, buf) if err != nil { select { case <-s.ctx.Done(): default: log.Fatal(common.NewError("tproxy failed to read from udp socket").Base(err)) } s.Close() return } log.Debug("udp packet from", src, "metadata", dst, "size", n) packetQueue <- &tproxyPacketInfo{ src: src, dst: dst, payload: buf[:n], } } }() for { var info *tproxyPacketInfo select { case info = <-packetQueue: case <-s.ctx.Done(): log.Debug("exiting") return } s.mappingLock.RLock() conn, found := s.mapping[info.src.String()] s.mappingLock.RUnlock() if !found { ctx, cancel := context.WithCancel(s.ctx) conn = &PacketConn{ input: make(chan *packetInfo, 128), output: make(chan *packetInfo, 128), PacketConn: s.udpListener, ctx: ctx, cancel: cancel, src: info.src, } s.mappingLock.Lock() s.mapping[info.src.String()] = conn s.mappingLock.Unlock() log.Info("new tproxy udp session from", info.src.String(), "metadata", info.dst.String()) s.packetChan <- conn go func(conn *PacketConn) { defer conn.Close() log.Debug("udp packet daemon for", conn.src.String()) for { select { case info := <-conn.output: if info.metadata.AddressType != tunnel.IPv4 && info.metadata.AddressType != tunnel.IPv6 { log.Error("tproxy invalid response metadata address", info.metadata) continue } back, err := DialUDP( "udp", &net.UDPAddr{ IP: info.metadata.IP, Port: info.metadata.Port, }, conn.src.(*net.UDPAddr), ) if err != nil { log.Error(common.NewError("failed to dial tproxy udp").Base(err)) return } n, err := back.Write(info.payload) if err != nil { log.Error(common.NewError("tproxy udp write error").Base(err)) return } log.Debug("recv packet, send back to", conn.src, "payload", len(info.payload), "sent", n) back.Close() case <-s.ctx.Done(): log.Debug("exiting") return case <-time.After(s.timeout): s.mappingLock.Lock() delete(s.mapping, conn.src.String()) s.mappingLock.Unlock() log.Debug("packet session ", conn.src.String(), "timeout") return } } }(conn) } newInfo := &packetInfo{ metadata: &tunnel.Metadata{ Address: tunnel.NewAddressFromHostPort("udp", info.dst.IP.String(), info.dst.Port), }, payload: info.payload, } select { case conn.input <- newInfo: log.Debug("tproxy packet sent with metadata", newInfo.metadata, "size", len(info.payload)) default: // if we got too many packets, simply drop it log.Warn("tproxy udp relay queue full!") } } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { select { case conn := <-s.packetChan: log.Info("tproxy packet conn accepted") return conn, nil case <-s.ctx.Done(): return nil, io.EOF } } func NewServer(ctx context.Context, _ tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) ctx, cancel := context.WithCancel(ctx) listenAddr := tunnel.NewAddressFromHostPort("tcp", cfg.LocalHost, cfg.LocalPort) ip, err := listenAddr.ResolveIP() if err != nil { cancel() return nil, common.NewError("invalid tproxy local address").Base(err) } tcpListener, err := ListenTCP("tcp", &net.TCPAddr{ IP: ip, Port: cfg.LocalPort, }) if err != nil { cancel() return nil, common.NewError("tproxy failed to listen tcp").Base(err) } udpListener, err := ListenUDP("udp", &net.UDPAddr{ IP: ip, Port: cfg.LocalPort, }) if err != nil { cancel() return nil, common.NewError("tproxy failed to listen udp").Base(err) } server := &Server{ tcpListener: tcpListener, udpListener: udpListener, ctx: ctx, cancel: cancel, timeout: time.Duration(cfg.UDPTimeout) * time.Second, mapping: make(map[string]*PacketConn), packetChan: make(chan tunnel.PacketConn, 32), } go server.packetDispatchLoop() log.Info("tproxy server listening on", tcpListener.Addr(), "(tcp)", udpListener.LocalAddr(), "(udp)") log.Debug("tproxy server created") return server, nil } ================================================ FILE: tunnel/tproxy/tcp.go ================================================ //go:build linux // +build linux package tproxy import ( "fmt" "net" "os" "syscall" "unsafe" ) // Listener describes a TCP Listener // with the Linux IP_TRANSPARENT option defined // on the listening socket type Listener struct { base net.Listener } // Accept waits for and returns // the next connection to the listener. // // This command wraps the AcceptTProxy // method of the Listener func (listener *Listener) Accept() (net.Conn, error) { tcpConn, err := listener.base.(*net.TCPListener).AcceptTCP() if err != nil { return nil, err } return tcpConn, nil } // Addr returns the network address // the listener is accepting connections // from func (listener *Listener) Addr() net.Addr { return listener.base.Addr() } // Close will close the listener from accepting // any more connections. Any blocked connections // will unblock and close func (listener *Listener) Close() error { return listener.base.Close() } // ListenTCP will construct a new TCP listener // socket with the Linux IP_TRANSPARENT option // set on the underlying socket func ListenTCP(network string, laddr *net.TCPAddr) (net.Listener, error) { listener, err := net.ListenTCP(network, laddr) if err != nil { return nil, err } fileDescriptorSource, err := listener.File() if err != nil { return nil, &net.OpError{Op: "listen", Net: network, Source: nil, Addr: laddr, Err: fmt.Errorf("get file descriptor: %s", err)} } defer fileDescriptorSource.Close() if err = syscall.SetsockoptInt(int(fileDescriptorSource.Fd()), syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { return nil, &net.OpError{Op: "listen", Net: network, Source: nil, Addr: laddr, Err: fmt.Errorf("set socket option: IP_TRANSPARENT: %s", err)} } return &Listener{listener}, nil } const ( IP6T_SO_ORIGINAL_DST = 80 SO_ORIGINAL_DST = 80 ) // getOriginalTCPDest retrieves the original destination address from // NATed connection. Currently, only Linux iptables using DNAT/REDIRECT // is supported. For other operating systems, this will just return // conn.LocalAddr(). // // Note that this function only works when nf_conntrack_ipv4 and/or // nf_conntrack_ipv6 is loaded in the kernel. func getOriginalTCPDest(conn *net.TCPConn) (*net.TCPAddr, error) { f, err := conn.File() if err != nil { return nil, err } defer f.Close() fd := int(f.Fd()) // revert to non-blocking mode. // see http://stackoverflow.com/a/28968431/1493661 if err = syscall.SetNonblock(fd, true); err != nil { return nil, os.NewSyscallError("setnonblock", err) } v6 := conn.LocalAddr().(*net.TCPAddr).IP.To4() == nil if v6 { var addr syscall.RawSockaddrInet6 var len uint32 len = uint32(unsafe.Sizeof(addr)) err = getsockopt(fd, syscall.IPPROTO_IPV6, IP6T_SO_ORIGINAL_DST, unsafe.Pointer(&addr), &len) if err != nil { return nil, os.NewSyscallError("getsockopt", err) } ip := make([]byte, 16) for i, b := range addr.Addr { ip[i] = b } pb := *(*[2]byte)(unsafe.Pointer(&addr.Port)) return &net.TCPAddr{ IP: ip, Port: int(pb[0])*256 + int(pb[1]), }, nil } // IPv4 var addr syscall.RawSockaddrInet4 var len uint32 len = uint32(unsafe.Sizeof(addr)) err = getsockopt(fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST, unsafe.Pointer(&addr), &len) if err != nil { return nil, os.NewSyscallError("getsockopt", err) } ip := make([]byte, 4) for i, b := range addr.Addr { ip[i] = b } pb := *(*[2]byte)(unsafe.Pointer(&addr.Port)) return &net.TCPAddr{ IP: ip, Port: int(pb[0])*256 + int(pb[1]), }, nil } ================================================ FILE: tunnel/tproxy/tproxy_stub.go ================================================ package tproxy ================================================ FILE: tunnel/tproxy/tunnel.go ================================================ //go:build linux // +build linux package tproxy import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "TPROXY" type Tunnel struct{} func (t *Tunnel) Name() string { return Name } func (t *Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { panic("not supported") } func (t *Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/tproxy/udp.go ================================================ //go:build linux // +build linux package tproxy import ( "bytes" "encoding/binary" "fmt" "net" "os" "strconv" "syscall" "unsafe" ) // ListenUDP will construct a new UDP listener // socket with the Linux IP_TRANSPARENT option // set on the underlying socket func ListenUDP(network string, laddr *net.UDPAddr) (*net.UDPConn, error) { listener, err := net.ListenUDP(network, laddr) if err != nil { return nil, err } fileDescriptorSource, err := listener.File() if err != nil { return nil, &net.OpError{Op: "listen", Net: network, Source: nil, Addr: laddr, Err: fmt.Errorf("get file descriptor: %s", err)} } defer fileDescriptorSource.Close() fileDescriptor := int(fileDescriptorSource.Fd()) if err = syscall.SetsockoptInt(fileDescriptor, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { return nil, &net.OpError{Op: "listen", Net: network, Source: nil, Addr: laddr, Err: fmt.Errorf("set socket option: IP_TRANSPARENT: %s", err)} } if err = syscall.SetsockoptInt(fileDescriptor, syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1); err != nil { return nil, &net.OpError{Op: "listen", Net: network, Source: nil, Addr: laddr, Err: fmt.Errorf("set socket option: IP_RECVORIGDSTADDR: %s", err)} } return listener, nil } // 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.Level == syscall.SOL_IPV6) && 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 } // DialUDP connects to the remote address raddr on the network net, // which must be "udp", "udp4", or "udp6". If laddr is not nil, it is // used as the local address for the connection. func DialUDP(network string, laddr *net.UDPAddr, raddr *net.UDPAddr) (*net.UDPConn, error) { remoteSocketAddress, err := udpAddrToSocketAddr(raddr) if err != nil { return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("build destination socket address: %s", err)} } localSocketAddress, err := udpAddrToSocketAddr(laddr) if err != nil { return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("build local socket address: %s", err)} } fileDescriptor, err := syscall.Socket(udpAddrFamily(network, laddr, raddr), syscall.SOCK_DGRAM, 0) if err != nil { return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("socket open: %s", err)} } if err = syscall.SetsockoptInt(fileDescriptor, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil { syscall.Close(fileDescriptor) return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("set socket option: SO_REUSEADDR: %s", err)} } if err = syscall.SetsockoptInt(fileDescriptor, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { syscall.Close(fileDescriptor) return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("set socket option: IP_TRANSPARENT: %s", err)} } if err = syscall.Bind(fileDescriptor, localSocketAddress); err != nil { syscall.Close(fileDescriptor) return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("socket bind: %s", err)} } if err = syscall.Connect(fileDescriptor, remoteSocketAddress); err != nil { syscall.Close(fileDescriptor) return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("socket connect: %s", err)} } fdFile := os.NewFile(uintptr(fileDescriptor), fmt.Sprintf("net-udp-dial-%s", raddr.String())) defer fdFile.Close() remoteConn, err := net.FileConn(fdFile) if err != nil { syscall.Close(fileDescriptor) return nil, &net.OpError{Op: "dial", Err: fmt.Errorf("convert file descriptor to connection: %s", err)} } return remoteConn.(*net.UDPConn), nil } // udpAddToSockerAddr will convert a UDPAddr // into a Sockaddr that may be used when // connecting and binding sockets func udpAddrToSocketAddr(addr *net.UDPAddr) (syscall.Sockaddr, error) { switch { case addr.IP.To4() != nil: ip := [4]byte{} copy(ip[:], addr.IP.To4()) return &syscall.SockaddrInet4{Addr: ip, Port: addr.Port}, nil default: ip := [16]byte{} copy(ip[:], addr.IP.To16()) zoneID, err := strconv.ParseUint(addr.Zone, 10, 32) if err != nil { return nil, err } return &syscall.SockaddrInet6{Addr: ip, Port: addr.Port, ZoneId: uint32(zoneID)}, nil } } // udpAddrFamily will attempt to work // out the address family based on the // network and UDP addresses func udpAddrFamily(net string, laddr, raddr *net.UDPAddr) int { switch net[len(net)-1] { case '4': return syscall.AF_INET case '6': return syscall.AF_INET6 } if (laddr == nil || laddr.IP.To4() != nil) && (raddr == nil || laddr.IP.To4() != nil) { return syscall.AF_INET } return syscall.AF_INET6 } ================================================ FILE: tunnel/transport/client.go ================================================ package transport import ( "context" "os" "os/exec" "strconv" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/freedom" ) // Client implements tunnel.Client type Client struct { serverAddress *tunnel.Address cmd *exec.Cmd ctx context.Context cancel context.CancelFunc direct *freedom.Client } func (c *Client) Close() error { c.cancel() if c.cmd != nil && c.cmd.Process != nil { c.cmd.Process.Kill() } return nil } func (c *Client) DialPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } // DialConn implements tunnel.Client. It will ignore the params and directly dial to the remote server func (c *Client) DialConn(*tunnel.Address, tunnel.Tunnel) (tunnel.Conn, error) { conn, err := c.direct.DialConn(c.serverAddress, nil) if err != nil { return nil, common.NewError("transport failed to connect to remote server").Base(err) } return &Conn{ Conn: conn, }, nil } // NewClient creates a transport layer client func NewClient(ctx context.Context, _ tunnel.Client) (*Client, error) { cfg := config.FromContext(ctx, Name).(*Config) var cmd *exec.Cmd serverAddress := tunnel.NewAddressFromHostPort("tcp", cfg.RemoteHost, cfg.RemotePort) if cfg.TransportPlugin.Enabled { log.Warn("trojan-go will use transport plugin and work in plain text mode") switch cfg.TransportPlugin.Type { case "shadowsocks": pluginHost := "127.0.0.1" pluginPort := common.PickPort("tcp", pluginHost) cfg.TransportPlugin.Env = append( cfg.TransportPlugin.Env, "SS_LOCAL_HOST="+pluginHost, "SS_LOCAL_PORT="+strconv.FormatInt(int64(pluginPort), 10), "SS_REMOTE_HOST="+cfg.RemoteHost, "SS_REMOTE_PORT="+strconv.FormatInt(int64(cfg.RemotePort), 10), "SS_PLUGIN_OPTIONS="+cfg.TransportPlugin.Option, ) cfg.RemoteHost = pluginHost cfg.RemotePort = pluginPort serverAddress = tunnel.NewAddressFromHostPort("tcp", cfg.RemoteHost, cfg.RemotePort) log.Debug("plugin address", serverAddress.String()) log.Debug("plugin env", cfg.TransportPlugin.Env) cmd = exec.Command(cfg.TransportPlugin.Command, cfg.TransportPlugin.Arg...) cmd.Env = append(cmd.Env, cfg.TransportPlugin.Env...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout cmd.Start() case "other": cmd = exec.Command(cfg.TransportPlugin.Command, cfg.TransportPlugin.Arg...) cmd.Env = append(cmd.Env, cfg.TransportPlugin.Env...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout cmd.Start() case "plaintext": // do nothing default: return nil, common.NewError("invalid plugin type: " + cfg.TransportPlugin.Type) } } direct, err := freedom.NewClient(ctx, nil) common.Must(err) ctx, cancel := context.WithCancel(ctx) client := &Client{ serverAddress: serverAddress, cmd: cmd, ctx: ctx, cancel: cancel, direct: direct, } return client, nil } ================================================ FILE: tunnel/transport/config.go ================================================ package transport import ( "github.com/p4gefau1t/trojan-go/config" ) type Config struct { LocalHost string `json:"local_addr" yaml:"local-addr"` LocalPort int `json:"local_port" yaml:"local-port"` RemoteHost string `json:"remote_addr" yaml:"remote-addr"` RemotePort int `json:"remote_port" yaml:"remote-port"` TransportPlugin TransportPluginConfig `json:"transport_plugin" yaml:"transport-plugin"` } type TransportPluginConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` Type string `json:"type" yaml:"type"` Command string `json:"command" yaml:"command"` Option string `json:"option" yaml:"option"` Arg []string `json:"arg" yaml:"arg"` Env []string `json:"env" yaml:"env"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(Config) }) } ================================================ FILE: tunnel/transport/conn.go ================================================ package transport import ( "net" "github.com/p4gefau1t/trojan-go/tunnel" ) type Conn struct { net.Conn } func (c *Conn) Metadata() *tunnel.Metadata { return nil } ================================================ FILE: tunnel/transport/server.go ================================================ package transport import ( "bufio" "context" "net" "net/http" "os" "os/exec" "strconv" "sync" "time" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) // Server is a server of transport layer type Server struct { tcpListener net.Listener cmd *exec.Cmd connChan chan tunnel.Conn wsChan chan tunnel.Conn httpLock sync.RWMutex nextHTTP bool ctx context.Context cancel context.CancelFunc } func (s *Server) Close() error { s.cancel() if s.cmd != nil && s.cmd.Process != nil { s.cmd.Process.Kill() } return s.tcpListener.Close() } func (s *Server) acceptLoop() { for { tcpConn, err := s.tcpListener.Accept() if err != nil { select { case <-s.ctx.Done(): default: log.Error(common.NewError("transport accept error").Base(err)) time.Sleep(time.Millisecond * 100) } return } go func(tcpConn net.Conn) { log.Info("tcp connection from", tcpConn.RemoteAddr()) s.httpLock.RLock() if s.nextHTTP { // plaintext mode enabled s.httpLock.RUnlock() // we use real http header parser to mimic a real http server rewindConn := common.NewRewindConn(tcpConn) rewindConn.SetBufferSize(512) defer rewindConn.StopBuffering() r := bufio.NewReader(rewindConn) httpReq, err := http.ReadRequest(r) rewindConn.Rewind() rewindConn.StopBuffering() if err != nil { // this is not a http request, pass it to trojan protocol layer for further inspection s.connChan <- &Conn{ Conn: rewindConn, } } else { // this is a http request, pass it to websocket protocol layer log.Debug("plaintext http request: ", httpReq) s.wsChan <- &Conn{ Conn: rewindConn, } } } else { s.httpLock.RUnlock() s.connChan <- &Conn{ Conn: tcpConn, } } }(tcpConn) } } func (s *Server) AcceptConn(overlay tunnel.Tunnel) (tunnel.Conn, error) { // TODO fix import cycle if overlay != nil && (overlay.Name() == "WEBSOCKET" || overlay.Name() == "HTTP") { s.httpLock.Lock() s.nextHTTP = true s.httpLock.Unlock() select { case conn := <-s.wsChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("transport server closed") } } select { case conn := <-s.connChan: return conn, nil case <-s.ctx.Done(): return nil, common.NewError("transport server closed") } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { panic("not supported") } // NewServer creates a transport layer server func NewServer(ctx context.Context, _ tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) listenAddress := tunnel.NewAddressFromHostPort("tcp", cfg.LocalHost, cfg.LocalPort) var cmd *exec.Cmd if cfg.TransportPlugin.Enabled { log.Warn("transport server will use plugin and work in plain text mode") switch cfg.TransportPlugin.Type { case "shadowsocks": trojanHost := "127.0.0.1" trojanPort := common.PickPort("tcp", trojanHost) cfg.TransportPlugin.Env = append( cfg.TransportPlugin.Env, "SS_REMOTE_HOST="+cfg.LocalHost, "SS_REMOTE_PORT="+strconv.FormatInt(int64(cfg.LocalPort), 10), "SS_LOCAL_HOST="+trojanHost, "SS_LOCAL_PORT="+strconv.FormatInt(int64(trojanPort), 10), "SS_PLUGIN_OPTIONS="+cfg.TransportPlugin.Option, ) cfg.LocalHost = trojanHost cfg.LocalPort = trojanPort listenAddress = tunnel.NewAddressFromHostPort("tcp", cfg.LocalHost, cfg.LocalPort) log.Debug("new listen address", listenAddress) log.Debug("plugin env", cfg.TransportPlugin.Env) cmd = exec.Command(cfg.TransportPlugin.Command, cfg.TransportPlugin.Arg...) cmd.Env = append(cmd.Env, cfg.TransportPlugin.Env...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout cmd.Start() case "other": cmd = exec.Command(cfg.TransportPlugin.Command, cfg.TransportPlugin.Arg...) cmd.Env = append(cmd.Env, cfg.TransportPlugin.Env...) cmd.Stdout = os.Stdout cmd.Stderr = os.Stdout cmd.Start() case "plaintext": // do nothing default: return nil, common.NewError("invalid plugin type: " + cfg.TransportPlugin.Type) } } tcpListener, err := net.Listen("tcp", listenAddress.String()) if err != nil { return nil, err } ctx, cancel := context.WithCancel(ctx) server := &Server{ tcpListener: tcpListener, cmd: cmd, ctx: ctx, cancel: cancel, connChan: make(chan tunnel.Conn, 32), wsChan: make(chan tunnel.Conn, 32), } go server.acceptLoop() return server, nil } ================================================ FILE: tunnel/transport/transport_test.go ================================================ package transport import ( "context" "net" "sync" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel/freedom" ) func TestTransport(t *testing.T) { serverCfg := &Config{ LocalHost: "127.0.0.1", LocalPort: common.PickPort("tcp", "127.0.0.1"), RemoteHost: "127.0.0.1", RemotePort: common.PickPort("tcp", "127.0.0.1"), } clientCfg := &Config{ LocalHost: "127.0.0.1", LocalPort: common.PickPort("tcp", "127.0.0.1"), RemoteHost: "127.0.0.1", RemotePort: serverCfg.LocalPort, } freedomCfg := &freedom.Config{} sctx := config.WithConfig(context.Background(), Name, serverCfg) cctx := config.WithConfig(context.Background(), Name, clientCfg) cctx = config.WithConfig(cctx, freedom.Name, freedomCfg) s, err := NewServer(sctx, nil) common.Must(err) c, err := NewClient(cctx, nil) common.Must(err) wg := sync.WaitGroup{} wg.Add(1) var conn1, conn2 net.Conn go func() { conn2, err = s.AcceptConn(nil) common.Must(err) wg.Done() }() conn1, err = c.DialConn(nil, nil) common.Must(err) common.Must2(conn1.Write([]byte("12345678\r\n"))) wg.Wait() buf := [10]byte{} conn2.Read(buf[:]) if !util.CheckConn(conn1, conn2) { t.Fail() } s.Close() c.Close() } func TestClientPlugin(t *testing.T) { clientCfg := &Config{ LocalHost: "127.0.0.1", LocalPort: common.PickPort("tcp", "127.0.0.1"), RemoteHost: "127.0.0.1", RemotePort: 12345, TransportPlugin: TransportPluginConfig{ Enabled: true, Type: "shadowsocks", Command: "echo $SS_REMOTE_PORT", Option: "", Arg: nil, Env: nil, }, } ctx := config.WithConfig(context.Background(), Name, clientCfg) freedomCfg := &freedom.Config{} ctx = config.WithConfig(ctx, freedom.Name, freedomCfg) c, err := NewClient(ctx, nil) common.Must(err) c.Close() } func TestServerPlugin(t *testing.T) { cfg := &Config{ LocalHost: "127.0.0.1", LocalPort: common.PickPort("tcp", "127.0.0.1"), RemoteHost: "127.0.0.1", RemotePort: 12345, TransportPlugin: TransportPluginConfig{ Enabled: true, Type: "shadowsocks", Command: "echo $SS_REMOTE_PORT", Option: "", Arg: nil, Env: nil, }, } ctx := config.WithConfig(context.Background(), Name, cfg) freedomCfg := &freedom.Config{} ctx = config.WithConfig(ctx, freedom.Name, freedomCfg) s, err := NewServer(ctx, nil) common.Must(err) s.Close() } ================================================ FILE: tunnel/transport/tunnel.go ================================================ package transport import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "TRANSPORT" type Tunnel struct{} func (*Tunnel) Name() string { return Name } func (*Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, client) } func (*Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/trojan/client.go ================================================ package trojan import ( "bytes" "context" "net" "sync" "sync/atomic" "time" "github.com/p4gefau1t/trojan-go/api" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/statistic" "github.com/p4gefau1t/trojan-go/statistic/memory" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/mux" ) const ( MaxPacketSize = 1024 * 8 ) const ( Connect tunnel.Command = 1 Associate tunnel.Command = 3 Mux tunnel.Command = 0x7f ) type OutboundConn struct { // WARNING: do not change the order of these fields. // 64-bit fields that use `sync/atomic` package functions // must be 64-bit aligned on 32-bit systems. // Reference: https://github.com/golang/go/issues/599 // Solution: https://github.com/golang/go/issues/11891#issuecomment-433623786 sent uint64 recv uint64 metadata *tunnel.Metadata user statistic.User headerWrittenOnce sync.Once net.Conn } func (c *OutboundConn) Metadata() *tunnel.Metadata { return c.metadata } func (c *OutboundConn) WriteHeader(payload []byte) (bool, error) { var err error written := false c.headerWrittenOnce.Do(func() { hash := c.user.Hash() buf := bytes.NewBuffer(make([]byte, 0, MaxPacketSize)) crlf := []byte{0x0d, 0x0a} buf.Write([]byte(hash)) buf.Write(crlf) c.metadata.WriteTo(buf) buf.Write(crlf) if payload != nil { buf.Write(payload) } _, err = c.Conn.Write(buf.Bytes()) if err == nil { written = true } }) return written, err } func (c *OutboundConn) Write(p []byte) (int, error) { written, err := c.WriteHeader(p) if err != nil { return 0, common.NewError("trojan failed to flush header with payload").Base(err) } if written { return len(p), nil } n, err := c.Conn.Write(p) c.user.AddTraffic(n, 0) atomic.AddUint64(&c.sent, uint64(n)) return n, err } func (c *OutboundConn) Read(p []byte) (int, error) { n, err := c.Conn.Read(p) c.user.AddTraffic(0, n) atomic.AddUint64(&c.recv, uint64(n)) return n, err } func (c *OutboundConn) Close() error { log.Info("connection to", c.metadata, "closed", "sent:", common.HumanFriendlyTraffic(atomic.LoadUint64(&c.sent)), "recv:", common.HumanFriendlyTraffic(atomic.LoadUint64(&c.recv))) return c.Conn.Close() } type Client struct { underlay tunnel.Client user statistic.User ctx context.Context cancel context.CancelFunc } func (c *Client) Close() error { c.cancel() return c.underlay.Close() } func (c *Client) DialConn(addr *tunnel.Address, overlay tunnel.Tunnel) (tunnel.Conn, error) { conn, err := c.underlay.DialConn(addr, &Tunnel{}) if err != nil { return nil, err } newConn := &OutboundConn{ Conn: conn, user: c.user, metadata: &tunnel.Metadata{ Command: Connect, Address: addr, }, } if _, ok := overlay.(*mux.Tunnel); ok { newConn.metadata.Command = Mux } go func(newConn *OutboundConn) { // if the trojan header is still buffered after 100 ms, the client may expect data from the server // so we flush the trojan header time.Sleep(time.Millisecond * 100) newConn.WriteHeader(nil) }(newConn) return newConn, nil } func (c *Client) DialPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { fakeAddr := &tunnel.Address{ DomainName: "UDP_CONN", AddressType: tunnel.DomainName, } conn, err := c.underlay.DialConn(fakeAddr, &Tunnel{}) if err != nil { return nil, err } return &PacketConn{ Conn: &OutboundConn{ Conn: conn, user: c.user, metadata: &tunnel.Metadata{ Command: Associate, Address: fakeAddr, }, }, }, nil } func NewClient(ctx context.Context, client tunnel.Client) (*Client, error) { ctx, cancel := context.WithCancel(ctx) auth, err := statistic.NewAuthenticator(ctx, memory.Name) if err != nil { cancel() return nil, err } cfg := config.FromContext(ctx, Name).(*Config) if cfg.API.Enabled { go api.RunService(ctx, Name+"_CLIENT", auth) } var user statistic.User for _, u := range auth.ListUsers() { user = u break } if user == nil { cancel() return nil, common.NewError("no valid user found") } log.Debug("trojan client created") return &Client{ underlay: client, ctx: ctx, user: user, cancel: cancel, }, nil } ================================================ FILE: tunnel/trojan/config.go ================================================ package trojan import "github.com/p4gefau1t/trojan-go/config" type Config struct { LocalHost string `json:"local_addr" yaml:"local-addr"` LocalPort int `json:"local_port" yaml:"local-port"` RemoteHost string `json:"remote_addr" yaml:"remote-addr"` RemotePort int `json:"remote_port" yaml:"remote-port"` DisableHTTPCheck bool `json:"disable_http_check" yaml:"disable-http-check"` MySQL MySQLConfig `json:"mysql" yaml:"mysql"` API APIConfig `json:"api" yaml:"api"` } type MySQLConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } type APIConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return &Config{} }) } ================================================ FILE: tunnel/trojan/packet.go ================================================ package trojan import ( "bytes" "encoding/binary" "io" "io/ioutil" "net" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type PacketConn struct { tunnel.Conn } func (c *PacketConn) ReadFrom(payload []byte) (int, net.Addr, error) { return c.ReadWithMetadata(payload) } func (c *PacketConn) WriteTo(payload []byte, addr net.Addr) (int, error) { address, err := tunnel.NewAddressFromAddr("udp", addr.String()) if err != nil { return 0, err } m := &tunnel.Metadata{ Address: address, } return c.WriteWithMetadata(payload, m) } func (c *PacketConn) WriteWithMetadata(payload []byte, metadata *tunnel.Metadata) (int, error) { packet := make([]byte, 0, MaxPacketSize) w := bytes.NewBuffer(packet) metadata.Address.WriteTo(w) length := len(payload) lengthBuf := [2]byte{} crlf := [2]byte{0x0d, 0x0a} binary.BigEndian.PutUint16(lengthBuf[:], uint16(length)) w.Write(lengthBuf[:]) w.Write(crlf[:]) w.Write(payload) _, err := c.Conn.Write(w.Bytes()) log.Debug("udp packet remote", c.RemoteAddr(), "metadata", metadata, "size", length) return len(payload), err } func (c *PacketConn) ReadWithMetadata(payload []byte) (int, *tunnel.Metadata, error) { addr := &tunnel.Address{ NetworkType: "udp", } if err := addr.ReadFrom(c.Conn); err != nil { return 0, nil, common.NewError("failed to parse udp packet addr").Base(err) } lengthBuf := [2]byte{} if _, err := io.ReadFull(c.Conn, lengthBuf[:]); err != nil { return 0, nil, common.NewError("failed to read length") } length := int(binary.BigEndian.Uint16(lengthBuf[:])) crlf := [2]byte{} if _, err := io.ReadFull(c.Conn, crlf[:]); err != nil { return 0, nil, common.NewError("failed to read crlf") } if len(payload) < length || length > MaxPacketSize { io.CopyN(ioutil.Discard, c.Conn, int64(length)) // drain the rest of the packet return 0, nil, common.NewError("incoming packet size is too large") } if _, err := io.ReadFull(c.Conn, payload[:length]); err != nil { return 0, nil, common.NewError("failed to read payload") } log.Debug("udp packet from", c.RemoteAddr(), "metadata", addr.String(), "size", length) return length, &tunnel.Metadata{ Address: addr, }, nil } ================================================ FILE: tunnel/trojan/server.go ================================================ package trojan import ( "context" "fmt" "io" "net" "sync/atomic" "github.com/p4gefau1t/trojan-go/api" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/redirector" "github.com/p4gefau1t/trojan-go/statistic" "github.com/p4gefau1t/trojan-go/statistic/memory" "github.com/p4gefau1t/trojan-go/statistic/mysql" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/mux" ) // InboundConn is a trojan inbound connection type InboundConn struct { // WARNING: do not change the order of these fields. // 64-bit fields that use `sync/atomic` package functions // must be 64-bit aligned on 32-bit systems. // Reference: https://github.com/golang/go/issues/599 // Solution: https://github.com/golang/go/issues/11891#issuecomment-433623786 sent uint64 recv uint64 net.Conn auth statistic.Authenticator user statistic.User hash string metadata *tunnel.Metadata ip string } func (c *InboundConn) Metadata() *tunnel.Metadata { return c.metadata } func (c *InboundConn) Write(p []byte) (int, error) { n, err := c.Conn.Write(p) atomic.AddUint64(&c.sent, uint64(n)) c.user.AddTraffic(n, 0) return n, err } func (c *InboundConn) Read(p []byte) (int, error) { n, err := c.Conn.Read(p) atomic.AddUint64(&c.recv, uint64(n)) c.user.AddTraffic(0, n) return n, err } func (c *InboundConn) Close() error { log.Info("user", c.hash, "from", c.Conn.RemoteAddr(), "tunneling to", c.metadata.Address, "closed", "sent:", common.HumanFriendlyTraffic(atomic.LoadUint64(&c.sent)), "recv:", common.HumanFriendlyTraffic(atomic.LoadUint64(&c.recv))) c.user.DelIP(c.ip) return c.Conn.Close() } func (c *InboundConn) Auth() error { userHash := [56]byte{} n, err := c.Conn.Read(userHash[:]) if err != nil || n != 56 { return common.NewError("failed to read hash").Base(err) } valid, user := c.auth.AuthUser(string(userHash[:])) if !valid { return common.NewError("invalid hash:" + string(userHash[:])) } c.hash = string(userHash[:]) c.user = user ip, _, err := net.SplitHostPort(c.Conn.RemoteAddr().String()) if err != nil { return common.NewError("failed to parse host:" + c.Conn.RemoteAddr().String()).Base(err) } c.ip = ip ok := user.AddIP(ip) if !ok { return common.NewError("ip limit reached") } crlf := [2]byte{} _, err = io.ReadFull(c.Conn, crlf[:]) if err != nil { return err } c.metadata = &tunnel.Metadata{} if err := c.metadata.ReadFrom(c.Conn); err != nil { return err } _, err = io.ReadFull(c.Conn, crlf[:]) if err != nil { return err } return nil } // Server is a trojan tunnel server type Server struct { auth statistic.Authenticator redir *redirector.Redirector redirAddr *tunnel.Address underlay tunnel.Server connChan chan tunnel.Conn muxChan chan tunnel.Conn packetChan chan tunnel.PacketConn ctx context.Context cancel context.CancelFunc } func (s *Server) Close() error { s.cancel() return s.underlay.Close() } func (s *Server) acceptLoop() { for { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { // Closing log.Error(common.NewError("trojan failed to accept conn").Base(err)) select { case <-s.ctx.Done(): return default: } continue } go func(conn tunnel.Conn) { rewindConn := common.NewRewindConn(conn) rewindConn.SetBufferSize(128) defer rewindConn.StopBuffering() inboundConn := &InboundConn{ Conn: rewindConn, auth: s.auth, } if err := inboundConn.Auth(); err != nil { rewindConn.Rewind() rewindConn.StopBuffering() log.Warn(common.NewError("connection with invalid trojan header from " + rewindConn.RemoteAddr().String()).Base(err)) s.redir.Redirect(&redirector.Redirection{ RedirectTo: s.redirAddr, InboundConn: rewindConn, }) return } rewindConn.StopBuffering() switch inboundConn.metadata.Command { case Connect: if inboundConn.metadata.DomainName == "MUX_CONN" { s.muxChan <- inboundConn log.Debug("mux(r) connection") } else { s.connChan <- inboundConn log.Debug("normal trojan connection") } case Associate: s.packetChan <- &PacketConn{ Conn: inboundConn, } log.Debug("trojan udp connection") case Mux: s.muxChan <- inboundConn log.Debug("mux connection") default: log.Error(common.NewError(fmt.Sprintf("unknown trojan command %d", inboundConn.metadata.Command))) } }(conn) } } func (s *Server) AcceptConn(nextTunnel tunnel.Tunnel) (tunnel.Conn, error) { switch nextTunnel.(type) { case *mux.Tunnel: select { case t := <-s.muxChan: return t, nil case <-s.ctx.Done(): return nil, common.NewError("trojan client closed") } default: select { case t := <-s.connChan: return t, nil case <-s.ctx.Done(): return nil, common.NewError("trojan client closed") } } } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { select { case t := <-s.packetChan: return t, nil case <-s.ctx.Done(): return nil, common.NewError("trojan client closed") } } func NewServer(ctx context.Context, underlay tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) ctx, cancel := context.WithCancel(ctx) // TODO replace this dirty code var auth statistic.Authenticator var err error if cfg.MySQL.Enabled { log.Debug("mysql enabled") auth, err = statistic.NewAuthenticator(ctx, mysql.Name) } else { log.Debug("auth by config file") auth, err = statistic.NewAuthenticator(ctx, memory.Name) } if err != nil { cancel() return nil, common.NewError("trojan failed to create authenticator") } if cfg.API.Enabled { go api.RunService(ctx, Name+"_SERVER", auth) } redirAddr := tunnel.NewAddressFromHostPort("tcp", cfg.RemoteHost, cfg.RemotePort) s := &Server{ underlay: underlay, auth: auth, redirAddr: redirAddr, connChan: make(chan tunnel.Conn, 32), muxChan: make(chan tunnel.Conn, 32), packetChan: make(chan tunnel.PacketConn, 32), ctx: ctx, cancel: cancel, redir: redirector.NewRedirector(ctx), } if !cfg.DisableHTTPCheck { redirConn, err := net.Dial("tcp", redirAddr.String()) if err != nil { cancel() return nil, common.NewError("invalid redirect address. check your http server: " + redirAddr.String()).Base(err) } redirConn.Close() } go s.acceptLoop() log.Debug("trojan server created") return s, nil } ================================================ FILE: tunnel/trojan/trojan_test.go ================================================ package trojan import ( "bytes" "context" "fmt" "io" "net" "testing" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/statistic/memory" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) func TestTrojan(t *testing.T) { port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } ctx, cancel := context.WithCancel(context.Background()) ctx = config.WithConfig(ctx, transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, &freedom.Config{}) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) serverPort := common.PickPort("tcp", "127.0.0.1") authConfig := &memory.Config{Passwords: []string{"password"}} clientConfig := &Config{ RemoteHost: "127.0.0.1", RemotePort: serverPort, } serverConfig := &Config{ LocalHost: "127.0.0.1", LocalPort: serverPort, RemoteHost: "127.0.0.1", RemotePort: util.EchoPort, } ctx = config.WithConfig(ctx, memory.Name, authConfig) clientCtx := config.WithConfig(ctx, Name, clientConfig) serverCtx := config.WithConfig(ctx, Name, serverConfig) c, err := NewClient(clientCtx, tcpClient) common.Must(err) s, err := NewServer(serverCtx, tcpServer) common.Must(err) conn1, err := c.DialConn(&tunnel.Address{ DomainName: "example.com", AddressType: tunnel.DomainName, }, nil) common.Must(err) common.Must2(conn1.Write([]byte("87654321"))) conn2, err := s.AcceptConn(nil) common.Must(err) buf := [8]byte{} conn2.Read(buf[:]) if !util.CheckConn(conn1, conn2) { t.Fail() } packet1, err := c.DialPacket(nil) common.Must(err) packet1.WriteWithMetadata([]byte("12345678"), &tunnel.Metadata{ Address: &tunnel.Address{ DomainName: "example.com", AddressType: tunnel.DomainName, Port: 80, }, }) packet2, err := s.AcceptPacket(nil) common.Must(err) _, m, err := packet2.ReadWithMetadata(buf[:]) common.Must(err) fmt.Println(m) if !util.CheckPacketOverConn(packet1, packet2) { t.Fail() } // redirecting conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) common.Must(err) sendBuf := util.GeneratePayload(1024) recvBuf := [1024]byte{} common.Must2(conn.Write(sendBuf)) common.Must2(io.ReadFull(conn, recvBuf[:])) if !bytes.Equal(sendBuf, recvBuf[:]) { fmt.Println(sendBuf) fmt.Println(recvBuf[:]) t.Fail() } conn1.Close() conn2.Close() packet1.Close() packet2.Close() conn.Close() c.Close() s.Close() cancel() } ================================================ FILE: tunnel/trojan/tunnel.go ================================================ package trojan import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "TROJAN" type Tunnel struct{} func (c *Tunnel) Name() string { return Name } func (c *Tunnel) NewClient(ctx context.Context, client tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, client) } func (c *Tunnel) NewServer(ctx context.Context, server tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, server) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/tunnel.go ================================================ package tunnel import ( "context" "io" "net" "github.com/p4gefau1t/trojan-go/common" ) // Conn is the TCP connection in the tunnel type Conn interface { net.Conn Metadata() *Metadata } // PacketConn is the UDP packet stream in the tunnel type PacketConn interface { net.PacketConn WriteWithMetadata([]byte, *Metadata) (int, error) ReadWithMetadata([]byte) (int, *Metadata, error) } // ConnDialer creates TCP connections from the tunnel type ConnDialer interface { DialConn(*Address, Tunnel) (Conn, error) } // PacketDialer creates UDP packet stream from the tunnel type PacketDialer interface { DialPacket(Tunnel) (PacketConn, error) } // ConnListener accept TCP connections type ConnListener interface { AcceptConn(Tunnel) (Conn, error) } // PacketListener accept UDP packet stream // We don't have any tunnel based on packet streams, so AcceptPacket will always receive a real PacketConn type PacketListener interface { AcceptPacket(Tunnel) (PacketConn, error) } // Dialer can dial to original server with a tunnel type Dialer interface { ConnDialer PacketDialer } // Listener can accept TCP and UDP streams from a tunnel type Listener interface { ConnListener PacketListener } // Client is the tunnel client based on stream connections type Client interface { Dialer io.Closer } // Server is the tunnel server based on stream connections type Server interface { Listener io.Closer } // Tunnel describes a tunnel, allowing creating a tunnel from another tunnel // We assume that the lower tunnels know exatly how upper tunnels work, and lower tunnels is transparent for the upper tunnels type Tunnel interface { Name() string NewClient(context.Context, Client) (Client, error) NewServer(context.Context, Server) (Server, error) } var tunnels = make(map[string]Tunnel) // RegisterTunnel register a tunnel by tunnel name func RegisterTunnel(name string, tunnel Tunnel) { tunnels[name] = tunnel } func GetTunnel(name string) (Tunnel, error) { if t, ok := tunnels[name]; ok { return t, nil } return nil, common.NewError("unknown tunnel name " + name) } ================================================ FILE: tunnel/websocket/client.go ================================================ package websocket import ( "context" "strings" "golang.org/x/net/websocket" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/tunnel" ) type Client struct { underlay tunnel.Client hostname string path string } func (c *Client) DialConn(*tunnel.Address, tunnel.Tunnel) (tunnel.Conn, error) { conn, err := c.underlay.DialConn(nil, &Tunnel{}) if err != nil { return nil, common.NewError("websocket cannot dial with underlying client").Base(err) } url := "wss://" + c.hostname + c.path origin := "https://" + c.hostname wsConfig, err := websocket.NewConfig(url, origin) if err != nil { return nil, common.NewError("invalid websocket config").Base(err) } wsConn, err := websocket.NewClient(wsConfig, conn) if err != nil { return nil, common.NewError("websocket failed to handshake with server").Base(err) } return &OutboundConn{ Conn: wsConn, tcpConn: conn, }, nil } func (c *Client) DialPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { return nil, common.NewError("not supported by websocket") } func (c *Client) Close() error { return c.underlay.Close() } func NewClient(ctx context.Context, underlay tunnel.Client) (*Client, error) { cfg := config.FromContext(ctx, Name).(*Config) if !strings.HasPrefix(cfg.Websocket.Path, "/") { return nil, common.NewError("websocket path must start with \"/\"") } if cfg.Websocket.Host == "" { cfg.Websocket.Host = cfg.RemoteHost log.Warn("empty websocket hostname") } log.Debug("websocket client created") return &Client{ hostname: cfg.Websocket.Host, path: cfg.Websocket.Path, underlay: underlay, }, nil } ================================================ FILE: tunnel/websocket/config.go ================================================ package websocket import "github.com/p4gefau1t/trojan-go/config" type WebsocketConfig struct { Enabled bool `json:"enabled" yaml:"enabled"` Host string `json:"host" yaml:"host"` Path string `json:"path" yaml:"path"` } type Config struct { RemoteHost string `json:"remote_addr" yaml:"remote-addr"` RemotePort int `json:"remote_port" yaml:"remote-port"` Websocket WebsocketConfig `json:"websocket" yaml:"websocket"` } func init() { config.RegisterConfigCreator(Name, func() interface{} { return new(Config) }) } ================================================ FILE: tunnel/websocket/conn.go ================================================ package websocket import ( "context" "net" "golang.org/x/net/websocket" "github.com/p4gefau1t/trojan-go/tunnel" ) type OutboundConn struct { *websocket.Conn tcpConn net.Conn } func (c *OutboundConn) Metadata() *tunnel.Metadata { return nil } func (c *OutboundConn) RemoteAddr() net.Addr { // override RemoteAddr of websocket.Conn, or it will return some url from "Origin" return c.tcpConn.RemoteAddr() } type InboundConn struct { OutboundConn ctx context.Context cancel context.CancelFunc } func (c *InboundConn) Close() error { c.cancel() return c.Conn.Close() } ================================================ FILE: tunnel/websocket/server.go ================================================ package websocket import ( "bufio" "context" "math/rand" "net" "net/http" "strings" "time" "golang.org/x/net/websocket" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/redirector" "github.com/p4gefau1t/trojan-go/tunnel" ) // Fake response writer // Websocket ServeHTTP method uses Hijack method to get the ReadWriter type fakeHTTPResponseWriter struct { http.Hijacker http.ResponseWriter ReadWriter *bufio.ReadWriter Conn net.Conn } func (w *fakeHTTPResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { return w.Conn, w.ReadWriter, nil } type Server struct { underlay tunnel.Server hostname string path string enabled bool redirAddr net.Addr redir *redirector.Redirector ctx context.Context cancel context.CancelFunc timeout time.Duration } func (s *Server) Close() error { s.cancel() return s.underlay.Close() } func (s *Server) AcceptConn(tunnel.Tunnel) (tunnel.Conn, error) { conn, err := s.underlay.AcceptConn(&Tunnel{}) if err != nil { return nil, common.NewError("websocket failed to accept connection from underlying server") } if !s.enabled { s.redir.Redirect(&redirector.Redirection{ InboundConn: conn, RedirectTo: s.redirAddr, }) return nil, common.NewError("websocket is disabled. redirecting http request from " + conn.RemoteAddr().String()) } rewindConn := common.NewRewindConn(conn) rewindConn.SetBufferSize(512) defer rewindConn.StopBuffering() rw := bufio.NewReadWriter(bufio.NewReader(rewindConn), bufio.NewWriter(rewindConn)) req, err := http.ReadRequest(rw.Reader) if err != nil { log.Debug("invalid http request") rewindConn.Rewind() rewindConn.StopBuffering() s.redir.Redirect(&redirector.Redirection{ InboundConn: rewindConn, RedirectTo: s.redirAddr, }) return nil, common.NewError("not a valid http request: " + conn.RemoteAddr().String()).Base(err) } if strings.ToLower(req.Header.Get("Upgrade")) != "websocket" || req.URL.Path != s.path { log.Debug("invalid http websocket handshake request") rewindConn.Rewind() rewindConn.StopBuffering() s.redir.Redirect(&redirector.Redirection{ InboundConn: rewindConn, RedirectTo: s.redirAddr, }) return nil, common.NewError("not a valid websocket handshake request: " + conn.RemoteAddr().String()).Base(err) } handshake := make(chan struct{}) url := "wss://" + s.hostname + s.path origin := "https://" + s.hostname wsConfig, err := websocket.NewConfig(url, origin) if err != nil { return nil, common.NewError("failed to create websocket config").Base(err) } var wsConn *websocket.Conn ctx, cancel := context.WithCancel(s.ctx) wsServer := websocket.Server{ Config: *wsConfig, Handler: func(conn *websocket.Conn) { wsConn = conn // store the websocket after handshaking wsConn.PayloadType = websocket.BinaryFrame // treat it as a binary websocket log.Debug("websocket obtained") handshake <- struct{}{} // this function SHOULD NOT return unless the connection is ended // or the websocket will be closed by ServeHTTP method <-ctx.Done() log.Debug("websocket closed") }, Handshake: func(wsConfig *websocket.Config, httpRequest *http.Request) error { log.Debug("websocket url", httpRequest.URL, "origin", httpRequest.Header.Get("Origin")) return nil }, } respWriter := &fakeHTTPResponseWriter{ Conn: conn, ReadWriter: rw, } go wsServer.ServeHTTP(respWriter, req) select { case <-handshake: case <-time.After(s.timeout): } if wsConn == nil { cancel() return nil, common.NewError("websocket failed to handshake") } return &InboundConn{ OutboundConn: OutboundConn{ tcpConn: conn, Conn: wsConn, }, ctx: ctx, cancel: cancel, }, nil } func (s *Server) AcceptPacket(tunnel.Tunnel) (tunnel.PacketConn, error) { return nil, common.NewError("not supported") } func NewServer(ctx context.Context, underlay tunnel.Server) (*Server, error) { cfg := config.FromContext(ctx, Name).(*Config) if cfg.Websocket.Enabled { if !strings.HasPrefix(cfg.Websocket.Path, "/") { return nil, common.NewError("websocket path must start with \"/\"") } } if cfg.RemoteHost == "" { log.Warn("empty websocket redirection hostname") cfg.RemoteHost = cfg.Websocket.Host } if cfg.RemotePort == 0 { log.Warn("empty websocket redirection port") cfg.RemotePort = 80 } ctx, cancel := context.WithCancel(ctx) log.Debug("websocket server created") return &Server{ enabled: cfg.Websocket.Enabled, hostname: cfg.Websocket.Host, path: cfg.Websocket.Path, ctx: ctx, cancel: cancel, underlay: underlay, timeout: time.Second * time.Duration(rand.Intn(10)+5), redir: redirector.NewRedirector(ctx), redirAddr: tunnel.NewAddressFromHostPort("tcp", cfg.RemoteHost, cfg.RemotePort), }, nil } ================================================ FILE: tunnel/websocket/tunnel.go ================================================ package websocket import ( "context" "github.com/p4gefau1t/trojan-go/tunnel" ) const Name = "WEBSOCKET" type Tunnel struct{} func (*Tunnel) Name() string { return Name } func (*Tunnel) NewServer(ctx context.Context, underlay tunnel.Server) (tunnel.Server, error) { return NewServer(ctx, underlay) } func (*Tunnel) NewClient(ctx context.Context, underlay tunnel.Client) (tunnel.Client, error) { return NewClient(ctx, underlay) } func init() { tunnel.RegisterTunnel(Name, &Tunnel{}) } ================================================ FILE: tunnel/websocket/websocket_test.go ================================================ package websocket import ( "context" "fmt" "net" "strings" "sync" "testing" "time" "golang.org/x/net/websocket" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/config" "github.com/p4gefau1t/trojan-go/test/util" "github.com/p4gefau1t/trojan-go/tunnel" "github.com/p4gefau1t/trojan-go/tunnel/freedom" "github.com/p4gefau1t/trojan-go/tunnel/transport" ) func TestWebsocket(t *testing.T) { cfg := &Config{ Websocket: WebsocketConfig{ Enabled: true, Host: "localhost", Path: "/ws", }, } ctx := config.WithConfig(context.Background(), Name, cfg) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, RemoteHost: "127.0.0.1", RemotePort: port, } freedomCfg := &freedom.Config{} ctx = config.WithConfig(ctx, transport.Name, transportConfig) ctx = config.WithConfig(ctx, freedom.Name, freedomCfg) tcpClient, err := transport.NewClient(ctx, nil) common.Must(err) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) c, err := NewClient(ctx, tcpClient) common.Must(err) s, err := NewServer(ctx, tcpServer) var conn2 tunnel.Conn wg := sync.WaitGroup{} wg.Add(1) go func() { conn2, err = s.AcceptConn(nil) common.Must(err) wg.Done() }() time.Sleep(time.Second) conn1, err := c.DialConn(nil, nil) common.Must(err) wg.Wait() if !util.CheckConn(conn1, conn2) { t.Fail() } if strings.HasPrefix(conn1.RemoteAddr().String(), "ws") { t.Fail() } if strings.HasPrefix(conn2.RemoteAddr().String(), "ws") { t.Fail() } conn1.Close() conn2.Close() s.Close() c.Close() } func TestRedirect(t *testing.T) { cfg := &Config{ RemoteHost: "127.0.0.1", Websocket: WebsocketConfig{ Enabled: true, Host: "localhost", Path: "/ws", }, } fmt.Sscanf(util.HTTPPort, "%d", &cfg.RemotePort) ctx := config.WithConfig(context.Background(), Name, cfg) port := common.PickPort("tcp", "127.0.0.1") transportConfig := &transport.Config{ LocalHost: "127.0.0.1", LocalPort: port, } ctx = config.WithConfig(ctx, transport.Name, transportConfig) tcpServer, err := transport.NewServer(ctx, nil) common.Must(err) s, err := NewServer(ctx, tcpServer) common.Must(err) go func() { _, err := s.AcceptConn(nil) if err == nil { t.Fail() } }() time.Sleep(time.Second) conn, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port)) common.Must(err) url := "wss://localhost/wrong-path" origin := "https://localhost" wsConfig, err := websocket.NewConfig(url, origin) common.Must(err) _, err = websocket.NewClient(wsConfig, conn) if err == nil { t.Fail() } conn.Close() s.Close() } ================================================ FILE: url/option.go ================================================ package url import ( "encoding/json" "flag" "net" "strconv" "strings" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/log" "github.com/p4gefau1t/trojan-go/option" "github.com/p4gefau1t/trojan-go/proxy" ) const Name = "URL" type Websocket struct { Enabled bool `json:"enabled"` Host string `json:"host"` Path string `json:"path"` } type TLS struct { SNI string `json:"sni"` } type Shadowsocks struct { Enabled bool `json:"enabled"` Method string `json:"method"` Password string `json:"password"` } type Mux struct { Enabled bool `json:"enabled"` } type API struct { Enabled bool `json:"enabled"` APIHost string `json:"api_addr"` APIPort int `json:"api_port"` } type UrlConfig struct { RunType string `json:"run_type"` LocalAddr string `json:"local_addr"` LocalPort int `json:"local_port"` RemoteAddr string `json:"remote_addr"` RemotePort int `json:"remote_port"` Password []string `json:"password"` Websocket `json:"websocket"` Shadowsocks `json:"shadowsocks"` TLS `json:"ssl"` Mux `json:"mux"` API `json:"api"` } type url struct { url *string option *string } func (u *url) Name() string { return Name } func (u *url) Handle() error { if u.url == nil || *u.url == "" { return common.NewError("") } info, err := NewShareInfoFromURL(*u.url) if err != nil { log.Fatal(err) } wsEnabled := false if info.Type == ShareInfoTypeWebSocket { wsEnabled = true } ssEnabled := false ssPassword := "" ssMethod := "" if strings.HasPrefix(info.Encryption, "ss;") { ssEnabled = true ssConfig := strings.Split(info.Encryption[3:], ":") if len(ssConfig) != 2 { log.Fatalf("invalid shadowsocks config: %s", info.Encryption) } ssMethod = ssConfig[0] ssPassword = ssConfig[1] } muxEnabled := false listenHost := "127.0.0.1" listenPort := 1080 apiEnabled := false apiHost := "127.0.0.1" apiPort := 10000 options := strings.Split(*u.option, ";") for _, o := range options { key := "" val := "" l := strings.Split(o, "=") if len(l) != 2 { log.Fatal("option format error, no \"key=value\" pair found:", o) } key = l[0] val = l[1] switch key { case "mux": muxEnabled, err = strconv.ParseBool(val) if err != nil { log.Fatal(err) } case "listen": h, p, err := net.SplitHostPort(val) if err != nil { log.Fatal(err) } listenHost = h lp, err := strconv.Atoi(p) if err != nil { log.Fatal(err) } listenPort = lp case "api": apiEnabled = true h, p, err := net.SplitHostPort(val) if err != nil { log.Fatal(err) } apiHost = h lp, err := strconv.Atoi(p) if err != nil { log.Fatal(err) } apiPort = lp default: log.Fatal("invalid option", o) } } config := UrlConfig{ RunType: "client", LocalAddr: listenHost, LocalPort: listenPort, RemoteAddr: info.TrojanHost, RemotePort: int(info.Port), Password: []string{info.TrojanPassword}, TLS: TLS{ SNI: info.SNI, }, Websocket: Websocket{ Enabled: wsEnabled, Path: info.Path, Host: info.Host, }, Mux: Mux{ Enabled: muxEnabled, }, Shadowsocks: Shadowsocks{ Enabled: ssEnabled, Password: ssPassword, Method: ssMethod, }, API: API{ Enabled: apiEnabled, APIHost: apiHost, APIPort: apiPort, }, } data, err := json.Marshal(&config) if err != nil { log.Fatal(err) } log.Debug(string(data)) client, err := proxy.NewProxyFromConfigData(data, true) if err != nil { log.Fatal(err) } return client.Run() } func (u *url) Priority() int { return 10 } func init() { option.RegisterHandler(&url{ url: flag.String("url", "", "Setup trojan-go client with a url link"), option: flag.String("url-option", "mux=true;listen=127.0.0.1:1080", "URL mode options"), }) } ================================================ FILE: url/option_test.go ================================================ package url import ( "testing" "time" _ "github.com/p4gefau1t/trojan-go/proxy/client" ) func TestUrl_Handle(t *testing.T) { urlCases := []string{ "trojan-go://password@server.com", "trojan-go://password@server.com/?type=ws&host=baidu.com&path=%2fwspath", "trojan-go://password@server.com/?encryption=ss%3baes-256-gcm%3afuckgfw", "trojan-go://password@server.com/?type=ws&host=baidu.com&path=%2fwspath&encryption=ss%3Baes-256-gcm%3Afuckgfw", } optionCases := []string{ "mux=true;listen=127.0.0.1:0", "mux=false;listen=127.0.0.1:0", "mux=false;listen=127.0.0.1:0;api=127.0.0.1:0", } for _, s := range urlCases { for _, option := range optionCases { s := s option := option u := &url{ url: &s, option: &option, } u.Name() u.Priority() errChan := make(chan error, 1) go func() { errChan <- u.Handle() }() select { case err := <-errChan: t.Fatal(err) case <-time.After(time.Second * 1): } } } } ================================================ FILE: url/share_link.go ================================================ package url import ( "errors" "fmt" neturl "net/url" "strconv" "strings" ) const ( ShareInfoTypeOriginal = "original" ShareInfoTypeWebSocket = "ws" ) var validTypes = map[string]struct{}{ ShareInfoTypeOriginal: {}, ShareInfoTypeWebSocket: {}, } var validEncryptionProviders = map[string]struct{}{ "ss": {}, "none": {}, } var validSSEncryptionMap = map[string]struct{}{ "aes-128-gcm": {}, "aes-256-gcm": {}, "chacha20-ietf-poly1305": {}, } type ShareInfo struct { TrojanHost string // 节点 IP / 域名 Port uint16 // 节点端口 TrojanPassword string // Trojan 密码 SNI string // SNI Type string // 类型 Host string // HTTP Host Header Path string // WebSocket / H2 Path Encryption string // 额外加密 Plugin string // 插件设定 Description string // 节点说明 } func NewShareInfoFromURL(shareLink string) (info ShareInfo, e error) { // share link must be valid url parse, e := neturl.Parse(shareLink) if e != nil { e = fmt.Errorf("invalid url: %s", e.Error()) return } // share link must have `trojan-go://` scheme if parse.Scheme != "trojan-go" { e = errors.New("url does not have a trojan-go:// scheme") return } // password if info.TrojanPassword = parse.User.Username(); info.TrojanPassword == "" { e = errors.New("no password specified") return } else if _, hasPassword := parse.User.Password(); hasPassword { e = errors.New("password possibly missing percentage encoding for colon") return } // trojanHost: not empty & strip [] from IPv6 addresses if info.TrojanHost = parse.Hostname(); info.TrojanHost == "" { e = errors.New("host is empty") return } // port if info.Port, e = handleTrojanPort(parse.Port()); e != nil { return } // strictly parse the query query, e := neturl.ParseQuery(parse.RawQuery) if e != nil { return } // sni if SNIs, ok := query["sni"]; !ok { info.SNI = info.TrojanHost } else if len(SNIs) > 1 { e = errors.New("multiple SNIs") return } else if info.SNI = SNIs[0]; info.SNI == "" { e = errors.New("empty SNI") return } // type if types, ok := query["type"]; !ok { info.Type = ShareInfoTypeOriginal } else if len(types) > 1 { e = errors.New("multiple transport types") return } else if info.Type = types[0]; info.Type == "" { e = errors.New("empty transport type") return } else if _, ok := validTypes[info.Type]; !ok { e = fmt.Errorf("unknown transport type: %s", info.Type) return } // host if hosts, ok := query["host"]; !ok { info.Host = info.TrojanHost } else if len(hosts) > 1 { e = errors.New("multiple hosts") return } else if info.Host = hosts[0]; info.Host == "" { e = errors.New("empty host") return } // path if info.Type == ShareInfoTypeWebSocket { if paths, ok := query["path"]; !ok { e = errors.New("path is required in websocket") return } else if len(paths) > 1 { e = errors.New("multiple paths") return } else if info.Path = paths[0]; info.Path == "" { e = errors.New("empty path") return } if !strings.HasPrefix(info.Path, "/") { e = errors.New("path must start with /") return } } // encryption if encryptionArr, ok := query["encryption"]; !ok { // no encryption. that's okay. } else if len(encryptionArr) > 1 { e = errors.New("multiple encryption fields") return } else if info.Encryption = encryptionArr[0]; info.Encryption == "" { e = errors.New("empty encryption") return } else { encryptionParts := strings.SplitN(info.Encryption, ";", 2) encryptionProviderName := encryptionParts[0] if _, ok := validEncryptionProviders[encryptionProviderName]; !ok { e = fmt.Errorf("unsupported encryption provider name: %s", encryptionProviderName) return } var encryptionParams string if len(encryptionParts) >= 2 { encryptionParams = encryptionParts[1] } if encryptionProviderName == "ss" { ssParams := strings.SplitN(encryptionParams, ":", 2) if len(ssParams) < 2 { e = errors.New("missing ss password") return } ssMethod, ssPassword := ssParams[0], ssParams[1] if _, ok := validSSEncryptionMap[ssMethod]; !ok { e = fmt.Errorf("unsupported ss method: %s", ssMethod) return } if ssPassword == "" { e = errors.New("ss password cannot be empty") return } } } // plugin if plugins, ok := query["plugin"]; !ok { // no plugin. that's okay. } else if len(plugins) > 1 { e = errors.New("multiple plugins") return } else if info.Plugin = plugins[0]; info.Plugin == "" { e = errors.New("empty plugin") return } // description info.Description = parse.Fragment return } func handleTrojanPort(p string) (port uint16, e error) { if p == "" { return 443, nil } portParsed, e := strconv.Atoi(p) if e != nil { return } if portParsed < 1 || portParsed > 65535 { e = fmt.Errorf("invalid port %d", portParsed) return } port = uint16(portParsed) return } ================================================ FILE: url/share_link_test.go ================================================ package url import ( crand "crypto/rand" "fmt" "io" "io/ioutil" "testing" "github.com/stretchr/testify/assert" ) func TestHandleTrojanPort_Default(t *testing.T) { port, e := handleTrojanPort("") assert.Nil(t, e, "empty port should not error") assert.EqualValues(t, 443, port, "empty port should fallback to 443") } func TestHandleTrojanPort_NotNumber(t *testing.T) { _, e := handleTrojanPort("fuck") assert.Error(t, e, "non-numerical port should error") } func TestHandleTrojanPort_GoodNumber(t *testing.T) { testCases := []string{"443", "8080", "10086", "80", "65535", "1"} for _, testCase := range testCases { _, e := handleTrojanPort(testCase) assert.Nil(t, e, "good port %s should not error", testCase) } } func TestHandleTrojanPort_InvalidNumber(t *testing.T) { testCases := []string{"443.0", "443.000", "8e2", "3.5", "9.99", "-1", "-65535", "65536", "0"} for _, testCase := range testCases { _, e := handleTrojanPort(testCase) assert.Error(t, e, "invalid number port %s should error", testCase) } } func TestNewShareInfoFromURL_Empty(t *testing.T) { _, e := NewShareInfoFromURL("") assert.Error(t, e, "empty link should lead to error") } func TestNewShareInfoFromURL_RandomCrap(t *testing.T) { for i := 0; i < 100; i++ { randomCrap, _ := ioutil.ReadAll(io.LimitReader(crand.Reader, 10)) _, e := NewShareInfoFromURL(string(randomCrap)) assert.Error(t, e, "random crap %v should lead to error", randomCrap) } } func TestNewShareInfoFromURL_NotTrojanGo(t *testing.T) { testCases := []string{ "trojan://what.ever@www.twitter.com:443?allowInsecure=1&allowInsecureHostname=1&allowInsecureCertificate=1&sessionTicket=0&tfo=1#some-trojan", "ssr://d3d3LnR3aXR0ZXIuY29tOjgwOmF1dGhfc2hhMV92NDpjaGFjaGEyMDpwbGFpbjpZbkpsWVd0M1lXeHMvP29iZnNwYXJhbT0mcmVtYXJrcz02TC1INXB5ZjVwZTI2WmUwNzd5YU1qQXlNQzB3TnkweE9DQXhNam8xTlRveU1RJmdyb3VwPVEzUkRiRzkxWkNCVFUxSQ", "vmess://eyJhZGQiOiJtb3RoZXIuZnVja2VyIiwiYWlkIjowLCJpZCI6IjFmYzI0NzVmLThmNDMtM2FlYi05MzUyLTU2MTFhZjg1NmQyOSIsIm5ldCI6InRjcCIsInBvcnQiOjEwMDg2LCJwcyI6Iui/h+acn+aXtumXtO+8mjIwMjAtMDYtMjMiLCJ0bHMiOiJub25lIiwidHlwZSI6Im5vbmUiLCJ2IjoyfQ==", } for _, testCase := range testCases { _, e := NewShareInfoFromURL(testCase) assert.Error(t, e, "non trojan-go link %s should not decode", testCase) } } func TestNewShareInfoFromURL_EmptyTrojanHost(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://fuckyou@:443/") assert.Error(t, e, "empty host should not decode") } func TestNewShareInfoFromURL_BadPassword(t *testing.T) { testCases := []string{ "trojan-go://we:are:the:champion@114514.go", "trojan-go://evilpassword:@1919810.me", "trojan-go://evilpassword::@1919810.me", "trojan-go://@password.404", "trojan-go://mother.fuck#yeah", } for _, testCase := range testCases { _, e := NewShareInfoFromURL(testCase) assert.Error(t, e, "bad password link %s should not decode", testCase) } } func TestNewShareInfoFromURL_GoodPassword(t *testing.T) { testCases := []string{ "trojan-go://we%3Aare%3Athe%3Achampion@114514.go", "trojan-go://evilpassword%3A@1919810.me", "trojan-go://passw0rd-is-a-must@password.200", } for _, testCase := range testCases { _, e := NewShareInfoFromURL(testCase) assert.Nil(t, e, "good password link %s should decode", testCase) } } func TestNewShareInfoFromURL_BadPort(t *testing.T) { testCases := []string{ "trojan-go://pswd@example.com:114514", "trojan-go://pswd@example.com:443.0", "trojan-go://pswd@example.com:-1", "trojan-go://pswd@example.com:8e2", "trojan-go://pswd@example.com:65536", } for _, testCase := range testCases { _, e := NewShareInfoFromURL(testCase) assert.Error(t, e, "decode url %s with invalid port should error", testCase) } } func TestNewShareInfoFromURL_BadQuery(t *testing.T) { testCases := []string{ "trojan-go://cao@ni.ma?NMSL=%CG%GE%CAONIMA", "trojan-go://ni@ta.ma:13/?#%2e%fu", } for _, testCase := range testCases { _, e := NewShareInfoFromURL(testCase) assert.Error(t, e, "parse bad query should error") } } func TestNewShareInfoFromURL_SNI_Empty(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?sni=") assert.Error(t, e, "empty SNI should not be allowed") } func TestNewShareInfoFromURL_SNI_Default(t *testing.T) { info, e := NewShareInfoFromURL("trojan-go://a@b.c") assert.Nil(t, e) assert.Equal(t, info.TrojanHost, info.SNI, "default sni should be trojan hostname") } func TestNewShareInfoFromURL_SNI_Multiple(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?sni=a&sni=b&sni=c") assert.Error(t, e, "multiple SNIs should not be allowed") } func TestNewShareInfoFromURL_Type_Empty(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?type=") assert.Error(t, e, "empty type should not be allowed") } func TestNewShareInfoFromURL_Type_Default(t *testing.T) { info, e := NewShareInfoFromURL("trojan-go://a@b.c") assert.Nil(t, e) assert.Equal(t, ShareInfoTypeOriginal, info.Type, "default type should be original") } func TestNewShareInfoFromURL_Type_Invalid(t *testing.T) { invalidTypes := []string{"nmsl", "dio"} for _, invalidType := range invalidTypes { _, e := NewShareInfoFromURL(fmt.Sprintf("trojan-go://a@b.c?type=%s", invalidType)) assert.Error(t, e, "%s should not be a valid type", invalidType) } } func TestNewShareInfoFromURL_Type_Multiple(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?type=a&type=b&type=c") assert.Error(t, e, "multiple types should not be allowed") } func TestNewShareInfoFromURL_Host_Empty(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?host=") assert.Error(t, e, "empty host should not be allowed") } func TestNewShareInfoFromURL_Host_Default(t *testing.T) { info, e := NewShareInfoFromURL("trojan-go://a@b.c") assert.Nil(t, e) assert.Equal(t, info.TrojanHost, info.Host, "default host should be trojan hostname") } func TestNewShareInfoFromURL_Host_Multiple(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?host=a&host=b&host=c") assert.Error(t, e, "multiple hosts should not be allowed") } func TestNewShareInfoFromURL_Type_WS_Multiple(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?type=ws&path=a&path=b&path=c") assert.Error(t, e, "multiple paths should not be allowed in wss") } func TestNewShareInfoFromURL_Path_WS_None(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?type=ws") assert.Error(t, e, "ws should require path") } func TestNewShareInfoFromURL_Path_WS_Empty(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?type=ws&path=") assert.Error(t, e, "empty path should not be allowed in ws") } func TestNewShareInfoFromURL_Path_WS_Invalid(t *testing.T) { invalidPaths := []string{"../", ".+!", " "} for _, invalidPath := range invalidPaths { _, e := NewShareInfoFromURL(fmt.Sprintf("trojan-go://a@b.c?type=ws&path=%s", invalidPath)) assert.Error(t, e, "%s should not be a valid path in ws", invalidPath) } } func TestNewShareInfoFromURL_Path_Plain_Empty(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?type=original&path=") assert.Nil(t, e, "empty path should be ignored in original mode") } func TestNewShareInfoFromURL_Encryption_Empty(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?encryption=") assert.Error(t, e, "encryption should not be empty") } func TestNewShareInfoFromURL_Encryption_Unknown(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?encryption=motherfucker") assert.Error(t, e, "unknown encryption should not be supported") } func TestNewShareInfoFromURL_Encryption_None(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://what@ever.me?encryption=none") assert.Nil(t, e, "should support none encryption") } func TestNewShareInfoFromURL_Encryption_SS_NotSupportedMethods(t *testing.T) { invalidMethods := []string{"rc4-md5", "rc4", "des-cfb", "table", "salsa20-ctr"} for _, invalidMethod := range invalidMethods { _, e := NewShareInfoFromURL(fmt.Sprintf("trojan-go://a@b.c?encryption=ss%%3B%s%%3Ashabi", invalidMethod)) assert.Error(t, e, "encryption %s should not be supported by ss", invalidMethod) } } func TestNewShareInfoFromURL_Encryption_SS_NoPassword(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?encryption=ss%3Baes-256-gcm%3A") assert.Error(t, e, "empty ss password should not be allowed") } func TestNewShareInfoFromURL_Encryption_SS_BadParams(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?encryption=ss%3Ba") assert.Error(t, e, "broken ss param should not be allowed") } func TestNewShareInfoFromURL_Encryption_Multiple(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?encryption=a&encryption=b&encryption=c") assert.Error(t, e, "multiple encryption should not be allowed") } func TestNewShareInfoFromURL_Plugin_Empty(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?plugin=") assert.Error(t, e, "plugin should not be empty") } func TestNewShareInfoFromURL_Plugin_Multiple(t *testing.T) { _, e := NewShareInfoFromURL("trojan-go://a@b.c?plugin=a&plugin=b&plugin=c") assert.Error(t, e, "multiple plugin should not be allowed") } ================================================ FILE: version/version.go ================================================ package version import ( "flag" "fmt" "runtime" "github.com/p4gefau1t/trojan-go/common" "github.com/p4gefau1t/trojan-go/constant" "github.com/p4gefau1t/trojan-go/option" ) type versionOption struct { flag *bool } func (*versionOption) Name() string { return "version" } func (*versionOption) Priority() int { return 10 } func (c *versionOption) Handle() error { if *c.flag { fmt.Println("Trojan-Go", constant.Version) fmt.Println("Go Version:", runtime.Version()) fmt.Println("OS/Arch:", runtime.GOOS+"/"+runtime.GOARCH) fmt.Println("Git Commit:", constant.Commit) fmt.Println("") fmt.Println("Developed by PageFault (p4gefau1t)") fmt.Println("Licensed under GNU General Public License version 3") fmt.Println("GitHub Repository:\thttps://github.com/p4gefau1t/trojan-go") fmt.Println("Trojan-Go Documents:\thttps://p4gefau1t.github.io/trojan-go/") return nil } return common.NewError("not set") } func init() { option.RegisterHandler(&versionOption{ flag: flag.Bool("version", false, "Display version and help info"), }) }