diff --git a/.travis.yml b/.travis.yml
index 67263fa449..214420015e 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -141,4 +141,4 @@ script:
- dig +short myip.opendns.com @resolver1.opendns.com || exit 0
- dig +short @$TEST_NGINX_RESOLVER openresty.org || exit 0
- dig +short @$TEST_NGINX_RESOLVER agentzh.org || exit 0
- - prove -I. -Itest-nginx/lib -r t
+ - prove -I. -Itest-nginx/lib -r t/
diff --git a/README.markdown b/README.markdown
index 17563b5757..8afd6257f2 100644
--- a/README.markdown
+++ b/README.markdown
@@ -3682,6 +3682,7 @@ Nginx API for Lua
* [udpsock:settimeout](#udpsocksettimeout)
* [ngx.socket.stream](#ngxsocketstream)
* [ngx.socket.tcp](#ngxsockettcp)
+* [tcpsock:bind](#tcpsockbind)
* [tcpsock:connect](#tcpsockconnect)
* [tcpsock:setclientcert](#tcpsocksetclientcert)
* [tcpsock:sslhandshake](#tcpsocksslhandshake)
@@ -7654,6 +7655,7 @@ ngx.socket.tcp
Creates and returns a TCP or stream-oriented unix domain socket object (also known as one type of the "cosocket" objects). The following methods are supported on this object:
+* [bind](#tcpsockbind)
* [connect](#tcpsockconnect)
* [setclientcert](#tcpsocksetclientcert)
* [sslhandshake](#tcpsocksslhandshake)
@@ -7693,6 +7695,42 @@ See also [ngx.socket.udp](#ngxsocketudp).
[Back to TOC](#nginx-api-for-lua)
+tcpsock:bind
+------------
+**syntax:** *ok, err = tcpsock:bind(address)*
+
+**context:** *rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua**
+
+Just like the standard [proxy_bind](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_bind) directive, this api makes the outgoing connection to a upstream server originate from the specified local IP address.
+
+Only IP addresses can be specified as the `address` argument.
+
+Here is an example for connecting to a TCP server from the specified local IP address:
+
+```nginx
+
+ location /test {
+ content_by_lua_block {
+ local sock = ngx.socket.tcp()
+ -- assume "192.168.1.10" is the local ip address
+ local ok, err = sock:bind("192.168.1.10")
+ if not ok then
+ ngx.say("failed to bind")
+ return
+ end
+ local ok, err = sock:connect("192.168.1.67", 80)
+ if not ok then
+ ngx.say("failed to connect server: ", err)
+ return
+ end
+ ngx.say("successfully connected!")
+ sock:close()
+ }
+ }
+```
+
+[Back to TOC](#nginx-api-for-lua)
+
tcpsock:connect
---------------
diff --git a/doc/HttpLuaModule.wiki b/doc/HttpLuaModule.wiki
index 371b9cb84f..74e298df8f 100644
--- a/doc/HttpLuaModule.wiki
+++ b/doc/HttpLuaModule.wiki
@@ -6498,6 +6498,7 @@ This API function was first added to the v0.10.1
release.
Creates and returns a TCP or stream-oriented unix domain socket object (also known as one type of the "cosocket" objects). The following methods are supported on this object:
+* [[#tcpsock:bind|bind]]
* [[#tcpsock:connect|connect]]
* [[#tcpsock:setclientcert|setclientcert]]
* [[#tcpsock:sslhandshake|sslhandshake]]
@@ -6535,6 +6536,38 @@ This feature was first introduced in the v0.5.0rc1
release.
See also [[#ngx.socket.udp|ngx.socket.udp]].
+== tcpsock:bind ==
+'''syntax:''' ''ok, err = tcpsock:bind(address)''
+
+'''context:''' ''rewrite_by_lua*, access_by_lua*, content_by_lua*, ngx.timer.*, ssl_certificate_by_lua*''
+
+Just like the standard [[HttpProxyModule#proxy_bind|proxy_bind]] directive, this api makes the outgoing connection to a upstream server originate from the specified local IP address.
+
+Only IP addresses can be specified as the address
argument.
+
+Here is an example for connecting to a TCP server from the specified local IP address:
+
+
+ location /test {
+ content_by_lua_block {
+ local sock = ngx.socket.tcp()
+ -- assume "192.168.1.10" is the local ip address
+ local ok, err = sock:bind("192.168.1.10")
+ if not ok then
+ ngx.say("failed to bind")
+ return
+ end
+ local ok, err = sock:connect("192.168.1.67", 80)
+ if not ok then
+ ngx.say("failed to connect server: ", err)
+ return
+ end
+ ngx.say("successfully connected!")
+ sock:close()
+ }
+ }
+
+
== tcpsock:connect ==
'''syntax:''' ''ok, err = tcpsock:connect(host, port, options_table?)''
diff --git a/src/ngx_http_lua_socket_tcp.c b/src/ngx_http_lua_socket_tcp.c
index d8f1d4d1c1..e40ecbb537 100644
--- a/src/ngx_http_lua_socket_tcp.c
+++ b/src/ngx_http_lua_socket_tcp.c
@@ -20,6 +20,7 @@
static int ngx_http_lua_socket_tcp(lua_State *L);
+static int ngx_http_lua_socket_tcp_bind(lua_State *L);
static int ngx_http_lua_socket_tcp_connect(lua_State *L);
#if (NGX_HTTP_SSL)
static void ngx_http_lua_ssl_handshake_handler(ngx_connection_t *c);
@@ -162,6 +163,7 @@ enum {
SOCKET_READ_TIMEOUT_INDEX = 5,
SOCKET_CLIENT_CERT_INDEX = 6 ,
SOCKET_CLIENT_PKEY_INDEX = 7 ,
+ SOCKET_BIND_INDEX = 8 /* only in upstream cosocket */
};
@@ -314,7 +316,10 @@ ngx_http_lua_inject_socket_tcp_api(ngx_log_t *log, lua_State *L)
/* {{{tcp object metatable */
lua_pushlightuserdata(L, ngx_http_lua_lightudata_mask(
tcp_socket_metatable_key));
- lua_createtable(L, 0 /* narr */, 15 /* nrec */);
+ lua_createtable(L, 0 /* narr */, 16 /* nrec */);
+
+ lua_pushcfunction(L, ngx_http_lua_socket_tcp_bind);
+ lua_setfield(L, -2, "bind");
lua_pushcfunction(L, ngx_http_lua_socket_tcp_connect);
lua_setfield(L, -2, "connect");
@@ -827,6 +832,61 @@ ngx_http_lua_socket_tcp_connect_helper(lua_State *L,
}
+static int
+ngx_http_lua_socket_tcp_bind(lua_State *L)
+{
+ ngx_http_request_t *r;
+ ngx_http_lua_ctx_t *ctx;
+ int n;
+ u_char *text;
+ size_t len;
+ ngx_addr_t *local;
+
+ n = lua_gettop(L);
+
+ if (n != 2) {
+ return luaL_error(L, "expecting 2 arguments, but got %d",
+ lua_gettop(L));
+ }
+
+ r = ngx_http_lua_get_req(L);
+ if (r == NULL) {
+ return luaL_error(L, "no request found");
+ }
+
+ ctx = ngx_http_get_module_ctx(r, ngx_http_lua_module);
+ if (ctx == NULL) {
+ return luaL_error(L, "no ctx found");
+ }
+
+ ngx_http_lua_check_context(L, ctx, NGX_HTTP_LUA_CONTEXT_REWRITE
+ | NGX_HTTP_LUA_CONTEXT_ACCESS
+ | NGX_HTTP_LUA_CONTEXT_CONTENT
+ | NGX_HTTP_LUA_CONTEXT_TIMER
+ | NGX_HTTP_LUA_CONTEXT_SSL_CERT);
+
+ luaL_checktype(L, 1, LUA_TTABLE);
+
+ text = (u_char *) luaL_checklstring(L, 2, &len);
+
+ local = ngx_http_lua_parse_addr(L, text, len);
+ if (local == NULL) {
+ lua_pushnil(L);
+ lua_pushfstring(L, "bad address");
+ return 2;
+ }
+
+ /* TODO: we may reuse the userdata here */
+ lua_rawseti(L, 1, SOCKET_BIND_INDEX);
+
+ ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
+ "lua tcp socket bind ip: %V", &local->name);
+
+ lua_pushboolean(L, 1);
+ return 1;
+}
+
+
static int
ngx_http_lua_socket_tcp_connect(lua_State *L)
{
@@ -838,6 +898,7 @@ ngx_http_lua_socket_tcp_connect(lua_State *L)
size_t len;
ngx_http_lua_loc_conf_t *llcf;
ngx_peer_connection_t *pc;
+ ngx_addr_t *local;
int connect_timeout, send_timeout, read_timeout;
unsigned custom_pool;
int key_index;
@@ -1068,6 +1129,14 @@ ngx_http_lua_socket_tcp_connect(lua_State *L)
dd("lua peer connection log: %p", pc->log);
+ lua_rawgeti(L, 1, SOCKET_BIND_INDEX);
+ local = lua_touserdata(L, -1);
+ lua_pop(L, 1);
+
+ if (local) {
+ u->peer.local = local;
+ }
+
lua_rawgeti(L, 1, SOCKET_CONNECT_TIMEOUT_INDEX);
lua_rawgeti(L, 1, SOCKET_SEND_TIMEOUT_INDEX);
lua_rawgeti(L, 1, SOCKET_READ_TIMEOUT_INDEX);
diff --git a/src/ngx_http_lua_util.c b/src/ngx_http_lua_util.c
index 39ba0b21fb..8fd26561a7 100644
--- a/src/ngx_http_lua_util.c
+++ b/src/ngx_http_lua_util.c
@@ -4396,4 +4396,77 @@ ngx_http_lua_copy_escaped_header(ngx_http_request_t *r,
return NGX_OK;
}
+
+ngx_addr_t *
+ngx_http_lua_parse_addr(lua_State *L, u_char *text, size_t len)
+{
+ ngx_addr_t *addr;
+ size_t socklen;
+ in_addr_t inaddr;
+ ngx_uint_t family;
+ struct sockaddr_in *sin;
+#if (NGX_HAVE_INET6)
+ struct in6_addr inaddr6;
+ struct sockaddr_in6 *sin6;
+
+ /*
+ * prevent MSVC8 warning:
+ * potentially uninitialized local variable 'inaddr6' used
+ */
+ ngx_memzero(&inaddr6, sizeof(struct in6_addr));
+#endif
+
+ inaddr = ngx_inet_addr(text, len);
+
+ if (inaddr != INADDR_NONE) {
+ family = AF_INET;
+ socklen = sizeof(struct sockaddr_in);
+
+#if (NGX_HAVE_INET6)
+
+ } else if (ngx_inet6_addr(text, len, inaddr6.s6_addr) == NGX_OK) {
+ family = AF_INET6;
+ socklen = sizeof(struct sockaddr_in6);
+#endif
+
+ } else {
+ return NULL;
+ }
+
+ addr = lua_newuserdata(L, sizeof(ngx_addr_t) + socklen + len);
+ if (addr == NULL) {
+ luaL_error(L, "no memory");
+ return NULL;
+ }
+
+ addr->sockaddr = (struct sockaddr *) ((u_char *) addr + sizeof(ngx_addr_t));
+
+ ngx_memzero(addr->sockaddr, socklen);
+
+ addr->sockaddr->sa_family = (u_char) family;
+ addr->socklen = socklen;
+
+ switch (family) {
+
+#if (NGX_HAVE_INET6)
+ case AF_INET6:
+ sin6 = (struct sockaddr_in6 *) addr->sockaddr;
+ ngx_memcpy(sin6->sin6_addr.s6_addr, inaddr6.s6_addr, 16);
+ break;
+#endif
+
+ default: /* AF_INET */
+ sin = (struct sockaddr_in *) addr->sockaddr;
+ sin->sin_addr.s_addr = inaddr;
+ break;
+ }
+
+ addr->name.data = (u_char *) addr->sockaddr + socklen;
+ addr->name.len = len;
+ ngx_memcpy(addr->name.data, text, len);
+
+ return addr;
+}
+
+
/* vi:set ft=c ts=4 sw=4 et fdm=marker: */
diff --git a/src/ngx_http_lua_util.h b/src/ngx_http_lua_util.h
index 4c4da976c5..faea7a072c 100644
--- a/src/ngx_http_lua_util.h
+++ b/src/ngx_http_lua_util.h
@@ -262,6 +262,8 @@ void ngx_http_lua_cleanup_free(ngx_http_request_t *r,
void ngx_http_lua_set_sa_restart(ngx_log_t *log);
#endif
+ngx_addr_t *ngx_http_lua_parse_addr(lua_State *L, u_char *text, size_t len);
+
size_t ngx_http_lua_escape_log(u_char *dst, u_char *src, size_t size);
diff --git a/t/062-count.t b/t/062-count.t
index d977909bb6..d524da47d8 100644
--- a/t/062-count.t
+++ b/t/062-count.t
@@ -459,7 +459,7 @@ worker: 4
--- request
GET /test
--- response_body
-n = 15
+n = 16
--- no_error_log
[error]
diff --git a/t/168-tcp-socket-bind.t b/t/168-tcp-socket-bind.t
new file mode 100644
index 0000000000..6aca00c51e
--- /dev/null
+++ b/t/168-tcp-socket-bind.t
@@ -0,0 +1,361 @@
+# vim:set ft= ts=4 sw=4 et fdm=marker:
+
+use Test::Nginx::Socket::Lua;
+
+# more times than usual(2) for test case 6
+repeat_each(4);
+
+plan tests => repeat_each() * (blocks() * 3 + 7);
+
+our $HtmlDir = html_dir;
+
+# get ip address in the dev which is default route outgoing dev
+my $dev = `ip route | awk '/default/ {printf "%s", \$5}'`;
+my $local_ip = `ip route | grep $dev | grep -o "src .*" | head -n 1 | awk '{print \$2}'`;
+chomp $local_ip;
+
+$ENV{TEST_NGINX_HTML_DIR} = $HtmlDir;
+$ENV{TEST_NGINX_NOT_EXIST_IP} ||= '8.8.8.8';
+$ENV{TEST_NGINX_INVALID_IP} ||= '127.0.0.1:8899';
+$ENV{TEST_NGINX_SERVER_IP} ||= $local_ip;
+
+no_long_string();
+#no_diff();
+
+#log_level 'warn';
+log_level 'debug';
+
+no_shuffle();
+
+run_tests();
+
+__DATA__
+
+=== TEST 1: upstream sockets bind 127.0.0.1
+--- config
+ server_tokens off;
+ location /t {
+ set $port $TEST_NGINX_SERVER_PORT;
+ content_by_lua_block {
+ local ip = "127.0.0.1"
+ local port = ngx.var.port
+
+ local sock = ngx.socket.tcp()
+ local ok, err = sock:bind(ip)
+ if not ok then
+ ngx.say("failed to bind", err)
+ return
+ end
+
+ local ok, err = sock:connect("127.0.0.1", port)
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local bytes, err = sock:send("GET /foo HTTP/1.1\r\nHost: localhost\r\nConnection: keepalive\r\n\r\n")
+ if not bytes then
+ ngx.say("failed to send request: ", err)
+ return
+ end
+
+ ngx.say("request sent")
+
+ local reader = sock:receiveuntil("\r\n0\r\n\r\n")
+ local data, err = reader()
+
+ if not data then
+ ngx.say("failed to receive response body: ", err)
+ return
+ end
+
+ ngx.say("received response")
+ local remote_ip = string.match(data, "(bind: %d+%.%d+%.%d+%.%d+)")
+ ngx.say(remote_ip)
+
+ ngx.say("done")
+ }
+ }
+
+ location /foo {
+ echo bind: $remote_addr;
+ }
+--- request
+GET /t
+--- response_body
+connected: 1
+request sent
+received response
+bind: 127.0.0.1
+done
+--- no_error_log
+["[error]",
+"bind(127.0.0.1) failed"]
+--- error_log eval
+"lua tcp socket bind ip: 127.0.0.1"
+
+
+
+=== TEST 2: upstream sockets bind server ip, not 127.0.0.1
+--- config
+ server_tokens off;
+ location /t {
+ set $port $TEST_NGINX_SERVER_PORT;
+ content_by_lua_block {
+ local ip = "$TEST_NGINX_SERVER_IP"
+ local port = ngx.var.port
+
+ local sock = ngx.socket.tcp()
+ local ok, err = sock:bind(ip)
+ if not ok then
+ ngx.say("failed to bind", err)
+ return
+ end
+
+ local ok, err = sock:connect("127.0.0.1", port)
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local bytes, err = sock:send("GET /foo HTTP/1.1\r\nHost: localhost\r\nConnection: keepalive\r\n\r\n")
+ if not bytes then
+ ngx.say("failed to send request: ", err)
+ return
+ end
+
+ ngx.say("request sent")
+
+ local reader = sock:receiveuntil("\r\n0\r\n\r\n")
+ local data, err = reader()
+
+ if not data then
+ ngx.say("failed to receive response body: ", err)
+ return
+ end
+
+ ngx.say("received response")
+ local remote_ip = string.match(data, "(bind: %d+%.%d+%.%d+%.%d+)")
+ if remote_ip == "bind: $TEST_NGINX_SERVER_IP" then
+ ngx.say("ip matched")
+ end
+
+ ngx.say("done")
+ }
+ }
+
+ location /foo {
+ echo bind: $remote_addr;
+ }
+--- request
+GET /t
+--- response_body
+connected: 1
+request sent
+received response
+ip matched
+done
+--- no_error_log eval
+["[error]",
+"bind($ENV{TEST_NGINX_SERVER_IP}) failed"]
+--- error_log eval
+"lua tcp socket bind ip: $ENV{TEST_NGINX_SERVER_IP}"
+
+
+
+=== TEST 3: add setkeepalive
+--- http_config eval
+ "lua_package_path '$::HtmlDir/?.lua;./?.lua;;';"
+--- config
+ server_tokens off;
+ location /t {
+ set $port $TEST_NGINX_SERVER_PORT;
+ content_by_lua_block {
+ local test = require "test"
+ local t1 = test.go()
+ local t2 = test.go()
+ ngx.say("t2 - t1: ", t2 - t1)
+ }
+ }
+--- user_files
+>>> test.lua
+local _M = {}
+
+function _M.go()
+ local ip = "127.0.0.1"
+ local port = ngx.var.port
+
+ local sock = ngx.socket.tcp()
+ local ok, err = sock:bind(ip)
+ if not ok then
+ ngx.say("failed to bind", err)
+ return
+ end
+
+ ngx.say("bind: ", ip)
+
+ local ok, err = sock:connect("127.0.0.1", port)
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ local reused = sock:getreusedtimes()
+
+ local ok, err = sock:setkeepalive()
+ if not ok then
+ ngx.say("failed to set reusable: ", err)
+ end
+
+ return reused
+end
+
+return _M
+--- request
+GET /t
+--- response_body
+bind: 127.0.0.1
+connected: 1
+bind: 127.0.0.1
+connected: 1
+t2 - t1: 1
+--- no_error_log
+["[error]",
+"bind(127.0.0.1) failed"]
+--- error_log eval
+"lua tcp socket bind ip: 127.0.0.1"
+
+
+
+=== TEST 4: upstream sockets bind not exist ip
+--- config
+ server_tokens off;
+ location /t {
+ set $port $TEST_NGINX_SERVER_PORT;
+ content_by_lua_block {
+ local ip = "$TEST_NGINX_NOT_EXIST_IP"
+ local port = ngx.var.port
+
+ local sock = ngx.socket.tcp()
+ local ok, err = sock:bind(ip)
+ if not ok then
+ ngx.say("failed to bind", err)
+ return
+ end
+
+ ngx.say("bind: ", ip)
+
+ local ok, err = sock:connect("127.0.0.1", port)
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+ }
+ }
+--- request
+GET /t
+--- response_body
+bind: 8.8.8.8
+failed to connect: cannot assign requested address
+--- error_log eval
+["bind(8.8.8.8) failed",
+"lua tcp socket bind ip: 8.8.8.8"]
+
+
+
+=== TEST 5: upstream sockets bind invalid ip
+--- config
+ server_tokens off;
+ location /t {
+ set $port $TEST_NGINX_SERVER_PORT;
+ content_by_lua_block {
+ local ip = "$TEST_NGINX_INVALID_IP"
+ local port = ngx.var.port
+
+ local sock = ngx.socket.tcp()
+ local ok, err = sock:bind(ip)
+ if not ok then
+ ngx.say("failed to bind: ", err)
+ return
+ end
+
+ ngx.say("bind: ", ip)
+
+ local ok, err = sock:connect("127.0.0.1", port)
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+ }
+ }
+--- request
+GET /t
+--- response_body
+failed to bind: bad address
+--- no_error_log
+[error]
+
+
+
+=== TEST 6: tcpsock across request after bind
+--- http_config
+ init_worker_by_lua_block {
+ -- this is not the recommend way, just for test
+ local function tcp()
+ local sock = ngx.socket.tcp()
+
+ ---[[
+ local ok, err = sock:bind("127.0.0.1")
+ if not ok then
+ ngx.log(ngx.ERR, "failed to bind")
+ end
+ --]]
+
+ package.loaded.share_sock = sock
+ end
+
+ local ok, err = ngx.timer.at(0, tcp)
+ if not ok then
+ ngx.log(ngx.ERR, "failed to create timer")
+ end
+ }
+--- config
+ server_tokens off;
+ location /t {
+ set $port $TEST_NGINX_SERVER_PORT;
+ content_by_lua_block {
+ local port = ngx.var.port
+
+ -- make sure share_sock is created
+ ngx.sleep(0.002)
+
+ local sock = package.loaded.share_sock
+
+ local ok, err = sock:connect("127.0.0.1", port)
+ if not ok then
+ ngx.say("failed to connect: ", err)
+ return
+ end
+
+ ngx.say("connected: ", ok)
+
+ sock:close()
+ collectgarbage("collect")
+ }
+ }
+--- request
+GET /t
+--- response_body
+connected: 1
+--- no_error_log
+[error]