diff --git a/src/config.c b/src/config.c index 6c03cbb476..871be1a388 100644 --- a/src/config.c +++ b/src/config.c @@ -3193,6 +3193,7 @@ standardConfig static_configs[] = { /* Integer configs */ createIntConfig("databases", NULL, IMMUTABLE_CONFIG, 1, INT_MAX, server.dbnum, 16, INTEGER_CONFIG, NULL, NULL), createIntConfig("port", NULL, MODIFIABLE_CONFIG, 0, 65535, server.port, 6379, INTEGER_CONFIG, NULL, updatePort), /* TCP port. */ + createIntConfig("admin-port", NULL, IMMUTABLE_CONFIG, 0, 65535, server.admin_port, 0, INTEGER_CONFIG, NULL, NULL), /* TCP admin port. */ createIntConfig("io-threads", NULL, DEBUG_CONFIG | IMMUTABLE_CONFIG, 1, 128, server.io_threads_num, 1, INTEGER_CONFIG, NULL, NULL), /* Single threaded by default */ createIntConfig("events-per-io-thread", NULL, MODIFIABLE_CONFIG, 0, INT_MAX, server.events_per_io_thread, 2, INTEGER_CONFIG, NULL, NULL), createIntConfig("prefetch-batch-max-size", NULL, MODIFIABLE_CONFIG, 0, 128, server.prefetch_batch_max_size, 16, INTEGER_CONFIG, NULL, NULL), diff --git a/src/networking.c b/src/networking.c index b18b798eaa..80ef917738 100644 --- a/src/networking.c +++ b/src/networking.c @@ -1389,6 +1389,7 @@ void clientAcceptHandler(connection *conn) { void acceptCommonHandler(connection *conn, struct ClientFlags flags, char *ip) { client *c; UNUSED(ip); + int port; if (connGetState(conn) != CONN_STATE_ACCEPTING) { char addr[NET_ADDR_STR_LEN] = {0}; @@ -1401,12 +1402,26 @@ void acceptCommonHandler(connection *conn, struct ClientFlags flags, char *ip) { return; } + if (connAddrSockName(conn, NULL, 0, &port)) { + serverLog(LL_WARNING, "Unable to retrieve socket port"); + connClose(conn); + return; + } + + if (port == server.admin_port && connIsLocal(conn) != 1) { + serverLog(LL_WARNING, "Denied connection. On admin-port, connections are" + " only accepted from the loopback interface."); + server.stat_rejected_conn++; + connClose(conn); + return; + } + /* Limit the number of connections we take at the same time. * * Admission control will happen before a client is created and connAccept() * called, because we don't want to even start transport-level negotiation * if rejected. */ - if (listLength(server.clients) + getClusterConnectionsCount() >= server.maxclients) { + if (port != server.admin_port && (listLength(server.clients) + getClusterConnectionsCount() >= server.maxclients)) { char *err; if (server.cluster_enabled) err = "-ERR max number of clients + cluster " diff --git a/src/server.c b/src/server.c index d9f7d73843..0f0f0770fc 100644 --- a/src/server.c +++ b/src/server.c @@ -2821,6 +2821,25 @@ void initListeners(void) { listener->priv2 = server.unixsocketgroup; /* Unix socket group specified */ } + if (server.admin_port != 0) { + conn_index = connectionIndexByType(CONN_TYPE_SOCKET); + if (conn_index < 0) serverPanic("Failed finding connection listener of %s", CONN_TYPE_SOCKET); + + // Check if the current index is already occupied + while (server.listeners[conn_index].ct != NULL) { + conn_index++; + if (conn_index >= CONN_TYPE_MAX) { + serverPanic("No available index for additional TCP listener."); + } + } + + listener = &server.listeners[conn_index]; + listener->bindaddr = server.bindaddr; + listener->bindaddr_count = server.bindaddr_count; + listener->port = server.admin_port; + listener->ct = connectionByType(CONN_TYPE_SOCKET); + } + /* create all the configured listener, and add handler to start to accept */ int listen_fds = 0; for (int j = 0; j < CONN_TYPE_MAX; j++) { diff --git a/src/server.h b/src/server.h index 84a282b6f5..4cbea99f24 100644 --- a/src/server.h +++ b/src/server.h @@ -1697,6 +1697,7 @@ struct valkeyServer { _Atomic int module_gil_acquiring; /* Indicates whether the GIL is being acquiring by the main thread. */ /* Networking */ int port; /* TCP listening port */ + int admin_port; /* TCP listening admin port */ int tls_port; /* TLS listening port */ int tcp_backlog; /* TCP listen() backlog */ char *bindaddr[CONFIG_BINDADDR_MAX]; /* Addresses we should bind to */ diff --git a/tests/support/server.tcl b/tests/support/server.tcl index 7257339042..c39bea0036 100644 --- a/tests/support/server.tcl +++ b/tests/support/server.tcl @@ -534,7 +534,9 @@ proc start_server {options {code undefined}} { dict set config "tls-cluster" "yes" dict set config "tls-replication" "yes" } else { - dict set config port $port + set aport [find_available_port $::baseport $::portcount] + dict set config "port" $port + dict set config "admin-port" $aport } set unixsocket [file normalize [format "%s/%s" [dict get $config "dir"] "socket"]] @@ -604,7 +606,9 @@ proc start_server {options {code undefined}} { dict set config port $pport dict set config "tls-port" $port } else { + set aport [find_available_port $::baseport $::portcount] dict set config port $port + dict set config admin-port $aport } create_server_config_file $config_file $config $config_lines @@ -638,8 +642,10 @@ proc start_server {options {code undefined}} { # setup properties to be able to initialize a client object set port_param [expr $::tls ? {"tls-port"} : {"port"}] set host $::host + set admin_port 0 if {[dict exists $config bind]} { set host [lindex [dict get $config bind] 0] } if {[dict exists $config $port_param]} { set port [dict get $config $port_param] } + if {[dict exists $config admin-port]} { set admin_port [lindex [dict get $config admin-port] 0] } # setup config dict dict set srv "config_file" $config_file @@ -647,6 +653,7 @@ proc start_server {options {code undefined}} { dict set srv "pid" $pid dict set srv "host" $host dict set srv "port" $port + dict set srv "admin-port" $admin_port dict set srv "stdout" $stdout dict set srv "stderr" $stderr dict set srv "unixsocket" $unixsocket diff --git a/tests/unit/admin-port.tcl b/tests/unit/admin-port.tcl new file mode 100644 index 0000000000..e253086240 --- /dev/null +++ b/tests/unit/admin-port.tcl @@ -0,0 +1,43 @@ +tags {external:skip tls:skip} { + test {admin-port: CONFIG SET port number should fail} { + start_server {} { + set avail_port [find_available_port $::baseport $::portcount] + set rd [valkey [srv 0 host] [srv 0 admin-port]] + assert_error {*can't set immutable config*} {$rd CONFIG SET admin-port $avail_port} + $rd PING + $rd close + } + } + + test {admin-port: connection should fail on non-loopback interface} { + start_server {} { + catch {valkey [get_nonloopback_addr] [srv 0 admin-port]} e + assert_match {*connection refused*} $e + } + } + + test {admin-port: setting admin-port to server port should fail} { + start_server {} { + catch {r CONFIG SET port [srv 0 admin-port]} e + assert_match {*Unable to listen on this port*} $e + } + } + + test {admin-port: client could connect on admin-port after maxclients reached} { + start_server {} { + set original_maxclients [lindex [r config get maxclients] 1] + r config set maxclients 2 + set rd [valkey [srv 0 host] [srv 0 port]] + assert_match "PONG" [$rd PING] + set rd1 [valkey [srv 0 host] [srv 0 port]] + catch {$rd1 PING} e + assert_match "*ERR max*reached*" $e + set rd2 [valkey [srv 0 host] [srv 0 admin-port]] + assert_match "PONG" [$rd2 PING] + r config set maxclients $original_maxclients + $rd close + $rd1 close + $rd2 close + } + } +} diff --git a/tests/unit/introspection.tcl b/tests/unit/introspection.tcl index 286b02b7d0..541365a3b5 100644 --- a/tests/unit/introspection.tcl +++ b/tests/unit/introspection.tcl @@ -559,6 +559,7 @@ start_server {tags {"introspection"}} { req-res-logfile client-default-resp dual-channel-replication-enabled + admin-port } if {!$::tls} { diff --git a/valkey.conf b/valkey.conf index 56110a52bd..1ee10b6b91 100644 --- a/valkey.conf +++ b/valkey.conf @@ -138,6 +138,15 @@ protected-mode yes # If port 0 is specified the server will not listen on a TCP socket. port 6379 +# admin-port +# +# Accept connections on the specified port and connections should be originating from +# local loopback (127.0.0.1), unix domain socket or IPv6 address (::1). +# Default is 0 and immutable. +# If admin-port is not specified the server will not listen on a TCP socket. +# +# admin-port 0 + # TCP listen() backlog. # # In high requests-per-second environments you need a high backlog in order