├── README.md ├── dist.ini ├── lib └── resty │ └── multiplexer │ ├── init.lua │ ├── matcher │ ├── client-host.lua │ ├── default.lua │ ├── protocol.lua │ └── time.lua │ └── protocol │ ├── dns.lua │ ├── http.lua │ ├── ssh.lua │ ├── tls.lua │ └── xmpp.lua ├── lua-resty-multiplexer-0.02-1.rockspec └── patches └── stream-lua-readpartial.patch /README.md: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-multiplexer - Transparent port service multiplexer for stream subsystem 5 | 6 | Table of Contents 7 | ================= 8 | 9 | - [Description](#description) 10 | - [Status](#status) 11 | - [Synopsis](#synopsis) 12 | - [Protocol](#protocol) 13 | * [Add new protocol](#add-new-protocol) 14 | - [Matcher](#matcher) 15 | * [client-host](#client-host) 16 | * [protocol](#protocol) 17 | * [time](#time) 18 | * [default](#default) 19 | * [Add new matcher](#add-new-matcher) 20 | - [API](#api) 21 | - [TODO](#todo) 22 | - [Copyright and License](#copyright-and-license) 23 | - [See Also](#see-also) 24 | 25 | 26 | Description 27 | =========== 28 | 29 | This library implemented a transparent port service multiplexer, which can be used to run multiple TCP services on the same port. 30 | 31 | Note that nginx [stream module](https://nginx.org/en/docs/stream/ngx_stream_core_module.html) and [stream-lua-nginx-module](https://github.com/openresty/stream-lua-nginx-module) is required. 32 | 33 | Tested on Openresty >= 1.13.6.1. 34 | 35 | With OpenResty 1.13.6.1, a customed [patch](patches/stream-lua-readpartial.patch) from [@fcicq](https://github.com/fcicq) is needed. The origin discussion can be found [here](https://github.com/fffonion/lua-resty-sniproxy/issues/1). And native 36 | proxying is not supported as `reqsock:peek` is missing. 37 | 38 | Starting OpenResty 1.15.8.1, only native proxying is supported and no patch is needed. Lua land proxying will be 39 | possible when stream-lua-nginx-module implemented `tcpsock:receiveany`. 40 | 41 | [Back to TOC](#table-of-contents) 42 | 43 | Status 44 | ======== 45 | 46 | Experimental. 47 | 48 | Synopsis 49 | ======== 50 | 51 | 52 | ```lua 53 | stream { 54 | init_by_lua_block { 55 | local mul = require("resty.multiplexer") 56 | mul.load_protocols( 57 | "http", "ssh", "dns", "tls", "xmpp" 58 | ) 59 | mul.set_rules( 60 | {{"client-host", "10.0.0.1"}, "internal-host", 80}, 61 | {{"protocol", "http"}, {"client-host", "10.0.0.2"}, "internal-host", 8001}, 62 | {{"protocol", "http"}, "example.com", 80}, 63 | {{"protocol", "ssh"}, "github.com", 22}, 64 | {{"protocol", "dns"}, "1.1.1.1", 53}, 65 | {{"protocol", "tls"}, {"time", nil}, "twitter.com", 443}, 66 | {{"protocol", "tls"}, "www.google.com", 443}, 67 | {{"default", nil}, "127.0.0.1", 80} 68 | ) 69 | mul.matcher_config.time = { 70 | minute_match = {0, 30}, 71 | minute_not_match = {{31, 59}}, 72 | } 73 | } 74 | 75 | resolver 8.8.8.8; 76 | 77 | # for OpenResty >= 1.15.8.1, native Nginx proxying 78 | lua_add_variable $multiplexer_upstream; 79 | server { 80 | error_log /var/log/nginx/multiplexer-error.log error; 81 | listen 443; 82 | 83 | resolver 8.8.8.8; 84 | 85 | preread_by_lua_block { 86 | local mul = require("resty.multiplexer") 87 | local mp = mul:new() 88 | mp:preread_by() 89 | } 90 | proxy_pass $multiplexer_upstream; 91 | } 92 | 93 | # for OpenResty < 1.15.8.1, with patch applied, Lua land proxying 94 | server { 95 | error_log /var/log/nginx/multiplexer-error.log error; 96 | listen 443; 97 | 98 | resolver 8.8.8.8; 99 | 100 | server { 101 | listen 80; 102 | content_by_lua_block { 103 | local mul = require("resty.multiplexer") 104 | local mp = mul:new() 105 | mp:content_by() 106 | } 107 | } 108 | } 109 | } 110 | ``` 111 | 112 | This module consists of two parts: protocol identifiers and matchers. 113 | 114 | Protocol identifies need to loaded through `load_protocols` in `init_by_lua_block` directive. See [protocol](#protocol) section for currently supported protocols and guide to add a new protocol. 115 | 116 | Rules are defined through `set_rules` to route traffic to different upstreams. For every matcher that is defined in the rule, the corresponding matcher is loaded automatically. See [matcher](#matcher) section for currently implmented matchers and guide to add a new matcher. 117 | 118 | See [API](#api) section for syntax of `load_protocols` and `set_rules`. 119 | 120 | The rules defined is prioritized. In the example above, we defined a rule such that: 121 | 122 | - If client address is `10.0.0.1`, proxy to **internal-host.com:80** 123 | - If protocol is `HTTP` and client address is `10.0.0.2`, proxy to **internal-host:8001** 124 | - If protocol is `SSH`, proxy to **github.com:22** 125 | - If protocol is `DNS`, proxy to **1.1.1.1:53** 126 | - If protocol is `SSL/TLS` and current minute is between **0** and **30**, proxy to **twitter:443** 127 | - If protocol is `SSL/TLS` and current minute is between **31** and **59**, proxy to **www.google.com:443** 128 | - Otherwise, proxy to **127.0.0.1:80** 129 | 130 | [Back to TOC](#table-of-contents) 131 | 132 | 133 | Protocol 134 | ======= 135 | 136 | The protocol part analyzes the first request that is sent from client and try to match it using known protocol signatures. 137 | 138 | Currently supported: `dns`, `http`, `ssh`, `tls`, `xmpp`. Based on the bytes of signature, each protocol may have different possibilities to be falsely identified. 139 | 140 | | Protocol | Length of signature | False rate | 141 | |---|---|---| 142 | | dns | 9 1/4 | 5.29e-23 | 143 | | http | 4 | 2.33e-10 | 144 | | ssh | 4 | 2.33e-10 | 145 | | tls | 6 | 3.55e-15 | 146 | | xmpp | 6 in 8 1/4 | ? | 147 | 148 | 149 | [Back to TOC](#table-of-contents) 150 | 151 | Add new protocol 152 | ----------------- 153 | 154 | Create a new `protocol_name.lua` file under `resty/multiplexer/protocol` in the format of: 155 | 156 | ```lua 157 | return { 158 | required_bytes = ?, 159 | check = function(buf) 160 | -- check with the buf and return true if the protocol is identified 161 | end 162 | } 163 | ``` 164 | 165 | `required_bytes` is the length of bytes we need to read before identifying the protocol. 166 | 167 | [Back to TOC](#table-of-contents) 168 | 169 | 170 | Matcher 171 | ======= 172 | 173 | client-host 174 | ----------- 175 | 176 | Match if `$remote_addr` equals to expected value. 177 | 178 | [Back to TOC](#table-of-contents) 179 | 180 | protocol 181 | -------- 182 | 183 | Match if protocol equals to expected value. 184 | 185 | [Back to TOC](#table-of-contents) 186 | 187 | time 188 | ---- 189 | 190 | Match if current time is in configured range in `mul.matcher_config.time`. If no range is defined, the matcher will always return *false*. 191 | 192 | For example, to match year `2018`, `January` and `March` and hour `6` to `24` except for hour `12`: 193 | 194 | ```lua 195 | init_by_lua_block { 196 | local mul = require("resty.multiplexer") 197 | mul.load_protocols( 198 | "http", "ssh", "dns", "tls", "xmpp" 199 | ) 200 | mul.set_rules( 201 | {{"time", ""}, "twitter.com", 443} 202 | ) 203 | mul.matcher_config.time = { 204 | year_match = {2018}, 205 | year_not_match = {}, 206 | month_match = {{1}, {3}}, 207 | month_not_match = {}, 208 | day_match = {}, -- day of month 209 | day_not_match = {}, 210 | hour_match = {{6, 24}}, 211 | hour_not_match = {{12}}, 212 | minute_match = {}, 213 | minute_not_match = {}, 214 | second_match = {}, 215 | second_not_match = {}, 216 | } 217 | } 218 | ``` 219 | 220 | [Back to TOC](#table-of-contents) 221 | 222 | default 223 | ------- 224 | 225 | Always matches. 226 | 227 | [Back to TOC](#table-of-contents) 228 | 229 | Add new matcher 230 | --------------- 231 | 232 | Create a new `matcher_name.lua` file under `resty/multiplexer/matchers` in the format of: 233 | 234 | ```lua 235 | local _M = {} 236 | 237 | function _M.match(protocol, expected) 238 | -- return true if it's a match 239 | end 240 | 241 | return _M 242 | ``` 243 | 244 | Where `protocol` is the identified protocol in lowercase string, and `expected` is the expected value for this matcher defined in `set_rules`. 245 | 246 | [Back to TOC](#table-of-contents) 247 | 248 | 249 | API 250 | ======= 251 | 252 | multiplexer.new 253 | -------------------------- 254 | **syntax:** *multiplexer:new(connect_timeout, send_timeout, read_timeout)* 255 | 256 | Initialize the multiplexer instance. And sets the connect timeout thresold, send timeout threshold, and read timeout threshold, as in [tcpsock:settimeouts](https://github.com/openresty/lua-nginx-module#tcpsocksettimeouts). 257 | 258 | 259 | [Back to TOC](#table-of-contents) 260 | 261 | multiplexer.load_protocols 262 | -------------------------- 263 | **syntax:** *multiplexer:load_protocols("protocol-1", "protocol-2", ...)* 264 | 265 | Load the protocol modules into memory. 266 | 267 | Supported protocols can be found in [protocol](lib/resty/multiplexer/protocol). 268 | 269 | [Back to TOC](#table-of-contents) 270 | 271 | multiplexer.set_rules 272 | -------------------------- 273 | **syntax:** *multiplexer:set_rules(rule1, rule2, ...)* 274 | 275 | Load rules in order. Each *rule* is an array table that is in the format of: 276 | 277 | ```lua 278 | {{"matcher-1", "expected-value-1"}, {"matcher-2", "expected-value-2"}, ..., "upstream_host", upstream_port} 279 | ``` 280 | 281 | Supported matchers can be found in [matcher](lib/resty/multiplexer/matcher). 282 | 283 | [Back to TOC](#table-of-contents) 284 | 285 | 286 | TODO 287 | ==== 288 | 289 | - Add tests. 290 | 291 | [Back to TOC](#table-of-contents) 292 | 293 | 294 | Copyright and License 295 | ===================== 296 | 297 | This module is licensed under the BSD license. 298 | 299 | Copyright (C) 2018, by fffonion . 300 | 301 | All rights reserved. 302 | 303 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 304 | 305 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 306 | 307 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 308 | 309 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 310 | 311 | [Back to TOC](#table-of-contents) 312 | 313 | See Also 314 | ======== 315 | * [Original patch to add the read partial mode](https://gist.github.com/fcicq/82e1c6d0c85cbc2d3f8e9f1523bfd1d1) 316 | * [stream-lua-nginx-module](https://github.com/openresty/stream-lua-nginx-module) 317 | * [yrutschle/sslh](https://github.com/yrutschle/sslh) 318 | 319 | [Back to TOC](#table-of-contents) 320 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name = lua-resty-multiplexer 2 | abstract = Transparent port service multiplexer for stream subsystem 3 | author = fffonion 4 | is_original = yes 5 | license = 3bsd 6 | lib_dir = lib 7 | doc_dir = lib 8 | repo_link = https://github.com/fffonion/lua-resty-multiplexer 9 | main_module = lib/resty/multiplexer/init.lua 10 | requires = luajit 11 | -------------------------------------------------------------------------------- /lib/resty/multiplexer/init.lua: -------------------------------------------------------------------------------- 1 | 2 | 3 | local sub = string.sub 4 | local byte = string.byte 5 | local format = string.format 6 | local tcp = ngx.socket.tcp 7 | local setmetatable = setmetatable 8 | local spawn = ngx.thread.spawn 9 | local wait = ngx.thread.wait 10 | 11 | 12 | local ok, new_tab = pcall(require, "table.new") 13 | if not ok or type(new_tab) ~= "function" then 14 | new_tab = function (narr, nrec) return {} end 15 | end 16 | 17 | 18 | local _M = new_tab(0, 8) 19 | _M._VERSION = '0.02' 20 | _M.protocols = nil -- cached protocol modules 21 | _M.matchers = nil -- cached matcher modules 22 | _M.rules = nil -- user-definsed routing rules 23 | 24 | local mt = { __index = _M } 25 | 26 | function _M.load_protocols(...) 27 | local protocols = {...} 28 | if not #protocols then 29 | return 30 | end 31 | -- loaded modules 32 | local modules = new_tab(0, #protocols) 33 | -- the offset of bytes we want to stop and check the protocol 34 | local positions = new_tab(0, #protocols) 35 | for _, proto in pairs(protocols) do 36 | local status, ret = pcall(require, "resty.multiplexer.protocol." .. proto) 37 | if not status then 38 | ngx.log(ngx.ERR, format("[multiplexer] can't load protocol '%s': %s", proto, ret)) 39 | elseif ret.required_bytes == nil or not type(ret.required_bytes) == "number" or ret.check == nil then 40 | ngx.log(ngx.ERR, format("[multiplexer] protocol module '%s' is malformed", proto)) 41 | else 42 | -- merge protocol filters with same required_bytes 43 | if modules[ret.required_bytes] == nil then 44 | modules[ret.required_bytes] = {} 45 | positions[#positions + 1] = ret.required_bytes 46 | end 47 | local mrec = modules[ret.required_bytes] 48 | -- add protocol name for reference 49 | ret.name = proto 50 | 51 | mrec[#mrec + 1] = ret 52 | 53 | ngx.log(ngx.INFO, format("[multiplexer] protocol '%s' loaded", proto)) 54 | end 55 | end 56 | 57 | if #positions == 0 then 58 | return 59 | end 60 | 61 | -- sort filters in ascending order of required_bytes 62 | table.sort(positions) 63 | _M.protocols = new_tab(#positions, 0) 64 | for _, k in ipairs(positions) do 65 | _M.protocols[#_M.protocols + 1] = {k, modules[k]} 66 | end 67 | 68 | end 69 | 70 | function _M.set_rules(...) 71 | local rules = {...} 72 | if not #rules then 73 | return 74 | end 75 | _M.rules = rules 76 | _M.matchers = new_tab(0, #rules) 77 | -- iterate all rules to cache matcher modules 78 | for _, rule in pairs(rules) do 79 | for i = 1, #rule - 2, 1 do 80 | local matcher = rule[i][1] 81 | if not _M.matchers[matcher] then 82 | local status, ret = pcall(require, "resty.multiplexer.matcher." .. matcher) 83 | if not status then 84 | ngx.log(ngx.ERR, format("[multiplexer] can't load matcher '%s': %s", matcher, ret)) 85 | else 86 | _M.matchers[matcher] = ret 87 | ngx.log(ngx.INFO, format("[multiplexer] matcher '%s' loaded", matcher)) 88 | end 89 | end 90 | end 91 | end 92 | end 93 | 94 | -- syntax sugar to proxy matcher_config to matchers[MATCHER].config 95 | _M.matcher_config = setmetatable({}, { 96 | __newindex = function(table, key, value) 97 | if _M.matchers[key] == nil then 98 | ngx.log(ngx.WARN, format("[multiplexer] try to set matcher config of '%s', which is not loaded", key)) 99 | return 100 | end 101 | _M.matchers[key].config = value 102 | end 103 | }) 104 | 105 | function _M.new(self, connect_timeout, send_timeout, read_timeout) 106 | if _M.rules == nil or _M.matchers == nil then 107 | return nil, "[multiplexer] no rule is defined" 108 | end 109 | 110 | local srvsock, err = tcp() 111 | if not srvsock then 112 | return nil, err 113 | end 114 | srvsock:settimeouts(connect_timeout or 10000, send_timeout or 10000, read_timeout or 3600000) 115 | 116 | local reqsock, err = ngx.req.socket() 117 | if not reqsock then 118 | return nil, err 119 | end 120 | reqsock:settimeouts(connect_timeout or 10000, send_timeout or 10000, read_timeout or 3600000) 121 | 122 | return setmetatable({ 123 | srvsock = srvsock, 124 | reqsock = reqsock, 125 | }, mt) 126 | end 127 | 128 | local function _cleanup(self) 129 | -- make sure buffers are clean 130 | ngx.flush(true) 131 | 132 | local srvsock = self.srvsock 133 | local reqsock = self.reqsock 134 | if srvsock ~= nil then 135 | if srvsock.shutdown then 136 | srvsock:shutdown("send") 137 | end 138 | if srvsock.close ~= nil then 139 | local ok, err = srvsock:setkeepalive() 140 | if not ok then 141 | -- 142 | end 143 | end 144 | end 145 | 146 | if reqsock ~= nil then 147 | if reqsock.shutdown then 148 | reqsock:shutdown("send") 149 | end 150 | if reqsock.close ~= nil then 151 | local ok, err = reqsock:close() 152 | if not ok then 153 | -- 154 | end 155 | end 156 | end 157 | 158 | end 159 | 160 | local function probe(sock, is_preread) 161 | local f 162 | if is_preread then 163 | local read = 0 164 | -- peek always start from beginning 165 | f = function(sock, len) 166 | local b, err = sock:peek(len + read) 167 | if err then 168 | return b, err 169 | end 170 | b = b:sub(read+1) 171 | read = read + len 172 | return b 173 | end 174 | else 175 | f = sock.receive 176 | end 177 | 178 | if _M.protocols == nil then 179 | return 0, nil, "" 180 | end 181 | local bytes_read = 0 182 | local buf = '' 183 | for _, v in pairs(_M.protocols) do 184 | ngx.log(ngx.INFO, "[multiplexer] waiting for ", v[1] - bytes_read, " more bytes") 185 | -- read more bytes 186 | local new_buf, err, partial = f(sock, v[1] - bytes_read) 187 | if err then 188 | return 0, nil, buf .. partial 189 | end 190 | -- concat buffer 191 | buf = buf .. new_buf 192 | -- check protocol 193 | for _, p in pairs(v[2]) do 194 | if p.check(buf) then 195 | return 0, p.name, buf 196 | end 197 | end 198 | -- update current read bytes position 199 | bytes_read = v[1] 200 | end 201 | return 0, nil, buf 202 | end 203 | 204 | local function _upl(self) 205 | -- proxy client request to server 206 | local buf, err, partial 207 | local rsock = self.reqsock 208 | local ssock = self.srvsock 209 | while true do 210 | buf, err, partial = rsock:receive("*p") 211 | if err then 212 | if ssock.close ~= nil and partial then 213 | _, err = ssock:send(partial) 214 | end 215 | break 216 | elseif buf == nil then 217 | break 218 | end 219 | 220 | _, err = ssock:send(buf) 221 | if err then 222 | break 223 | end 224 | end 225 | end 226 | 227 | local function _dwn(self) 228 | -- proxy response to client 229 | local buf, err, partial 230 | local rsock = self.reqsock 231 | local ssock = self.srvsock 232 | while true do 233 | buf, err, partial = ssock:receive("*p") 234 | if err then 235 | if rsock.close ~= nil and partial then 236 | _, err = rsock:send(partial) 237 | end 238 | break 239 | elseif buf == nil then 240 | break 241 | end 242 | 243 | _, err = rsock:send(buf) 244 | if err then 245 | break 246 | end 247 | end 248 | end 249 | 250 | local function _select_upstream(protocol_name) 251 | local upstream, port 252 | for _, v in pairs(_M.rules) do 253 | local is_match = false 254 | -- stop before last to elements of rules, which is server addr and port 255 | for i = 1, #v - 2, 1 do 256 | local m = _M.matchers[v[i][1]] 257 | if not m then 258 | ngx.log(ngx.WARN, "[multiplexer] try to use a matcher '", v[i][1], "', which is not loaded ") 259 | elseif m.match(protocol_name, v[i][2]) then 260 | is_match = true 261 | end 262 | end 263 | if is_match then 264 | upstream = v[#v - 1] 265 | port = v[#v] 266 | break 267 | end 268 | end 269 | return upstream, port, nil 270 | end 271 | 272 | function _M.preread_by(self) 273 | local code, protocol, _ = probe(self.reqsock, true) 274 | if code ~= 0 then 275 | ngx.log(ngx.INFO, "[multiplexer] cleaning up with an exit code ", code) 276 | return 277 | end 278 | ngx.log(ngx.NOTICE, format("[multiplexer] protocol:%s exit:%d", protocol, code)) 279 | 280 | local upstream, port, _ = _select_upstream(protocol) 281 | if upstream == nil or port == nil then 282 | ngx.log(ngx.NOTICE, "[multiplexer] no matches found for this request") 283 | return 284 | end 285 | 286 | if upstream:sub(1, 5) ~= "unix:" then 287 | upstream = upstream .. ":" .. tostring(port) 288 | end 289 | ngx.log(ngx.INFO, "[multiplexer] selecting upstream: ", upstream) 290 | ngx.var.multiplexer_upstream = upstream 291 | end 292 | 293 | function _M.content_by(self) 294 | while true do 295 | local code, protocol, buffer = probe(self.reqsock) 296 | if code ~= 0 then 297 | ngx.log(ngx.INFO, "[multiplexer] cleaning up with an exit code ", code) 298 | break 299 | end 300 | ngx.log(ngx.NOTICE, format("[multiplexer] protocol:%s exit:%d", protocol, code)) 301 | local upstream, port = _select_upstream(protocol) 302 | 303 | if upstream == nil or port == nil then 304 | ngx.log(ngx.NOTICE, "[multiplexer] no matches found for this request") 305 | break 306 | end 307 | ngx.log(ngx.INFO, format("[multiplexer] selecting upstream: %s:%d", upstream, port, err)) 308 | local ok, err = self.srvsock:connect(upstream, port) 309 | if not ok then 310 | ngx.log(ngx.ERR, format("[multiplexer] failed to connect to proxy upstream: %s:%s, err:%s", upstream, port, err)) 311 | break 312 | end 313 | -- send buffer 314 | self.srvsock:send(buffer) 315 | 316 | local co_upl = spawn(_upl, self) 317 | local co_dwn = spawn(_dwn, self) 318 | wait(co_upl) 319 | wait(co_dwn) 320 | 321 | break 322 | end 323 | _cleanup(self) 324 | 325 | end 326 | 327 | -- backward compatibility 328 | function _M.run(self) 329 | local phase = ngx.get_phase() 330 | if phase == 'content' then 331 | ngx.log(ngx.ERR, "content_by") 332 | self:content_by() 333 | elseif phase == 'preread' then 334 | ngx.log(ngx.ERR, "preread_by") 335 | self:preread_by() 336 | else 337 | ngx.log(ngx.ERR, "multiplexer doesn't support running in ", phase) 338 | ngx.exit(ngx.ERROR) 339 | end 340 | end 341 | 342 | 343 | return _M 344 | -------------------------------------------------------------------------------- /lib/resty/multiplexer/matcher/client-host.lua: -------------------------------------------------------------------------------- 1 | -- a matcher that matches the client host 2 | local _M = {} 3 | 4 | function _M.match(protocol, expected) 5 | return ngx.var.remote_addr == expected 6 | end 7 | 8 | return _M -------------------------------------------------------------------------------- /lib/resty/multiplexer/matcher/default.lua: -------------------------------------------------------------------------------- 1 | -- a matcher that matches everything 2 | local _M = {} 3 | 4 | function _M.match(protocol, expected) 5 | return true 6 | end 7 | 8 | return _M -------------------------------------------------------------------------------- /lib/resty/multiplexer/matcher/protocol.lua: -------------------------------------------------------------------------------- 1 | -- a matcher that matches the protocol 2 | local _M = {} 3 | 4 | function _M.match(protocol, expected) 5 | return protocol == expected 6 | end 7 | 8 | return _M -------------------------------------------------------------------------------- /lib/resty/multiplexer/matcher/time.lua: -------------------------------------------------------------------------------- 1 | -- a matcher that matches time 2 | local localtime = ngx.localtime 3 | local sub = string.sub 4 | local _M = { 5 | config = { 6 | year_match = {}, 7 | year_not_match = {}, 8 | month_match = {}, 9 | month_not_match = {}, 10 | day_match = {}, -- day of month 11 | day_not_match = {}, 12 | -- dow_match = {}, -- day of weak 13 | -- dow_not_match = {}, 14 | hour_match = {}, 15 | hour_not_match = {}, 16 | minute_match = {}, 17 | minute_not_match = {}, 18 | second_match = {}, 19 | second_not_match = {}, 20 | } 21 | } 22 | 23 | function _M.match(protocol, expected) 24 | local t = ngx.localtime() 25 | local tm = { 26 | year = tonumber(sub(t, 1, 4)), 27 | month = tonumber(sub(t, 6, 7)), 28 | day = tonumber(sub(t, 9, 10)), 29 | hour = tonumber(sub(t, 12, 13)), 30 | minute = tonumber(sub(t, 15, 16)), 31 | second = tonumber(sub(t, 18, 19)) 32 | } 33 | local is_match = false 34 | for tm_k, tm_v in pairs(tm) do 35 | local nm = _M.config[tm_k .. "_not_match"] 36 | if nm and #nm > 0 then 37 | for _, r in ipairs(nm) do 38 | if tm_v == r[1] or ( 39 | r[2] ~= nil and tm_v > r[1] and tm_v < r[2]) then 40 | return false 41 | end 42 | end 43 | end 44 | local m = _M.config[tm_k .. "_match"] 45 | if m and #m > 0 then 46 | for _, r in ipairs(m) do 47 | if tm_v == r[1] or ( 48 | r[2] ~= nil and tm_v > r[1] and tm_v < r[2]) then 49 | is_match = true 50 | break 51 | end 52 | end 53 | end 54 | end 55 | return is_match 56 | end 57 | 58 | return _M -------------------------------------------------------------------------------- /lib/resty/multiplexer/protocol/dns.lua: -------------------------------------------------------------------------------- 1 | local sub = string.sub 2 | local byte = string.byte 3 | local band = bit.band 4 | local bor = bit.bor 5 | local rshift = bit.rshift 6 | 7 | return { 8 | required_bytes = 14, 9 | check = function(buf) 10 | -- https://www.ietf.org/rfc/rfc1035.txt 11 | -- https://www.ietf.org/rfc/rfc2065.txt 12 | --[[ 13 | 1 1 1 1 1 1 14 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 15 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 16 | | ID | 17 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 18 | |QR| Opcode |AA|TC|RD|RA| Z|AD|CD| RCODE | 19 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 20 | | QDCOUNT | 21 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 22 | | ANCOUNT | 23 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 24 | | NSCOUNT | 25 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 26 | | ARCOUNT | 27 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 28 | ]] 29 | local is_dns = false 30 | -- for tcp, first 2 bytes are length 31 | local offset = 2 32 | while true do 33 | offset = offset + 3 -- 3 34 | -- 2bytes ID, no signature 35 | local byte_3 = byte(buf, offset) 36 | -- 1bit QR = 0 37 | if band(rshift(byte_3, 7), 0x1) ~= 0 then 38 | break 39 | end 40 | -- 4bit Opcode = 0, 1, 2, 4, 5 41 | local Opcode = band(rshift(byte_3, offset), 0xf) 42 | if Opcode == 3 or Opcode > 5 then 43 | break 44 | end 45 | offset = offset + 1 -- 4 46 | -- 1bit x 3 AA, TC, RD no signature 47 | local byte_4 = byte(buf, offset) 48 | -- 1bit RA, no signature 49 | -- 1bit Z = 0 50 | -- 1bit AD, no signature 51 | -- 1bit CD, no signature 52 | -- 4bit RCODE = 0 53 | -- mask: 0b01001111 = 0x4f 54 | if band(byte_4, 0x4f) ~= 0 then 55 | break 56 | end 57 | offset = offset + 1 -- 5 58 | -- 16bit QDCOUNT > 0 59 | if byte(buf, offset) == 0 and byte(buf, offset + 1) == 0 then 60 | break 61 | end 62 | offset = offset + 2 -- 7 63 | -- 16bit x 3 ANCOUNT, NSCOUNT 64 | for i = offset, offset + 3, 1 do 65 | if byte(buf, i) ~= 0 then 66 | is_dns = false 67 | break 68 | end 69 | is_dns = true 70 | end 71 | -- ARCOUNT can be bigger than 0 72 | break 73 | end 74 | return is_dns 75 | end 76 | } -------------------------------------------------------------------------------- /lib/resty/multiplexer/protocol/http.lua: -------------------------------------------------------------------------------- 1 | local re = ngx.re 2 | return { 3 | required_bytes = 8, 4 | check = function(buf) 5 | return re.match(buf, "^(GET|PUT|POST|HEAD|PATCH|TRACE|DELETE|CONNECT|OPTIONS) ", "jo") 6 | end 7 | } -------------------------------------------------------------------------------- /lib/resty/multiplexer/protocol/ssh.lua: -------------------------------------------------------------------------------- 1 | local sub = string.sub 2 | return { 3 | required_bytes = 4, 4 | check = function(buf) 5 | return sub(buf, 1, 4) == "SSH-" 6 | end 7 | } -------------------------------------------------------------------------------- /lib/resty/multiplexer/protocol/tls.lua: -------------------------------------------------------------------------------- 1 | local sub = string.sub 2 | local byte = string.byte 3 | local lshift = bit.lshift 4 | 5 | -- local timestamp_threshold = 3600 -- 1 hour is long enough 6 | 7 | return { 8 | required_bytes = 16, 9 | check = function(buf) 10 | -- SSL3.0 https://www.ietf.org/rfc/rfc6101.txt 11 | -- TLS1.0 https://www.ietf.org/rfc/2246.txt 12 | -- TLS1.1 https://www.ietf.org/rfc/4346.txt 13 | -- TLS1.2 https://www.ietf.org/rfc/5246.txt 14 | -- TLS1.3 https://tools.ietf.org/html/draft-ietf-tls-tls13-20 15 | -- probe ClientHello only 16 | --[[ 17 | 1 1 1 1 1 1 18 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 19 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 20 | | Content Type | Version(major) | 21 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 22 | | Version(minor) | Length (15..8) | 23 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 24 | | Length (7..1) | Message Type | 25 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 26 | ]] 27 | local is_tls = false 28 | local offset = 0 29 | while true do 30 | offset = offset + 1 -- 1 31 | -- Start SSL/TLS protocol 32 | -- 1byte Content Type = 0x16 (Handshake) 33 | if byte(buf, offset) ~= 0x16 then 34 | break 35 | end 36 | offset = offset + 1 -- 2 37 | -- 1byte version(major) = 3 38 | -- 1byte version(minor) = 0, 1, 2, 3 39 | if byte(buf, offset) ~= 0x3 or byte(buf, offset + 1) > 3 then 40 | break 41 | end 42 | offset = offset + 2 -- 4 43 | -- 2byte Length 44 | offset = offset + 2 -- 6 45 | -- Start Handshake Message 46 | -- 1byte Message Type = 0x1 (ClientHello) 47 | if byte(buf, offset) ~= 0x1 then 48 | break 49 | end 50 | offset = offset + 1 -- 1 51 | --[[ 52 | Bytes +0 | +1 | +2 | +3 | 53 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 54 | | Handshake Length | Ver.major | 55 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 56 | | Ver.minor | Timestamp (63..16) | 57 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 58 | | (15..0) | | 59 | +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ 60 | ]] 61 | -- 3byte Message Length 62 | offset = offset + 3 -- 4 63 | -- 1byte version(major) = 3 64 | -- 1byte version(minor) = 0, 1, 2, 3 65 | if byte(buf, offset) ~= 0x3 or byte(buf, offset + 1) > 3 then 66 | break 67 | end 68 | offset = offset + 2 -- 6 69 | -- Random bytes 70 | -- 4byte Unix timestamp, modern ssl library doesn't expose epoch in random bytes 71 | --[[if ngx.time() - lshift(byte(buf, offset), 24) - lshift(byte(buf, offset + 1), 16) - 72 | lshift(byte(buf, offset + 2), 8) - byte(buf, offset + 3) > timestamp_threshold then 73 | break 74 | end]] 75 | -- End Handshake Message 76 | -- End SSL/TLS protocol 77 | is_tls = true 78 | break 79 | end 80 | return is_tls 81 | end 82 | } -------------------------------------------------------------------------------- /lib/resty/multiplexer/protocol/xmpp.lua: -------------------------------------------------------------------------------- 1 | local match = ngx.re.match 2 | return { 3 | required_bytes = 50, 4 | check = function(buf) 5 | return match(buf, "jabber", "jo") 6 | end 7 | } 8 | -------------------------------------------------------------------------------- /lua-resty-multiplexer-0.02-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "lua-resty-multiplexer" 2 | version = "0.02-1" 3 | source = { 4 | url = "git+ssh://git@github.com/fffonion/lua-resty-multiplexer.git", 5 | tag = "0.02" 6 | } 7 | description = { 8 | detailed = "lua-resty-multiplexer - Transparent port service multiplexer for stream subsystem ", 9 | homepage = "https://github.com/fffonion/lua-resty-multiplexer", 10 | license = "BSD", 11 | } 12 | build = { 13 | type = "builtin", 14 | modules = { 15 | ["resty.multiplexer.init"] = "lib/resty/multiplexer/init.lua", 16 | ["resty.multiplexer.matcher.client-host"] = "lib/resty/multiplexer/matcher/client-host.lua", 17 | ["resty.multiplexer.matcher.default"] = "lib/resty/multiplexer/matcher/default.lua", 18 | ["resty.multiplexer.matcher.protocol"] = "lib/resty/multiplexer/matcher/protocol.lua", 19 | ["resty.multiplexer.matcher.time"] = "lib/resty/multiplexer/matcher/time.lua", 20 | ["resty.multiplexer.protocol.dns"] = "lib/resty/multiplexer/protocol/dns.lua", 21 | ["resty.multiplexer.protocol.http"] = "lib/resty/multiplexer/protocol/http.lua", 22 | ["resty.multiplexer.protocol.ssh"] = "lib/resty/multiplexer/protocol/ssh.lua", 23 | ["resty.multiplexer.protocol.tls"] = "lib/resty/multiplexer/protocol/tls.lua", 24 | ["resty.multiplexer.protocol.xmpp"] = "lib/resty/multiplexer/protocol/xmpp.lua" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /patches/stream-lua-readpartial.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/ngx_stream_lua_socket_tcp.c b/src/ngx_stream_lua_socket_tcp.c 2 | index 4680811..4da1ac6 100644 3 | --- a/src/ngx_stream_lua_socket_tcp.c 4 | +++ b/src/ngx_stream_lua_socket_tcp.c 5 | @@ -88,6 +88,7 @@ static int ngx_stream_lua_socket_write_error_retval_handler( 6 | ngx_stream_session_t *s, ngx_stream_lua_socket_tcp_upstream_t *u, 7 | lua_State *L); 8 | static ngx_int_t ngx_stream_lua_socket_read_all(void *data, ssize_t bytes); 9 | +static ngx_int_t ngx_stream_lua_socket_read_partial(void *data, ssize_t bytes); 10 | static ngx_int_t ngx_stream_lua_socket_read_until(void *data, ssize_t bytes); 11 | static ngx_int_t ngx_stream_lua_socket_read_chunk(void *data, ssize_t bytes); 12 | static int ngx_stream_lua_socket_tcp_receiveuntil(lua_State *L); 13 | @@ -1736,6 +1737,10 @@ ngx_stream_lua_socket_tcp_receive(lua_State *L) 14 | u->input_filter = ngx_stream_lua_socket_read_all; 15 | break; 16 | 17 | + case 'p': 18 | + u->input_filter = ngx_stream_lua_socket_read_partial; 19 | + break; 20 | + 21 | default: 22 | return luaL_argerror(L, 2, "bad pattern argument"); 23 | break; 24 | @@ -1918,6 +1923,35 @@ ngx_stream_lua_socket_read_all(void *data, ssize_t bytes) 25 | 26 | 27 | static ngx_int_t 28 | +ngx_stream_lua_socket_read_partial(void *data, ssize_t bytes) 29 | +{ 30 | + ngx_stream_lua_socket_tcp_upstream_t *u = data; 31 | + 32 | + ngx_buf_t *b; 33 | +#if (NGX_DEBUG) 34 | + ngx_stream_session_t *s; 35 | + 36 | + s = u->request; 37 | +#endif 38 | + 39 | + ngx_log_debug0(NGX_LOG_DEBUG_STREAM, s->connection->log, 0, 40 | + "stream lua tcp socket read partial"); 41 | + 42 | + if (bytes == 0) { 43 | + u->ft_type |= NGX_STREAM_LUA_SOCKET_FT_CLOSED; 44 | + return NGX_ERROR; 45 | + } 46 | + 47 | + b = &u->buffer; 48 | + 49 | + u->buf_in->buf->last += bytes; 50 | + b->pos += bytes; 51 | + 52 | + return NGX_OK; 53 | +} 54 | + 55 | + 56 | +static ngx_int_t 57 | ngx_stream_lua_socket_read_line(void *data, ssize_t bytes) 58 | { 59 | ngx_stream_lua_socket_tcp_upstream_t *u = data; 60 | --------------------------------------------------------------------------------