├── .github
├── dependabot.yml
└── workflows
│ ├── docker.yml
│ └── release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── cipher
└── cipher.go
├── config
├── config.go
├── outline.go
├── outline_test.go
├── upstream.go
└── userContext.go
├── dispatcher
├── dispatcher.go
├── infra
│ └── infra.go
├── tcp
│ ├── tcp.go
│ └── tcp_test.go
└── udp
│ ├── ipMTUTrie.go
│ ├── udp.go
│ ├── udpConn.go
│ └── udp_test.go
├── example.json
├── example_fullview.json
├── go.mod
├── go.sum
├── infra
├── linklist
│ └── linklist.go
├── lru
│ └── lru.go
├── lrulist
│ ├── growingPool.go
│ ├── lrulist.go
│ └── updater.go
├── pool
│ └── pool.go
└── trie
│ ├── trie.go
│ └── trie_test.go
├── logo.png
├── logo.svg
├── main.go
├── release
└── friendly-filenames.json
├── reload.go
├── signal_other.go
├── signal_windows.go
└── systemd
├── README.md
├── mmp-go-reload.service
├── mmp-go-reload.timer
├── mmp-go-reload@.service
├── mmp-go-reload@.timer
├── mmp-go.service
└── mmp-go@.service
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: gomod
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | - package-ecosystem: github-actions
8 | directory: "/"
9 | schedule:
10 | interval: daily
11 |
--------------------------------------------------------------------------------
/.github/workflows/docker.yml:
--------------------------------------------------------------------------------
1 | name: docker image
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*
7 |
8 | env:
9 | DOCKER_CLI_EXPERIMENTAL: enabled
10 | DOCKER_BUILDKIT: 1
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Get the version
17 | id: get_version
18 | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3)
19 | - uses: actions/checkout@v3
20 | - name: Set up QEMU
21 | uses: docker/setup-qemu-action@v1
22 | - name: Set up Docker Buildx
23 | uses: docker/setup-buildx-action@v1
24 | - name: Build the Docker image
25 | run: |
26 | docker login -u ${{secrets.DOCKER_USERNAME}} -p ${{secrets.DOCKER_PASSWORD}}
27 | v=$(echo ${{ steps.get_version.outputs.VERSION }} | cut -c2-)
28 | docker buildx build -t mzz2017/mmp-go:$v -f Dockerfile --platform=linux/arm,linux/arm64,linux/amd64 . --push
29 | docker buildx build -t mzz2017/mmp-go:latest -f Dockerfile --platform=linux/arm,linux/arm64,linux/amd64 . --push
30 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types: [prereleased]
6 | push:
7 | branches:
8 | - main
9 | - v*
10 | - dev*
11 | paths:
12 | - "**/*.go"
13 | - "go.mod"
14 | - "go.sum"
15 | - ".github/workflows/*.yml"
16 | pull_request:
17 | types: [opened, synchronize, reopened]
18 | paths:
19 | - "**/*.go"
20 | - "go.mod"
21 | - "go.sum"
22 | - ".github/workflows/*.yml"
23 |
24 | jobs:
25 | build:
26 | strategy:
27 | matrix:
28 | # Include amd64 on all platforms.
29 | goos: [windows, freebsd, openbsd, linux, dragonfly, darwin]
30 | goarch: [amd64, 386]
31 | exclude:
32 | # Exclude i386 on darwin and dragonfly.
33 | - goarch: 386
34 | goos: dragonfly
35 | - goarch: 386
36 | goos: darwin
37 | include:
38 | # BEGIN Linux ARM 5 6 7
39 | - goos: linux
40 | goarch: arm
41 | goarm: 7
42 | - goos: linux
43 | goarch: arm
44 | goarm: 6
45 | - goos: linux
46 | goarch: arm
47 | goarm: 5
48 | # END Linux ARM 5 6 7
49 | # Windows ARM 7
50 | - goos: windows
51 | goarch: arm
52 | goarm: 7
53 | # BEGIN Other architectures
54 | - goos: darwin
55 | goarch: arm64
56 | - goos: linux
57 | goarch: arm64
58 | - goos: linux
59 | goarch: riscv64
60 | - goos: windows
61 | goarch: arm64
62 | # BEGIN MIPS
63 | - goos: linux
64 | goarch: mips64
65 | - goos: linux
66 | goarch: mips64le
67 | - goos: linux
68 | goarch: mipsle
69 | - goos: linux
70 | goarch: mips
71 | # END MIPS
72 | # END Other architectures
73 | fail-fast: false
74 |
75 | runs-on: ubuntu-latest
76 | env:
77 | GOOS: ${{ matrix.goos }}
78 | GOARCH: ${{ matrix.goarch }}
79 | GOARM: ${{ matrix.goarm }}
80 | CGO_ENABLED: 0
81 |
82 | steps:
83 | - name: Get the version
84 | id: get_version
85 | run: echo ::set-output name=VERSION::$(echo $GITHUB_REF | cut -d / -f 3)
86 |
87 | - name: Checkout codebase
88 | uses: actions/checkout@v3
89 | with:
90 | fetch-depth: 0
91 |
92 | - name: Show workflow information
93 | id: get_filename
94 | run: |
95 | export _NAME=$(jq ".[\"$GOOS-$GOARCH$GOARM\"].friendlyName" -r < release/friendly-filenames.json)
96 | echo "GOOS: $GOOS, GOARCH: $GOARCH, GOARM: $GOARM, RELEASE_NAME: $_NAME"
97 | echo "::set-output name=ASSET_NAME::$_NAME"
98 | echo "ASSET_NAME=$_NAME" >> $GITHUB_ENV
99 | - name: Set up Go
100 | uses: actions/setup-go@v2
101 | with:
102 | stable: true
103 | go-version: '1.17'
104 |
105 | - name: Get project dependencies
106 | run: go mod download
107 |
108 | - name: Build mmp-go
109 | run: |
110 | v=$(echo ${{ steps.get_version.outputs.VERSION }} | cut -c2-)
111 | mkdir -p build_assets
112 | go build -v -o build_assets/mmp-go -trimpath -ldflags "-X github.com/Qv2ray/mmp-go/config.Version=$v -s -w -buildid=" .
113 | - name: Build Windows wmmp-go
114 | if: matrix.goos == 'windows'
115 | run: |
116 | v=$(echo ${{ steps.get_version.outputs.VERSION }} | cut -c2-)
117 | go build -v -o build_assets/wmmp-go.exe -trimpath -ldflags "-X github.com/Qv2ray/mmp-go/config.Version=$v -s -w -H windowsgui -buildid=" .
118 | cd ./build_assets || exit 1
119 | mv mmp-go mmp-go.exe
120 | - name: Prepare package
121 | run: cp -v ./example.json ./build_assets
122 |
123 | - name: Prepare package for Linux
124 | if: matrix.goos == 'linux'
125 | run: cp -rv ./systemd ./build_assets/
126 |
127 | - name: Create ZIP archive
128 | run: |
129 | pushd build_assets || exit 1
130 | zip -9vr ../mmp-go-$ASSET_NAME.zip .
131 | popd || exit 1
132 | FILE=./mmp-go-$ASSET_NAME.zip
133 | DGST=$FILE.dgst
134 | openssl dgst -md5 $FILE | sed 's/([^)]*)//g' >>$DGST
135 | openssl dgst -sha1 $FILE | sed 's/([^)]*)//g' >>$DGST
136 | openssl dgst -sha256 $FILE | sed 's/([^)]*)//g' >>$DGST
137 | openssl dgst -sha512 $FILE | sed 's/([^)]*)//g' >>$DGST
138 | - name: Upload ZIP file to Artifacts
139 | uses: actions/upload-artifact@v2
140 | with:
141 | name: mmp-go-${{ steps.get_filename.outputs.ASSET_NAME }}.zip
142 | path: mmp-go-${{ steps.get_filename.outputs.ASSET_NAME }}.zip
143 |
144 | - name: Upload files to GitHub release
145 | uses: svenstaro/upload-release-action@v2
146 | if: github.event_name == 'release'
147 | with:
148 | repo_token: ${{ secrets.GITHUB_TOKEN }}
149 | file_glob: true
150 | file: ./mmp-go-${{ steps.get_filename.outputs.ASSET_NAME }}.zip*
151 | tag: ${{ github.ref }}
152 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 | /shadomplexer-go
8 | /mmp-go*
9 |
10 | # Test binary, built with `go test -c`
11 | *.test
12 |
13 | # Output of the go coverage tool, specifically when used with LiteIDE
14 | *.out
15 |
16 | # Dependency directories (remove the comment below to include it)
17 | # vendor/
18 |
19 | # IDE
20 | .idea
21 | .vscode
22 |
23 | *_bak.json
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mzz2017/git:alpine AS version
2 | WORKDIR /build
3 | ADD .git ./.git
4 | RUN git describe --abbrev=0 --tags > ./version
5 |
6 | FROM golang:1.16-alpine AS builder
7 | WORKDIR /build
8 | ADD . .
9 | ENV GO111MODULE=on
10 | ENV GOPROXY=https://goproxy.io
11 | COPY --from=version /build/version ./
12 | RUN export VERSION=$(cat ./version) && CGO_ENABLED=0 go build -ldflags '-X github.com/Qv2ray/mmp-go/config.Version=${VERSION} -s -w -extldflags "-static"' -o mmp-go .
13 |
14 | FROM alpine
15 | COPY --from=builder /build/mmp-go /usr/bin/
16 | VOLUME /etc/mmp-go
17 | ENTRYPOINT ["mmp-go", "-conf", "/etc/mmp-go/config.json"]
18 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU AFFERO GENERAL PUBLIC LICENSE
2 | Version 3, 19 November 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 | Preamble
9 |
10 | The GNU Affero General Public License is a free, copyleft license for
11 | software and other kinds of works, specifically designed to ensure
12 | cooperation with the community in the case of network server software.
13 |
14 | The licenses for most software and other practical works are designed
15 | to take away your freedom to share and change the works. By contrast,
16 | our General Public Licenses are intended to guarantee your freedom to
17 | share and change all versions of a program--to make sure it remains free
18 | software for all its users.
19 |
20 | When we speak of free software, we are referring to freedom, not
21 | price. Our General Public Licenses are designed to make sure that you
22 | have the freedom to distribute copies of free software (and charge for
23 | them if you wish), that you receive source code or can get it if you
24 | want it, that you can change the software or use pieces of it in new
25 | free programs, and that you know you can do these things.
26 |
27 | Developers that use our General Public Licenses protect your rights
28 | with two steps: (1) assert copyright on the software, and (2) offer
29 | you this License which gives you legal permission to copy, distribute
30 | and/or modify the software.
31 |
32 | A secondary benefit of defending all users' freedom is that
33 | improvements made in alternate versions of the program, if they
34 | receive widespread use, become available for other developers to
35 | incorporate. Many developers of free software are heartened and
36 | encouraged by the resulting cooperation. However, in the case of
37 | software used on network servers, this result may fail to come about.
38 | The GNU General Public License permits making a modified version and
39 | letting the public access it on a server without ever releasing its
40 | source code to the public.
41 |
42 | The GNU Affero General Public License is designed specifically to
43 | ensure that, in such cases, the modified source code becomes available
44 | to the community. It requires the operator of a network server to
45 | provide the source code of the modified version running there to the
46 | users of that server. Therefore, public use of a modified version, on
47 | a publicly accessible server, gives the public access to the source
48 | code of the modified version.
49 |
50 | An older license, called the Affero General Public License and
51 | published by Affero, was designed to accomplish similar goals. This is
52 | a different license, not a version of the Affero GPL, but Affero has
53 | released a new version of the Affero GPL which permits relicensing under
54 | this license.
55 |
56 | The precise terms and conditions for copying, distribution and
57 | modification follow.
58 |
59 | TERMS AND CONDITIONS
60 |
61 | 0. Definitions.
62 |
63 | "This License" refers to version 3 of the GNU Affero General Public License.
64 |
65 | "Copyright" also means copyright-like laws that apply to other kinds of
66 | works, such as semiconductor masks.
67 |
68 | "The Program" refers to any copyrightable work licensed under this
69 | License. Each licensee is addressed as "you". "Licensees" and
70 | "recipients" may be individuals or organizations.
71 |
72 | To "modify" a work means to copy from or adapt all or part of the work
73 | in a fashion requiring copyright permission, other than the making of an
74 | exact copy. The resulting work is called a "modified version" of the
75 | earlier work or a work "based on" the earlier work.
76 |
77 | A "covered work" means either the unmodified Program or a work based
78 | on the Program.
79 |
80 | To "propagate" a work means to do anything with it that, without
81 | permission, would make you directly or secondarily liable for
82 | infringement under applicable copyright law, except executing it on a
83 | computer or modifying a private copy. Propagation includes copying,
84 | distribution (with or without modification), making available to the
85 | public, and in some countries other activities as well.
86 |
87 | To "convey" a work means any kind of propagation that enables other
88 | parties to make or receive copies. Mere interaction with a user through
89 | a computer network, with no transfer of a copy, is not conveying.
90 |
91 | An interactive user interface displays "Appropriate Legal Notices"
92 | to the extent that it includes a convenient and prominently visible
93 | feature that (1) displays an appropriate copyright notice, and (2)
94 | tells the user that there is no warranty for the work (except to the
95 | extent that warranties are provided), that licensees may convey the
96 | work under this License, and how to view a copy of this License. If
97 | the interface presents a list of user commands or options, such as a
98 | menu, a prominent item in the list meets this criterion.
99 |
100 | 1. Source Code.
101 |
102 | The "source code" for a work means the preferred form of the work
103 | for making modifications to it. "Object code" means any non-source
104 | form of a work.
105 |
106 | A "Standard Interface" means an interface that either is an official
107 | standard defined by a recognized standards body, or, in the case of
108 | interfaces specified for a particular programming language, one that
109 | is widely used among developers working in that language.
110 |
111 | The "System Libraries" of an executable work include anything, other
112 | than the work as a whole, that (a) is included in the normal form of
113 | packaging a Major Component, but which is not part of that Major
114 | Component, and (b) serves only to enable use of the work with that
115 | Major Component, or to implement a Standard Interface for which an
116 | implementation is available to the public in source code form. A
117 | "Major Component", in this context, means a major essential component
118 | (kernel, window system, and so on) of the specific operating system
119 | (if any) on which the executable work runs, or a compiler used to
120 | produce the work, or an object code interpreter used to run it.
121 |
122 | The "Corresponding Source" for a work in object code form means all
123 | the source code needed to generate, install, and (for an executable
124 | work) run the object code and to modify the work, including scripts to
125 | control those activities. However, it does not include the work's
126 | System Libraries, or general-purpose tools or generally available free
127 | programs which are used unmodified in performing those activities but
128 | which are not part of the work. For example, Corresponding Source
129 | includes interface definition files associated with source files for
130 | the work, and the source code for shared libraries and dynamically
131 | linked subprograms that the work is specifically designed to require,
132 | such as by intimate data communication or control flow between those
133 | subprograms and other parts of the work.
134 |
135 | The Corresponding Source need not include anything that users
136 | can regenerate automatically from other parts of the Corresponding
137 | Source.
138 |
139 | The Corresponding Source for a work in source code form is that
140 | same work.
141 |
142 | 2. Basic Permissions.
143 |
144 | All rights granted under this License are granted for the term of
145 | copyright on the Program, and are irrevocable provided the stated
146 | conditions are met. This License explicitly affirms your unlimited
147 | permission to run the unmodified Program. The output from running a
148 | covered work is covered by this License only if the output, given its
149 | content, constitutes a covered work. This License acknowledges your
150 | rights of fair use or other equivalent, as provided by copyright law.
151 |
152 | You may make, run and propagate covered works that you do not
153 | convey, without conditions so long as your license otherwise remains
154 | in force. You may convey covered works to others for the sole purpose
155 | of having them make modifications exclusively for you, or provide you
156 | with facilities for running those works, provided that you comply with
157 | the terms of this License in conveying all material for which you do
158 | not control copyright. Those thus making or running the covered works
159 | for you must do so exclusively on your behalf, under your direction
160 | and control, on terms that prohibit them from making any copies of
161 | your copyrighted material outside their relationship with you.
162 |
163 | Conveying under any other circumstances is permitted solely under
164 | the conditions stated below. Sublicensing is not allowed; section 10
165 | makes it unnecessary.
166 |
167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
168 |
169 | No covered work shall be deemed part of an effective technological
170 | measure under any applicable law fulfilling obligations under article
171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or
172 | similar laws prohibiting or restricting circumvention of such
173 | measures.
174 |
175 | When you convey a covered work, you waive any legal power to forbid
176 | circumvention of technological measures to the extent such circumvention
177 | is effected by exercising rights under this License with respect to
178 | the covered work, and you disclaim any intention to limit operation or
179 | modification of the work as a means of enforcing, against the work's
180 | users, your or third parties' legal rights to forbid circumvention of
181 | technological measures.
182 |
183 | 4. Conveying Verbatim Copies.
184 |
185 | You may convey verbatim copies of the Program's source code as you
186 | receive it, in any medium, provided that you conspicuously and
187 | appropriately publish on each copy an appropriate copyright notice;
188 | keep intact all notices stating that this License and any
189 | non-permissive terms added in accord with section 7 apply to the code;
190 | keep intact all notices of the absence of any warranty; and give all
191 | recipients a copy of this License along with the Program.
192 |
193 | You may charge any price or no price for each copy that you convey,
194 | and you may offer support or warranty protection for a fee.
195 |
196 | 5. Conveying Modified Source Versions.
197 |
198 | You may convey a work based on the Program, or the modifications to
199 | produce it from the Program, in the form of source code under the
200 | terms of section 4, provided that you also meet all of these conditions:
201 |
202 | a) The work must carry prominent notices stating that you modified
203 | it, and giving a relevant date.
204 |
205 | b) The work must carry prominent notices stating that it is
206 | released under this License and any conditions added under section
207 | 7. This requirement modifies the requirement in section 4 to
208 | "keep intact all notices".
209 |
210 | c) You must license the entire work, as a whole, under this
211 | License to anyone who comes into possession of a copy. This
212 | License will therefore apply, along with any applicable section 7
213 | additional terms, to the whole of the work, and all its parts,
214 | regardless of how they are packaged. This License gives no
215 | permission to license the work in any other way, but it does not
216 | invalidate such permission if you have separately received it.
217 |
218 | d) If the work has interactive user interfaces, each must display
219 | Appropriate Legal Notices; however, if the Program has interactive
220 | interfaces that do not display Appropriate Legal Notices, your
221 | work need not make them do so.
222 |
223 | A compilation of a covered work with other separate and independent
224 | works, which are not by their nature extensions of the covered work,
225 | and which are not combined with it such as to form a larger program,
226 | in or on a volume of a storage or distribution medium, is called an
227 | "aggregate" if the compilation and its resulting copyright are not
228 | used to limit the access or legal rights of the compilation's users
229 | beyond what the individual works permit. Inclusion of a covered work
230 | in an aggregate does not cause this License to apply to the other
231 | parts of the aggregate.
232 |
233 | 6. Conveying Non-Source Forms.
234 |
235 | You may convey a covered work in object code form under the terms
236 | of sections 4 and 5, provided that you also convey the
237 | machine-readable Corresponding Source under the terms of this License,
238 | in one of these ways:
239 |
240 | a) Convey the object code in, or embodied in, a physical product
241 | (including a physical distribution medium), accompanied by the
242 | Corresponding Source fixed on a durable physical medium
243 | customarily used for software interchange.
244 |
245 | b) Convey the object code in, or embodied in, a physical product
246 | (including a physical distribution medium), accompanied by a
247 | written offer, valid for at least three years and valid for as
248 | long as you offer spare parts or customer support for that product
249 | model, to give anyone who possesses the object code either (1) a
250 | copy of the Corresponding Source for all the software in the
251 | product that is covered by this License, on a durable physical
252 | medium customarily used for software interchange, for a price no
253 | more than your reasonable cost of physically performing this
254 | conveying of source, or (2) access to copy the
255 | Corresponding Source from a network server at no charge.
256 |
257 | c) Convey individual copies of the object code with a copy of the
258 | written offer to provide the Corresponding Source. This
259 | alternative is allowed only occasionally and noncommercially, and
260 | only if you received the object code with such an offer, in accord
261 | with subsection 6b.
262 |
263 | d) Convey the object code by offering access from a designated
264 | place (gratis or for a charge), and offer equivalent access to the
265 | Corresponding Source in the same way through the same place at no
266 | further charge. You need not require recipients to copy the
267 | Corresponding Source along with the object code. If the place to
268 | copy the object code is a network server, the Corresponding Source
269 | may be on a different server (operated by you or a third party)
270 | that supports equivalent copying facilities, provided you maintain
271 | clear directions next to the object code saying where to find the
272 | Corresponding Source. Regardless of what server hosts the
273 | Corresponding Source, you remain obligated to ensure that it is
274 | available for as long as needed to satisfy these requirements.
275 |
276 | e) Convey the object code using peer-to-peer transmission, provided
277 | you inform other peers where the object code and Corresponding
278 | Source of the work are being offered to the general public at no
279 | charge under subsection 6d.
280 |
281 | A separable portion of the object code, whose source code is excluded
282 | from the Corresponding Source as a System Library, need not be
283 | included in conveying the object code work.
284 |
285 | A "User Product" is either (1) a "consumer product", which means any
286 | tangible personal property which is normally used for personal, family,
287 | or household purposes, or (2) anything designed or sold for incorporation
288 | into a dwelling. In determining whether a product is a consumer product,
289 | doubtful cases shall be resolved in favor of coverage. For a particular
290 | product received by a particular user, "normally used" refers to a
291 | typical or common use of that class of product, regardless of the status
292 | of the particular user or of the way in which the particular user
293 | actually uses, or expects or is expected to use, the product. A product
294 | is a consumer product regardless of whether the product has substantial
295 | commercial, industrial or non-consumer uses, unless such uses represent
296 | the only significant mode of use of the product.
297 |
298 | "Installation Information" for a User Product means any methods,
299 | procedures, authorization keys, or other information required to install
300 | and execute modified versions of a covered work in that User Product from
301 | a modified version of its Corresponding Source. The information must
302 | suffice to ensure that the continued functioning of the modified object
303 | code is in no case prevented or interfered with solely because
304 | modification has been made.
305 |
306 | If you convey an object code work under this section in, or with, or
307 | specifically for use in, a User Product, and the conveying occurs as
308 | part of a transaction in which the right of possession and use of the
309 | User Product is transferred to the recipient in perpetuity or for a
310 | fixed term (regardless of how the transaction is characterized), the
311 | Corresponding Source conveyed under this section must be accompanied
312 | by the Installation Information. But this requirement does not apply
313 | if neither you nor any third party retains the ability to install
314 | modified object code on the User Product (for example, the work has
315 | been installed in ROM).
316 |
317 | The requirement to provide Installation Information does not include a
318 | requirement to continue to provide support service, warranty, or updates
319 | for a work that has been modified or installed by the recipient, or for
320 | the User Product in which it has been modified or installed. Access to a
321 | network may be denied when the modification itself materially and
322 | adversely affects the operation of the network or violates the rules and
323 | protocols for communication across the network.
324 |
325 | Corresponding Source conveyed, and Installation Information provided,
326 | in accord with this section must be in a format that is publicly
327 | documented (and with an implementation available to the public in
328 | source code form), and must require no special password or key for
329 | unpacking, reading or copying.
330 |
331 | 7. Additional Terms.
332 |
333 | "Additional permissions" are terms that supplement the terms of this
334 | License by making exceptions from one or more of its conditions.
335 | Additional permissions that are applicable to the entire Program shall
336 | be treated as though they were included in this License, to the extent
337 | that they are valid under applicable law. If additional permissions
338 | apply only to part of the Program, that part may be used separately
339 | under those permissions, but the entire Program remains governed by
340 | this License without regard to the additional permissions.
341 |
342 | When you convey a copy of a covered work, you may at your option
343 | remove any additional permissions from that copy, or from any part of
344 | it. (Additional permissions may be written to require their own
345 | removal in certain cases when you modify the work.) You may place
346 | additional permissions on material, added by you to a covered work,
347 | for which you have or can give appropriate copyright permission.
348 |
349 | Notwithstanding any other provision of this License, for material you
350 | add to a covered work, you may (if authorized by the copyright holders of
351 | that material) supplement the terms of this License with terms:
352 |
353 | a) Disclaiming warranty or limiting liability differently from the
354 | terms of sections 15 and 16 of this License; or
355 |
356 | b) Requiring preservation of specified reasonable legal notices or
357 | author attributions in that material or in the Appropriate Legal
358 | Notices displayed by works containing it; or
359 |
360 | c) Prohibiting misrepresentation of the origin of that material, or
361 | requiring that modified versions of such material be marked in
362 | reasonable ways as different from the original version; or
363 |
364 | d) Limiting the use for publicity purposes of names of licensors or
365 | authors of the material; or
366 |
367 | e) Declining to grant rights under trademark law for use of some
368 | trade names, trademarks, or service marks; or
369 |
370 | f) Requiring indemnification of licensors and authors of that
371 | material by anyone who conveys the material (or modified versions of
372 | it) with contractual assumptions of liability to the recipient, for
373 | any liability that these contractual assumptions directly impose on
374 | those licensors and authors.
375 |
376 | All other non-permissive additional terms are considered "further
377 | restrictions" within the meaning of section 10. If the Program as you
378 | received it, or any part of it, contains a notice stating that it is
379 | governed by this License along with a term that is a further
380 | restriction, you may remove that term. If a license document contains
381 | a further restriction but permits relicensing or conveying under this
382 | License, you may add to a covered work material governed by the terms
383 | of that license document, provided that the further restriction does
384 | not survive such relicensing or conveying.
385 |
386 | If you add terms to a covered work in accord with this section, you
387 | must place, in the relevant source files, a statement of the
388 | additional terms that apply to those files, or a notice indicating
389 | where to find the applicable terms.
390 |
391 | Additional terms, permissive or non-permissive, may be stated in the
392 | form of a separately written license, or stated as exceptions;
393 | the above requirements apply either way.
394 |
395 | 8. Termination.
396 |
397 | You may not propagate or modify a covered work except as expressly
398 | provided under this License. Any attempt otherwise to propagate or
399 | modify it is void, and will automatically terminate your rights under
400 | this License (including any patent licenses granted under the third
401 | paragraph of section 11).
402 |
403 | However, if you cease all violation of this License, then your
404 | license from a particular copyright holder is reinstated (a)
405 | provisionally, unless and until the copyright holder explicitly and
406 | finally terminates your license, and (b) permanently, if the copyright
407 | holder fails to notify you of the violation by some reasonable means
408 | prior to 60 days after the cessation.
409 |
410 | Moreover, your license from a particular copyright holder is
411 | reinstated permanently if the copyright holder notifies you of the
412 | violation by some reasonable means, this is the first time you have
413 | received notice of violation of this License (for any work) from that
414 | copyright holder, and you cure the violation prior to 30 days after
415 | your receipt of the notice.
416 |
417 | Termination of your rights under this section does not terminate the
418 | licenses of parties who have received copies or rights from you under
419 | this License. If your rights have been terminated and not permanently
420 | reinstated, you do not qualify to receive new licenses for the same
421 | material under section 10.
422 |
423 | 9. Acceptance Not Required for Having Copies.
424 |
425 | You are not required to accept this License in order to receive or
426 | run a copy of the Program. Ancillary propagation of a covered work
427 | occurring solely as a consequence of using peer-to-peer transmission
428 | to receive a copy likewise does not require acceptance. However,
429 | nothing other than this License grants you permission to propagate or
430 | modify any covered work. These actions infringe copyright if you do
431 | not accept this License. Therefore, by modifying or propagating a
432 | covered work, you indicate your acceptance of this License to do so.
433 |
434 | 10. Automatic Licensing of Downstream Recipients.
435 |
436 | Each time you convey a covered work, the recipient automatically
437 | receives a license from the original licensors, to run, modify and
438 | propagate that work, subject to this License. You are not responsible
439 | for enforcing compliance by third parties with this License.
440 |
441 | An "entity transaction" is a transaction transferring control of an
442 | organization, or substantially all assets of one, or subdividing an
443 | organization, or merging organizations. If propagation of a covered
444 | work results from an entity transaction, each party to that
445 | transaction who receives a copy of the work also receives whatever
446 | licenses to the work the party's predecessor in interest had or could
447 | give under the previous paragraph, plus a right to possession of the
448 | Corresponding Source of the work from the predecessor in interest, if
449 | the predecessor has it or can get it with reasonable efforts.
450 |
451 | You may not impose any further restrictions on the exercise of the
452 | rights granted or affirmed under this License. For example, you may
453 | not impose a license fee, royalty, or other charge for exercise of
454 | rights granted under this License, and you may not initiate litigation
455 | (including a cross-claim or counterclaim in a lawsuit) alleging that
456 | any patent claim is infringed by making, using, selling, offering for
457 | sale, or importing the Program or any portion of it.
458 |
459 | 11. Patents.
460 |
461 | A "contributor" is a copyright holder who authorizes use under this
462 | License of the Program or a work on which the Program is based. The
463 | work thus licensed is called the contributor's "contributor version".
464 |
465 | A contributor's "essential patent claims" are all patent claims
466 | owned or controlled by the contributor, whether already acquired or
467 | hereafter acquired, that would be infringed by some manner, permitted
468 | by this License, of making, using, or selling its contributor version,
469 | but do not include claims that would be infringed only as a
470 | consequence of further modification of the contributor version. For
471 | purposes of this definition, "control" includes the right to grant
472 | patent sublicenses in a manner consistent with the requirements of
473 | this License.
474 |
475 | Each contributor grants you a non-exclusive, worldwide, royalty-free
476 | patent license under the contributor's essential patent claims, to
477 | make, use, sell, offer for sale, import and otherwise run, modify and
478 | propagate the contents of its contributor version.
479 |
480 | In the following three paragraphs, a "patent license" is any express
481 | agreement or commitment, however denominated, not to enforce a patent
482 | (such as an express permission to practice a patent or covenant not to
483 | sue for patent infringement). To "grant" such a patent license to a
484 | party means to make such an agreement or commitment not to enforce a
485 | patent against the party.
486 |
487 | If you convey a covered work, knowingly relying on a patent license,
488 | and the Corresponding Source of the work is not available for anyone
489 | to copy, free of charge and under the terms of this License, through a
490 | publicly available network server or other readily accessible means,
491 | then you must either (1) cause the Corresponding Source to be so
492 | available, or (2) arrange to deprive yourself of the benefit of the
493 | patent license for this particular work, or (3) arrange, in a manner
494 | consistent with the requirements of this License, to extend the patent
495 | license to downstream recipients. "Knowingly relying" means you have
496 | actual knowledge that, but for the patent license, your conveying the
497 | covered work in a country, or your recipient's use of the covered work
498 | in a country, would infringe one or more identifiable patents in that
499 | country that you have reason to believe are valid.
500 |
501 | If, pursuant to or in connection with a single transaction or
502 | arrangement, you convey, or propagate by procuring conveyance of, a
503 | covered work, and grant a patent license to some of the parties
504 | receiving the covered work authorizing them to use, propagate, modify
505 | or convey a specific copy of the covered work, then the patent license
506 | you grant is automatically extended to all recipients of the covered
507 | work and works based on it.
508 |
509 | A patent license is "discriminatory" if it does not include within
510 | the scope of its coverage, prohibits the exercise of, or is
511 | conditioned on the non-exercise of one or more of the rights that are
512 | specifically granted under this License. You may not convey a covered
513 | work if you are a party to an arrangement with a third party that is
514 | in the business of distributing software, under which you make payment
515 | to the third party based on the extent of your activity of conveying
516 | the work, and under which the third party grants, to any of the
517 | parties who would receive the covered work from you, a discriminatory
518 | patent license (a) in connection with copies of the covered work
519 | conveyed by you (or copies made from those copies), or (b) primarily
520 | for and in connection with specific products or compilations that
521 | contain the covered work, unless you entered into that arrangement,
522 | or that patent license was granted, prior to 28 March 2007.
523 |
524 | Nothing in this License shall be construed as excluding or limiting
525 | any implied license or other defenses to infringement that may
526 | otherwise be available to you under applicable patent law.
527 |
528 | 12. No Surrender of Others' Freedom.
529 |
530 | If conditions are imposed on you (whether by court order, agreement or
531 | otherwise) that contradict the conditions of this License, they do not
532 | excuse you from the conditions of this License. If you cannot convey a
533 | covered work so as to satisfy simultaneously your obligations under this
534 | License and any other pertinent obligations, then as a consequence you may
535 | not convey it at all. For example, if you agree to terms that obligate you
536 | to collect a royalty for further conveying from those to whom you convey
537 | the Program, the only way you could satisfy both those terms and this
538 | License would be to refrain entirely from conveying the Program.
539 |
540 | 13. Remote Network Interaction; Use with the GNU General Public License.
541 |
542 | Notwithstanding any other provision of this License, if you modify the
543 | Program, your modified version must prominently offer all users
544 | interacting with it remotely through a computer network (if your version
545 | supports such interaction) an opportunity to receive the Corresponding
546 | Source of your version by providing access to the Corresponding Source
547 | from a network server at no charge, through some standard or customary
548 | means of facilitating copying of software. This Corresponding Source
549 | shall include the Corresponding Source for any work covered by version 3
550 | of the GNU General Public License that is incorporated pursuant to the
551 | following paragraph.
552 |
553 | Notwithstanding any other provision of this License, you have
554 | permission to link or combine any covered work with a work licensed
555 | under version 3 of the GNU General Public License into a single
556 | combined work, and to convey the resulting work. The terms of this
557 | License will continue to apply to the part which is the covered work,
558 | but the work with which it is combined will remain governed by version
559 | 3 of the GNU General Public License.
560 |
561 | 14. Revised Versions of this License.
562 |
563 | The Free Software Foundation may publish revised and/or new versions of
564 | the GNU Affero General Public License from time to time. Such new versions
565 | will be similar in spirit to the present version, but may differ in detail to
566 | address new problems or concerns.
567 |
568 | Each version is given a distinguishing version number. If the
569 | Program specifies that a certain numbered version of the GNU Affero General
570 | Public License "or any later version" applies to it, you have the
571 | option of following the terms and conditions either of that numbered
572 | version or of any later version published by the Free Software
573 | Foundation. If the Program does not specify a version number of the
574 | GNU Affero General Public License, you may choose any version ever published
575 | by the Free Software Foundation.
576 |
577 | If the Program specifies that a proxy can decide which future
578 | versions of the GNU Affero General Public License can be used, that proxy's
579 | public statement of acceptance of a version permanently authorizes you
580 | to choose that version for the Program.
581 |
582 | Later license versions may give you additional or different
583 | permissions. However, no additional obligations are imposed on any
584 | author or copyright holder as a result of your choosing to follow a
585 | later version.
586 |
587 | 15. Disclaimer of Warranty.
588 |
589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
597 |
598 | 16. Limitation of Liability.
599 |
600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
608 | SUCH DAMAGES.
609 |
610 | 17. Interpretation of Sections 15 and 16.
611 |
612 | If the disclaimer of warranty and limitation of liability provided
613 | above cannot be given local legal effect according to their terms,
614 | reviewing courts shall apply local law that most closely approximates
615 | an absolute waiver of all civil liability in connection with the
616 | Program, unless a warranty or assumption of liability accompanies a
617 | copy of the Program in return for a fee.
618 |
619 | END OF TERMS AND CONDITIONS
620 |
621 | How to Apply These Terms to Your New Programs
622 |
623 | If you develop a new program, and you want it to be of the greatest
624 | possible use to the public, the best way to achieve this is to make it
625 | free software which everyone can redistribute and change under these terms.
626 |
627 | To do so, attach the following notices to the program. It is safest
628 | to attach them to the start of each source file to most effectively
629 | state the exclusion of warranty; and each file should have at least
630 | the "copyright" line and a pointer to where the full notice is found.
631 |
632 |
633 | Copyright (C)
634 |
635 | This program is free software: you can redistribute it and/or modify
636 | it under the terms of the GNU Affero General Public License as published
637 | by the Free Software Foundation, either version 3 of the License, or
638 | (at your option) any later version.
639 |
640 | This program is distributed in the hope that it will be useful,
641 | but WITHOUT ANY WARRANTY; without even the implied warranty of
642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
643 | GNU Affero General Public License for more details.
644 |
645 | You should have received a copy of the GNU Affero General Public License
646 | along with this program. If not, see .
647 |
648 | Also add information on how to contact you by electronic and paper mail.
649 |
650 | If your software can interact with users remotely through a computer
651 | network, you should also make sure that it provides a way for users to
652 | get its source. For example, if your program is a web application, its
653 | interface could display a "Source" link that leads users to an archive
654 | of the code. There are many ways you could offer source, and different
655 | solutions will be better for different programs; see section 13 for the
656 | specific requirements.
657 |
658 | You should also get your employer (if you work as a programmer) or school,
659 | if any, to sign a "copyright disclaimer" for the program, if necessary.
660 | For more information on this, and how to apply and follow the GNU AGPL, see
661 | .
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # mmp-go
2 |
3 | 
4 |
5 | Mega Multiplexer, port mutiplexer for shadowsocks, supports AEAD methods only.
6 |
7 | ### Intro
8 |
9 | You can use mmp-go to reuse single port to forward to multiple shadowsocks servers.
10 |
11 | It is suitable for relay servers with a limited number of ports.
12 |
13 | Consider the following:
14 |
15 | > \- 草,这破 NAT 🐔怎么就俩端口?
16 | > \- mmp,go!
17 |
18 | ```bash
19 | |------> SS Server1
20 | Client --> Single Port --> Validator / Multiplexer |------> SS Server2
21 | |------> SS Server3
22 | ```
23 |
24 | ### Usage
25 |
26 | ```shell
27 | ./mmp-go -conf example.json
28 | ```
29 |
30 | Refer to `example.json`
31 |
32 | ### AEAD methods supported
33 |
34 | - chacha20-ietf-poly1305 (chacha20-poly1305)
35 | - aes-256-gcm
36 | - aes-128-gcm
37 |
38 | ### Related projects
39 |
40 | - [Qv2ray/mmp-rs](https://github.com/Qv2ray/mmp-rs) A rust-lang implementation of Mega Multiplexer.
41 | - [mzz2017/outline-watch-dog](https://github.com/mzz2017/outline-watch-dog) Watch and keep read privilege for `/opt/outline` recursively to avoid permission denied.
42 |
43 | ### Spark of thought from:
44 |
45 | - [DuckSoft](https://github.com/DuckSoft)
46 |
47 | ### Special thanks:
48 |
49 | - [studentmain](https://github.com/studentmain)
50 | - Qv2ray Developer Community
51 |
--------------------------------------------------------------------------------
/cipher/cipher.go:
--------------------------------------------------------------------------------
1 | package cipher
2 |
3 | import (
4 | "crypto/aes"
5 | "crypto/cipher"
6 | "crypto/md5"
7 | "crypto/sha1"
8 | "github.com/Qv2ray/mmp-go/infra/pool"
9 | "github.com/qv2ray/smaead"
10 | "golang.org/x/crypto/chacha20poly1305"
11 | "golang.org/x/crypto/hkdf"
12 | "io"
13 | )
14 |
15 | type CipherConf struct {
16 | KeyLen int
17 | SaltLen int
18 | NonceLen int
19 | TagLen int
20 | NewCipher func(key []byte) (cipher.AEAD, error)
21 | NewPartialCipher func(key []byte) (smaead.PartialAEAD, error)
22 | }
23 |
24 | const (
25 | MaxNonceSize = 12
26 | ATypeIPv4 = 1
27 | ATypeDomain = 3
28 | ATypeIpv6 = 4
29 | )
30 |
31 | var (
32 | CiphersConf = map[string]CipherConf{
33 | "chacha20-ietf-poly1305": {KeyLen: 32, SaltLen: 32, NonceLen: 12, TagLen: 16, NewCipher: chacha20poly1305.New, NewPartialCipher: NewPC20P1305},
34 | "chacha20-poly1305": {KeyLen: 32, SaltLen: 32, NonceLen: 12, TagLen: 16, NewCipher: chacha20poly1305.New, NewPartialCipher: NewPC20P1305},
35 | "aes-256-gcm": {KeyLen: 32, SaltLen: 32, NonceLen: 12, TagLen: 16, NewCipher: NewGcm, NewPartialCipher: NewPGcm},
36 | "aes-128-gcm": {KeyLen: 16, SaltLen: 16, NonceLen: 12, TagLen: 16, NewCipher: NewGcm, NewPartialCipher: NewPGcm},
37 | }
38 | ZeroNonce [MaxNonceSize]byte
39 | ReusedInfo = []byte("ss-subkey")
40 | )
41 |
42 | func (conf *CipherConf) Verify(buf []byte, masterKey []byte, salt []byte, cipherText []byte, subKey *[]byte) ([]byte, bool) {
43 | var sk []byte
44 | if subKey != nil && len(*subKey) == conf.KeyLen {
45 | sk = *subKey
46 | } else {
47 | sk = pool.Get(conf.KeyLen)
48 | defer pool.Put(sk)
49 | kdf := hkdf.New(
50 | sha1.New,
51 | masterKey,
52 | salt,
53 | ReusedInfo,
54 | )
55 | io.ReadFull(kdf, sk)
56 | if subKey != nil && cap(*subKey) >= conf.KeyLen {
57 | *subKey = (*subKey)[:conf.KeyLen]
58 | copy(*subKey, sk)
59 | }
60 | }
61 |
62 | ciph, _ := conf.NewCipher(sk)
63 |
64 | if _, err := ciph.Open(buf[:0], ZeroNonce[:conf.NonceLen], cipherText, nil); err != nil {
65 | return nil, false
66 | }
67 | return buf[:len(cipherText)-ciph.Overhead()], true
68 | }
69 |
70 | // Warning:
71 | // UnsafeVerifyATyp brings less than 25% performance improvement in most cases.
72 | // It is dangerous and NOT recommended.
73 | // Do not use it if you feel unnecessary.
74 | func (conf *CipherConf) UnsafeVerifyATyp(buf []byte, masterKey []byte, salt []byte, cipherText []byte, subKey *[]byte) bool {
75 | var sk []byte
76 | if subKey != nil && len(*subKey) == conf.KeyLen {
77 | sk = *subKey
78 | } else {
79 | sk = pool.Get(conf.KeyLen)
80 | defer pool.Put(sk)
81 | kdf := hkdf.New(
82 | sha1.New,
83 | masterKey,
84 | salt,
85 | ReusedInfo,
86 | )
87 | io.ReadFull(kdf, sk)
88 | if subKey != nil && cap(*subKey) >= conf.KeyLen {
89 | *subKey = (*subKey)[:conf.KeyLen]
90 | copy(*subKey, sk)
91 | }
92 | }
93 |
94 | ciph, _ := conf.NewPartialCipher(sk)
95 | plain := ciph.OpenWithoutCheck(buf[:0], ZeroNonce[:conf.NonceLen], cipherText[:1])
96 | atyp := plain[0]
97 | switch atyp {
98 | case ATypeIPv4, ATypeDomain, ATypeIpv6:
99 | return true
100 | }
101 | return false
102 | }
103 |
104 | func MD5Sum(d []byte) []byte {
105 | h := md5.New()
106 | h.Write(d)
107 | return h.Sum(nil)
108 | }
109 |
110 | func EVPBytesToKey(password string, keyLen int) (key []byte) {
111 | const md5Len = 16
112 |
113 | cnt := (keyLen-1)/md5Len + 1
114 | m := make([]byte, cnt*md5Len)
115 | copy(m, MD5Sum([]byte(password)))
116 |
117 | // Repeatedly call md5 until bytes generated is enough.
118 | // Each call to md5 uses data: prev md5 sum + password.
119 | d := make([]byte, md5Len+len(password))
120 | start := 0
121 | for i := 1; i < cnt; i++ {
122 | start += md5Len
123 | copy(d, m[start-md5Len:start])
124 | copy(d[md5Len:], password)
125 | copy(m[start:], MD5Sum(d))
126 | }
127 | return m[:keyLen]
128 | }
129 |
130 | func NewGcm(key []byte) (cipher.AEAD, error) {
131 | block, err := aes.NewCipher(key)
132 | if err != nil {
133 | return nil, err
134 | }
135 | return cipher.NewGCM(block)
136 | }
137 |
138 | func NewPC20P1305(key []byte) (smaead.PartialAEAD, error) {
139 | return smaead.NewPartialChacha20Poly1305(key)
140 | }
141 |
142 | func NewPGcm(key []byte) (smaead.PartialAEAD, error) {
143 | block, err := aes.NewCipher(key)
144 | if err != nil {
145 | return nil, err
146 | }
147 | return smaead.NewPartialGCM(block)
148 | }
149 |
--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "encoding/json"
5 | "flag"
6 | "fmt"
7 | "log"
8 | "net/http"
9 | "os"
10 | "sync"
11 | "time"
12 |
13 | "github.com/Qv2ray/mmp-go/cipher"
14 | "github.com/Qv2ray/mmp-go/infra/lru"
15 | )
16 |
17 | type Config struct {
18 | ConfPath string `json:"-"`
19 | HttpClient *http.Client `json:"-"`
20 | Groups []Group `json:"groups"`
21 | }
22 |
23 | type Server struct {
24 | Name string `json:"name"`
25 | Target string `json:"target"`
26 | TCPFastOpen bool `json:"TCPFastOpen"`
27 | Method string `json:"method"`
28 | Password string `json:"password"`
29 | MasterKey []byte `json:"-"`
30 | UpstreamConf *UpstreamConf `json:"-"`
31 | }
32 |
33 | type Group struct {
34 | Name string `json:"name"`
35 | Port int `json:"port"`
36 | ListenerTCPFastOpen bool `json:"listenerTCPFastOpen"`
37 | Servers []Server `json:"servers"`
38 | Upstreams []UpstreamConf `json:"upstreams"`
39 | UserContextPool *UserContextPool `json:"-"`
40 |
41 | // AuthTimeoutSec sets a TCP read timeout to drop connections that fail to finish auth in time.
42 | // Default: no timeout
43 | // outline-ss-server uses 59s, which is claimed to be the most common timeout for servers that do not respond to invalid requests.
44 | AuthTimeoutSec int `json:"authTimeoutSec"`
45 |
46 | // DialTimeoutSec sets the connect timeout when dialing the target server.
47 | // Default: no timeout (respect system default behavior)
48 | // Set to a value greater than zero to override the platform's default behavior.
49 | DialTimeoutSec int `json:"dialTimeoutSec"`
50 |
51 | // DrainOnAuthFail controls whether to fallback to the first server in the group when authentication fails.
52 | // Default: fallback to 1st server
53 | // Set to true to drain the connection when authentication fails.
54 | DrainOnAuthFail bool `json:"drainOnAuthFail"`
55 | }
56 |
57 | type UpstreamConf struct {
58 | Name string `json:"name"`
59 | Type string `json:"type"`
60 | Settings json.RawMessage `json:"settings"`
61 | PullingError error `json:"-"`
62 | Upstream Upstream `json:"-"`
63 | }
64 |
65 | func (uc UpstreamConf) Equal(that UpstreamConf) bool {
66 | return uc.Name == that.Name && uc.Type == that.Type && uc.Upstream.Equal(that.Upstream)
67 | }
68 |
69 | const (
70 | LRUTimeout = 30 * time.Minute
71 | )
72 |
73 | var (
74 | config *Config
75 | Version = "debug"
76 | )
77 |
78 | func (g *Group) BuildMasterKeys() {
79 | servers := g.Servers
80 | for j := range servers {
81 | s := &servers[j]
82 | s.MasterKey = cipher.EVPBytesToKey(s.Password, cipher.CiphersConf[s.Method].KeyLen)
83 | }
84 | }
85 |
86 | func (g *Group) BuildUserContextPool(timeout time.Duration) {
87 | g.UserContextPool = (*UserContextPool)(lru.New(lru.FixedTimeout, int64(timeout)))
88 | }
89 |
90 | func (config *Config) CheckMethodSupported() error {
91 | for _, g := range config.Groups {
92 | for _, s := range g.Servers {
93 | if _, ok := cipher.CiphersConf[s.Method]; !ok {
94 | return fmt.Errorf("unsupported method: %v", s.Method)
95 | }
96 | }
97 | }
98 | return nil
99 | }
100 |
101 | func (config *Config) CheckDiverseCombinations() error {
102 | groups := config.Groups
103 | type methodPasswd struct {
104 | method string
105 | passwd string
106 | }
107 | for _, g := range groups {
108 | m := make(map[methodPasswd]struct{})
109 | for _, s := range g.Servers {
110 | mp := methodPasswd{
111 | method: s.Method,
112 | passwd: s.Password,
113 | }
114 | if _, exists := m[mp]; exists {
115 | return fmt.Errorf("make sure combinantions of method and password in the same group are diverse. counterexample: (%v,%v)", mp.method, mp.passwd)
116 | }
117 | }
118 | }
119 | return nil
120 | }
121 |
122 | func pullFromUpstream(upstreamConf *UpstreamConf, c *http.Client) ([]Server, error) {
123 | servers, err := upstreamConf.Upstream.GetServers(c)
124 | if err != nil {
125 | return nil, err
126 | }
127 | for i := range servers {
128 | servers[i].UpstreamConf = upstreamConf
129 | }
130 | return servers, nil
131 | }
132 |
133 | func parseUpstreams(config *Config) (err error) {
134 | var wg sync.WaitGroup
135 |
136 | for i := range config.Groups {
137 | group := &config.Groups[i]
138 | mu := sync.Mutex{}
139 | for i := range group.Upstreams {
140 | upstreamConf := &group.Upstreams[i]
141 |
142 | switch upstreamConf.Type {
143 | case "outline":
144 | upstreamConf.Upstream = &Outline{
145 | Name: upstreamConf.Name,
146 | }
147 | err = json.Unmarshal(upstreamConf.Settings, upstreamConf.Upstream)
148 | if err != nil {
149 | return err
150 | }
151 | default:
152 | return fmt.Errorf("unknown upstream type: %v", upstreamConf.Type)
153 | }
154 |
155 | wg.Add(1)
156 | go func(group *Group, upstreamConf *UpstreamConf) {
157 | defer wg.Done()
158 | servers, err := pullFromUpstream(upstreamConf, config.HttpClient)
159 | if err != nil {
160 | upstreamConf.PullingError = err
161 | log.Printf("[warning] Failed to pull from group %s upstream %s: %v\n", group.Name, upstreamConf.Name, err)
162 | return
163 | }
164 | mu.Lock()
165 | group.Servers = append(group.Servers, servers...)
166 | mu.Unlock()
167 | log.Printf("Pulled %d servers from group %s upstream %s\n", len(servers), group.Name, upstreamConf.Name)
168 | }(group, upstreamConf)
169 | }
170 | }
171 |
172 | wg.Wait()
173 | return nil
174 | }
175 |
176 | func check(config *Config) (err error) {
177 | if err = config.CheckMethodSupported(); err != nil {
178 | return
179 | }
180 | if err = config.CheckDiverseCombinations(); err != nil {
181 | return
182 | }
183 | return
184 | }
185 |
186 | func build(config *Config) {
187 | for i := range config.Groups {
188 | g := &config.Groups[i]
189 | g.BuildUserContextPool(LRUTimeout)
190 | g.BuildMasterKeys()
191 | }
192 | }
193 |
194 | func BuildConfig(confPath string, c *http.Client) (conf *Config, err error) {
195 | conf = new(Config)
196 | conf.ConfPath = confPath
197 | conf.HttpClient = c
198 | b, err := os.ReadFile(confPath)
199 | if err != nil {
200 | return nil, err
201 | }
202 | if err = json.Unmarshal(b, conf); err != nil {
203 | return nil, err
204 | }
205 | if err = parseUpstreams(conf); err != nil {
206 | return nil, err
207 | }
208 | if err = check(conf); err != nil {
209 | return nil, err
210 | }
211 | build(conf)
212 | return
213 | }
214 |
215 | func SetConfig(conf *Config) {
216 | config = conf
217 | }
218 |
219 | func NewConfig(c *http.Client) *Config {
220 | var err error
221 |
222 | version := flag.Bool("v", false, "version")
223 | confPath := flag.String("conf", "example.json", "config file path")
224 | suppressTimestamps := flag.Bool("suppress-timestamps", false, "do not include timestamps in log")
225 | flag.Parse()
226 |
227 | if *version {
228 | fmt.Println(Version)
229 | os.Exit(0)
230 | }
231 |
232 | if *suppressTimestamps {
233 | log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
234 | }
235 |
236 | if config, err = BuildConfig(*confPath, c); err != nil {
237 | log.Fatalln(err)
238 | }
239 | return config
240 | }
241 |
--------------------------------------------------------------------------------
/config/outline.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "bytes"
5 | "crypto"
6 | "crypto/tls"
7 | "crypto/x509"
8 | "encoding/hex"
9 | "encoding/json"
10 | "fmt"
11 | "io"
12 | "net"
13 | "net/http"
14 | "strconv"
15 | "strings"
16 | "time"
17 |
18 | "golang.org/x/crypto/ssh"
19 | )
20 |
21 | type Outline struct {
22 | Name string `json:"-"`
23 | Server string `json:"server"`
24 | Link string `json:"link"`
25 | SSHPort string `json:"sshPort"`
26 | SSHUsername string `json:"sshUsername"`
27 | SSHPrivateKey string `json:"sshPrivateKey"`
28 | SSHPassword string `json:"sshPassword"`
29 | ApiUrl string `json:"apiUrl"`
30 | ApiCertSha256 string `json:"apiCertSha256"`
31 | TCPFastOpen bool `json:"TCPFastOpen"`
32 | AccessKeyPortOverride int `json:"accessKeyPortOverride"`
33 | }
34 |
35 | const timeout = 10 * time.Second
36 |
37 | var ErrIncorrectPassword = fmt.Errorf("incorrect password")
38 |
39 | func (outline Outline) getConfig(c *http.Client) ([]byte, error) {
40 | if outline.Server == "" {
41 | return nil, fmt.Errorf("server field cannot be empty")
42 | }
43 | tryList := []func(*http.Client) ([]byte, error){
44 | outline.getConfigFromLink,
45 | outline.getConfigFromApi,
46 | outline.getConfigFromSSH,
47 | }
48 | var (
49 | err error
50 | errs []error
51 | b []byte
52 | )
53 | for _, f := range tryList {
54 | b, err = f(c)
55 | if err != nil {
56 | // try next func
57 | b = nil
58 | errs = append(errs, err)
59 | continue
60 | }
61 | if b != nil {
62 | // valid result, break
63 | break
64 | }
65 | }
66 | if b != nil {
67 | // valid result
68 | return b, nil
69 | }
70 | if len(errs) > 0 {
71 | // concatenate errors
72 | err = errs[0]
73 | for i := 1; i < len(errs); i++ {
74 | err = fmt.Errorf("%w; %s", err, errs[i].Error())
75 | }
76 | return nil, err
77 | }
78 | // b and err is both nil, no valid info to get configure
79 | return nil, ErrInvalidUpstream
80 | }
81 |
82 | func (outline Outline) getConfigFromLink(c *http.Client) ([]byte, error) {
83 | if outline.Link == "" {
84 | return nil, nil
85 | }
86 | resp, err := c.Get(outline.Link)
87 | if err != nil {
88 | return nil, fmt.Errorf("getConfigFromLink failed: %w", err)
89 | }
90 | defer resp.Body.Close()
91 | return io.ReadAll(resp.Body)
92 | }
93 |
94 | func (outline Outline) getConfigFromApi(c *http.Client) ([]byte, error) {
95 | if outline.ApiUrl == "" {
96 | return nil, nil
97 | }
98 | if outline.ApiCertSha256 != "" {
99 | c = &http.Client{
100 | Transport: &http.Transport{TLSClientConfig: &tls.Config{
101 | InsecureSkipVerify: true,
102 | VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
103 | h := crypto.SHA256.New()
104 | for _, line := range rawCerts {
105 | h.Write(line)
106 | }
107 | fingerprint := hex.EncodeToString(h.Sum(nil))
108 | if !strings.EqualFold(fingerprint, outline.ApiCertSha256) {
109 | return fmt.Errorf("incorrect certSha256 from server: %v", strings.ToUpper(fingerprint))
110 | }
111 | return nil
112 | },
113 | }},
114 | Timeout: timeout,
115 | }
116 | }
117 | outline.ApiUrl = strings.TrimSuffix(outline.ApiUrl, "/")
118 | resp, err := c.Get(fmt.Sprintf("%v/access-keys", outline.ApiUrl))
119 | if err != nil {
120 | return nil, fmt.Errorf("getConfigFromApi failed: %w", err)
121 | }
122 | defer resp.Body.Close()
123 | return io.ReadAll(resp.Body)
124 | }
125 |
126 | func (outline Outline) getConfigFromSSH(*http.Client) ([]byte, error) {
127 | if outline.SSHUsername == "" || (outline.SSHPrivateKey == "" && outline.SSHPassword == "") {
128 | return nil, nil
129 | }
130 | var (
131 | conf *ssh.ClientConfig
132 | authMethods []ssh.AuthMethod
133 | )
134 | if outline.SSHPrivateKey != "" {
135 | signer, err := ssh.ParsePrivateKey([]byte(outline.SSHPrivateKey))
136 | if err != nil {
137 | return nil, fmt.Errorf("parse privateKey error: %w", err)
138 | }
139 | authMethods = append(authMethods, ssh.PublicKeys(signer))
140 | }
141 | authMethods = append(authMethods, ssh.Password(outline.SSHPassword))
142 | username := outline.SSHUsername
143 | if username == "" {
144 | username = "root"
145 | }
146 | conf = &ssh.ClientConfig{
147 | User: username,
148 | Auth: authMethods,
149 | HostKeyCallback: ssh.InsecureIgnoreHostKey(),
150 | Timeout: timeout,
151 | }
152 | port := outline.SSHPort
153 | if port == "" {
154 | port = "22"
155 | }
156 | client, err := ssh.Dial("tcp", net.JoinHostPort(outline.Server, port), conf)
157 | if err != nil {
158 | return nil, fmt.Errorf("failed to dial: %w", err)
159 | }
160 | defer client.Close()
161 |
162 | const cmd = "cat /opt/outline/persisted-state/shadowbox_config.json"
163 | gid, err := getGroupID(client)
164 | if err != nil {
165 | return nil, fmt.Errorf("failed to get gid: %w", err)
166 | }
167 | if gid == "0" {
168 | session, err := client.NewSession()
169 | if err != nil {
170 | return nil, fmt.Errorf("failed to create session: %w", err)
171 | }
172 | defer session.Close()
173 | out, err := session.CombinedOutput(cmd)
174 | if err != nil {
175 | err = fmt.Errorf("%v: %w", string(bytes.TrimSpace(out)), err)
176 | return nil, err
177 | }
178 | return out, nil
179 | } else {
180 | out, err := sudoCombinedOutput(client, outline.SSHPassword, cmd)
181 | if err != nil {
182 | return nil, fmt.Errorf("failed to execute sudo: %w", err)
183 | }
184 | return out, nil
185 | }
186 | }
187 |
188 | func getGroupID(client *ssh.Client) (gid string, err error) {
189 | session, err := client.NewSession()
190 | if err != nil {
191 | return "", err
192 | }
193 | defer session.Close()
194 | b, err := session.Output("id -g")
195 | return strings.TrimSpace(string(b)), err
196 | }
197 |
198 | func sudoCombinedOutput(client *ssh.Client, password string, cmd string) (b []byte, err error) {
199 | const prompt = "[INPUT YOUR PASSWORD]"
200 | session, err := client.NewSession()
201 | if err != nil {
202 | return nil, err
203 | }
204 | defer session.Close()
205 | b, err = session.CombinedOutput("sh -c " + strconv.Quote(fmt.Sprintf("echo %v|sudo -p %v -S %v", strconv.Quote(password), strconv.Quote(prompt), cmd)))
206 | b = bytes.TrimPrefix(bytes.TrimSpace(b), []byte(prompt))
207 | if bytes.Contains(b, []byte(prompt)) {
208 | return b, ErrIncorrectPassword
209 | }
210 | return b, err
211 | }
212 |
213 | func (outline Outline) GetServers(c *http.Client) (servers []Server, err error) {
214 | defer func() {
215 | if err != nil {
216 | err = fmt.Errorf("outline.GetGroups: %w", err)
217 | }
218 | }()
219 | b, err := outline.getConfig(c)
220 | if err != nil {
221 | return
222 | }
223 | var conf ShadowboxConfig
224 | err = json.Unmarshal(b, &conf)
225 | if err != nil {
226 | return
227 | }
228 | return conf.ToServers(outline.Name, outline.Server, outline.TCPFastOpen, outline.AccessKeyPortOverride), nil
229 | }
230 |
231 | func (outline Outline) Equal(that Upstream) bool {
232 | return outline == that
233 | }
234 |
235 | type AccessKey struct {
236 | ID string `json:"id"`
237 | Name string `json:"name"`
238 | Password string `json:"password"`
239 | Port int `json:"port"`
240 | EncryptionMethod string `json:"encryptionMethod"`
241 | Method string `json:"method"` // the alias of EncryptionMethod
242 | }
243 |
244 | func (key *AccessKey) ToServer(name, host string, tfo bool, portOverride int) Server {
245 | method := key.EncryptionMethod
246 | if method == "" {
247 | method = key.Method
248 | }
249 | if portOverride == 0 {
250 | portOverride = key.Port
251 | }
252 | return Server{
253 | Name: fmt.Sprintf("%s - %s", name, key.Name),
254 | Target: net.JoinHostPort(host, strconv.Itoa(portOverride)),
255 | TCPFastOpen: tfo,
256 | Method: method,
257 | Password: key.Password,
258 | }
259 | }
260 |
261 | type ShadowboxConfig struct {
262 | AccessKeys []AccessKey `json:"accessKeys"`
263 | }
264 |
265 | func (c *ShadowboxConfig) ToServers(name, host string, tfo bool, portOverride int) []Server {
266 | var servers []Server
267 | for _, k := range c.AccessKeys {
268 | servers = append(servers, k.ToServer(name, host, tfo, portOverride))
269 | }
270 | return servers
271 | }
272 |
--------------------------------------------------------------------------------
/config/outline_test.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "golang.org/x/crypto/ssh"
5 | "net"
6 | "testing"
7 | )
8 |
9 | func TestSudoOutput(t *testing.T) {
10 | username := "testuser"
11 | password := "testpassword"
12 | conf := &ssh.ClientConfig{
13 | User: username,
14 | Auth: []ssh.AuthMethod{ssh.Password(password)},
15 | HostKeyCallback: ssh.InsecureIgnoreHostKey(),
16 | Timeout: timeout,
17 | }
18 | client, err := ssh.Dial("tcp", net.JoinHostPort("localhost", "22"), conf)
19 | if err != nil {
20 | t.Fatal(err)
21 | }
22 | defer client.Close()
23 | b, err := sudoCombinedOutput(client, password, "echo ok")
24 | if err != nil {
25 | t.Fatal(string(b), err)
26 | }
27 | b, err = sudoCombinedOutput(client, "fakepassword", "echo ok")
28 | if err != nil {
29 | t.Log(string(b), err)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/config/upstream.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | )
7 |
8 | type Upstream interface {
9 | GetServers(*http.Client) (servers []Server, err error)
10 | Equal(Upstream) bool
11 | }
12 |
13 | var ErrInvalidUpstream = fmt.Errorf("invalid upstream")
14 |
--------------------------------------------------------------------------------
/config/userContext.go:
--------------------------------------------------------------------------------
1 | package config
2 |
3 | import (
4 | "github.com/Qv2ray/mmp-go/infra/lru"
5 | "github.com/Qv2ray/mmp-go/infra/lrulist"
6 | "math/rand"
7 | "net"
8 | "time"
9 | )
10 |
11 | // encapsulating semantic types
12 | type UserContext lrulist.LruList
13 | type UserContextPool lru.LRU
14 |
15 | func NewUserContext(servers []Server) *UserContext {
16 | list := make([]interface{}, len(servers))
17 | for i := range servers {
18 | list[i] = &servers[i]
19 | }
20 | basicInterval := 10 * time.Second
21 | offsetRange := 6.0
22 | offset := time.Duration((rand.Float64()-0.5)*offsetRange*1000) * time.Millisecond
23 | ctx := lrulist.NewWithList(basicInterval+offset, lrulist.InsertFront, list)
24 | return (*UserContext)(ctx)
25 | }
26 |
27 | func (ctx *UserContext) Infra() *lrulist.LruList {
28 | return (*lrulist.LruList)(ctx)
29 | }
30 |
31 | func (ctx *UserContext) Close() error {
32 | return ctx.Infra().Close()
33 | }
34 |
35 | func (ctx *UserContext) Auth(probe func(*Server) ([]byte, bool)) (hit *Server, content []byte) {
36 | lruList := ctx.Infra()
37 | listCopy := lruList.GetListCopy()
38 | defer lruList.GiveBackListCopy(listCopy)
39 | // probe every server
40 | for i := range listCopy {
41 | server := listCopy[i].Val.(*Server)
42 | if content, ok := probe(server); ok {
43 | lruList.Promote(listCopy[i])
44 | return server, content
45 | }
46 | }
47 | return nil, nil
48 | }
49 |
50 | func (pool *UserContextPool) Infra() *lru.LRU {
51 | return (*lru.LRU)(pool)
52 | }
53 |
54 | func (pool *UserContextPool) GetOrInsert(addr net.Addr, servers []Server) *UserContext {
55 | userIdent, _, _ := net.SplitHostPort(addr.String())
56 | value, removed := pool.Infra().GetOrInsert(userIdent, func() (val interface{}) {
57 | return NewUserContext(servers)
58 | })
59 | for _, ev := range removed {
60 | ev.Value.(*UserContext).Close()
61 | }
62 | return value.(*UserContext)
63 | }
64 |
--------------------------------------------------------------------------------
/dispatcher/dispatcher.go:
--------------------------------------------------------------------------------
1 | package dispatcher
2 |
3 | import (
4 | "github.com/Qv2ray/mmp-go/config"
5 | "sync"
6 | )
7 |
8 | type Dispatcher interface {
9 | Listen() (err error)
10 | // buf is a buffer to store decrypted text
11 | Auth(buf []byte, data []byte, userContext *config.UserContext) (hit *config.Server, content []byte)
12 | UpdateGroup(group *config.Group)
13 | Close() (err error)
14 | }
15 |
16 | type DispatcherCreator func(group *config.Group) Dispatcher
17 |
18 | var mapDispatherCreator sync.Map
19 |
20 | func Register(name string, creator DispatcherCreator) {
21 | mapDispatherCreator.Store(name, creator)
22 | }
23 |
24 | func New(name string, group *config.Group) (Dispatcher, bool) {
25 | c, ok := mapDispatherCreator.Load(name)
26 | if !ok {
27 | return nil, false
28 | }
29 | creator := c.(DispatcherCreator)
30 | return creator(group), ok
31 | }
32 |
--------------------------------------------------------------------------------
/dispatcher/infra/infra.go:
--------------------------------------------------------------------------------
1 | package infra
2 |
3 | import "errors"
4 |
5 | var ErrNetClosing = errors.New("use of closed network connection")
6 |
7 |
8 | func AddrLen(packet []byte) int {
9 | if len(packet) < 5 {
10 | return 0 // invalid addr field
11 | }
12 | l := 1 + 2 // type + port
13 | // host
14 | switch packet[0] {
15 | case 0x01:
16 | l += 4
17 | case 0x03:
18 | l += 1 + int(packet[1])
19 | case 0x04:
20 | l += 16
21 | }
22 | return l
23 | }
--------------------------------------------------------------------------------
/dispatcher/tcp/tcp.go:
--------------------------------------------------------------------------------
1 | package tcp
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "io"
8 | "log"
9 | "net"
10 | "sync"
11 | "time"
12 |
13 | "github.com/Qv2ray/mmp-go/cipher"
14 | "github.com/Qv2ray/mmp-go/config"
15 | "github.com/Qv2ray/mmp-go/dispatcher"
16 | "github.com/Qv2ray/mmp-go/infra/pool"
17 | "github.com/database64128/tfo-go"
18 | )
19 |
20 | //[salt][encrypted payload length][length tag][encrypted payload][payload tag]
21 | const (
22 | BasicLen = 32 + 2 + 16
23 | MaxLen = BasicLen + 16383 + 16
24 | )
25 |
26 | func init() {
27 | dispatcher.Register("tcp", New)
28 | }
29 |
30 | // DuplexConn is a net.Conn that allows for closing only the reader or writer end of
31 | // it, supporting half-open state.
32 | type DuplexConn interface {
33 | net.Conn
34 | // Closes the Read end of the connection, allowing for the release of resources.
35 | // No more reads should happen.
36 | CloseRead() error
37 | // Closes the Write end of the connection. An EOF or FIN signal may be
38 | // sent to the connection target.
39 | CloseWrite() error
40 | }
41 |
42 | type TCP struct {
43 | gMutex sync.RWMutex
44 | group *config.Group
45 | l net.Listener
46 | }
47 |
48 | func New(g *config.Group) (d dispatcher.Dispatcher) {
49 | return &TCP{group: g}
50 | }
51 |
52 | func (d *TCP) Listen() (err error) {
53 | lc := tfo.ListenConfig{
54 | DisableTFO: !d.group.ListenerTCPFastOpen,
55 | }
56 | d.l, err = lc.Listen(context.Background(), "tcp", fmt.Sprintf(":%d", d.group.Port))
57 | if err != nil {
58 | return
59 | }
60 | defer d.l.Close()
61 | log.Printf("[tcp] listen on :%v\n", d.group.Port)
62 | for {
63 | conn, err := d.l.Accept()
64 | if err != nil {
65 | if errors.Is(err, net.ErrClosed) {
66 | return nil
67 | }
68 | log.Printf("[error] ReadFrom: %v", err)
69 | continue
70 | }
71 | go func() {
72 | err := d.handleConn(conn)
73 | if err != nil {
74 | log.Println(err)
75 | }
76 | }()
77 | }
78 | }
79 |
80 | func (d *TCP) UpdateGroup(group *config.Group) {
81 | d.gMutex.Lock()
82 | defer d.gMutex.Unlock()
83 | d.group = group
84 | }
85 |
86 | func (d *TCP) Close() (err error) {
87 | log.Printf("[tcp] closed :%v\n", d.group.Port)
88 | return d.l.Close()
89 | }
90 |
91 | func (d *TCP) handleConn(conn net.Conn) error {
92 | /*
93 | https://github.com/shadowsocks/shadowsocks-org/blob/master/whitepaper/whitepaper.md
94 | */
95 | defer conn.Close()
96 |
97 | if d.group.AuthTimeoutSec > 0 {
98 | conn.SetReadDeadline(time.Now().Add(time.Duration(d.group.AuthTimeoutSec) * time.Second))
99 | }
100 |
101 | data := pool.Get(MaxLen)
102 | defer pool.Put(data)
103 | buf := pool.Get(BasicLen)
104 | defer pool.Put(buf)
105 | n, err := io.ReadAtLeast(conn, data, BasicLen)
106 | if err != nil {
107 | return fmt.Errorf("[tcp] %s <-x-> %s handleConn ReadAtLeast error: %w", conn.RemoteAddr(), conn.LocalAddr(), err)
108 | }
109 |
110 | // get user's context (preference)
111 | d.gMutex.RLock() // avoid insert old servers to the new userContextPool
112 | userContext := d.group.UserContextPool.GetOrInsert(conn.RemoteAddr(), d.group.Servers)
113 | d.gMutex.RUnlock()
114 |
115 | // auth every server
116 | server, _ := d.Auth(buf, data, userContext)
117 | if server == nil {
118 | if d.group.DrainOnAuthFail {
119 | log.Printf("[tcp] auth failed, draining conn %s <-> %s", conn.RemoteAddr(), conn.LocalAddr())
120 | io.Copy(io.Discard, conn)
121 | return nil
122 | }
123 |
124 | if len(d.group.Servers) == 0 {
125 | return nil
126 | }
127 |
128 | // fallback
129 | server = &d.group.Servers[0]
130 | }
131 |
132 | if d.group.AuthTimeoutSec > 0 {
133 | conn.SetReadDeadline(time.Time{})
134 | }
135 |
136 | // dial and relay
137 | dialer := tfo.Dialer{
138 | DisableTFO: !server.TCPFastOpen,
139 | }
140 | dialer.Timeout = time.Duration(d.group.DialTimeoutSec) * time.Second
141 | rc, err := dialer.Dial("tcp", server.Target)
142 | if err != nil {
143 | return fmt.Errorf("[tcp] %s <-> %s <-x-> %s handleConn dial error: %w", conn.RemoteAddr(), conn.LocalAddr(), server.Target, err)
144 | }
145 |
146 | _, err = rc.Write(data[:n])
147 | if err != nil {
148 | return fmt.Errorf("[tcp] %s <-> %s <-x-> %s handleConn write error: %w", conn.RemoteAddr(), conn.LocalAddr(), server.Target, err)
149 | }
150 |
151 | log.Printf("[tcp] %s <-> %s <-> %s", conn.RemoteAddr(), conn.LocalAddr(), server.Target)
152 |
153 | if err := relay(conn.(DuplexConn), rc.(DuplexConn)); err != nil {
154 | if err, ok := err.(net.Error); ok && err.Timeout() {
155 | return nil // ignore i/o timeout
156 | }
157 | return fmt.Errorf("[tcp] handleConn relay error: %w", err)
158 | }
159 | return nil
160 | }
161 |
162 | func relay(lc, rc DuplexConn) error {
163 | defer rc.Close()
164 | ch := make(chan error, 1)
165 | go func() {
166 | _, err := io.Copy(lc, rc)
167 | lc.CloseWrite()
168 | ch <- err
169 | }()
170 | _, err := io.Copy(rc, lc)
171 | rc.CloseWrite()
172 | innerErr := <-ch
173 | if err != nil {
174 | return err
175 | }
176 | return innerErr
177 | }
178 |
179 | func (d *TCP) Auth(buf []byte, data []byte, userContext *config.UserContext) (hit *config.Server, content []byte) {
180 | if len(data) < BasicLen {
181 | return nil, nil
182 | }
183 | return userContext.Auth(func(server *config.Server) ([]byte, bool) {
184 | return probe(buf, data, server)
185 | })
186 | }
187 |
188 | func probe(buf []byte, data []byte, server *config.Server) ([]byte, bool) {
189 | //[salt][encrypted payload length][length tag][encrypted payload][payload tag]
190 | conf := cipher.CiphersConf[server.Method]
191 |
192 | salt := data[:conf.SaltLen]
193 | cipherText := data[conf.SaltLen : conf.SaltLen+2+conf.TagLen]
194 |
195 | return conf.Verify(buf, server.MasterKey, salt, cipherText, nil)
196 | }
197 |
--------------------------------------------------------------------------------
/dispatcher/tcp/tcp_test.go:
--------------------------------------------------------------------------------
1 | package tcp
2 |
3 | import (
4 | "github.com/Qv2ray/mmp-go/config"
5 | "math/rand"
6 | "net"
7 | "testing"
8 | )
9 |
10 | func BenchmarkDispatcher_Auth(b *testing.B) {
11 | const nServers = 100
12 | g := new(config.Group)
13 | for i := 0; i < nServers; i++ {
14 | var b [10]byte
15 | rand.Read(b[:])
16 | g.Servers = append(g.Servers, config.Server{
17 | Target: "127.0.0.1:1080",
18 | Method: "chacha20-ietf-poly1305",
19 | Password: string(b[:]),
20 | })
21 | }
22 | g.BuildMasterKeys()
23 | g.BuildUserContextPool(10)
24 | var buf [50]byte
25 | var data [50]byte
26 | var d = New(g)
27 | addr, _ := net.ResolveIPAddr("tcp", "127.0.0.1:50000")
28 | for i := 0; i < b.N; i++ {
29 | d.Auth(buf[:], data[:], g.UserContextPool.GetOrInsert(addr, g.Servers))
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/dispatcher/udp/ipMTUTrie.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "github.com/Qv2ray/mmp-go/infra/trie"
5 | "net"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | var MTUTrie *IPMTUTrie
11 |
12 | func init() {
13 | var err error
14 | MTUTrie, err = NewIPMTUTrieFromInterfaces()
15 | if err != nil {
16 | MTUTrie = new(IPMTUTrie)
17 | }
18 | }
19 |
20 | type IPMTUTrie struct {
21 | v4Trie *trie.Trie
22 | v4Prefix2MTU map[string]int
23 | v6Trie *trie.Trie
24 | v6Prefix2MTU map[string]int
25 | }
26 |
27 | func (t *IPMTUTrie) GetMTU(ip net.IP) int {
28 | mtu := MTU
29 | if ip := ip.To4(); ip != nil {
30 | if t.v4Trie == nil {
31 | return mtu
32 | }
33 | prefix := t.v4Trie.Match(IPToBin(ip))
34 | if m, ok := t.v4Prefix2MTU[prefix]; ok && m < mtu {
35 | mtu = m
36 | }
37 | } else {
38 | if t.v6Trie == nil {
39 | return mtu
40 | }
41 | prefix := t.v6Trie.Match(IPToBin(ip))
42 | if m, ok := t.v6Prefix2MTU[prefix]; ok && m < mtu {
43 | mtu = m
44 | }
45 | }
46 | return mtu
47 | }
48 |
49 | func NewIPMTUTrieFromInterfaces() (*IPMTUTrie, error) {
50 | ifces, err := net.Interfaces()
51 | if err != nil {
52 | return nil, err
53 | }
54 | var (
55 | v4dict []string
56 | v4m = make(map[string]int)
57 | v6dict []string
58 | v6m = make(map[string]int)
59 | )
60 | for _, ifce := range ifces {
61 | addrs, _ := ifce.Addrs()
62 | for _, addr := range addrs {
63 | if ipnet, ok := addr.(*net.IPNet); ok {
64 | ones, bits := ipnet.Mask.Size()
65 | prefix := IPToBin(ipnet.IP)[:ones]
66 | switch bits {
67 | case 32:
68 | v4dict = append(v4dict, prefix)
69 | v4m[prefix] = ifce.MTU
70 | case 128:
71 | v6dict = append(v6dict, prefix)
72 | v6m[prefix] = ifce.MTU
73 | }
74 | }
75 | }
76 | }
77 | return &IPMTUTrie{
78 | v4Trie: trie.New(v4dict),
79 | v4Prefix2MTU: v4m,
80 | v6Trie: trie.New(v6dict),
81 | v6Prefix2MTU: v6m,
82 | }, nil
83 | }
84 |
85 | func IPToBin(ip net.IP) string {
86 | var buf strings.Builder
87 | if ip.To4() != nil {
88 | ip = ip.To4()
89 | } else {
90 | ip = ip.To16()
91 | }
92 | for _, b := range ip {
93 | tmp := strconv.FormatInt(int64(b), 2)
94 | buf.WriteString(strings.Repeat("0", 8-len(tmp)) + tmp)
95 | }
96 | return buf.String()
97 | }
98 |
--------------------------------------------------------------------------------
/dispatcher/udp/udp.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "github.com/Qv2ray/mmp-go/cipher"
7 | "github.com/Qv2ray/mmp-go/config"
8 | "github.com/Qv2ray/mmp-go/dispatcher"
9 | "github.com/Qv2ray/mmp-go/dispatcher/infra"
10 | "github.com/Qv2ray/mmp-go/infra/pool"
11 | "golang.org/x/net/dns/dnsmessage"
12 | "log"
13 | "net"
14 | "sync"
15 | "time"
16 | )
17 |
18 | const (
19 | MTU = 65535
20 | BasicLen = 32
21 | DefaultNatTimeout = 3 * time.Minute
22 | DnsQueryTimeout = 17 * time.Second // RFC 5452
23 | )
24 |
25 | var AuthFailedErr = fmt.Errorf("auth failed")
26 |
27 | func init() {
28 | dispatcher.Register("udp", New)
29 | }
30 |
31 | type UDP struct {
32 | gMutex sync.RWMutex
33 | group *config.Group
34 | c *net.UDPConn
35 | nm *UDPConnMapping
36 | }
37 |
38 | func New(g *config.Group) (d dispatcher.Dispatcher) {
39 | return &UDP{group: g, nm: NewUDPConnMapping()}
40 | }
41 |
42 | func (d *UDP) Listen() (err error) {
43 | d.c, err = net.ListenUDP("udp", &net.UDPAddr{Port: d.group.Port})
44 | if err != nil {
45 | return
46 | }
47 | defer d.c.Close()
48 | log.Printf("[udp] listen on :%v\n", d.group.Port)
49 | var buf [MTU]byte
50 | for {
51 | n, laddr, err := d.c.ReadFrom(buf[:])
52 | if err != nil {
53 | if errors.Is(err, net.ErrClosed) {
54 | return nil
55 | }
56 | log.Printf("[error] ReadFrom: %v", err)
57 | continue
58 | }
59 | data := pool.Get(n)
60 | copy(data, buf[:n])
61 | go func() {
62 | err := d.handleConn(laddr, data, n)
63 | if err != nil {
64 | log.Println(err)
65 | }
66 | pool.Put(data)
67 | }()
68 | }
69 | }
70 |
71 | func (d *UDP) UpdateGroup(group *config.Group) {
72 | d.gMutex.Lock()
73 | defer d.gMutex.Unlock()
74 | d.group = group
75 | }
76 |
77 | func (d *UDP) handleConn(laddr net.Addr, data []byte, n int) (err error) {
78 | // get conn or dial and relay
79 | rc, err := d.GetOrBuildUCPConn(laddr, data[:n])
80 | if err != nil {
81 | if err == AuthFailedErr {
82 | return nil
83 | }
84 | return fmt.Errorf("[udp] handleConn dial target error: %w", err)
85 | }
86 |
87 | // send packet
88 | if _, err = rc.Write(data[:n]); err != nil {
89 | return fmt.Errorf("[udp] handleConn write error: %w", err)
90 | }
91 | return nil
92 | }
93 |
94 | // select an appropriate timeout
95 | func selectTimeout(packet []byte) time.Duration {
96 | al := infra.AddrLen(packet)
97 | if len(packet) < al {
98 | // err: packet with inadequate length
99 | return DefaultNatTimeout
100 | }
101 | packet = packet[al:]
102 | var dmessage dnsmessage.Message
103 | if err := dmessage.Unpack(packet); err != nil {
104 | return DefaultNatTimeout
105 | }
106 | return DnsQueryTimeout
107 | }
108 |
109 | // connTimeout is the timeout of connection to build if not exists
110 | func (d *UDP) GetOrBuildUCPConn(laddr net.Addr, data []byte) (rc *net.UDPConn, err error) {
111 | socketIdent := laddr.String()
112 | d.nm.Lock()
113 | var conn *UDPConn
114 | var ok bool
115 | if conn, ok = d.nm.Get(socketIdent); !ok {
116 | // not exist such socket mapping, build one
117 | d.nm.Insert(socketIdent, nil)
118 | d.nm.Unlock()
119 |
120 | // get user's context (preference)
121 | d.gMutex.RLock() // avoid insert old servers to the new userContextPool
122 | userContext := d.group.UserContextPool.GetOrInsert(laddr, d.group.Servers)
123 | d.gMutex.RUnlock()
124 |
125 | buf := pool.Get(len(data))
126 | defer pool.Put(buf)
127 | // auth every server
128 | server, content := d.Auth(buf, data, userContext)
129 | if server == nil {
130 | d.nm.Lock()
131 | // remove socketIdent to avoid goroutine leak
132 | if conn, ok = d.nm.Get(socketIdent); ok {
133 | select {
134 | case <-conn.Establishing:
135 | default:
136 | d.nm.Remove(socketIdent)
137 | }
138 | }
139 | d.nm.Unlock()
140 | return nil, AuthFailedErr
141 | }
142 |
143 | // dial
144 | rconn, err := net.Dial("udp", server.Target)
145 | if err != nil {
146 | d.nm.Lock()
147 | d.nm.Remove(socketIdent) // close channel to inform that establishment ends
148 | d.nm.Unlock()
149 | return nil, fmt.Errorf("GetOrBuildUCPConn dial error: %w", err)
150 | }
151 | rc = rconn.(*net.UDPConn)
152 | d.nm.Lock()
153 | d.nm.Remove(socketIdent) // close channel to inform that establishment ends
154 | conn = d.nm.Insert(socketIdent, rc)
155 | conn.timeout = selectTimeout(content)
156 | d.nm.Unlock()
157 | // relay
158 | log.Printf("[udp] %s <-> %s <-> %s", laddr.String(), d.c.LocalAddr(), rc.RemoteAddr())
159 | go func() {
160 | _ = relay(d.c, laddr, rc, conn.timeout)
161 | d.nm.Lock()
162 | d.nm.Remove(socketIdent)
163 | d.nm.Unlock()
164 | }()
165 | } else {
166 | // such socket mapping exists; just verify or wait for its establishment
167 | d.nm.Unlock()
168 | <-conn.Establishing
169 | if conn.UDPConn == nil {
170 | // establishment ended and retrieve the result
171 | return d.GetOrBuildUCPConn(laddr, data)
172 | } else {
173 | // establishment succeeded
174 | rc = conn.UDPConn
175 | }
176 | }
177 | // countdown
178 | _ = conn.UDPConn.SetReadDeadline(time.Now().Add(conn.timeout))
179 | return rc, nil
180 | }
181 |
182 | func relay(dst *net.UDPConn, laddr net.Addr, src *net.UDPConn, timeout time.Duration) (err error) {
183 | var n int
184 | buf := pool.Get(MTUTrie.GetMTU(src.LocalAddr().(*net.UDPAddr).IP))
185 | defer pool.Put(buf)
186 | for {
187 | _ = src.SetReadDeadline(time.Now().Add(timeout))
188 | n, _, err = src.ReadFrom(buf)
189 | if err != nil {
190 | return
191 | }
192 | _ = dst.SetWriteDeadline(time.Now().Add(DefaultNatTimeout)) // should keep consistent
193 | _, err = dst.WriteTo(buf[:n], laddr)
194 | if err != nil {
195 | return
196 | }
197 | }
198 | }
199 |
200 | func (d *UDP) Auth(buf []byte, data []byte, userContext *config.UserContext) (hit *config.Server, content []byte) {
201 | if len(data) < BasicLen {
202 | return nil, nil
203 | }
204 | return userContext.Auth(func(server *config.Server) ([]byte, bool) {
205 | return probe(buf, data, server)
206 | })
207 | }
208 |
209 | func (d *UDP) Close() (err error) {
210 | log.Printf("[udp] closed :%v\n", d.group.Port)
211 | return d.c.Close()
212 | }
213 |
214 | func probe(buf []byte, data []byte, server *config.Server) ([]byte, bool) {
215 | //[salt][encrypted payload][tag]
216 | conf := cipher.CiphersConf[server.Method]
217 | if len(data) < conf.SaltLen+conf.TagLen {
218 | return nil, false
219 | }
220 | salt := data[:conf.SaltLen]
221 | cipherText := data[conf.SaltLen:]
222 |
223 | subKey := pool.Get(conf.KeyLen)[:0]
224 | defer pool.Put(subKey)
225 | if !conf.UnsafeVerifyATyp(buf, server.MasterKey, salt, cipherText, &subKey) {
226 | return nil, false
227 | }
228 | return conf.Verify(buf, server.MasterKey, salt, cipherText, &subKey)
229 | }
230 |
--------------------------------------------------------------------------------
/dispatcher/udp/udpConn.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "net"
5 | "sync"
6 | "time"
7 | )
8 |
9 | type UDPConn struct {
10 | Establishing chan struct{}
11 | timeout time.Duration
12 | *net.UDPConn
13 | }
14 |
15 | func NewUDPConn(conn *net.UDPConn) *UDPConn {
16 | c := &UDPConn{
17 | UDPConn: conn,
18 | Establishing: make(chan struct{}),
19 | }
20 | if c.UDPConn != nil {
21 | close(c.Establishing)
22 | }
23 | return c
24 | }
25 |
26 | type UDPConnMapping struct {
27 | nm map[string]*UDPConn
28 | sync.Mutex
29 | }
30 |
31 | func NewUDPConnMapping() *UDPConnMapping {
32 | m := &UDPConnMapping{
33 | nm: make(map[string]*UDPConn),
34 | }
35 | return m
36 | }
37 |
38 | func (m *UDPConnMapping) Get(key string) (conn *UDPConn, ok bool) {
39 | v, ok := m.nm[key]
40 | if ok {
41 | conn = v
42 | }
43 | return
44 | }
45 |
46 | // pass val=nil for stating it is establishing
47 | func (m *UDPConnMapping) Insert(key string, val *net.UDPConn) *UDPConn {
48 | c := NewUDPConn(val)
49 | m.nm[key] = c
50 | return c
51 | }
52 |
53 | func (m *UDPConnMapping) Remove(key string) {
54 | v, ok := m.nm[key]
55 | if !ok {
56 | return
57 | }
58 | select {
59 | case <-v.Establishing:
60 | _ = v.Close()
61 | default:
62 | close(v.Establishing)
63 | }
64 | delete(m.nm, key)
65 | }
66 |
--------------------------------------------------------------------------------
/dispatcher/udp/udp_test.go:
--------------------------------------------------------------------------------
1 | package udp
2 |
3 | import (
4 | "bytes"
5 | "crypto/sha1"
6 | "encoding/binary"
7 | "github.com/Qv2ray/mmp-go/cipher"
8 | "github.com/Qv2ray/mmp-go/config"
9 | "golang.org/x/crypto/hkdf"
10 | "math/rand"
11 | "net"
12 | "testing"
13 | )
14 |
15 | func BenchmarkDispatcher_Auth(b *testing.B) {
16 | const nServers = 100
17 | g := new(config.Group)
18 | for i := 0; i < nServers; i++ {
19 | var b [10]byte
20 | rand.Read(b[:])
21 | g.Servers = append(g.Servers, config.Server{
22 | Target: "127.0.0.1:1080",
23 | Method: "chacha20-ietf-poly1305",
24 | Password: string(b[:]),
25 | })
26 | }
27 | g.BuildMasterKeys()
28 | g.BuildUserContextPool(10)
29 | var buf [65535]byte
30 | var data [65535]byte
31 | var d = New(g)
32 | addr, _ := net.ResolveIPAddr("udp", "127.0.0.1:50000")
33 | for i := 0; i < b.N; i++ {
34 | d.Auth(buf[:], data[:], g.UserContextPool.GetOrInsert(addr, g.Servers))
35 | }
36 | }
37 |
38 | // TestDispatcher_Auth just test if single server auth works
39 | func TestDispatcher_Auth(t *testing.T) {
40 | const testSize = 500000
41 | const method = "chacha20-ietf-poly1305"
42 | const password = "password"
43 |
44 | g := new(config.Group)
45 | g.Servers = []config.Server{{
46 | Target: "127.0.0.1:1080",
47 | Method: method,
48 | Password: password,
49 | }}
50 | g.BuildMasterKeys()
51 | g.BuildUserContextPool(10)
52 | var d = New(g)
53 |
54 | type test struct {
55 | data []byte
56 | positive bool
57 | }
58 |
59 | var tests []test
60 | var salt [32]byte
61 | for i := 0; i < testSize; i++ {
62 | rand.Read(salt[:])
63 | atyp := rand.Intn(6) //0 2 5 is invalid atyp
64 | var addr []byte
65 | var tt test
66 | switch atyp {
67 | case cipher.ATypeIPv4:
68 | addr = make([]byte, 1+4+2)
69 | copy(addr[1:], net.IPv4(byte(rand.Intn(255)), byte(rand.Intn(255)), byte(rand.Intn(255)), byte(rand.Intn(255))).To4())
70 | binary.BigEndian.PutUint16(addr[1+4:], uint16(rand.Intn(65536)))
71 | case cipher.ATypeIpv6:
72 | addr = make([]byte, 1+16+2)
73 | // v4 in v6; fake v6
74 | copy(addr[1:], net.IPv4(byte(rand.Intn(255)), byte(rand.Intn(255)), byte(rand.Intn(255)), byte(rand.Intn(255))))
75 | binary.BigEndian.PutUint16(addr[1+16:], uint16(rand.Intn(65536)))
76 | case cipher.ATypeDomain:
77 | dm := "apple.com"
78 | addr = make([]byte, 1+1+len(dm)+2)
79 | addr[1] = byte(len(dm))
80 | copy(addr[1+1:], dm)
81 | binary.BigEndian.PutUint16(addr[1+1+len(dm):], uint16(rand.Intn(65536)))
82 | default:
83 | addr = make([]byte, 1+6+rand.Intn(16))
84 | loop:
85 | for {
86 | switch addr[0] {
87 | case cipher.ATypeIPv4, cipher.ATypeDomain, cipher.ATypeIpv6:
88 | rand.Read(addr)
89 | default:
90 | break loop
91 | }
92 | }
93 | }
94 | var payload = make([]byte, 16+rand.Intn(512))
95 | rand.Read(payload)
96 |
97 | switch atyp {
98 | case cipher.ATypeIPv4, cipher.ATypeDomain, cipher.ATypeIpv6:
99 | tt.positive = true
100 | addr[0] = byte(atyp)
101 |
102 | conf := cipher.CiphersConf[method]
103 | kdf := hkdf.New(sha1.New, g.Servers[0].MasterKey, salt[:], cipher.ReusedInfo)
104 | sk := make([]byte, conf.KeyLen)
105 | kdf.Read(sk)
106 | aead, _ := conf.NewCipher(sk)
107 | dataLen := len(addr) + len(payload) + aead.Overhead()
108 | data := make([]byte, dataLen)
109 | aead.Seal(data[:0], cipher.ZeroNonce[:aead.NonceSize()], bytes.Join([][]byte{addr, payload}, nil), nil)
110 | tt.data = bytes.Join([][]byte{salt[:], data[:dataLen]}, nil)
111 | default:
112 | tt.data = bytes.Join([][]byte{salt[:], addr, payload}, nil)
113 | }
114 | tests = append(tests, tt)
115 | }
116 |
117 | var buf [65535]byte
118 | addr, _ := net.ResolveIPAddr("udp", "127.0.0.1:50000")
119 | for _, test := range tests {
120 | hit, _ := d.Auth(buf[:], test.data, g.UserContextPool.GetOrInsert(addr, g.Servers))
121 | valid := hit != nil
122 | if valid != test.positive {
123 | t.Fail()
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/example.json:
--------------------------------------------------------------------------------
1 | {
2 | "groups": [
3 | {
4 | "name": "Group A",
5 | "port": 1090,
6 | "servers": [
7 | {
8 | "name": "Server A0",
9 | "target": "45.10.10.10:8081",
10 | "method": "chacha20-ietf-poly1305",
11 | "password": "mypassword"
12 | },
13 | {
14 | "name": "Server A1",
15 | "target": "jp.myss.cloudflare.com:18080",
16 | "method": "aes-128-gcm",
17 | "password": "hereismypasswrod"
18 | }
19 | ]
20 | },
21 | {
22 | "name": "Group B",
23 | "port": 1091,
24 | "servers": [
25 | {
26 | "name": "Server B0",
27 | "target": "45.10.10.11:8088",
28 | "method": "chacha20-ietf-poly1305",
29 | "password": "mypassword"
30 | },
31 | {
32 | "name": "Server B1",
33 | "target": "45.10.10.12:18088",
34 | "method": "chacha20-ietf-poly1305",
35 | "password": "mypassword2"
36 | },
37 | {
38 | "name": "Server B2",
39 | "target": "jp.myss.cloudflare.com:18080",
40 | "method": "aes-128-gcm",
41 | "password": "hereismypasswrod"
42 | }
43 | ]
44 | }
45 | ]
46 | }
47 |
--------------------------------------------------------------------------------
/example_fullview.json:
--------------------------------------------------------------------------------
1 | {
2 | "groups": [
3 | {
4 | "name": "Group A",
5 | "port": 1090,
6 | "authTimeoutSec": 59,
7 | "dialTimeoutSec": 10,
8 | "listenerTCPFastOpen": false,
9 | "drainOnAuthFail": false,
10 | "upstreams": [
11 | {
12 | "name": "Outline A0",
13 | "type": "outline",
14 | "settings": {
15 | "server": "23.115.204.133",
16 | "sshPort": "22",
17 | "sshUsername": "root",
18 | "sshPassword": "bt92ew4yTTRfPHL335",
19 | "TCPFastOpen": false
20 | }
21 | },
22 | {
23 | "name": "Outline A1",
24 | "type": "outline",
25 | "settings": {
26 | "server": "103.10.23.145",
27 | "sshPort": "22",
28 | "sshUsername": "outline",
29 | "sshPrivateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn\nNhAAAAAwEAAQAAAYEAynXZl9Uu+XmZ+tFNWxG6hn0FrIC+BgNvtsiqdLag7P/xpJ+84sSS\nT032JydbYCbkD2E+qLAJXdH6ZFw65HKj31kYo8jWUUjZLe5WZFEVunI027/s496SI6e6o9\n2pRjyAFmh3s1sd/a1HOWLYCeyOHEt2SVB/EKxlf/KMC6z4JDmZmMtUquPYXBCDPbwhG6jW\nIyYqNv7d/G6cno6CK0WuyHueufSEn8AFRRv4ptunR1xWv4JSavqnHubJJxBFMmSE8pNren\nZy94ccLpOTIif+lbq2F3DmzMCM+8xbrv8EfQmwLz+Kb3be8zORcHGgiv2CJ4hoa/joBA7U\n25rQ+KFE4m9T5ro7Gf2vbjuPNDTgHe4twUM0dM3ue5p2eL3VEFpqcDdhz27SVWZXPzXrui\ndt1oQDcv20won8ooUmqVaTWc2E+Sj4ul057VUsaZzRPbinAfwk00Nrxqg/DVTswej1ah6+\nNjwjmISX8or7MYKH/i5C7Q31pa/tJ9S1mp3zEkczAAAFiMT+hhPE/oYTAAAAB3NzaC1yc2\nEAAAABAMp12ZfVLvl5mfrRTVsRuoZ9BayAvgYDb7bIqnS2oOz/8aSfvOLEkk9N9icnW2Am\n5A9hPqiwCV3R+mRcOuhyo+tZGKPI1lFI2S3uVmRRFbpyNNu/7OPekiOnuqPdqUY8gBZod7\nNbHf2tRzli2AnsjhxLdklQfxCsZX/yjAus+CQ5mZjLVKrj2FwQgz28IRuo1iMmKjb+3fxu\nnJ6OgitFrsh7nrn0hJ/ABUUb+Kbbp0dcVr+CUmr6px7myScQUzJkhPKTa3p2cveHHC6Tky\nIn/pW6thdw5szAjPvMW67/BH0JsC8/im923vMzkXBxoIr9gieIaGv46AQO1Nna0PihROJv\nU+a6Oxn6r247jzQ04B3uLcFDNHTN7rOadni91RBaanA3Yc9u0lVmVz8167onbdaEA3L9tM\nKJ/KKFJqlWk1nNhPko+LpdOe1VLGmc0T24pwH8JNNDa8aoPw1U7MHo9WoevjY8I5iEl/KK\n+zGCh/4uQu0N9aWv7SfUtZqd8xJHMwAAAAMBAAEAAAGASIkghD1krwzaFfqW9GHNqhFwzv\nTxH8ZrZ9lM+LPVxBOOx6RTUAuNP8x2vGBlZHWKj9gPUvB+6pYoV3yTvmQURmWNZmC2KDkp\nVkNlwFsspbf1KCYDAUDkqtGVFCB9rSRP37dd62xhulkyg2Tece/GmmyO3IVygM7DLqv/cM\n9vt8rLNOrkUrV+9r0TyDJ2yiobTkyGI138ukwG4Oe9yzMUA9AGdikcuv9Y5AG5fE6GCBMV\nIVfXn2xeI7wbpVs783n/ZF/6ZvnDgaUUr3cA2VqcqdM8J/5Yj+B6g5iiYttoohMn14e90Y\n2SdbL8mR/F1QHuhkrqOHLjoPL7NAbe2hYAq4r1p/pHfbej2GrsJ/o1DYz91uABi1/gix6l\nEiHChD7VMj8S9pi58kjg6Df+LlCGBEu9mfHddB52kwQFxj55XBrWhnQby/e8Yn0gERLWTk\nD8iNPtiUSs4tWPIUlHk1THHY5kGBurDA84XGbYtLfuHAQ8p6jshNcpPScGATAKaw2BABAA\nwQCA8zyHFFGYrpm3KFMho6jnkqAcDVijtiStubX25YHaeQi5e83y1TZvh31zvj3LhApz6d\ngUw546O/5ucMO3B+xOWHITxfHoen5UxjkRZML9Ob8wfZEppB9hAtTJHe985qG1rwfxAXXO\nZYDSyowGBJQ+e/FgaSIm5bQkCxHupTXTLk3rPxEXvZI1+BnYzu7rqj6tcUChJ9tgagJBAy\nJkMkEJha4U6NQi0jXvYPTzQXPASrpg3tFjXMAC1mb5V02s2vwAAADBAPFl5K4CH5aS6/Op\nhuGvaLVMMHthDUCMk5nmrUIX5IOVQSB5Y74bPqst0U7qs56Y40XoAxgV8oti7RpjQiueOx\n2co5usU1oWVVHQSyUT2YTOSMnwOe5CMeigAXlgVokmoxEOB9lRDD6L2i1KpIxJkWMPvtS6\nKTIyWprA9JTyZ2lSo5Raj5ncNcQSpdxPoW25HEgqUi7XA8ftjx7bMYMoHT2wzZHoQvnDpk\nETe3PAyNjwwTVypMuIrTh4YZbLrUDk2wAAAMEA1rT/WjvdgMiup3eFOfqR0Og4YcUBmjwP\nUXcEY65kTDA0U8WoHizq/vUQ2tQhHjM2vASFvO9M079l4+jDj3dzd2qeSw4V5AYJmnhMqZ\n58LXgKQWZswEWnkKKIs5S+OQkcZw1iGTodipFcWW6nUUsaw1TY1esFLMcpeMeAYZ/sgADp\n7XTO1A7WWEG2dQWVVA/SfaFuimgnBOU7EKmbiGjPQFfxGCmgg5pBjNd528ExRw6pGX9b3d\n0hiMiJp6XWOcqJAAAAC216ekBtenprZXRpAQIDBAUGBw==\n-----END OPENSSH PRIVATE KEY-----\n",
30 | "TCPFastOpen": false
31 | }
32 | },
33 | {
34 | "name": "Outline A2",
35 | "type": "outline",
36 | "settings": {
37 | "server": "131.13.130.120",
38 | "link": "https://example.com/3BlbnNzaC1rZXktdjEAAAAABG/shadowbox_config.json",
39 | "TCPFastOpen": false
40 | }
41 | },
42 | {
43 | "name": "Outline A3",
44 | "type": "outline",
45 | "settings": {
46 | "server": "131.13.130.121",
47 | "apiUrl": "https://131.13.130.121:21230/4pn_faGFTa-bci6IA6ctYB",
48 | "apiCertSha256": "07B19FB83B9EFDF12DC971C311B6B7931A589BC4BE389F306F45532813DEFC9A",
49 | "TCPFastOpen": false,
50 | "accessKeyPortOverride": 8388
51 | }
52 | }
53 | ],
54 | "servers": [
55 | {
56 | "name": "Server A0",
57 | "target": "45.10.10.10:8081",
58 | "TCPFastOpen": false,
59 | "method": "chacha20-ietf-poly1305",
60 | "password": "mypassword"
61 | },
62 | {
63 | "name": "Server A1",
64 | "target": "jp.myss.cloudflare.com:18080",
65 | "TCPFastOpen": true,
66 | "method": "aes-128-gcm",
67 | "password": "hereismypasswrod"
68 | }
69 | ]
70 | }
71 | ]
72 | }
73 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Qv2ray/mmp-go
2 |
3 | go 1.17
4 |
5 | require (
6 | github.com/qv2ray/smaead v0.0.0-20211021072225-a01f7e01d185
7 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
8 | golang.org/x/net v0.0.0-20211020060615-d418f374d309
9 | )
10 |
11 | require golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
12 |
13 | require github.com/database64128/tfo-go v1.0.2
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/database64128/tfo-go v1.0.2 h1:Cq5+I9fJ4zngnHNLWolMknwK1fn6eYx2MoZSMlmUcIE=
2 | github.com/database64128/tfo-go v1.0.2/go.mod h1:XojFCk0XfoROhrdKxJQO7g6L2evWTNEHZTlQxeqd2Kg=
3 | github.com/qv2ray/smaead v0.0.0-20211021072225-a01f7e01d185 h1:MoLEK/RvsbuOrbymLBfQ1J5/8lAYbTeVj2xMxMxl0Tc=
4 | github.com/qv2ray/smaead v0.0.0-20211021072225-a01f7e01d185/go.mod h1:if5Sn4tlqxuTVNGBCm50lBBG7cqUQEOIM2hE2Ywd5V8=
5 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
6 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
7 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
8 | golang.org/x/net v0.0.0-20211020060615-d418f374d309 h1:A0lJIi+hcTR6aajJH4YqKWwohY4aW9RO7oRMcdv+HKI=
9 | golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
10 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
11 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
12 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13 | golang.org/x/sys v0.0.0-20211020174200-9d6173849985/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
15 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
16 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
17 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
18 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
19 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
20 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
21 |
--------------------------------------------------------------------------------
/infra/linklist/linklist.go:
--------------------------------------------------------------------------------
1 | package linklist
2 |
3 | // linklist with head node and tail node
4 | type Node struct {
5 | prior *Node
6 | next *Node
7 | Val interface{}
8 | }
9 |
10 | func (p *Node) Next() *Node {
11 | return p.next
12 | }
13 |
14 | func (p *Node) Prior() *Node {
15 | return p.prior
16 | }
17 |
18 | type Linklist struct {
19 | head *Node
20 | tail *Node
21 | }
22 |
23 | func NewLinklist() *Linklist {
24 | head := new(Node)
25 | tail := new(Node)
26 | head.next = tail
27 | tail.prior = head
28 | return &Linklist{
29 | head: head,
30 | tail: tail,
31 | }
32 | }
33 |
34 | func (l *Linklist) Front() *Node {
35 | node := l.head.next
36 | if node == l.tail {
37 | return nil
38 | }
39 | return node
40 | }
41 |
42 | func (l *Linklist) Back() *Node {
43 | node := l.tail.prior
44 | if node == l.head {
45 | return nil
46 | }
47 | return node
48 | }
49 |
50 | func (l *Linklist) Head() *Node {
51 | return l.head
52 | }
53 |
54 | func (l *Linklist) Tail() *Node {
55 | return l.tail
56 | }
57 |
58 | func (l *Linklist) Empty() bool {
59 | return l.head.next == l.tail
60 | }
61 |
62 | func (l *Linklist) InsertAfter(prior *Node, val interface{}) *Node {
63 | if prior == l.tail {
64 | return nil
65 | }
66 | p := new(Node)
67 | p.Val = val
68 | p.prior = prior
69 | p.next = prior.next
70 | prior.next.prior = p
71 | prior.next = p
72 | return p
73 | }
74 |
75 | func (l *Linklist) PushFront(val interface{}) *Node {
76 | return l.InsertAfter(l.head, val)
77 | }
78 |
79 | func (l *Linklist) PushBack(val interface{}) *Node {
80 | return l.InsertAfter(l.tail.prior, val)
81 | }
82 |
83 | func (l *Linklist) Promote(p *Node) {
84 | if p == l.Front() {
85 | return
86 | }
87 | p.prior.next = p.next
88 | p.next.prior = p.prior
89 | p.prior = l.head
90 | p.next = l.head.next
91 | l.head.next.prior = p
92 | l.head.next = p
93 | }
94 |
95 | func (l *Linklist) Demote(p *Node) {
96 | if p == l.Back() {
97 | return
98 | }
99 | p.prior.next = p.next
100 | p.next.prior = p.prior
101 | p.prior = l.tail.prior
102 | p.next = l.tail
103 | l.tail.prior.next = p
104 | l.tail.prior = p
105 | }
106 |
107 | func (l *Linklist) Remove(p *Node) {
108 | if p == l.head || p == l.tail {
109 | return
110 | }
111 | p.prior.next = p.next
112 | p.next.prior = p.prior
113 | }
114 |
--------------------------------------------------------------------------------
/infra/lru/lru.go:
--------------------------------------------------------------------------------
1 | package lru
2 |
3 | import (
4 | "github.com/Qv2ray/mmp-go/infra/linklist"
5 | "sync"
6 | "time"
7 | )
8 |
9 | type LimitStrategy int
10 |
11 | const (
12 | FixedLength LimitStrategy = iota
13 | FixedTimeout
14 | )
15 |
16 | type LRU struct {
17 | list *linklist.Linklist
18 | index map[interface{}]*linklist.Node
19 | reverseIndex map[*linklist.Node]interface{}
20 | mutex sync.Mutex
21 | strategy LimitStrategy
22 | limit int64
23 | }
24 |
25 | type EncapsulatedValue struct {
26 | Value interface{}
27 | LastUseTime time.Time
28 | }
29 |
30 | func New(strategy LimitStrategy, limit int64) *LRU {
31 | return &LRU{
32 | index: make(map[interface{}]*linklist.Node),
33 | reverseIndex: make(map[*linklist.Node]interface{}),
34 | list: linklist.NewLinklist(),
35 | strategy: strategy,
36 | limit: limit,
37 | }
38 | }
39 |
40 | func (l *LRU) GetOrInsert(key interface{}, valFunc func() (val interface{})) (val interface{}, removed []*EncapsulatedValue) {
41 | l.mutex.Lock()
42 | defer l.mutex.Unlock()
43 | val = l.get(key)
44 | if val == nil {
45 | val := valFunc()
46 | return val, l.insert(key, val)
47 | }
48 | return val, nil
49 | }
50 |
51 | func (l *LRU) Get(key interface{}) (value interface{}) {
52 | l.mutex.Lock()
53 | defer l.mutex.Unlock()
54 | return l.get(key)
55 | }
56 |
57 | func (l *LRU) get(key interface{}) (value interface{}) {
58 | v, ok := l.index[key]
59 | if !ok {
60 | return nil
61 | }
62 | l.list.Promote(v)
63 | ev := v.Val.(*EncapsulatedValue)
64 | ev.LastUseTime = time.Now()
65 | return ev.Value
66 | }
67 |
68 | func (l *LRU) Insert(key interface{}, val interface{}) (removed []*EncapsulatedValue) {
69 | l.mutex.Lock()
70 | defer l.mutex.Unlock()
71 | return l.insert(key, val)
72 | }
73 |
74 | func (l *LRU) insert(key interface{}, val interface{}) (removed []*EncapsulatedValue) {
75 | ev := &EncapsulatedValue{
76 | Value: val,
77 | LastUseTime: time.Now(),
78 | }
79 | node := l.list.PushFront(ev)
80 | l.index[key] = node
81 | l.reverseIndex[node] = key
82 | switch l.strategy {
83 | case FixedLength:
84 | if int64(len(l.index)) > l.limit {
85 | back := l.list.Back()
86 | removed = []*EncapsulatedValue{back.Val.(*EncapsulatedValue)}
87 | key := l.reverseIndex[back]
88 | l.list.Remove(back)
89 | delete(l.index, key)
90 | delete(l.reverseIndex, back)
91 | }
92 | case FixedTimeout:
93 | now := time.Now()
94 | // pop timeout exceeded nodes until the last node does not exceed
95 | for {
96 | back := l.list.Back()
97 | if back == nil {
98 | break
99 | }
100 | ev := back.Val.(*EncapsulatedValue)
101 | if int64(now.Sub(ev.LastUseTime)) < l.limit {
102 | break
103 | }
104 | removed = append(removed, ev)
105 | key := l.reverseIndex[back]
106 | l.list.Remove(back)
107 | delete(l.index, key)
108 | delete(l.reverseIndex, back)
109 | }
110 | }
111 | return
112 | }
113 |
--------------------------------------------------------------------------------
/infra/lrulist/growingPool.go:
--------------------------------------------------------------------------------
1 | package lrulist
2 |
3 | import (
4 | "math/bits"
5 | "sync"
6 | )
7 |
8 | type growingPool struct {
9 | pool *sync.Pool
10 | size int
11 | mu sync.Mutex
12 | }
13 |
14 | func newGrowingPool(size int) *growingPool {
15 | return &growingPool{
16 | pool: &sync.Pool{New: func() interface{} {
17 | return make([]*Node, size)
18 | }},
19 | size: size,
20 | }
21 | }
22 |
23 | func (p *growingPool) Get(need int) []*Node {
24 | p.mu.Lock()
25 | defer p.mu.Unlock()
26 | if need > p.size {
27 | p.size = 1 << bits.Len32(uint32(need))
28 | p.pool = &sync.Pool{New: func() interface{} {
29 | return make([]*Node, p.size)
30 | }}
31 | }
32 | return p.pool.Get().([]*Node)[:need]
33 | }
34 |
35 | func (p *growingPool) Put(l []*Node) {
36 | if cap(l) != p.size {
37 | return
38 | }
39 | p.pool.Put(l[:p.size])
40 | return
41 | }
42 |
--------------------------------------------------------------------------------
/infra/lrulist/lrulist.go:
--------------------------------------------------------------------------------
1 | package lrulist
2 |
3 | import (
4 | "sync"
5 | "sync/atomic"
6 | "time"
7 | )
8 |
9 | type Node struct {
10 | Val interface{}
11 | weight uint32
12 | }
13 |
14 | type LruList struct {
15 | list []*Node
16 | muList sync.Mutex
17 | updateTicker *time.Ticker
18 | avg uint32
19 | max uint32
20 | insertStrategy InsertStrategy
21 | pool *growingPool
22 | }
23 |
24 | type InsertStrategy int
25 |
26 | const (
27 | InsertFront InsertStrategy = iota
28 | InsertAverage
29 | )
30 |
31 | func New(updateInterval time.Duration, insertStrategy InsertStrategy) *LruList {
32 | list := &LruList{
33 | insertStrategy: insertStrategy,
34 | updateTicker: time.NewTicker(updateInterval),
35 | pool: newGrowingPool(1),
36 | }
37 | list.list = list.pool.Get(1)
38 | go list.updater()
39 | return list
40 | }
41 |
42 | func NewWithList(updateInterval time.Duration, insertStrategy InsertStrategy, list []interface{}) *LruList {
43 | lruList := &LruList{
44 | insertStrategy: insertStrategy,
45 | updateTicker: time.NewTicker(updateInterval),
46 | pool: newGrowingPool(len(list)),
47 | }
48 | l := lruList.pool.Get(len(list))
49 | for i := range list {
50 | l[i] = &Node{Val: list[i]}
51 | }
52 | lruList.list = l
53 | return lruList
54 | }
55 |
56 | func (l *LruList) Close() (err error) {
57 | l.updateTicker.Stop()
58 | return nil
59 | }
60 |
61 | // GetListCopy should be called when you want to traverse the list
62 | func (l *LruList) GetListCopy() []*Node {
63 | l.muList.Lock()
64 | list := l.pool.Get(len(l.list))
65 | copy(list, l.list)
66 | l.muList.Unlock()
67 | return list
68 | }
69 |
70 | // GiveBackListCopy should be called when the list copy is no longer used
71 | func (l *LruList) GiveBackListCopy(list []*Node) {
72 | l.pool.Put(list)
73 | }
74 |
75 | // promote but lazy sort. O(1)
76 | func (l *LruList) Promote(node *Node) {
77 | atomic.AddUint32(&node.weight, 1)
78 | }
79 |
80 | // spend O(n) time to insert
81 | func (l *LruList) Insert(val interface{}) *Node {
82 | node := &Node{Val: val}
83 | if l.insertStrategy == InsertFront {
84 | node.weight = l.max + 1
85 | } else {
86 | node.weight = l.avg + 1
87 | }
88 | l.muList.Lock()
89 | defer l.muList.Unlock()
90 |
91 | // insert into a roughly right position
92 | insertBefore := len(l.list)
93 | for i := range l.list {
94 | if l.list[i].weight < node.weight {
95 | insertBefore = i
96 | break
97 | }
98 | }
99 |
100 | // expand the list
101 | if cap(l.list) > len(l.list) {
102 | l.list = l.list[:len(l.list)+1]
103 | for i := len(l.list) - 1; i > insertBefore; i-- {
104 | l.list[i] = l.list[i-1]
105 | }
106 | l.list[insertBefore] = node
107 | } else {
108 | list := l.pool.Get(len(l.list) + 1)
109 | for i := len(l.list) - 1; i >= insertBefore; i-- {
110 | list[i+1] = l.list[i]
111 | }
112 | list[insertBefore] = node
113 | copy(list[:insertBefore], l.list[:insertBefore])
114 | l.pool.Put(l.list)
115 | l.list = list
116 | }
117 | return node
118 | }
119 |
120 | // O(n) scan to remove a node
121 | func (l *LruList) Remove(node *Node) {
122 | l.muList.Lock()
123 | defer l.muList.Unlock()
124 | targetIndex := -1
125 | for i := range l.list {
126 | if l.list[i] == node {
127 | targetIndex = i
128 | break
129 | }
130 | }
131 | if targetIndex == -1 {
132 | return
133 | }
134 | for i := targetIndex; i < len(l.list)-1; i++ {
135 | l.list[i] = l.list[i+1]
136 | }
137 | l.list = l.list[:len(l.list)-1]
138 | }
139 |
--------------------------------------------------------------------------------
/infra/lrulist/updater.go:
--------------------------------------------------------------------------------
1 | package lrulist
2 |
3 | import (
4 | "sort"
5 | )
6 |
7 | func (l *LruList) updater() {
8 | // every interval time, updater divide counts by 2
9 | for range l.updateTicker.C {
10 | l.muList.Lock()
11 | sort.SliceStable(l.list, func(i, j int) bool {
12 | return l.list[i].weight > l.list[j].weight
13 | })
14 | l.avg = 0
15 | l.max = 0
16 | var sum uint64
17 | var cnt uint64
18 | for i := range l.list {
19 | if l.list[i].weight == 0 {
20 | break
21 | }
22 | cnt++
23 | if l.list[i].weight > l.max {
24 | l.max = l.list[i].weight
25 | }
26 | sum += uint64(l.list[i].weight)
27 | l.list[i].weight /= 2
28 | }
29 | if cnt != 0 {
30 | l.avg = uint32(sum / cnt)
31 | }
32 | l.muList.Unlock()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/infra/pool/pool.go:
--------------------------------------------------------------------------------
1 | // modified from https://github.com/nadoo/glider/blob/master/pool/buffer.go
2 |
3 | package pool
4 |
5 | import (
6 | "math/bits"
7 | "sync"
8 | )
9 |
10 | const (
11 | // number of pools.
12 | num = 17
13 | maxsize = 1 << (num - 1)
14 | )
15 |
16 | var (
17 | sizes [num]int
18 | pools [num]sync.Pool
19 | )
20 |
21 | func init() {
22 | for i := 0; i < num; i++ {
23 | size := 1 << i
24 | sizes[i] = size
25 | pools[i].New = func() interface{} {
26 | return make([]byte, size)
27 | }
28 | }
29 | }
30 |
31 | func GetClosestN(need int) (n int) {
32 | // if need is exactly 2^n, return n-1
33 | if need&(need-1) == 0 {
34 | return bits.Len32(uint32(need)) - 1
35 | }
36 | // or return its closest n
37 | return bits.Len32(uint32(need))
38 | }
39 |
40 | // Get gets a buffer from pool, size should in range: [1, 65536],
41 | // otherwise, this function will call make([]byte, size) directly.
42 | func Get(size int) []byte {
43 | if size >= 1 && size <= maxsize {
44 | i := GetClosestN(size)
45 | return pools[i].Get().([]byte)[:size]
46 | }
47 | return make([]byte, size)
48 | }
49 |
50 | // Put puts a buffer into pool.
51 | func Put(buf []byte) {
52 | if size := cap(buf); size >= 1 && size <= maxsize {
53 | i := GetClosestN(size)
54 | if i < num {
55 | pools[i].Put(buf)
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/infra/trie/trie.go:
--------------------------------------------------------------------------------
1 | // Static trie
2 | package trie
3 |
4 | import (
5 | "strings"
6 | )
7 |
8 | type cast map[rune]*next
9 |
10 | type next struct {
11 | *node
12 | str *string
13 | }
14 |
15 | type node struct {
16 | c cast
17 | end bool
18 | }
19 |
20 | type Trie struct {
21 | root *node
22 | }
23 |
24 | func newNode() *node {
25 | return &node{
26 | c: cast{},
27 | end: false,
28 | }
29 | }
30 |
31 | func New(dict []string) (trie *Trie) {
32 | var t Trie
33 | var ok bool
34 | var p *node
35 | t.root = newNode()
36 | for _, d := range dict {
37 | p = t.root
38 | for i, r := range d {
39 | _, ok = p.c[r]
40 | if !ok {
41 | n := next{
42 | node: newNode(),
43 | str: nil,
44 | }
45 | p.c[r] = &n
46 | }
47 | p = p.c[r].node
48 | if i == len(d)-1 {
49 | p.end = true
50 | }
51 | }
52 | }
53 | //make jump
54 | makeJump(t.root)
55 | return &t
56 | }
57 |
58 | func fastJump(from *next, to *next, str *string) {
59 | from.str = str
60 | from.node = to.node
61 | }
62 |
63 | func _makeJump(cur *next, from *next, builder *strings.Builder) {
64 | var fork bool
65 | if cur.node.end || len(cur.node.c) > 1 {
66 | if builder.Len() > 1 {
67 | s := builder.String()
68 | fastJump(from, cur, &s)
69 | }
70 | fork = true
71 | }
72 | for k := range cur.node.c {
73 | child := cur.node.c[k]
74 | if fork {
75 | from = child
76 | builder = new(strings.Builder)
77 | }
78 | builder.WriteRune(k)
79 | _makeJump(child, from, builder)
80 | }
81 | }
82 |
83 | func makeJump(root *node) {
84 | //DFS
85 | for k := range root.c {
86 | builder := new(strings.Builder)
87 | builder.WriteRune(k)
88 | _makeJump(root.c[k], root.c[k], builder)
89 | }
90 | }
91 |
92 | func (t *Trie) Match(str string) (prefix string) {
93 | var builder strings.Builder
94 | var runes = []rune(str)
95 | var length = len(runes)
96 | p := t.root
97 | for i := 0; i < length; i++ {
98 | r := runes[i]
99 | tmp, ok := p.c[r]
100 | if !ok {
101 | return
102 | }
103 | if tmp.str == nil {
104 | builder.WriteRune(r)
105 | } else {
106 | if lenTmp := len(*tmp.str); builder.Len()+lenTmp <= length {
107 | if string(runes[i:lenTmp+i]) == *tmp.str {
108 | builder.WriteString(*tmp.str)
109 | i += len(*tmp.str) - 1
110 | } else {
111 | break
112 | }
113 | }
114 | }
115 | if tmp.node.end {
116 | if builder.Len() <= length {
117 | prefix = builder.String()
118 | if len(prefix) == length {
119 | break
120 | }
121 | } else {
122 | break
123 | }
124 | }
125 | p = tmp.node
126 | }
127 | return
128 | }
129 |
--------------------------------------------------------------------------------
/infra/trie/trie_test.go:
--------------------------------------------------------------------------------
1 | package trie
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestTrie_Match(t *testing.T) {
8 | trie := New([]string{
9 | "12",
10 | "12345",
11 | "1234567",
12 | "2222",
13 | "1",
14 | })
15 | test := [][2]string{
16 | {"1", "1"},
17 | {"123", "12"},
18 | {"1233", "12"},
19 | {"12345", "12345"},
20 | {"123456", "12345"},
21 | {"1234567", "1234567"},
22 | {"123456789", "1234567"},
23 | {"222", ""},
24 | {"2222", "2222"},
25 | {"22222", "2222"},
26 | {"122", "12"},
27 | }
28 | for _, tt := range test {
29 | if p := trie.Match(tt[0]); p == tt[1] {
30 | t.Log(tt[0], "match prefix", p)
31 | } else {
32 | t.Error(tt[0], "expect", tt[1], "wrong prefix", p)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qv2ray/mmp-go/4937afb470f3a2aae2dccf403ccd2ca6cac6bd99/logo.png
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
55 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "sync"
7 | "time"
8 |
9 | "github.com/Qv2ray/mmp-go/config"
10 | "github.com/Qv2ray/mmp-go/dispatcher"
11 | _ "github.com/Qv2ray/mmp-go/dispatcher/tcp"
12 | _ "github.com/Qv2ray/mmp-go/dispatcher/udp"
13 | )
14 |
15 | const HttpClientTimeout = 10 * time.Second
16 |
17 | type MapPortDispatcher map[int]*[len(protocols)]dispatcher.Dispatcher
18 |
19 | type SyncMapPortDispatcher struct {
20 | sync.Mutex
21 | Map MapPortDispatcher
22 | }
23 |
24 | func NewSyncMapPortDispatcher() *SyncMapPortDispatcher {
25 | return &SyncMapPortDispatcher{Map: make(MapPortDispatcher)}
26 | }
27 |
28 | var (
29 | protocols = [...]string{"tcp", "udp"}
30 | groupWG sync.WaitGroup
31 | mPortDispatcher = NewSyncMapPortDispatcher()
32 | )
33 |
34 | func listenGroup(group *config.Group) {
35 | err := listenProtocols(group, protocols[:])
36 | if err != nil {
37 | mPortDispatcher.Lock()
38 | // error but listening
39 | if _, ok := mPortDispatcher.Map[group.Port]; ok {
40 | log.Fatalln(err)
41 | }
42 | mPortDispatcher.Unlock()
43 | }
44 | }
45 |
46 | func listenProtocols(group *config.Group, protocols []string) error {
47 | mPortDispatcher.Lock()
48 | if _, ok := mPortDispatcher.Map[group.Port]; !ok {
49 | mPortDispatcher.Map[group.Port] = new([2]dispatcher.Dispatcher)
50 | }
51 | t := mPortDispatcher.Map[group.Port]
52 | mPortDispatcher.Unlock()
53 |
54 | ch := make(chan error, len(protocols))
55 | for i, protocol := range protocols {
56 | d, _ := dispatcher.New(protocol, group)
57 | (*t)[i] = d
58 | go func() {
59 | err := d.Listen()
60 | ch <- err
61 | }()
62 | }
63 | return <-ch
64 | }
65 |
66 | func main() {
67 | conf := config.NewConfig(&http.Client{
68 | Timeout: HttpClientTimeout,
69 | })
70 |
71 | // handle reload
72 | go signalHandler(conf)
73 |
74 | mPortDispatcher.Lock()
75 | for i := range conf.Groups {
76 | groupWG.Add(1)
77 | go func(group *config.Group) {
78 | listenGroup(group)
79 | groupWG.Done()
80 | }(&conf.Groups[i])
81 | }
82 | mPortDispatcher.Unlock()
83 | groupWG.Wait()
84 | }
85 |
--------------------------------------------------------------------------------
/release/friendly-filenames.json:
--------------------------------------------------------------------------------
1 | {
2 | "darwin-amd64": { "friendlyName": "macos-64" },
3 | "darwin-arm64": { "friendlyName": "macos-arm64-v8a" },
4 | "dragonfly-amd64": { "friendlyName": "dragonfly-64" },
5 | "freebsd-386": { "friendlyName": "freebsd-32" },
6 | "freebsd-amd64": { "friendlyName": "freebsd-64" },
7 | "linux-386": { "friendlyName": "linux-32" },
8 | "linux-amd64": { "friendlyName": "linux-64" },
9 | "linux-arm5": { "friendlyName": "linux-arm32-v5" },
10 | "linux-arm64": { "friendlyName": "linux-arm64-v8a" },
11 | "linux-arm6": { "friendlyName": "linux-arm32-v6" },
12 | "linux-arm7": { "friendlyName": "linux-arm32-v7a" },
13 | "linux-mips64le": { "friendlyName": "linux-mips64le" },
14 | "linux-mips64": { "friendlyName": "linux-mips64" },
15 | "linux-mipsle": { "friendlyName": "linux-mips32le" },
16 | "linux-mips": { "friendlyName": "linux-mips32" },
17 | "linux-riscv64": { "friendlyName": "linux-riscv64" },
18 | "openbsd-386": { "friendlyName": "openbsd-32" },
19 | "openbsd-amd64": { "friendlyName": "openbsd-64" },
20 | "windows-amd64": { "friendlyName": "windows-64" },
21 | "windows-386": { "friendlyName": "windows-32" },
22 | "windows-arm64": { "friendlyName": "windows-arm64-v8a" },
23 | "windows-arm7": { "friendlyName": "windows-arm32-v7a" },
24 | "android-arm64": { "friendlyName": "android-arm64-v8a" }
25 | }
26 |
--------------------------------------------------------------------------------
/reload.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/Qv2ray/mmp-go/config"
7 | )
8 |
9 | func ReloadConfig(oldConf *config.Config) {
10 | log.Println("Reloading configuration")
11 | mPortDispatcher.Lock()
12 | defer mPortDispatcher.Unlock()
13 |
14 | // rebuild config
15 | confPath := oldConf.ConfPath
16 | httpClient := oldConf.HttpClient
17 | newConf, err := config.BuildConfig(confPath, httpClient)
18 | if err != nil {
19 | log.Printf("failed to reload configuration: %v", err)
20 | return
21 | }
22 | // check if there is any net error when pulling the upstream configurations
23 | for i := range newConf.Groups {
24 | newGroup := &newConf.Groups[i]
25 | for j := range newGroup.Upstreams {
26 | newUpstream := newGroup.Upstreams[j]
27 | pErr := newUpstream.PullingError
28 | if pErr != nil {
29 | log.Printf("skip to update some servers in group %v , error on upstream %v: %v", newGroup.Name, newUpstream.Name, pErr)
30 | // error occurred, remain those servers
31 |
32 | // find the group in the oldConf
33 | var oldGroup *config.Group
34 | for k := range oldConf.Groups {
35 | // they should have the same port
36 | if oldConf.Groups[k].Port != newGroup.Port {
37 | continue
38 | }
39 | oldGroup = &oldConf.Groups[k]
40 | break
41 | }
42 | if oldGroup == nil {
43 | // cannot find the corresponding old group
44 | continue
45 | }
46 | // check if upstreamConf can match
47 | for k := range oldGroup.Servers {
48 | oldServer := oldGroup.Servers[k]
49 | if oldServer.UpstreamConf != nil && newUpstream.Equal(*oldServer.UpstreamConf) {
50 | // remain the server
51 | newGroup.Servers = append(newGroup.Servers, oldServer)
52 | }
53 | }
54 | }
55 | }
56 | }
57 | config.SetConfig(newConf)
58 | c := newConf
59 |
60 | // update dispatchers
61 | newConfPortSet := make(map[int]struct{})
62 | for i := range c.Groups {
63 | newConfPortSet[c.Groups[i].Port] = struct{}{}
64 |
65 | if t, ok := mPortDispatcher.Map[c.Groups[i].Port]; ok {
66 | // update the existing dispatcher
67 | for j := range protocols {
68 | t[j].UpdateGroup(&c.Groups[i])
69 | }
70 | } else {
71 | // add a new port dispatcher
72 | groupWG.Add(1)
73 | go func(group *config.Group) {
74 | listenGroup(group)
75 | groupWG.Done()
76 | }(&c.Groups[i])
77 | }
78 | }
79 | // close all removed port dispatcher
80 | for port := range mPortDispatcher.Map {
81 | if _, ok := newConfPortSet[port]; !ok {
82 | t := mPortDispatcher.Map[port]
83 | delete(mPortDispatcher.Map, port)
84 | for j := range protocols {
85 | _ = (*t)[j].Close()
86 | }
87 | }
88 | }
89 | log.Println("Reloaded configuration")
90 | }
91 |
--------------------------------------------------------------------------------
/signal_other.go:
--------------------------------------------------------------------------------
1 | //go:build !windows
2 | // +build !windows
3 |
4 | package main
5 |
6 | import (
7 | "os"
8 | "os/signal"
9 | "syscall"
10 |
11 | "github.com/Qv2ray/mmp-go/config"
12 | )
13 |
14 | func signalHandler(oldConf *config.Config) {
15 | ch := make(chan os.Signal, 1)
16 | signal.Notify(ch, syscall.SIGUSR1)
17 | for range ch {
18 | ReloadConfig(oldConf)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/signal_windows.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/Qv2ray/mmp-go/config"
7 | )
8 |
9 | // not support windows
10 | func signalHandler(*config.Config) {
11 | log.Println(`Signal-triggered configuration reloading is not supported on Windows`)
12 | }
13 |
--------------------------------------------------------------------------------
/systemd/README.md:
--------------------------------------------------------------------------------
1 | ## Service
2 |
3 | ### Install
4 |
5 | #### 1. copy binary file
6 |
7 | ```bash
8 | cp mmp-go /usr/bin/
9 | ```
10 |
11 | #### 2. add service file
12 |
13 | ```bash
14 | # copy service file to systemd
15 | cp systemd/mmp-go.service /etc/systemd/system/
16 | ```
17 |
18 | #### 3. add config file: config.json
19 |
20 | ```bash
21 | # copy config file to /etc/mmp-go/
22 | mkdir /etc/mmp-go/
23 | cp example.json /etc/mmp-go/config.json
24 | ```
25 |
26 | #### 4. enable and start service: mmp-go
27 |
28 | ```bash
29 | # enable and start service
30 | systemctl enable --now mmp-go
31 | ```
32 |
33 | See [mmp-go.service](mmp-go.service)
34 |
35 | ### Auto-Reload
36 | #### 1. enable and start
37 | ```bash
38 | systemctl enable --now mmp-go-reload.timer
39 | ```
40 |
41 | #### 2. customize
42 | Execute the following command:
43 | ```bash
44 | systemctl edit mmp-go-reload.timer
45 | ```
46 |
47 | Fill in your customized values, for example:
48 | ```
49 | # empty value means to remove the preset value
50 | [Unit]
51 | Description=
52 | Description=Eight-Hourly Reload mmp-go service
53 |
54 | [Timer]
55 | OnActiveSec=
56 | OnActiveSec=8h
57 | OnUnitActiveSec=
58 | OnUnitActiveSec=8h
59 | ```
60 |
61 | optionally do a daemon-reload afterwards:
62 | ```bash
63 | systemctl daemon-reload
64 | ```
65 |
--------------------------------------------------------------------------------
/systemd/mmp-go-reload.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Reload mmp-go service
3 | Documentation=https://github.com/Qv2ray/mmp-go/
4 | After=mmp-go.service
5 |
6 | [Service]
7 | Type=oneshot
8 | ExecStart=/bin/systemctl reload mmp-go.service
9 |
--------------------------------------------------------------------------------
/systemd/mmp-go-reload.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Daily Reload mmp-go service
3 | Documentation=https://github.com/Qv2ray/mmp-go/
4 | After=mmp-go.service
5 |
6 | [Timer]
7 | OnActiveSec=1d
8 | OnUnitActiveSec=1d
9 | RandomizedDelaySec=30min
10 |
11 | [Install]
12 | WantedBy=timer.target
13 |
--------------------------------------------------------------------------------
/systemd/mmp-go-reload@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Reload mmp-go service (%i)
3 | Documentation=https://github.com/Qv2ray/mmp-go/
4 | After=mmp-go@%i.service
5 |
6 | [Service]
7 | Type=oneshot
8 | ExecStart=/bin/systemctl reload mmp-go@%i.service
9 |
--------------------------------------------------------------------------------
/systemd/mmp-go-reload@.timer:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Daily Reload mmp-go service (%i)
3 | Documentation=https://github.com/Qv2ray/mmp-go/
4 | After=mmp-go@%i.service
5 |
6 | [Timer]
7 | OnActiveSec=1d
8 | OnUnitActiveSec=1d
9 | RandomizedDelaySec=30min
10 |
11 | [Install]
12 | WantedBy=timer.target
13 |
--------------------------------------------------------------------------------
/systemd/mmp-go.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=mmp-go Shadowsocks Multiplexer
3 | Documentation=https://github.com/Qv2ray/mmp-go/
4 | After=network.target nss-lookup.target
5 | Wants=network.target nss-lookup.target
6 |
7 | [Service]
8 | Type=simple
9 | User=nobody
10 | Restart=always
11 | LimitNOFILE=102400
12 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE
13 | AmbientCapabilities=CAP_NET_BIND_SERVICE
14 | NoNewPrivileges=true
15 | Environment="GODEBUG=madvdontneed=1"
16 | ExecStart=/usr/bin/mmp-go -conf /etc/mmp-go/config.json -suppress-timestamps
17 | ExecReload=/bin/kill -USR1 $MAINPID
18 |
19 | [Install]
20 | WantedBy=multi-user.target
21 |
--------------------------------------------------------------------------------
/systemd/mmp-go@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=mmp-go Shadowsocks Multiplexer (%i)
3 | Documentation=https://github.com/Qv2ray/mmp-go/
4 | After=network.target nss-lookup.target
5 | Wants=network.target nss-lookup.target
6 |
7 | [Service]
8 | Type=simple
9 | User=nobody
10 | Restart=always
11 | LimitNOFILE=102400
12 | CapabilityBoundingSet=CAP_NET_BIND_SERVICE
13 | AmbientCapabilities=CAP_NET_BIND_SERVICE
14 | NoNewPrivileges=true
15 | Environment="GODEBUG=madvdontneed=1"
16 | ExecStart=/usr/bin/mmp-go -conf /etc/mmp-go/%i.json -suppress-timestamps
17 | ExecReload=/bin/kill -USR1 $MAINPID
18 |
19 | [Install]
20 | WantedBy=multi-user.target
21 |
--------------------------------------------------------------------------------