├── .gitignore
├── LICENSE
├── README.md
├── README_zh_CN.md
├── config.json5
├── package.sh
├── pom.xml
└── src
└── main
├── kotlin
└── org
│ └── ffpy
│ └── portmux
│ ├── App.kt
│ ├── client
│ ├── ClientHandler.kt
│ └── ClientManager.kt
│ ├── commandparam
│ ├── CommandParamManager.kt
│ └── Param.kt
│ ├── config
│ ├── Config.kt
│ ├── ConfigManager.kt
│ ├── ForwardConfig.kt
│ ├── ForwardConfigManager.kt
│ └── ProtocolConfig.kt
│ ├── logger
│ ├── LogDataType.kt
│ └── LoggerManger.kt
│ ├── matcher
│ ├── MatchData.kt
│ ├── MatchResult.kt
│ ├── MatchState.kt
│ └── Matcher.kt
│ ├── protocol
│ ├── BaseProtocol.kt
│ ├── ByteBufPrefixProtocol.kt
│ ├── BytesProtocol.kt
│ ├── HexProtocol.kt
│ ├── PrefixProtocol.kt
│ ├── Protocol.kt
│ ├── Protocols.kt
│ └── RegexProtocol.kt
│ ├── server
│ ├── ForwardHandler.kt
│ ├── ForwardServer.kt
│ ├── MatchHandler.kt
│ └── WatchServer.kt
│ └── util
│ ├── AddressUtils.kt
│ ├── JsonUtils.kt
│ ├── NettyUtils.kt
│ └── StreamUtils.kt
└── resources
└── logback.xml
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
35 | *.log
36 | log/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # port-mux
2 | [](README.md)
3 | [](README_zh_CN.md)
4 |
5 | This is a proxy server that allows you to connect SSH, HTTP, VNC and other services on same port.
6 |
7 | This project refers to the idea of [switcher](https://github.com/jackyspy/switcher),
8 | and is implemented by Kotlin and Netty.
9 |
10 | By configuring in the configuration file, the traffic on the listening port can be easily forwarded to
11 | SSH, HTTP, VNC and other ports. This tool can be used for NAT traversal, limited number of outer net, etc.
12 |
13 | This tool also supports listening to the modification of the configuration file,
14 | so the program configuration can be updated without restarting the program.
15 |
16 | ## Usage
17 | - Requires: JDK8 or later
18 | - Start command:
19 | ```bash
20 | java -Xmx20m -jar port-mux.jar
21 | ```
22 | - Help
23 | ```
24 | Usage: java -jar port-mux.jar [options]
25 | Options:
26 | --help
27 |
28 | -config
29 | The path of configuration file.
30 | Default: config.json5
31 | -epoll
32 | Whether to use epoll mode. Epoll mode has better performance, but some systems do not support this mode.
33 | Default: false
34 | -watch-config
35 | Listening for modification of configuration file.
36 | Default: true
37 | ```
38 |
39 | ### config.json5
40 | The configuration file for this tool.
41 |
42 | The following is an example, listening on port 8200
43 | - SSH traffic is forwarded to port 22
44 | - HTTP traffic is forwarded to port 8080
45 | - RDP traffic is forwarded to port 3389
46 | - Other traffic is forwarded to port 8080
47 | ```json5
48 | {
49 | listen: ":8200",
50 | default: "127.0.0.1:8080",
51 | read_timeout_address: "127.0.0.1:5900",
52 | protocols: [
53 | {
54 | name: "ssh",
55 | type: "prefix",
56 | addr: "127.0.0.1:22",
57 | patterns: ["SSH"]
58 | },
59 | {
60 | name: "http",
61 | type: "prefix",
62 | addr: "127.0.0.1:8080",
63 | patterns: ["GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "OPTIONS "]
64 | },
65 | {
66 | name: "RDP",
67 | type: "hex",
68 | addr: "127.0.0.1:3389",
69 | patterns: ["030000130ee00000000000010008000b000000"]
70 | }
71 | ]
72 | }
73 | ```
74 |
75 | #### All items of configuration
76 | ```json5
77 | {
78 | // Listening address. Dynamic update is not supported for this
79 | listen: ":80",
80 | // The number of threads used by the forwarding service. The default value is twice the number of cpu cores. Dynamic update is not supported for this
81 | thread_num: 4,
82 | // The level for logging. The default value is info
83 | log_level: "info",
84 | // The format for printing data. The optional values are string, byte, hex, pretty_hex
85 | log_data_type: "pretty_hex",
86 | // The length of printed data. The default value is 1000
87 | log_data_len: 1000,
88 | // Default forwarding address
89 | default: "127.0.0.1:8080",
90 | // Timeout for connecting to forwarding address. The default value is 1000
91 | connect_timeout: 1000,
92 | // Timeout for reading data. The default value is 1000
93 | read_timeout : 1000,
94 | // The forwarding address if read timeout
95 | read_timeout_address: "127.0.0.1:5900",
96 | // Timeout for protocol matching. The default value is 5000
97 | match_timeout: 5000,
98 | // The list of forwarding protocols
99 | protocols: [
100 | {
101 | name: "ssh",
102 | type: "prefix",
103 | addr: "127.0.0.1:22",
104 | patterns: ["SSH"]
105 | },
106 | {
107 | name: "http",
108 | type: "prefix",
109 | addr: "127.0.0.1:8080",
110 | patterns: ["GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "OPTIONS "]
111 | },
112 | {
113 | name: "RDP",
114 | type: "hex",
115 | addr: "127.0.0.1:3389",
116 | patterns: ["030000130ee00000000000010008000b000000"]
117 | }
118 | ]
119 | }
120 | ```
121 |
122 | ## Protocol type
123 | ### prefix
124 | ```json5
125 | {
126 | // The name of protocol
127 | name: "ssh",
128 | // The type of protocol
129 | type: "prefix",
130 | // Forwarding address
131 | addr: "127.0.0.1:22",
132 | // The prefix string for matching
133 | patterns: ["SSH"]
134 | }
135 | ```
136 |
137 | ### regex
138 | Note:The regex type does not support multiple matches. It only matches the data received for the first time.
139 | ```json5
140 | {
141 | // The name of protocol
142 | name: "http_regex",
143 | // The type of protocol
144 | type: "regex",
145 | // Forwarding address
146 | addr: "127.0.0.1:80",
147 | // The minimum number of matching bytes, which means that data less than 4 bytes will fail to match
148 | min_len: 4,
149 | // The maximum number of matching bytes, which means that only the first 8 bytes of data will be converted to a string for matching
150 | max_len: 8,
151 | // The regular expression for matching
152 | patterns: ["^(GET|POST|PUT|DELETE|HEAD|OPTIONS) "]
153 | }
154 | ```
155 |
156 | ### bytes
157 | ```json5
158 | {
159 | // The name of protocol
160 | name: "RDP",
161 | // The type of protocol
162 | type: "bytes",
163 | // Forwarding address
164 | addr: "127.0.0.1:3389",
165 | // The byte array value for matching
166 | patterns: ["3, 0, 0, 19, 14, -32, 0, 0, 0, 0, 0, 1, 0, 8, 0, 11, 0, 0, 0"]
167 | }
168 | ```
169 |
170 | ### hex
171 | ```json5
172 | {
173 | // The name of protocol
174 | name: "RDP",
175 | // The type of protocol
176 | type: "hex",
177 | // Forwarding address
178 | addr: "127.0.0.1:3389",
179 | // The hex bytes for matching
180 | patterns: ["030000130ee00000000000010008000b000000"]
181 | }
182 | ```
183 |
184 | ## Debug
185 | You can print the data content in the forwarding process.
186 | ```json5
187 | {
188 | // The level for logging
189 | log_level: "debug",
190 | // The format for printing data. The optional values are string, byte, hex, pretty_hex
191 | log_data_type: "pretty_hex",
192 | // The length of printed data. The default value is 1000
193 | log_data_len: 1000,
194 | }
195 | ```
196 | 1. Set `log_level` to `debug` level.
197 | 2. Set `log_data_type` to the print format you want.
198 |
199 | ### log_data_type
200 | #### string
201 | ```text
202 | 2021-12-30 10:10:14.006 DEBUG [nioEventLoopGroup-3-1] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51012 发送数据(78): GET / HTTP/1.1
203 | Host: 127.0.0.1:8200
204 | User-Agent: curl/7.68.0
205 | Accept: */*
206 |
207 |
208 | ```
209 |
210 | #### byte
211 | ```text
212 | 2021-12-30 10:11:57.554 DEBUG [nioEventLoopGroup-3-3] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51668 发送数据(78): [71, 69, 84, 32, 47, 32, 72, 84, 84, 80, 47, 49, 46, 49, 13, 10, 72, 111, 115, 116, 58, 32, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 56, 50, 48, 48, 13, 10, 85, 115, 101, 114, 45, 65, 103, 101, 110, 116, 58, 32, 99, 117, 114, 108, 47, 55, 46, 54, 56, 46, 48, 13, 10, 65, 99, 99, 101, 112, 116, 58, 32, 42, 47, 42, 13, 10, 13, 10]
213 | ```
214 |
215 | #### hex
216 | ```text
217 | 2021-12-30 10:12:27.446 DEBUG [nioEventLoopGroup-3-4] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51687 发送数据(78): 474554202f20485454502f312e310d0a486f73743a203132372e302e302e313a383230300d0a557365722d4167656e743a206375726c2f372e36382e300d0a4163636570743a202a2f2a0d0a0d0a
218 | ```
219 |
220 | #### pretty_hex
221 | ```text
222 | 2021-12-30 10:12:48.586 DEBUG [nioEventLoopGroup-3-5] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51702 发送数据(78):
223 | +-------------------------------------------------+
224 | | 0 1 2 3 4 5 6 7 8 9 a b c d e f |
225 | +--------+-------------------------------------------------+----------------+
226 | |00000000| 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a |GET / HTTP/1.1..|
227 | |00000010| 48 6f 73 74 3a 20 31 32 37 2e 30 2e 30 2e 31 3a |Host: 127.0.0.1:|
228 | |00000020| 38 32 30 30 0d 0a 55 73 65 72 2d 41 67 65 6e 74 |8200..User-Agent|
229 | |00000030| 3a 20 63 75 72 6c 2f 37 2e 36 38 2e 30 0d 0a 41 |: curl/7.68.0..A|
230 | |00000040| 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 0d 0a |ccept: */*.... |
231 | +--------+-------------------------------------------------+----------------+
232 | ```
233 |
234 | ## License
235 | port-mux is licensed under the Apache License, Version 2.0
--------------------------------------------------------------------------------
/README_zh_CN.md:
--------------------------------------------------------------------------------
1 | # port-mux
2 | [](README.md)
3 | [](README_zh_CN.md)
4 |
5 | 这是一个代理服务工具,让你可以在同一个端口上连接SSH、HTTP、VNC等多种服务。
6 |
7 | 此项目参考了[switcher](https://github.com/jackyspy/switcher) 项目的思想,用 Kotlin 和 Netty 来实现。
8 |
9 | 通过在配置文件中进行配置,即可方便地把监听端口上的流量转发到SSH、HTTP、VNC等端口上。可用于内网穿透、外网端口数受限等情况。
10 |
11 | 此工具还支持监听配置文件的修改,因此不需要重启程序即可更新程序配置。
12 |
13 | ## 用法
14 | - 运行环境: JDK8或以上
15 | - 启动命令:
16 | ```bash
17 | java -Xmx20m -jar port-mux.jar
18 | ```
19 | - 帮助
20 | ```
21 | Usage: java -jar port-mux.jar [options]
22 | Options:
23 | --help
24 |
25 | -config
26 | 配置文件路径
27 | Default: config.json5
28 | -epoll
29 | 是否使用epoll模式,epoll模式的性能更好,但是有的系统不支持这个模式
30 | Default: false
31 | -watch-config
32 | 是否监听配置文件改变
33 | Default: true
34 | ```
35 |
36 | ### config.json5
37 | 程序配置文件
38 |
39 | 下面是一个示例,监听 8200 端口
40 | - SSH 流量转发到 22 端口
41 | - HTTP 流量转发到 8080 端口
42 | - Windows 远程桌面流量转发到 3389 端口
43 | - 其他流量转发到 8080 端口
44 | ```json5
45 | {
46 | listen: ":8200",
47 | default: "127.0.0.1:8080",
48 | read_timeout_address: "127.0.0.1:5900",
49 | protocols: [
50 | {
51 | name: "ssh",
52 | type: "prefix",
53 | addr: "127.0.0.1:22",
54 | patterns: ["SSH"]
55 | },
56 | {
57 | name: "http",
58 | type: "prefix",
59 | addr: "127.0.0.1:8080",
60 | patterns: ["GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "OPTIONS "]
61 | },
62 | {
63 | name: "Windows远程桌面",
64 | type: "hex",
65 | addr: "127.0.0.1:3389",
66 | patterns: ["030000130ee00000000000010008000b000000"]
67 | }
68 | ]
69 | }
70 | ```
71 |
72 | #### 全部配置项
73 | ```json5
74 | {
75 | // 监听地址,不支持动态更新
76 | listen: ":80",
77 | // 转发服务使用的线程数,默认为CPU核心数的2倍,不支持动态更新
78 | thread_num: 4,
79 | // 日志级别
80 | log_level: "info",
81 | // 打印转发数据方式,可选值: string, byte, hex, pretty_hex
82 | log_data_type: "pretty_hex",
83 | // 打印转发数据的长度,默认为1000
84 | log_data_len: 1000,
85 | // 默认转发地址
86 | default: "127.0.0.1:8080",
87 | // 连接转发地址超时时间(毫秒),默认为1000
88 | connect_timeout: 1000,
89 | // 读取超时时间(毫秒),默认为1000
90 | read_timeout : 1000,
91 | // 读取超时的转发地址
92 | read_timeout_address: "127.0.0.1:5900",
93 | // 匹配超时时间(毫秒),默认为5000
94 | match_timeout: 5000,
95 | // 转发协议配置
96 | protocols: [
97 | {
98 | name: "ssh",
99 | type: "prefix",
100 | addr: "127.0.0.1:22",
101 | patterns: ["SSH"]
102 | },
103 | {
104 | name: "http",
105 | type: "prefix",
106 | addr: "127.0.0.1:8080",
107 | patterns: ["GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "OPTIONS "]
108 | },
109 | {
110 | name: "Windows远程桌面",
111 | type: "hex",
112 | addr: "127.0.0.1:3389",
113 | patterns: ["030000130ee00000000000010008000b000000"]
114 | }
115 | ]
116 | }
117 | ```
118 |
119 | ## 匹配类型
120 | ### prefix
121 | ```json5
122 | {
123 | // 协议名称
124 | name: "ssh",
125 | // 匹配类型
126 | type: "prefix",
127 | // 转发地址
128 | addr: "127.0.0.1:22",
129 | // 匹配前缀字符串
130 | patterns: ["SSH"]
131 | }
132 | ```
133 |
134 | ### regex
135 | 注意:regex 类型不支持多次匹配,只会匹配第一次接收到的数据
136 | ```json5
137 | {
138 | // 协议名称
139 | name: "http_regex",
140 | // 匹配类型
141 | type: "regex",
142 | // 转发地址
143 | addr: "127.0.0.1:80",
144 | // 最小匹配字节数,这里的意思是小于4字节的数据直接算匹配失败
145 | min_len: 4,
146 | // 最大匹配字节数,这里的意思是只会把前8个字节的数据转为字符串进行匹配
147 | max_len: 8,
148 | // 匹配正则表达式
149 | patterns: ["^(GET|POST|PUT|DELETE|HEAD|OPTIONS) "]
150 | }
151 | ```
152 |
153 | ### bytes
154 | ```json5
155 | {
156 | // 协议名称
157 | name: "Windows远程连接",
158 | // 匹配类型
159 | type: "bytes",
160 | // 转发地址
161 | addr: "127.0.0.1:3389",
162 | // 匹配前缀字节数组
163 | patterns: ["3, 0, 0, 19, 14, -32, 0, 0, 0, 0, 0, 1, 0, 8, 0, 11, 0, 0, 0"]
164 | }
165 | ```
166 |
167 | ### hex
168 | ```json5
169 | {
170 | // 协议名称
171 | name: "Windows远程连接",
172 | // 匹配类型
173 | type: "hex",
174 | // 转发地址
175 | addr: "127.0.0.1:3389",
176 | // 匹配前缀十六进制字节
177 | patterns: ["030000130ee00000000000010008000b000000"]
178 | }
179 | ```
180 |
181 | ## 调试
182 | 调试功能让你能够打印转发过程中的数据内容
183 | ```json5
184 | {
185 | // 日志级别
186 | log_level: "debug",
187 | // 打印转发数据格式,默认为pretty_hex,可选值: string, byte, hex, pretty_hex
188 | log_data_type: "pretty_hex",
189 | // 打印转发数据的长度,默认为1000
190 | log_data_len: 1000,
191 | }
192 | ```
193 | 1. `log_level` 设置为 `debug` 级别
194 | 2. 设置 `log_data_type` 为你想要的打印格式
195 |
196 | ### log_data_type
197 | #### string
198 | ```text
199 | 2021-12-30 10:10:14.006 DEBUG [nioEventLoopGroup-3-1] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51012 发送数据(78): GET / HTTP/1.1
200 | Host: 127.0.0.1:8200
201 | User-Agent: curl/7.68.0
202 | Accept: */*
203 |
204 |
205 | ```
206 |
207 | #### byte
208 | ```text
209 | 2021-12-30 10:11:57.554 DEBUG [nioEventLoopGroup-3-3] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51668 发送数据(78): [71, 69, 84, 32, 47, 32, 72, 84, 84, 80, 47, 49, 46, 49, 13, 10, 72, 111, 115, 116, 58, 32, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 56, 50, 48, 48, 13, 10, 85, 115, 101, 114, 45, 65, 103, 101, 110, 116, 58, 32, 99, 117, 114, 108, 47, 55, 46, 54, 56, 46, 48, 13, 10, 65, 99, 99, 101, 112, 116, 58, 32, 42, 47, 42, 13, 10, 13, 10]
210 | ```
211 |
212 | #### hex
213 | ```text
214 | 2021-12-30 10:12:27.446 DEBUG [nioEventLoopGroup-3-4] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51687 发送数据(78): 474554202f20485454502f312e310d0a486f73743a203132372e302e302e313a383230300d0a557365722d4167656e743a206375726c2f372e36382e300d0a4163636570743a202a2f2a0d0a0d0a
215 | ```
216 |
217 | #### pretty_hex
218 | ```text
219 | 2021-12-30 10:12:48.586 DEBUG [nioEventLoopGroup-3-5] org.ffpy.portmux.server.MatchHandler : /127.0.0.1:51702 发送数据(78):
220 | +-------------------------------------------------+
221 | | 0 1 2 3 4 5 6 7 8 9 a b c d e f |
222 | +--------+-------------------------------------------------+----------------+
223 | |00000000| 47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a |GET / HTTP/1.1..|
224 | |00000010| 48 6f 73 74 3a 20 31 32 37 2e 30 2e 30 2e 31 3a |Host: 127.0.0.1:|
225 | |00000020| 38 32 30 30 0d 0a 55 73 65 72 2d 41 67 65 6e 74 |8200..User-Agent|
226 | |00000030| 3a 20 63 75 72 6c 2f 37 2e 36 38 2e 30 0d 0a 41 |: curl/7.68.0..A|
227 | |00000040| 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a 0d 0a |ccept: */*.... |
228 | +--------+-------------------------------------------------+----------------+
229 | ```
230 |
231 | ## License
232 | port-mux is licensed under the Apache License, Version 2.0
--------------------------------------------------------------------------------
/config.json5:
--------------------------------------------------------------------------------
1 | {
2 | // 监听地址,不支持动态更新
3 | listen: ":8200",
4 | // 转发服务使用的线程数,默认为CPU核心数的2倍,不支持动态更新
5 | thread_num: 4,
6 | // 日志级别,默认为info
7 | log_level: "debug",
8 | // 打印转发数据格式,默认为pretty_hex,可选值: string, byte, hex, pretty_hex
9 | log_data_type: "pretty_hex",
10 | // 打印转发数据的长度,默认为1000
11 | log_data_len: 1000,
12 | // 默认转发地址
13 | default: "127.0.0.1:8080",
14 | // 连接转发地址超时时间(毫秒)
15 | connect_timeout: 1000,
16 | // 读取超时时间(毫秒),默认为1000
17 | read_timeout : 1000,
18 | // 读取超时的转发地址
19 | read_timeout_address: "127.0.0.1:5900",
20 | // 匹配超时时间(毫秒),默认为5000
21 | match_timeout: 5000,
22 | // 转发协议配置
23 | protocols: [
24 | {
25 | name: "ssh",
26 | type: "prefix",
27 | addr: "127.0.0.1:22",
28 | patterns: ["SSH"]
29 | },
30 | {
31 | name: "http",
32 | type: "prefix",
33 | addr: "127.0.0.1:80",
34 | patterns: ["GET ", "POST ", "PUT ", "DELETE ", "HEAD ", "OPTIONS "]
35 | },
36 | {
37 | name: "Windows远程桌面",
38 | type: "hex",
39 | addr: "127.0.0.1:3389",
40 | patterns: ["030000130ee00000000000010008000b000000"]
41 | }
42 | ]
43 | }
44 |
--------------------------------------------------------------------------------
/package.sh:
--------------------------------------------------------------------------------
1 | mvn clean kotlin:compile package
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | port-mux
8 | org.ffpy
9 | 1.2.1
10 | jar
11 |
12 | port-mux
13 |
14 |
15 | UTF-8
16 | official
17 | 1.8
18 |
19 |
20 |
21 |
22 | mavenCentral
23 | https://repo1.maven.org/maven2/
24 |
25 |
26 |
27 |
28 | src/main/kotlin
29 | src/test/kotlin
30 |
31 |
32 | org.jetbrains.kotlin
33 | kotlin-maven-plugin
34 | 1.6.10
35 |
36 |
37 | compile
38 | compile
39 |
40 | compile
41 |
42 |
43 |
44 | test-compile
45 | test-compile
46 |
47 | test-compile
48 |
49 |
50 |
51 |
52 |
53 | maven-surefire-plugin
54 | 2.22.2
55 |
56 |
57 |
58 | org.apache.maven.plugins
59 | maven-assembly-plugin
60 | 3.3.0
61 |
62 | port-mux
63 |
64 |
65 | org.ffpy.portmux.AppKt
66 |
67 |
68 |
69 | jar-with-dependencies
70 |
71 |
72 |
73 |
74 | make-assembly
75 | package
76 |
77 | single
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | org.jetbrains.kotlin
89 | kotlin-stdlib-jdk8
90 | 1.6.10
91 |
92 |
93 |
94 |
95 | blue.endless
96 | jankson
97 | 1.2.1
98 |
99 |
100 |
101 |
102 | com.google.code.gson
103 | gson
104 | 2.8.9
105 |
106 |
107 |
108 |
109 | io.netty
110 | netty-all
111 | 4.1.72.Final
112 |
113 |
114 |
115 |
116 | com.beust
117 | jcommander
118 | 1.81
119 |
120 |
121 |
122 |
123 | org.slf4j
124 | slf4j-api
125 | 1.7.32
126 |
127 |
128 | ch.qos.logback
129 | logback-classic
130 | 1.2.10
131 |
132 |
133 | ch.qos.logback
134 | logback-core
135 | 1.2.10
136 |
137 |
138 |
139 |
140 | org.jetbrains.kotlin
141 | kotlin-test-junit5
142 | 1.6.10
143 | test
144 |
145 |
146 | org.junit.jupiter
147 | junit-jupiter-api
148 | 5.8.2
149 | test
150 |
151 |
152 | org.junit.jupiter
153 | junit-jupiter-engine
154 | 5.8.2
155 | test
156 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/App.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux
2 |
3 | import org.ffpy.portmux.commandparam.CommandParamManager
4 | import org.ffpy.portmux.config.ConfigManager
5 | import org.ffpy.portmux.config.ForwardConfigManager
6 | import org.ffpy.portmux.logger.LoggerManger
7 | import org.ffpy.portmux.server.ForwardServer
8 | import org.ffpy.portmux.server.WatchServer
9 | import org.slf4j.Logger
10 | import org.slf4j.LoggerFactory
11 | import java.nio.file.Paths
12 | import kotlin.system.exitProcess
13 |
14 | class App
15 |
16 | fun main(vararg args: String) {
17 | val log: Logger = LoggerFactory.getLogger("main")
18 |
19 | try {
20 | CommandParamManager.init(args)
21 | val configPath = Paths.get(CommandParamManager.param.config)
22 |
23 | ConfigManager.init(configPath)
24 | LoggerManger.init()
25 | ForwardConfigManager.init()
26 |
27 | if (CommandParamManager.param.watchConfig) {
28 | WatchServer().start(configPath)
29 | }
30 |
31 | ForwardServer().start()
32 | } catch (e: Exception) {
33 | log.error(e.message)
34 | exitProcess(-1)
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/client/ClientHandler.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.client
2 |
3 | import io.netty.buffer.ByteBuf
4 | import io.netty.channel.Channel
5 | import io.netty.channel.ChannelHandlerContext
6 | import io.netty.channel.ChannelInboundHandlerAdapter
7 | import org.ffpy.portmux.logger.LoggerManger
8 | import org.slf4j.LoggerFactory
9 |
10 | /**
11 | * 目标连接处理器
12 | *
13 | * @param serverChannel 对应的源连接
14 | */
15 | class ClientHandler(private val serverChannel: Channel) : ChannelInboundHandlerAdapter() {
16 | companion object {
17 | private val log = LoggerFactory.getLogger(ClientHandler::class.java)
18 | }
19 |
20 | override fun channelInactive(ctx: ChannelHandlerContext) {
21 | log.info("${ctx.channel().remoteAddress()} client disconnect")
22 | serverChannel.close()
23 | }
24 |
25 | override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
26 | LoggerManger.logData(log, msg as ByteBuf, ctx.channel().remoteAddress())
27 | serverChannel.write(msg)
28 | }
29 |
30 | override fun channelReadComplete(ctx: ChannelHandlerContext) {
31 | serverChannel.flush()
32 | }
33 |
34 | override fun channelWritabilityChanged(ctx: ChannelHandlerContext) {
35 | serverChannel.config().isAutoRead = ctx.channel().isWritable
36 | }
37 |
38 | override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
39 | log.error("${serverChannel.remoteAddress()} error: ${cause.message}", cause)
40 | ctx.close()
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/client/ClientManager.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.client
2 |
3 | import io.netty.bootstrap.Bootstrap
4 | import io.netty.channel.Channel
5 | import io.netty.channel.ChannelFuture
6 | import io.netty.channel.ChannelInitializer
7 | import io.netty.channel.ChannelOption
8 | import io.netty.channel.socket.SocketChannel
9 | import org.ffpy.portmux.util.NettyUtils
10 | import java.net.SocketAddress
11 |
12 | /**
13 | * 客户端管理器
14 | */
15 | object ClientManager {
16 |
17 | private val bootstrap = Bootstrap()
18 | .channel(NettyUtils.getSocketChannelClass())
19 | .option(ChannelOption.SO_KEEPALIVE, true)
20 |
21 | /**
22 | * 连接客户端
23 | *
24 | * @param serverChannel 服务端的Channel
25 | * @param address 客户端地址
26 | * @param connectTimeout 连接超时时间(毫秒)
27 | */
28 | fun connect(serverChannel: Channel, address: SocketAddress, connectTimeout: Int): ChannelFuture =
29 | bootstrap.clone(serverChannel.eventLoop())
30 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeout)
31 | .handler(object : ChannelInitializer() {
32 | override fun initChannel(ch: SocketChannel) {
33 | ch.pipeline().addLast(ClientHandler(serverChannel))
34 | }
35 | })
36 | .connect(address)
37 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/commandparam/CommandParamManager.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.commandparam
2 |
3 | import com.beust.jcommander.JCommander
4 | import kotlin.system.exitProcess
5 |
6 | /**
7 | * 命令行参数解析管理器
8 | */
9 | object CommandParamManager {
10 | /** 命令行参数对象 */
11 | val param: Param
12 | get() = param_ ?: throw IllegalStateException("Not initialized yet")
13 |
14 | private var param_: Param? = null
15 |
16 | /**
17 | * 初始化
18 | * @param args 命令行参数
19 | */
20 | fun init(args: Array) {
21 | val param = Param()
22 | val jCommander = JCommander.newBuilder()
23 | .addObject(param)
24 | .build()
25 | jCommander.parse(*args)
26 |
27 | if (param.help) {
28 | jCommander.usage()
29 | exitProcess(0)
30 | }
31 |
32 | param_ = param
33 | }
34 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/commandparam/Param.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.commandparam
2 |
3 | import com.beust.jcommander.Parameter
4 |
5 | /**
6 | * 命令行参数对象
7 | */
8 | data class Param(
9 | @Parameter(names = ["--help"], help = true)
10 | var help: Boolean = false,
11 |
12 | @Parameter(names = ["-config"], description = "The path of configuration file.")
13 | var config: String = "config.json5",
14 |
15 | @Parameter(names = ["-watch-config"], description = "Listening for modification of configuration file.")
16 | var watchConfig: Boolean = true,
17 |
18 | @Parameter(names = ["-epoll"], description = "Whether to use epoll mode. Epoll mode has better performance, but some systems do not support this mode.")
19 | var epoll: Boolean = false,
20 | )
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/config/Config.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.config
2 |
3 | import io.netty.util.NettyRuntime
4 | import org.ffpy.portmux.logger.LogDataType
5 |
6 | /**
7 | * 项目配置
8 | */
9 | data class Config(
10 | /** 监听端口 */
11 | var listen: String = "",
12 |
13 | /** 转发服务使用的线程数 */
14 | var threadNum: Int = NettyRuntime.availableProcessors() * 2,
15 |
16 | /** 日志级别 */
17 | var logLevel: String = "",
18 |
19 | /** 调试模式 */
20 | var logDataType: String = LogDataType.PRETTY_HEX.code,
21 |
22 | /** 打印转发数据的长度 */
23 | var logDataLen: Int = 1000,
24 |
25 | /** 默认转发地址 */
26 | var default: String = "",
27 |
28 | /** 连接超时时间(毫秒) */
29 | var connectTimeout: Int = 1000,
30 |
31 | /** 读取超时时间(毫秒) */
32 | var readTimeout: Int = 1000,
33 |
34 | /** 读取超时的转发地址 */
35 | var readTimeoutAddress: String = "",
36 |
37 | /** 匹配超时时间(毫秒) */
38 | var matchTimeout: Int = 5000,
39 |
40 | /** 转发配置 */
41 | var protocols: List = emptyList(),
42 | )
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/config/ConfigManager.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.config
2 |
3 | import org.ffpy.portmux.logger.LogDataType
4 | import org.ffpy.portmux.protocol.Protocols
5 | import org.ffpy.portmux.util.AddressUtils
6 | import org.ffpy.portmux.util.JsonUtils
7 | import java.nio.file.Path
8 | import java.util.*
9 |
10 | typealias OnChangedListener = (Config) -> Unit
11 |
12 | /**
13 | * 配置文件管理器
14 | */
15 | object ConfigManager {
16 |
17 | /** 配置信息对象 */
18 | val config: Config
19 | get() = config_ ?: throw IllegalStateException("Not initialized yet")
20 |
21 | private var config_: Config? = null
22 |
23 | private val onChangedListeners: MutableList = Collections.synchronizedList(ArrayList())
24 |
25 | /**
26 | * 加载配置文件
27 | *
28 | * @param path 文件路径
29 | */
30 | @Throws(Exception::class)
31 | fun init(path: Path) {
32 | if (!path.toFile().exists()) {
33 | throw Exception("The configuration file is not found: $path")
34 | }
35 | config_ = check(parseConfig(path))
36 | onChangedListeners.forEach { it(config) }
37 | }
38 |
39 | private fun parseConfig(path: Path): Config {
40 | try {
41 | return JsonUtils.parse(path, Config::class.java)
42 | } catch (e: Exception) {
43 | throw Exception("Parse configuration fail: ${e.message}", e)
44 | }
45 | }
46 |
47 | /**
48 | * 添加配置文件改变监听器
49 | */
50 | fun addOnChangedListener(listener: OnChangedListener) {
51 | onChangedListeners.add(listener)
52 | }
53 |
54 | /**
55 | * 检查配置信息是否有效,如果无效则打印错误信息,并且结束程序
56 | *
57 | * @param config 要检查的配置信息
58 | * @return 原样放回config
59 | */
60 | private fun check(config: Config): Config {
61 | if (!AddressUtils.validAddress(config.listen)) {
62 | throw Exception("The listen address is invalid: ${config.listen}")
63 | }
64 | if (config.threadNum < 1) {
65 | throw Exception("thread_num cannot be less than 1")
66 | }
67 | if (config.logDataType.isNotEmpty()) {
68 | LogDataType.of(config.logDataType)
69 | }
70 | if (config.logDataLen < 1) {
71 | throw Exception("log_data_len cannot be less than 1")
72 | }
73 | if (config.default.isNotEmpty() && !AddressUtils.validAddress(config.default)) {
74 | throw Exception("default address is invalid: ${config.default}")
75 | }
76 | if (config.connectTimeout < 1) {
77 | throw Exception("connect_timeout cannot be less than 1")
78 | }
79 | if (config.readTimeout < 1) {
80 | throw Exception("read_timeout cannot be less than 1")
81 | }
82 | if (config.readTimeoutAddress.isNotEmpty() && !AddressUtils.validAddress(config.readTimeoutAddress)) {
83 | throw Exception("read_timeout_address is invalid")
84 | }
85 | if (config.matchTimeout < 1) {
86 | throw Exception("match_timeout cannot be less than 1")
87 | }
88 |
89 | config.protocols.forEachIndexed { index, protocol -> checkProtocol(index, protocol) }
90 |
91 | return config
92 | }
93 |
94 | private fun checkProtocol(index: Int, protocol: ProtocolConfig) {
95 | if (protocol.name.isEmpty()) {
96 | throw Exception("protocol[${index}].name cannot be empty")
97 | }
98 | if (protocol.type.isEmpty()) {
99 | throw Exception("protocol[${index}].type cannot be empty")
100 | }
101 | val type = Protocols.values().asSequence()
102 | .filter { it.type == protocol.type }
103 | .firstOrNull() ?: throw Exception("Unknown protocol[${index}].type: ${protocol.type}")
104 | type.check(protocol, index)
105 |
106 | if (!AddressUtils.validAddress(protocol.addr)) {
107 | throw Exception("protocol[${index}].addr is invalid: ${protocol.addr}")
108 | }
109 |
110 | if (protocol.patterns.isEmpty()) {
111 | throw Exception("protocol[${index}].patterns cannot be empty")
112 | }
113 | }
114 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/config/ForwardConfig.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.config
2 |
3 | import org.ffpy.portmux.protocol.Protocols
4 | import org.ffpy.portmux.util.AddressUtils
5 | import java.net.SocketAddress
6 |
7 | /**
8 | * 转发配置
9 | */
10 | class ForwardConfig(config: Config) {
11 |
12 | /** 转发协议列表 */
13 | val protocols = config.protocols.map { Protocols.create(it) }
14 |
15 | /** 默认转发地址 */
16 | val defaultAddress: SocketAddress? =
17 | if (config.default.isEmpty()) null else AddressUtils.parseAddress(config.default)
18 |
19 | /** 连接超时时间(毫秒) */
20 | val connectTimeout = config.connectTimeout
21 |
22 | /** 读取超时时间(毫秒) */
23 | val readTimeout = config.readTimeout
24 |
25 | /** 匹配超时时间(毫秒) */
26 | val matchTimeout = config.matchTimeout
27 |
28 | /** 读取超时的转发地址 */
29 | val readTimeoutAddress: SocketAddress? =
30 | if (config.readTimeoutAddress.isEmpty()) null else AddressUtils.parseAddress(config.readTimeoutAddress)
31 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/config/ForwardConfigManager.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.config
2 |
3 | object ForwardConfigManager {
4 |
5 | lateinit var forwardConfig: ForwardConfig
6 | private set
7 |
8 | fun init() {
9 | forwardConfig = ForwardConfig(ConfigManager.config)
10 | ConfigManager.addOnChangedListener {
11 | forwardConfig = ForwardConfig(it)
12 | }
13 | }
14 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/config/ProtocolConfig.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.config
2 |
3 | /**
4 | * 协议配置
5 | */
6 | data class ProtocolConfig(
7 | /** 名称 */
8 | var name: String = "",
9 |
10 | /** 转发类型 */
11 | var type: String = "",
12 |
13 | /** 转发地址 */
14 | var addr: String = "",
15 |
16 | /** 最短字节数,regex类型时有效 */
17 | var minLen: Int = 0,
18 |
19 | /** 最长字节数,regex类型时有效 */
20 | var maxLen: Int = 0,
21 |
22 | /** 匹配字符串 */
23 | var patterns: List = emptyList(),
24 | )
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/logger/LogDataType.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.logger
2 |
3 | import io.netty.buffer.ByteBuf
4 | import io.netty.buffer.ByteBufUtil
5 |
6 | /**
7 | * 打印数据类型
8 | *
9 | * @param code 类型码
10 | * @param action ByteBuf转字符串的函数
11 | */
12 | enum class LogDataType(
13 | val code: String,
14 | private val action: (ByteBuf) -> String
15 | ) {
16 | STRING("string", { it.toString(Charsets.UTF_8) }),
17 |
18 | BYTE("byte", { ByteBufUtil.getBytes(it).contentToString() }),
19 |
20 | HEX("hex", { ByteBufUtil.hexDump(it) }),
21 |
22 | PRETTY_HEX("pretty_hex", { "\n" + ByteBufUtil.prettyHexDump(it) }),
23 | ;
24 |
25 | companion object {
26 |
27 | /**
28 | * 类型码转 [LogDataType]
29 | *
30 | * @param code 类型码
31 | * @return 对应的[LogDataType]
32 | * @throws Exception 如果不支持此类型码
33 | */
34 | fun of(code: String): LogDataType {
35 | for (value in values()) {
36 | if (value.code == code) return value
37 | }
38 | throw Exception("log_data_type not support this type: $code")
39 | }
40 | }
41 |
42 | /**
43 | * 执行转换
44 | *
45 | * @param buf 要转换的ByteBuf
46 | * @return 转换后的字符串
47 | */
48 | fun apply(buf: ByteBuf) = action(buf)
49 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/logger/LoggerManger.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.logger
2 |
3 | import ch.qos.logback.classic.Level
4 | import ch.qos.logback.classic.LoggerContext
5 | import io.netty.buffer.ByteBuf
6 | import org.ffpy.portmux.App
7 | import org.ffpy.portmux.config.ConfigManager
8 | import org.slf4j.Logger
9 | import org.slf4j.LoggerFactory
10 | import java.net.SocketAddress
11 | import kotlin.math.min
12 |
13 | object LoggerManger {
14 |
15 | private val PACKAGE_NAME = App::class.java.`package`.name
16 |
17 | fun init() {
18 | refreshLevel()
19 | ConfigManager.addOnChangedListener { refreshLevel() }
20 | }
21 |
22 | /**
23 | * 打印转发数据
24 | *
25 | * @param log [Logger]
26 | * @param buf 要打印的数据
27 | * @param address 发送数据的地址
28 | */
29 | fun logData(log: Logger, buf: ByteBuf, address: SocketAddress?) {
30 | if (!log.isDebugEnabled) return
31 |
32 | val config = ConfigManager.config
33 | val debug = config.logDataType
34 | if (debug.isNotEmpty()) {
35 | val sliceBuf = buf.slice(buf.readerIndex(), min(buf.readableBytes(), config.logDataLen))
36 | val data = LogDataType.of(debug).apply(sliceBuf)
37 | log.debug("{} send({}): {}", address, buf.readableBytes(), data)
38 | }
39 | }
40 |
41 | private fun refreshLevel() {
42 | setLevel(Level.toLevel(ConfigManager.config.logLevel, Level.INFO))
43 | }
44 |
45 | private fun setLevel(level: Level) {
46 | val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext
47 | loggerContext.getLogger(PACKAGE_NAME).level = level
48 | }
49 |
50 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/matcher/MatchData.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.matcher
2 |
3 | import io.netty.buffer.ByteBuf
4 |
5 | data class MatchData(val buf: ByteBuf)
6 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/matcher/MatchResult.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.matcher
2 |
3 | import java.net.SocketAddress
4 |
5 | /**
6 | * 匹配结果
7 | */
8 | data class MatchResult(
9 | /** 是否已经匹配结束 */
10 | val finish: Boolean,
11 |
12 | /** 匹配结果地址,为空则说明匹配失败 */
13 | val address: SocketAddress?
14 | )
15 |
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/matcher/MatchState.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.matcher
2 |
3 | /**
4 | * 匹配状态
5 | */
6 | enum class MatchState {
7 | /** 匹配 */
8 | MATCH,
9 |
10 | /** 不匹配 */
11 | NOT_MATCH,
12 |
13 | /** 部分匹配 */
14 | MAYBE
15 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/matcher/Matcher.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.matcher
2 |
3 | import io.netty.buffer.ByteBuf
4 | import io.netty.buffer.Unpooled
5 | import org.ffpy.portmux.protocol.Protocol
6 | import org.slf4j.LoggerFactory
7 | import java.net.SocketAddress
8 |
9 | class Matcher(
10 | private var protocols: List,
11 | ) {
12 |
13 | companion object {
14 | private val log = LoggerFactory.getLogger(Matcher::class.java)
15 | }
16 |
17 | fun match(buf: ByteBuf, remoteAddress: SocketAddress?, defaultAddress: SocketAddress?): MatchResult {
18 | val data = MatchData(Unpooled.wrappedUnmodifiableBuffer(buf))
19 |
20 | val maybeList = ArrayList()
21 | for (protocol in protocols) {
22 | when (protocol.match(data)) {
23 | MatchState.MATCH -> {
24 | log.info("{} match protocol: {}", remoteAddress, protocol.name)
25 | return MatchResult(true, protocol.address)
26 | }
27 | MatchState.MAYBE -> {
28 | maybeList.add(protocol)
29 | }
30 | MatchState.NOT_MATCH -> {
31 | // do nothing
32 | }
33 | }
34 | }
35 |
36 | if (maybeList.isEmpty()) {
37 | log.info("{} match fail, forward to default address", remoteAddress)
38 | return MatchResult(true, defaultAddress)
39 | }
40 |
41 | protocols = maybeList
42 |
43 | return MatchResult(false, null)
44 | }
45 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/BaseProtocol.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import org.ffpy.portmux.config.ProtocolConfig
4 | import org.ffpy.portmux.util.AddressUtils
5 | import java.net.SocketAddress
6 |
7 | abstract class BaseProtocol(val config: ProtocolConfig) : Protocol {
8 |
9 | override val name: String
10 | get() = config.name
11 |
12 | override val address: SocketAddress by lazy {
13 | AddressUtils.parseAddress(config.addr)
14 | }
15 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/ByteBufPrefixProtocol.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import io.netty.buffer.ByteBuf
4 | import io.netty.buffer.ByteBufUtil
5 | import org.ffpy.portmux.config.ProtocolConfig
6 | import org.ffpy.portmux.matcher.MatchData
7 | import org.ffpy.portmux.matcher.MatchState
8 | import kotlin.math.min
9 |
10 | /**
11 | * 基于ByteBuf的前缀匹配
12 | */
13 | abstract class ByteBufPrefixProtocol(
14 | config: ProtocolConfig,
15 | private val patterns: List
16 | ) : BaseProtocol(config) {
17 |
18 | override fun match(data: MatchData): MatchState {
19 | val buf = data.buf
20 | var state = MatchState.NOT_MATCH
21 | for (pattern in patterns) {
22 | val matchLength = min(buf.readableBytes(), pattern.readableBytes())
23 | if (ByteBufUtil.equals(
24 | buf, buf.readerIndex(),
25 | pattern, pattern.readerIndex(),
26 | matchLength
27 | )
28 | ) {
29 | if (matchLength == pattern.readableBytes()) {
30 | return MatchState.MATCH
31 | } else {
32 | state = MatchState.MAYBE
33 | }
34 | }
35 | }
36 | return state
37 | }
38 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/BytesProtocol.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import io.netty.buffer.Unpooled
4 | import org.ffpy.portmux.config.ProtocolConfig
5 |
6 | /**
7 | * 字节数组匹配
8 | */
9 | class BytesProtocol(config: ProtocolConfig) : ByteBufPrefixProtocol(config, getPatterns(config)) {
10 |
11 | companion object {
12 | private fun getPatterns(config: ProtocolConfig) = config.patterns.asSequence()
13 | .map { pattern ->
14 | pattern.split(",").asSequence()
15 | .map { it.trim() }
16 | .map { it.toByte() }
17 | .toList()
18 | .toByteArray()
19 | }
20 | .map { Unpooled.wrappedBuffer(it) }
21 | .toList()
22 | }
23 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/HexProtocol.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import io.netty.buffer.ByteBufUtil
4 | import io.netty.buffer.Unpooled
5 | import org.ffpy.portmux.config.ProtocolConfig
6 |
7 | /**
8 | * 十六进制数据匹配
9 | */
10 | class HexProtocol(config: ProtocolConfig) : ByteBufPrefixProtocol(config, getPatterns(config)) {
11 |
12 | companion object {
13 | private fun getPatterns(config: ProtocolConfig) = config.patterns.asSequence()
14 | .map { ByteBufUtil.decodeHexDump(it) }
15 | .map { Unpooled.wrappedBuffer(it) }
16 | .toList()
17 | }
18 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/PrefixProtocol.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import io.netty.buffer.Unpooled
4 | import org.ffpy.portmux.config.ProtocolConfig
5 |
6 | /**
7 | * 前缀匹配
8 | */
9 | class PrefixProtocol(config: ProtocolConfig) : ByteBufPrefixProtocol(config, getPatterns(config)) {
10 |
11 | companion object {
12 | private fun getPatterns(config: ProtocolConfig) =
13 | config.patterns.asSequence()
14 | .map { it.toByteArray() }
15 | .map { Unpooled.wrappedBuffer(it) }
16 | .toList()
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/Protocol.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import org.ffpy.portmux.matcher.MatchData
4 | import org.ffpy.portmux.matcher.MatchState
5 | import java.net.SocketAddress
6 |
7 | /**
8 | * 转发匹配协议
9 | */
10 | interface Protocol {
11 |
12 | /** 名称 */
13 | val name: String
14 |
15 | /** 转发地址 */
16 | val address: SocketAddress
17 |
18 | /**
19 | * 是否匹配数据
20 | *
21 | * @param data 用于匹配的数据
22 | * @return true为匹配,false为不匹配
23 | */
24 | fun match(data: MatchData): MatchState
25 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/Protocols.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import org.ffpy.portmux.config.ProtocolConfig
4 |
5 | enum class Protocols(val type: String, private val factory: (ProtocolConfig) -> Protocol) {
6 |
7 | PREFIX("prefix", { PrefixProtocol(it) }),
8 |
9 | REGEX("regex", { RegexProtocol(it) }) {
10 | override fun check(config: ProtocolConfig, index: Int) {
11 | if (config.minLen <= 0) throw Exception("protocol[${index}].min_len must be greater than 0")
12 | if (config.maxLen <= 0) throw Exception("protocol[${index}].max_len must be greater than 0")
13 | }
14 | },
15 |
16 | BYTES("bytes", { BytesProtocol(it) }),
17 |
18 | HEX("hex", { HexProtocol(it) }),
19 |
20 | ;
21 |
22 | companion object {
23 |
24 | /**
25 | * 根据转发配置生成对应的转发匹配对象
26 | */
27 | @Throws(Exception::class)
28 | fun create(config: ProtocolConfig): Protocol {
29 | for (item in values()) {
30 | if (item.type == config.type) {
31 | return item.factory(config)
32 | }
33 | }
34 | throw Exception("Unknown protocol: ${config.type}")
35 | }
36 | }
37 |
38 | /**
39 | * 检查配置文件
40 | * @throws Exception 如果配置文件有问题
41 | */
42 | open fun check(config: ProtocolConfig, index: Int) {}
43 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/protocol/RegexProtocol.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.protocol
2 |
3 | import org.ffpy.portmux.config.ProtocolConfig
4 | import org.ffpy.portmux.matcher.MatchData
5 | import org.ffpy.portmux.matcher.MatchState
6 | import kotlin.math.min
7 |
8 | /**
9 | * 正则匹配
10 | */
11 | class RegexProtocol(config: ProtocolConfig) : BaseProtocol(config) {
12 |
13 | private val regexes = config.patterns.map { Regex(it) }
14 |
15 | override fun match(data: MatchData): MatchState {
16 | val buf = data.buf
17 | if (buf.readableBytes() < config.minLen) return MatchState.NOT_MATCH
18 |
19 | val len = min(buf.readableBytes(), config.maxLen)
20 | val str = buf.toString(buf.readerIndex(), len, Charsets.UTF_8)
21 |
22 | val match = regexes.any { it.containsMatchIn(str) }
23 | return if (match) MatchState.MATCH else MatchState.NOT_MATCH
24 | }
25 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/server/ForwardHandler.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.server
2 |
3 | import io.netty.buffer.ByteBuf
4 | import io.netty.channel.Channel
5 | import io.netty.channel.ChannelHandlerContext
6 | import io.netty.channel.ChannelInboundHandlerAdapter
7 | import org.ffpy.portmux.logger.LoggerManger
8 | import org.slf4j.LoggerFactory
9 |
10 | /**
11 | * 转发处理器
12 | *
13 | * @param clientChannel 目标Channel
14 | */
15 | class ForwardHandler(private val clientChannel: Channel) : ChannelInboundHandlerAdapter() {
16 | companion object {
17 | private val log = LoggerFactory.getLogger(ForwardHandler::class.java)
18 | }
19 |
20 | override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
21 | LoggerManger.logData(log, msg as ByteBuf, ctx.channel().remoteAddress())
22 | clientChannel.write(msg)
23 | }
24 |
25 | override fun channelReadComplete(ctx: ChannelHandlerContext) {
26 | clientChannel.flush()
27 | }
28 |
29 | override fun channelWritabilityChanged(ctx: ChannelHandlerContext) {
30 | clientChannel.config().isAutoRead = ctx.channel().isWritable
31 | }
32 |
33 | override fun channelInactive(ctx: ChannelHandlerContext) {
34 | log.info("${ctx.channel().remoteAddress()} disconnect")
35 | clientChannel.close()
36 | }
37 |
38 | override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
39 | log.error("${ctx.channel().remoteAddress()} error: ${cause.message}", cause)
40 | ctx.close()
41 | }
42 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/server/ForwardServer.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.server
2 |
3 | import io.netty.bootstrap.ServerBootstrap
4 | import io.netty.channel.ChannelInitializer
5 | import io.netty.channel.ChannelOption
6 | import io.netty.channel.socket.SocketChannel
7 | import org.ffpy.portmux.config.ConfigManager
8 | import org.ffpy.portmux.config.ForwardConfigManager
9 | import org.ffpy.portmux.util.AddressUtils
10 | import org.ffpy.portmux.util.NettyUtils
11 | import org.slf4j.LoggerFactory
12 |
13 | /**
14 | * 转发服务
15 | */
16 | class ForwardServer {
17 | companion object {
18 | private val log = LoggerFactory.getLogger(ForwardServer::class.java)
19 | }
20 |
21 | /**
22 | * 启动服务
23 | */
24 | fun start() {
25 | val boosGroup = NettyUtils.createEventLoopGroup(1)
26 | val workerGroup = NettyUtils.createEventLoopGroup(ConfigManager.config.threadNum)
27 | try {
28 | val future = ServerBootstrap()
29 | .group(boosGroup, workerGroup)
30 | .channel(NettyUtils.getServerSocketChannelClass())
31 | .childOption(ChannelOption.SO_KEEPALIVE, true)
32 | .childHandler(object : ChannelInitializer() {
33 | override fun initChannel(ch: SocketChannel) {
34 | ch.pipeline().addLast("matchHandler", MatchHandler(ForwardConfigManager.forwardConfig))
35 | }
36 | })
37 | .bind(AddressUtils.parseAddress(ConfigManager.config.listen))
38 | .sync()
39 |
40 | log.info("Current mode is " + NettyUtils.getMode())
41 | log.info("Listening at ${ConfigManager.config.listen}")
42 |
43 | future.channel().closeFuture().sync()
44 | } finally {
45 | boosGroup.shutdownGracefully().syncUninterruptibly()
46 | workerGroup.shutdownGracefully().syncUninterruptibly()
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/server/MatchHandler.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.server
2 |
3 | import io.netty.buffer.ByteBuf
4 | import io.netty.buffer.CompositeByteBuf
5 | import io.netty.channel.Channel
6 | import io.netty.channel.ChannelFutureListener
7 | import io.netty.channel.ChannelHandlerContext
8 | import io.netty.channel.ChannelInboundHandlerAdapter
9 | import org.ffpy.portmux.client.ClientManager
10 | import org.ffpy.portmux.config.ForwardConfig
11 | import org.ffpy.portmux.logger.LoggerManger
12 | import org.ffpy.portmux.matcher.Matcher
13 | import org.slf4j.LoggerFactory
14 | import java.net.SocketAddress
15 | import java.util.concurrent.ScheduledFuture
16 | import java.util.concurrent.TimeUnit
17 |
18 | /**
19 | * 匹配处理器
20 | */
21 | class MatchHandler(private val config: ForwardConfig) : ChannelInboundHandlerAdapter() {
22 | companion object {
23 | private val log = LoggerFactory.getLogger(MatchHandler::class.java)
24 | }
25 |
26 | /** 读取超时定时器 */
27 | private var readTimeout: ScheduledFuture<*>? = null
28 |
29 | /** 匹配超时定时器 */
30 | private var matchTimeout: ScheduledFuture<*>? = null
31 |
32 | /** 匹配器 */
33 | private val matcher = Matcher(config.protocols)
34 |
35 | /** 是否正在连接客户端 */
36 | private var connecting = false
37 |
38 | /** 已经读取到的所有数据 */
39 | private lateinit var readBuf: CompositeByteBuf
40 |
41 | override fun channelActive(ctx: ChannelHandlerContext) {
42 | log.info("${ctx.channel().remoteAddress()} new connect")
43 | readBuf = ctx.alloc().compositeBuffer()
44 |
45 | createReadTimeout(ctx)
46 | }
47 |
48 | override fun channelRead(ctx: ChannelHandlerContext, msg: Any?) {
49 | cancelReadTimeout()
50 | matchTimeout ?: createMatchTimeout(ctx)
51 |
52 | LoggerManger.logData(log, msg as ByteBuf, ctx.channel().remoteAddress())
53 |
54 | readBuf.addComponent(true, msg)
55 |
56 | if (connecting) return
57 |
58 | val result = matcher.match(readBuf, ctx.channel().remoteAddress(), config.defaultAddress)
59 | if (!result.finish) return
60 |
61 | cancelMatchTimeout()
62 |
63 | if (result.address == null) {
64 | log.info("No default forward address, close this connect")
65 | ctx.close()
66 | } else {
67 | connectClient(result.address, ctx.channel(), ctx)
68 | }
69 | }
70 |
71 | override fun channelInactive(ctx: ChannelHandlerContext) {
72 | log.info("${ctx.channel().remoteAddress()} disconnect")
73 | cancelReadTimeout()
74 | cancelMatchTimeout()
75 | ctx.fireChannelInactive()
76 | }
77 |
78 | override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) {
79 | log.error("${ctx.channel().remoteAddress()} error: ${cause.message}", cause)
80 | ctx.close()
81 | }
82 |
83 | /**
84 | * 连接转发地址
85 | *
86 | * @param address 转发地址
87 | * @param serverChannel 源连接
88 | * @param ctx ChannelHandlerContext
89 | */
90 | private fun connectClient(address: SocketAddress, serverChannel: Channel, ctx: ChannelHandlerContext) {
91 | connecting = true
92 | if (!serverChannel.isActive) return
93 |
94 | ctx.channel().config().isAutoRead = false
95 |
96 | log.info("${serverChannel.remoteAddress()} => $address")
97 |
98 | ClientManager.connect(serverChannel, address, config.connectTimeout).addListener(ChannelFutureListener { f ->
99 | if (!serverChannel.isActive) {
100 | f.channel().close()
101 | return@ChannelFutureListener
102 | }
103 |
104 | if (f.isSuccess) {
105 | log.info("${f.channel().remoteAddress()} client connect success")
106 |
107 | ctx.pipeline().replace(this, "forwardHandler", ForwardHandler(f.channel()))
108 | if (readBuf.readableBytes() > 0) {
109 | ctx.pipeline()
110 | .fireChannelRead(readBuf)
111 | .fireChannelReadComplete()
112 | } else {
113 | readBuf.release()
114 | }
115 | ctx.channel().config().isAutoRead = true
116 | } else {
117 | log.error("$address client connect fail")
118 | serverChannel.close()
119 | }
120 | })
121 | }
122 |
123 | /**
124 | * 创建读取超时定时器
125 | */
126 | private fun createReadTimeout(ctx: ChannelHandlerContext) {
127 | readTimeout = ctx.channel().eventLoop().schedule({
128 | cancelMatchTimeout()
129 | if (connecting) return@schedule
130 |
131 | val address = config.readTimeoutAddress
132 | if (address == null) {
133 | log.info("Timeout for waiting data. Close this connection because no read timeout address is configured.")
134 | ctx.close()
135 | } else {
136 | log.info("Timeout for waiting data. It will be forwarded to the read timeout address.")
137 | connectClient(address, ctx.channel(), ctx)
138 | }
139 | }, config.readTimeout.toLong(), TimeUnit.MILLISECONDS)
140 | }
141 |
142 | /**
143 | * 创建匹配超时定时器
144 | */
145 | private fun createMatchTimeout(ctx: ChannelHandlerContext) {
146 | matchTimeout = ctx.channel().eventLoop().schedule({
147 | cancelReadTimeout()
148 | if (connecting) return@schedule
149 |
150 | val address = config.defaultAddress
151 | if (address == null) {
152 | log.info("Timeout for matching. Close this connection because no default address is configured.")
153 | ctx.close()
154 | } else {
155 | log.info("Timeout for matching. It will be forwarded to the default address.")
156 | connectClient(address, ctx.channel(), ctx)
157 | }
158 | }, config.matchTimeout.toLong(), TimeUnit.MILLISECONDS)
159 | }
160 |
161 | /**
162 | * 关闭读取超时定时器
163 | */
164 | private fun cancelReadTimeout() {
165 | readTimeout?.let {
166 | it.cancel(true)
167 | readTimeout = null
168 | }
169 | }
170 |
171 | /**
172 | * 关闭匹配超时定时器
173 | */
174 | private fun cancelMatchTimeout() {
175 | matchTimeout?.let {
176 | it.cancel(true)
177 | matchTimeout = null
178 | }
179 | }
180 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/server/WatchServer.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.server
2 |
3 | import org.ffpy.portmux.config.ConfigManager
4 | import org.ffpy.portmux.util.StreamUtils
5 | import org.slf4j.Logger
6 | import org.slf4j.LoggerFactory
7 | import java.nio.file.*
8 | import java.util.stream.Stream
9 | import kotlin.concurrent.thread
10 |
11 | /**
12 | * 配置文件监听服务
13 | */
14 | class WatchServer {
15 |
16 | companion object {
17 | private val log: Logger = LoggerFactory.getLogger(WatchServer::class.java)
18 | }
19 |
20 | fun start(configPath: Path) {
21 | val watcher = FileSystems.getDefault().newWatchService()
22 | val watchPath = configPath.toAbsolutePath().parent
23 | watchPath.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY)
24 | thread {
25 | Stream.generate(eventGenerator(watcher))
26 | .filter(StreamUtils.filterByShortTime>>(1000))
27 | .forEach { events ->
28 | events.asSequence()
29 | .map { it.context() as Path }
30 | .firstOrNull() { it == configPath.fileName }
31 | ?.let {
32 | try {
33 | Thread.sleep(500)
34 | log.info("Updating config: $it")
35 | ConfigManager.init(configPath)
36 | log.info("Update config successful")
37 | } catch (e: Exception) {
38 | log.error("Update config fail: ${e.message}")
39 | }
40 | }
41 | }
42 | }
43 | }
44 |
45 | private fun eventGenerator(watcher: WatchService): () -> MutableList> = {
46 | val key = watcher.take()
47 | val events = key.pollEvents()
48 | if (!key.reset()) throw RuntimeException("Fail to watching configuration file")
49 | events
50 | }
51 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/util/AddressUtils.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.util
2 |
3 | import java.net.InetSocketAddress
4 |
5 | /**
6 | * 地址工具类
7 | */
8 | object AddressUtils {
9 | /** 地址格式正则 */
10 | private val addressRegex = Regex("(\\d{1,3}(\\.\\d{1,3}){3})?:\\d{1,5}")
11 |
12 | /**
13 | * 解析地址字符串
14 | * 例如 192.168.31.9:8080或:8080
15 | *
16 | * @param addr 地址字符串
17 | * @return 地址
18 | */
19 | fun parseAddress(addr: String): InetSocketAddress {
20 | val split = addr.split(":")
21 |
22 | if (split[0].isEmpty()) return InetSocketAddress(split[1].toInt())
23 | return InetSocketAddress(split[0], split[1].toInt())
24 | }
25 |
26 | /**
27 | * 校验地址格式是否正确
28 | *
29 | * @throws IllegalArgumentException 如果地址格式不正确
30 | * @return true为正确,false为不正确
31 | */
32 | fun validAddress(addr: String): Boolean = addr.matches(addressRegex)
33 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/util/JsonUtils.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.util
2 |
3 | import blue.endless.jankson.Jankson
4 | import com.google.gson.FieldNamingPolicy
5 | import com.google.gson.GsonBuilder
6 | import java.io.IOException
7 | import java.nio.file.NoSuchFileException
8 | import java.nio.file.Path
9 |
10 | /**
11 | * JSON工具类
12 | */
13 | object JsonUtils {
14 |
15 | /**
16 | * JSON文件转对象
17 | *
18 | * @param path JSON文件路径
19 | * @param type 要转换的对象类型
20 | * @param T 对象类型
21 | * @return 转换后的对象
22 | * @throws NoSuchFileException 如果找不到文件
23 | * @throws IOException 如果读取文件出错
24 | */
25 | @Throws(NoSuchFileException::class, IOException::class)
26 | fun parse(path: Path, type: Class): T {
27 | val filteredJson = Jankson.builder()
28 | .build()
29 | .load(path.toFile())
30 | .toJson()
31 | return createGson().fromJson(filteredJson, type)
32 | }
33 |
34 | private fun createGson() = GsonBuilder()
35 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
36 | .create()
37 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/util/NettyUtils.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.util
2 |
3 | import io.netty.channel.Channel
4 | import io.netty.channel.EventLoopGroup
5 | import io.netty.channel.ServerChannel
6 | import io.netty.channel.epoll.EpollEventLoopGroup
7 | import io.netty.channel.epoll.EpollServerSocketChannel
8 | import io.netty.channel.epoll.EpollSocketChannel
9 | import io.netty.channel.nio.NioEventLoopGroup
10 | import io.netty.channel.socket.nio.NioServerSocketChannel
11 | import io.netty.channel.socket.nio.NioSocketChannel
12 | import org.ffpy.portmux.commandparam.CommandParamManager
13 |
14 | object NettyUtils {
15 |
16 | fun createEventLoopGroup(nThreads: Int): EventLoopGroup {
17 | return if (CommandParamManager.param.epoll) EpollEventLoopGroup(nThreads) else NioEventLoopGroup(nThreads)
18 | }
19 |
20 | fun getServerSocketChannelClass(): Class {
21 | return if (CommandParamManager.param.epoll) EpollServerSocketChannel::class.java else NioServerSocketChannel::class.java
22 | }
23 |
24 | fun getSocketChannelClass(): Class {
25 | return if (CommandParamManager.param.epoll) EpollSocketChannel::class.java else NioSocketChannel::class.java
26 | }
27 |
28 | fun getMode() = if (CommandParamManager.param.epoll) "epoll" else "nio"
29 | }
--------------------------------------------------------------------------------
/src/main/kotlin/org/ffpy/portmux/util/StreamUtils.kt:
--------------------------------------------------------------------------------
1 | package org.ffpy.portmux.util
2 |
3 | import java.time.Duration
4 | import java.time.Instant
5 |
6 | object StreamUtils {
7 |
8 | /**
9 | * 过滤掉距离上一个元素时间太短的元素
10 | *
11 | * @param time 距离上一个元素的最短时间(毫秒),小于这个时间的元素会被过滤掉
12 | */
13 | fun filterByShortTime(time: Int): (T) -> Boolean {
14 | var lastTime = Instant.now().minusMillis(time.toLong())
15 | return {
16 | val now = Instant.now()
17 | val between = Duration.between(lastTime, now).toMillis()
18 | lastTime = now
19 | between >= time
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ${PATTERN}
12 |
13 |
14 |
15 |
16 |
17 | ${LOG_FILE}
18 |
19 |
20 | ${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
21 | 30
22 | 10MB
23 |
24 |
25 | ${PATTERN}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------