├── .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 | [![EN doc](https://img.shields.io/badge/document-English-blue.svg)](README.md) 3 | [![CN doc](https://img.shields.io/badge/文档-中文版-blue.svg)](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 | [![EN doc](https://img.shields.io/badge/document-English-blue.svg)](README.md) 3 | [![CN doc](https://img.shields.io/badge/文档-中文版-blue.svg)](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 | --------------------------------------------------------------------------------