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)/$*
@zip -du $(NAME)-$@ example/*
@-zip -du $(NAME)-$@ *.dat
@echo "<<< ---- $(NAME)-$@"
release: geosite.dat geoip.dat geoip-only-cn-private.dat darwin-amd64.zip darwin-arm64.zip linux-386.zip linux-amd64.zip \
linux-arm.zip linux-armv5.zip linux-armv6.zip linux-armv7.zip linux-armv8.zip \
linux-mips-softfloat.zip linux-mips-hardfloat.zip linux-mipsle-softfloat.zip linux-mipsle-hardfloat.zip \
linux-mips64.zip linux-mips64le.zip freebsd-386.zip freebsd-amd64.zip \
windows-386.zip windows-amd64.zip windows-arm.zip windows-armv6.zip windows-armv7.zip windows-arm64.zip
darwin-amd64:
mkdir -p $(BUILD_DIR)/$@
GOARCH=amd64 GOOS=darwin $(GOBUILD)/$@
darwin-arm64:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm64 GOOS=darwin $(GOBUILD)/$@
linux-386:
mkdir -p $(BUILD_DIR)/$@
GOARCH=386 GOOS=linux $(GOBUILD)/$@
linux-amd64:
mkdir -p $(BUILD_DIR)/$@
GOARCH=amd64 GOOS=linux $(GOBUILD)/$@
linux-arm:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm GOOS=linux $(GOBUILD)/$@
linux-armv5:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm GOOS=linux GOARM=5 $(GOBUILD)/$@
linux-armv6:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm GOOS=linux GOARM=6 $(GOBUILD)/$@
linux-armv7:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm GOOS=linux GOARM=7 $(GOBUILD)/$@
linux-armv8:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm64 GOOS=linux $(GOBUILD)/$@
linux-mips-softfloat:
mkdir -p $(BUILD_DIR)/$@
GOARCH=mips GOMIPS=softfloat GOOS=linux $(GOBUILD)/$@
linux-mips-hardfloat:
mkdir -p $(BUILD_DIR)/$@
GOARCH=mips GOMIPS=hardfloat GOOS=linux $(GOBUILD)/$@
linux-mipsle-softfloat:
mkdir -p $(BUILD_DIR)/$@
GOARCH=mipsle GOMIPS=softfloat GOOS=linux $(GOBUILD)/$@
linux-mipsle-hardfloat:
mkdir -p $(BUILD_DIR)/$@
GOARCH=mipsle GOMIPS=hardfloat GOOS=linux $(GOBUILD)/$@
linux-mips64:
mkdir -p $(BUILD_DIR)/$@
GOARCH=mips64 GOOS=linux $(GOBUILD)/$@
linux-mips64le:
mkdir -p $(BUILD_DIR)/$@
GOARCH=mips64le GOOS=linux $(GOBUILD)/$@
freebsd-386:
mkdir -p $(BUILD_DIR)/$@
GOARCH=386 GOOS=freebsd $(GOBUILD)/$@
freebsd-amd64:
mkdir -p $(BUILD_DIR)/$@
GOARCH=amd64 GOOS=freebsd $(GOBUILD)/$@
windows-386:
mkdir -p $(BUILD_DIR)/$@
GOARCH=386 GOOS=windows $(GOBUILD)/$@
windows-amd64:
mkdir -p $(BUILD_DIR)/$@
GOARCH=amd64 GOOS=windows $(GOBUILD)/$@
windows-arm:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm GOOS=windows $(GOBUILD)/$@
windows-armv6:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm GOOS=windows GOARM=6 $(GOBUILD)/$@
windows-armv7:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm GOOS=windows GOARM=7 $(GOBUILD)/$@
windows-arm64:
mkdir -p $(BUILD_DIR)/$@
GOARCH=arm64 GOOS=windows $(GOBUILD)/$@
================================================
FILE: README.md
================================================
# Trojan-Go [](https://goreportcard.com/report/github.com/p4gefau1t/trojan-go) [](https://img.shields.io/github/downloads/p4gefau1t/trojan-go/total?label=downloads&logo=github&style=flat-square)
使用 Go 实现的完整 Trojan 代理,兼容原版 Trojan 协议及配置文件格式。安全、高效、轻巧、易用。
Trojan-Go 支持[多路复用](#多路复用)提升并发性能;使用[路由模块](#路由模块)实现国内外分流;支持 [CDN 流量中转](#Websocket)(基于 WebSocket over TLS);支持使用 AEAD 对 Trojan 流量进行[二次加密](#aead-加密)(基于 Shadowsocks AEAD);支持可插拔的[传输层插件](#传输层插件),允许替换 TLS,使用其他加密隧道传输 Trojan 协议流量。
预编译二进制可执行文件可在 [Release 页面](https://github.com/p4gefau1t/trojan-go/releases)下载。解压后即可直接运行,无其他组件依赖。
如遇到配置和使用问题、发现 bug,或是有更好的想法,欢迎加入 [Telegram 交流反馈群](https://t.me/trojan_go_chat)。
## 简介
**完整介绍和配置教程,参见 [Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。**
Trojan-Go 兼容原版 Trojan 的绝大多数功能,包括但不限于:
- TLS 隧道传输
- UDP 代理
- 透明代理 (NAT 模式,iptables 设置参考[这里](https://github.com/shadowsocks/shadowsocks-libev/tree/v3.3.1#transparent-proxy))
- 对抗 GFW 被动检测 / 主动检测的机制
- MySQL 数据持久化方案
- MySQL 用户权限认证
- 用户流量统计和配额限制
同时,Trojan-Go 还扩展实现了更多高效易用的功能特性:
- 便于快速部署的「简易模式」
- Socks5 / HTTP 代理自动适配
- 基于 TProxy 的透明代理(TCP / UDP)
- 全平台支持,无特殊依赖
- 基于多路复用(smux)降低延迟,提升并发性能
- 自定义路由模块,可实现国内外分流 / 广告屏蔽等功能
- Websocket 传输支持,以实现 CDN 流量中转(基于 WebSocket over TLS)和对抗 GFW 中间人攻击
- TLS 指纹伪造,以对抗 GFW 针对 TLS Client Hello 的特征识别
- 基于 gRPC 的 API 支持,以实现用户管理和速度限制等
- 可插拔传输层,可将 TLS 替换为其他协议或明文传输,同时有完整的 Shadowsocks 混淆插件支持
- 支持对用户更友好的 YAML 配置文件格式
## 图形界面客户端
Trojan-Go 服务端兼容所有原 Trojan 客户端,如 Igniter、ShadowRocket 等。以下是支持 Trojan-Go 扩展特性(Websocket / Mux 等)的客户端:
- [Qv2ray](https://github.com/Qv2ray/Qv2ray):跨平台客户端,支持 Windows / macOS / Linux,使用 Trojan-Go 核心,支持所有 Trojan-Go 扩展特性。
- [Igniter-Go](https://github.com/p4gefau1t/trojan-go-android):Android 客户端,Fork 自 Igniter,将 Igniter 核心替换为 Trojan-Go 并做了一定修改,支持所有 Trojan-Go 扩展特性。
## 使用方法
1. 快速启动服务端和客户端(简易模式)
- 服务端
```shell
sudo ./trojan-go -server -remote 127.0.0.1:80 -local 0.0.0.0:443 -key ./your_key.key -cert ./your_cert.crt -password your_password
```
- 客户端
```shell
./trojan-go -client -remote example.com:443 -local 127.0.0.1:1080 -password your_password
```
2. 使用配置文件启动客户端 / 服务端 / 透明代理 / 中继(一般模式)
```shell
./trojan-go -config config.json
```
3. 使用 URL 启动客户端(格式参见文档)
```shell
./trojan-go -url 'trojan-go://password@cloudflare.com/?type=ws&path=%2Fpath&host=your-site.com'
```
4. 使用 Docker 部署
```shell
docker run \
--name trojan-go \
-d \
-v /etc/trojan-go/:/etc/trojan-go \
--network host \
p4gefau1t/trojan-go
```
或者
```shell
docker run \
--name trojan-go \
-d \
-v /path/to/host/config:/path/in/container \
--network host \
p4gefau1t/trojan-go \
/path/in/container/config.json
```
## 特性
一般情况下,Trojan-Go 和 Trojan 是互相兼容的,但一旦使用下面介绍的扩展特性(如多路复用、Websocket 等),则无法兼容。
### 移植性
编译得到的 Trojan-Go 单个可执行文件不依赖其他组件。同时,你可以很方便地编译(或交叉编译) Trojan-Go,然后在你的服务器、PC、树莓派,甚至路由器上部署;可以方便地使用 build tag 删减模块,以缩小可执行文件体积。
例如,交叉编译一个可在 mips 处理器、Linux 操作系统上运行的、只有客户端功能的 Trojan-Go,只需执行下面的命令,得到的可执行文件可以直接在目标平台运行:
```shell
CGO_ENABLED=0 GOOS=linux GOARCH=mips go build -tags "client" -trimpath -ldflags "-s -w -buildid="
```
完整的 tag 说明参见 [Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。
### 易用
配置文件格式与原版 Trojan 兼容,但做了大幅简化,未指定的字段会被赋予默认值,由此可以更方便地部署服务端和客户端。以下是一个简单例子,完整的配置文件可以参见[这里](https://p4gefau1t.github.io/trojan-go)。
服务端配置文件 `server.json`:
```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": "your_cert.crt",
"key": "your_key.key",
"sni": "www.your-awesome-domain-name.com"
}
}
```
客户端配置文件 `client.json`:
```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_awesome_password"]
}
```
可以使用更简明易读的 YAML 语法进行配置。以下是一个客户端的例子,与上面的 `client.json` 等价:
客户端配置文件 `client.yaml`:
```yaml
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_awesome_password
```
### WebSocket
Trojan-Go 支持使用 TLS + Websocket 承载 Trojan 协议,使得利用 CDN 进行流量中转成为可能。
服务端和客户端配置文件中同时添加 `websocket` 选项即可启用 Websocket 支持,例如
```json
"websocket": {
"enabled": true,
"path": "/your-websocket-path",
"hostname": "www.your-awesome-domain-name.com"
}
```
完整的选项说明参见 [Trojan-Go 文档](https://p4gefau1t.github.io/trojan-go)。
可以省略 `hostname`, 但服务端和客户端的 `path` 必须一致。服务端开启 Websocket 支持后,可以同时支持 Websocket 和一般 Trojan 流量。未配置 Websocket 选项的客户端依然可以正常使用。
由于 Trojan 并不支持 Websocket,因此,虽然开启了 Websocket 支持的 Trojan-Go 服务端可以兼容所有客户端,但如果要使用 Websocket 承载流量,请确保双方都使用 Trojan-Go。
### 多路复用
在很差的网络条件下,一次 TLS 握手可能会花费很多时间。Trojan-Go 支持多路复用(基于 [smux](https://github.com/xtaci/smux)),通过一条 TLS 隧道连接承载多条 TCP 连接的方式,减少 TCP 和 TLS 握手带来的延迟,以期提升高并发情景下的性能。
> 启用多路复用并不能提高测速得到的链路速度,但能降低延迟、提升大量并发请求时的网络体验,例如浏览含有大量图片的网页等。
你可以通过设置客户端的 `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
[](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"),
})
}