commit 74a71dbeeea3a2ce4cd28f0a21038dbac15756f6
parent a4c87a53fff698e749a3f5354277decf9a1e2029
Author: finwo <finwo@pm.me>
Date: Sat, 3 Jan 2026 19:38:46 +0100
2.70
Diffstat:
27 files changed, 391 insertions(+), 35 deletions(-)
diff --git a/ChangeLog b/ChangeLog
@@ -1,3 +1,18 @@
+2.70 [KK 2010-10-28]
+- Bugfix in 3-secs delay during out of file handles conditions (see
+ xr/balancer/serve.cc)
+- Potential bug fixed in web interface responder (check for validity
+ of client socket added).
+- Browser requests for /favicon.ico are silently dropped (404 return),
+ instead of showing up in the error log as unknown url.
+- Small codechanges in back end testing via TCP. E.g, when tcp-connect
+ checking a back end, the connection is attempted twice by default.
+ Just once appears to fail on some systems, twice fixes the problem.
+- Basic authentication for the web interface implemented. This can be
+ given on the commandline using --webinterface-auth, in xrctl.xml
+ using <webinterfaceauth>, or injected later on the url
+ /server/webinterfaceauth/USER:PASS
+
2.69 [KK 2010-10-08]
- Installation paths in the make install process can be tweaked,
thanks Felix A. W. O. for suggesting this.
diff --git a/Makefile b/Makefile
@@ -1,7 +1,7 @@
# Top-level Makefile for XR
# -------------------------
-VER ?= 2.69
+VER ?= 2.70
PREFIX ?= $(DESTDIR)/usr
BINDIR ?= $(PREFIX)/sbin
MANDIR ?= $(PREFIX)/share/man
diff --git a/doc/xr.odt b/doc/xr.odt
Binary files differ.
diff --git a/test/backendmon b/test/backendmon
@@ -0,0 +1,61 @@
+#!/usr/bin/env perl
+
+use strict;
+use XML::Simple;
+use LWP::UserAgent;
+
+# Check arguments
+if ($#ARGV != 1) {
+ die << "ENDUSAGE";
+
+Usage: $0 webinterface-url period
+Example: $0 http://localhost:20001 5
+
+The XR web interface at the stated URL is checked and the back end states
+are reported.
+
+ENDUSAGE
+}
+
+# Process
+while (1) {
+ check($ARGV[0]);
+ sleep($ARGV[1]);
+}
+
+# Check the web interface. Take unavailable back ends offline.
+sub check($) {
+ my $url = shift;
+
+ # Access web interface
+ my $ua = LWP::UserAgent->new();
+ my $resp = $ua->get($url);
+ if (! $resp->is_success()) {
+ warn("Failed to access the XR web interface on '$url': ",
+ $resp->status_line(), "\n");
+ return;
+ }
+
+ # Parse the XML
+ my $xml;
+ eval {
+ $xml = XMLin($resp->content());
+ };
+ if ($@) {
+ warn("Failed to parse web interface response: $@\n");
+ return;
+ }
+
+ # print Dumper $xml;
+
+ my @backends = @{ $xml->{backend} };
+ print("\n", scalar(localtime()), "\n");
+ for my $b (@backends) {
+ print(" Back end ", $b->{nr}, " at ", $b->{address},
+ ": available=", $b->{available}, " up=", $b->{up});
+ print(" DEAD") if ($b->{live} ne 'alive');
+ print("\n");
+ }
+}
+
+
diff --git a/test/downer b/test/downer
@@ -0,0 +1,65 @@
+#!/usr/bin/env perl
+
+use strict;
+use XML::Simple;
+use LWP::UserAgent;
+use Data::Dumper;
+
+# Check arguments
+if ($#ARGV != 1) {
+ die << "ENDUSAGE";
+
+Usage: $0 webinterface-url period
+Example: $0 http://localhost:20001 5
+
+The XR web interface at the stated URL is checked for unavailable back ends.
+Such back ends are taken offline. This is repeated each 'period' seconds.
+
+ENDUSAGE
+}
+
+# Process
+while (1) {
+ check($ARGV[0]);
+ sleep($ARGV[1]);
+}
+
+# Check the web interface. Take unavailable back ends offline.
+sub check($) {
+ my $url = shift;
+
+ # Access web interface
+ my $ua = LWP::UserAgent->new();
+ my $resp = $ua->get($url);
+ if (! $resp->is_success()) {
+ warn("Failed to access the XR web interface on '$url': ",
+ $resp->status_line(), "\n");
+ return;
+ }
+
+ # Parse the XML
+ my $xml;
+ eval {
+ $xml = XMLin($resp->content());
+ };
+ if ($@) {
+ warn("Failed to parse web interface response: $@\n");
+ return;
+ }
+
+ # print Dumper $xml;
+
+ my @backends = @{ $xml->{backend} };
+ for my $b (@backends) {
+ print("Back end ", $b->{nr}, " at ", $b->{address},
+ ": available=", $b->{available}, " up=", $b->{up}, "\n");
+ if ($b->{available} ne 'available' and $b->{up} eq 'up') {
+ print(" Marking back end as 'down'.\n");
+ my $resp = $ua->get($url . '/backend/' . $b->{nr} . '/up/0');
+ warn("Failed to mark back end down: ", $resp->status_line(), "\n")
+ unless ($resp->is_success());
+ }
+ }
+}
+
+
diff --git a/test/externalchecker b/test/externalchecker
@@ -0,0 +1,10 @@
+#!/usr/bin/env perl
+
+# Example of an external checker
+
+print STDERR (">>>> External checker: ", scalar(localtime()), " @ARGV\n");
+if ($ARGV[0] eq 'localhost:8000') {
+ print("1\n");
+} else {
+ print("0\n");
+}
diff --git a/test/externaldispatcher b/test/externaldispatcher
@@ -0,0 +1,8 @@
+#!/usr/bin/env perl
+
+# Example of an external dispatcher.
+# ----------------------------------
+
+print STDERR (">>>> External dispatcher called on ", scalar(localtime()),
+ " with arguments @ARGV\n");
+print("0\n");
diff --git a/test/onfail b/test/onfail
@@ -1,4 +1,3 @@
#!/bin/sh
-echo Back end $2 with $3 connections failed for client $1 \
- >> /tmp/activity.log
+echo `date` Back end $2 with $3 connections failed for client $1 1>&2
diff --git a/test/onstart b/test/onstart
@@ -1,6 +1,5 @@
#!/bin/sh
-echo Client $1 will be handled by $2, $3 connections so far \
- >> /tmp/activity.log
+echo `date` Client $1 will be handled by $2, $3 connections so far 1>&2
diff --git a/xr/Dispatchers/tcpdispatcher/execute.cc b/xr/Dispatchers/tcpdispatcher/execute.cc
@@ -8,7 +8,7 @@ void TcpDispatcher::execute() {
!check_acl())
return;
- msg("Dispatch request for client fd " << clientfd() << '\n');
+ debugmsg("Dispatch request for client fd " << clientfd() << '\n');
// Try to determine the back end.
try {
@@ -33,7 +33,7 @@ void TcpDispatcher::execute() {
}
// Dispatch!
- msg ("Dispatching client fd " << clientfd() << " to " <<
+ msg("Dispatching client fd " << clientfd() << " to " <<
balancer.backend(targetbackend()).description() << ", fd " <<
backendfd() << '\n');
diff --git a/xr/backend/check.cc b/xr/backend/check.cc
@@ -12,8 +12,27 @@ void Backend::check() {
case BackendCheck::c_connect:
if (backendcheck().server() == "" && backendcheck().port() == 0) {
// Most common: TCP connect to the actual back end
- connect();
- socketclose(sock());
+ tester = *this;
+ // Retry CONNCHECK times (see etc/Makefile.class).
+ for (int i = 0; i < CONNCHECKS; i++) {
+ tester.connect();
+ socketclose(tester.sock());
+ debugmsg("TCP-connect testing back end " << tester.description() <<
+ " try " << i << ": " <<
+ (tester.live() ? "alive" : "not-alive")
+ << '\n');
+ }
+ if ( (tester.live() && !live()) ||
+ (!tester.live() && live()) ) {
+ debugmsg("State of back end " << tester.description() <<
+ " is now " << (tester.live() ? "alive" : "not-alive") <<
+ '\n');
+ live(tester.live());
+ }
+ /* This was:
+ connect();
+ socketclose(sock());
+ */
} else {
// TCP connects to an alternative server or port.
// We instantiate a dummy backend and let it connect to the "other"
@@ -26,8 +45,8 @@ void Backend::check() {
tester.connect();
socketclose (tester.sock());
live(tester.live());
- msg ("Alternative back end for testing " <<
- tester.description() << " is " << livestr() << '\n');
+ debugmsg("Alternative back end for testing " <<
+ tester.description() << " is " << livestr() << '\n');
}
break;
diff --git a/xr/backend/connect.cc b/xr/backend/connect.cc
@@ -54,8 +54,6 @@ bool Backend::connect() {
fdset.add (clsocket);
fdset.wait_rw();
- debugmsg("Connecting to " << description() << '\n');
-
# ifdef CONNECTCHECK_ONLY_WRITABLE
if (fdset.writeable(clsocket))
islive = true;
@@ -67,6 +65,11 @@ bool Backend::connect() {
if (fdset.writeable(clsocket) && !fdset.readable(clsocket))
islive = true;
else {
+ debugmsg("Connect socket writable: " <<
+ (fdset.writeable(clsocket) ? "yes" : "no") <<
+ ", readable: " <<
+ (fdset.readable(clsocket) ? "yes" : "no") <<
+ '\n');
socketclose(clsocket);
markconnecterror();
}
diff --git a/xr/backend/live.cc b/xr/backend/live.cc
@@ -4,10 +4,6 @@ void Backend::live (bool state) {
PROFILE("Backend::live");
mutex_lock(&islive);
- bool oldstate = islive;
islive = state;
mutex_unlock(&islive);
-
- if (oldstate != state)
- msg ("Marking back end " + description() + " as " + livestr() + "\n");
}
diff --git a/xr/config/config b/xr/config/config
@@ -111,6 +111,9 @@ public:
}
int webinterfaceport() const { return webinterface_port; }
+ void webinterface_auth(string const &s);
+ string webinterface_auth() const { return web_auth; }
+
unsigned nserverheaders() const { return (serverheaders.size()); }
string const &serverheader (unsigned n) {
return (serverheaders[n]);
@@ -229,6 +232,7 @@ private:
static bool use_webinterface;
static string webinterface_ip;
static int webinterface_port;
+ static string web_auth;
static string dump_dir;
static unsigned soft_maxconnrate;
static unsigned hard_maxconnrate;
diff --git a/xr/config/config1.cc b/xr/config/config1.cc
@@ -31,6 +31,7 @@ int Config::ipstore_timeout;
bool Config::use_webinterface = false;
string Config::webinterface_ip;
int Config::webinterface_port;
+string Config::web_auth;
string Config::dump_dir;
unsigned Config::soft_maxconnrate = 0;
unsigned Config::hard_maxconnrate = 0;
diff --git a/xr/config/parsecmdline.cc b/xr/config/parsecmdline.cc
@@ -19,7 +19,7 @@ void Config::parsecmdline (int ac, char **av) {
throw Error("Bad command line '" + cmdline + "'\n" + USAGE);
# define OPTSTRING "?a:A:B:b:c:CDd:E:e:fF:Gg:hH:Ij:l:" \
- "m:M:nPQ:r:R:Ss:t:T:u:U:vVW:w:xXy:z:Z:"
+ "m:M:nPQ:r:R:Ss:t:T:u:U:vVW:w:xXy:Y:z:Z:"
# ifdef HAVE_GETOPT_LONG
static struct option longopts[] = {
{ "allow-from", required_argument, 0, 'a' },
@@ -59,6 +59,7 @@ void Config::parsecmdline (int ac, char **av) {
{ "version", no_argument, 0, 'V' },
{ "wakeup-interval", required_argument, 0, 'w' },
{ "web-interface", required_argument, 0, 'W' },
+ { "web-interface-auth", required_argument, 0, 'Y' },
{ "add-xr-version", no_argument, 0, 'X' },
{ "add-x-forwarded-for", no_argument, 0, 'x' },
{ "onfail", required_argument, 0, 'y' },
@@ -262,6 +263,9 @@ void Config::parsecmdline (int ac, char **av) {
case 'y':
onfail(optarg);
break;
+ case 'Y':
+ webinterface_auth(optarg);
+ break;
case 'z':
onstart(optarg);
break;
diff --git a/xr/config/webinterface_auth.cc b/xr/config/webinterface_auth.cc
@@ -0,0 +1,11 @@
+#include "config"
+
+void Config::webinterface_auth(string const &s) {
+ // Make sure there is a ':' delimiter in the parameter
+ if (s.length() > 0 && s.find_first_of(':') == string::npos)
+ throw Error("Bad web interface authentication parameter, "
+ "format must be username:password");
+
+ web_auth = s;
+}
+
diff --git a/xr/etc/Makefile.class b/xr/etc/Makefile.class
@@ -3,9 +3,21 @@ OBJ = $(patsubst %.cc, $(BASE)/xr/$(BUILDDIR)/$(DIR)_%.o, $(SRC))
DIR = $(shell pwd | sed 's:.*/::')
SYS = $(shell uname)
HST = $(shell hostname)
+
+# How many times should XR check a back end using plain TCP connects?
+# Just once fails on some systems. Twice seems to do the job.
+CONNCHECKS = 2
+
+# When connecting to a back end with the socket in nonblocking mode, is
+# the connection alive when the socket is just writable? When commented out,
+# the socket must also be non-readable (which should be the standard, but on
+# some systems appears not to work). Leave this commented out unless you are
+# experimenting or really know what you are doing.
# CCC = -DCONNECTCHECK_ONLY_WRITABLE
-ifeq ($(HST), Thera.local)
+# On my dev system, I want warnings to stop the compilation. Does not apply to
+# others where warnings won't stop the build.
+ifeq ($(HST), Kostunrix.local)
ERRFLAG = -Werror
endif
@@ -19,6 +31,7 @@ $(BASE)/xr/$(BUILDDIR)/$(DIR)_%.o: %.cc
-DSYS='"$(SYS)"' -D$(SYS) $(MEMDEBUG) $(CCC) \
-DCONF_CC='"$(CONF_CC)"' -DCONF_LIB='"$(CONF_LIB)"' \
-DCONF_OPTFLAGS='"$(CONF_OPTFLAGS)"' $(CONF_STRNSTR) \
+ -DCONNCHECKS=$(CONNCHECKS) \
$(CONF_GETOPT) $(CONF_GETOPT_LONG) $(CONF_INET_ATON) \
-I$(BASE)/xr \
-c -g -Wall $(ERRFLAG) -o $@ $<
diff --git a/xr/etc/status-nosavebutton.xslt b/xr/etc/status-nosavebutton.xslt
@@ -608,6 +608,16 @@
</td>
</tr>
<xsl:apply-templates select="/status/server/acl/deny"/>
+
+ <tr>
+ <td>Web interface credentials</td>
+ <td>Format username:password</td>
+ <td colspan="2" align="right">
+ <input type="text" size="30" name="webinterfaceauth" class="input"
+ id="webinterfaceauth" value="{webinterfaceauth}"
+ onchange="goto('/server/webinterfaceauth/', 'webinterfaceauth');"/>
+ </td>
+ </tr>
<xsl:if test="/status/server/type = 'http'">
<xsl:apply-templates select="/status/server/http"/>
diff --git a/xr/etc/usage.txt b/xr/etc/usage.txt
@@ -122,7 +122,7 @@ may not exist on your platform):
to the command are: the client's IP address, and the back end address.
--onstart CMD, -z CMD
Runs CMD just before letting a back end handle a client's connection.
- For the arguments of CMD see -y.
+ For the arguments of CMD see --onfail, -y.
--pidfile FILE, -p FILE
FILE is written with the process id of XR upon startup, and
removed upon exit.
@@ -167,6 +167,8 @@ may not exist on your platform):
matching URL. Only available when the server is in http mode.
--web-interface IP:PORT, -W IP:PORT
Starts a web interface on specified IP address and port.
+ --web-interface-auth USER:PASS, -Y USER:PASS
+ Access to the web interface will be protected by basic authentication.
--verbose, -v
Increases verbosity, default is silent operation.
--version, -V
diff --git a/xr/sys/base64decode.cc b/xr/sys/base64decode.cc
@@ -0,0 +1,93 @@
+
+#include "sys"
+
+/* =========================================================================
+ * The remainder of the code originates from http://base64.sourceforge.net/.
+ * The original version was written by Bob Trower.
+ * Adapted for C++ by me [kk]
+ * =========================================================================
+ */
+
+/*
+** Translation Table as described in RFC1113
+** Not used during decode phase
+static
+const char *xx_cb64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789+/";
+*/
+
+/*
+** Translation Table to decode (created by author)
+*/
+static
+const char *xx_cd64 = "|$$$}rstuvwxyz{$$$$$$$>?@"
+ "ABCDEFGHIJKLMNOPQRSTUVW$$$$$$XYZ"
+ "[\\]^_`abcdefghijklmnopq";
+
+/*
+** decodeblock
+**
+** decode 4 '6-bit' characters into 3 8-bit binary bytes
+*/
+static void decodeblock(char in[4], char out[4])
+{
+ out[0] = (unsigned char ) (in[0] << 2 | in[1] >> 4);
+ out[1] = (unsigned char ) (in[1] << 4 | in[2] >> 2);
+ out[2] = (unsigned char ) (((in[2] << 6) & 0xc0) | in[3]);
+}
+
+/*
+** decode
+**
+** decode a base64 encoded stream discarding padding, line breaks and noise
+*/
+static
+string base64_decode(char const *srcstr, int srclen) {
+ char in[4], out[4], v;
+ int i, j, len, morechars;
+ string decbuf;
+ int declen = 0;
+
+ /* Initialize. Only the first 3 chars of out will get overwritten. */
+ out[3] = 0;
+
+ /* Examine all characters. */
+ j = 0;
+ morechars = 1;
+ while (morechars) {
+ for (len = 0, i = 0; i < 4 && morechars; i++) {
+ v = 0;
+
+ while (morechars && !v) {
+ v = (unsigned char) srcstr[j++];
+ if (j > srclen)
+ morechars = 0;
+ v = (unsigned char) ((v < 43 || v > 122) ?
+ 0 :
+ xx_cd64[v - 43]);
+ if (v)
+ v = (unsigned char) ((v == '$') ? 0 : v - 61);
+ }
+ if (morechars) {
+ len++;
+ if (v)
+ in[i] = (unsigned char) (v - 1);
+ }
+ else
+ in[i] = 0;
+ }
+ if (len) {
+ decodeblock (in, out);
+ decbuf += string(out);
+ declen += len - 1;
+ }
+ }
+
+ /* All done. */
+ return (decbuf);
+}
+
+string base64_decode(string const &s) {
+ return base64_decode(s.c_str(), s.length());
+}
diff --git a/xr/sys/sys b/xr/sys/sys
@@ -88,6 +88,7 @@ unsigned long mt_rand(void);
bool check_acl(string const &ipstr, struct in_addr ipaddr);
int sysrun (string const &s);
int maxtimeout(int a, int b);
+string base64_decode(string const &s);
void mutex_lock(void *obj);
void mutex_unlock(void *obj);
diff --git a/xr/webinterface/answer.cc b/xr/webinterface/answer.cc
@@ -106,6 +106,26 @@ void Webinterface::answer(Httpbuffer req) {
if (req.requestmethod() != Httpbuffer::m_get)
throw Error("Only request method GET supported");
+ // If web interface authentication is in effect, then we need the creds
+ // for all requests.
+ if (config.webinterface_auth() != "") {
+ // Incoming auth headers have the format:
+ // Authorization: Basic BASE64-ENCODED-UN:PW
+ string given_auth = req.headerval("Authorization:");
+ if (given_auth.length() > 6)
+ given_auth = given_auth.substr(6);
+ if (base64_decode(given_auth) != config.webinterface_auth()) {
+ string resp =
+ "HTTP/1.0 401 Authorization Required\r\n"
+ "WWW-Authenticate: Basic realm=\"Crossroads Web Interface\"\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n";
+ Netbuffer buf(resp);
+ buf.netwrite(cfd, config.client_write_timeout());
+ return;
+ }
+ }
+
string uri = req.requesturi();
// Status overview
@@ -120,6 +140,17 @@ void Webinterface::answer(Httpbuffer req) {
return;
}
+ // /favicon.ico requests (those pesky browsers)
+ if (uri == "/favicon.ico") {
+ string resp =
+ "HTTP/1.0 404 Not Found\r\n"
+ "Content-Length: 0\r\n"
+ "\r\n";
+ Netbuffer buf(resp);
+ buf.netwrite(cfd, config.client_write_timeout());
+ return;
+ }
+
if (uri[0] == '/')
uri = uri.substr(1);
vector<string> parts = str2parts (uri, '/');
@@ -137,6 +168,15 @@ void Webinterface::answer(Httpbuffer req) {
return;
}
+ // /server/webinterfaceauth/
+ // /server/webinterfaceauth/USER:PASS
+ if (parts.size() == 3 &&
+ parts[0] == "server" && parts[1] == "webinterfaceauth") {
+ config.webinterface_auth(parts[2]);
+ answer_status();
+ return;
+ }
+
// /server/maxconnections/
// /server/maxconnections/NUMBER
if (parts.size() == 3 &&
diff --git a/xr/webinterface/answerstatus.cc b/xr/webinterface/answerstatus.cc
@@ -26,6 +26,7 @@ void Webinterface::answer_status() {
" <dispatchmode>" << config.dispatchmodestr() << "</dispatchmode>\n"
" <removereservations>" << config.removereservations() << "</removereservations>\n"
" <webinterface>" << config.webinterfaceip() << ':' << config.webinterfaceport() << "</webinterface>\n"
+ " <webinterfaceauth>" << config.webinterface_auth() << "</webinterfaceauth>\n"
" <dnscachetimeout>" << config.dnscachetimeout() << "</dnscachetimeout>\n"
" <buffersize>" << config.buffersize() << "</buffersize>\n"
" <closesocketsfast>" << config.fastclose() << "</closesocketsfast>\n"
diff --git a/xr/webinterface/execute.cc b/xr/webinterface/execute.cc
@@ -16,7 +16,7 @@ void Webinterface::execute() {
config.webinterfaceport(),
"web interface", Servertype::t_tcp);
} catch (Error const &e) {
- cerr << e.what() << "(webinterface, retrying in a sec)\n";
+ cerr << e.what() << " (webinterface, retrying in a sec)\n";
sleep (1);
continue;
}
@@ -37,26 +37,26 @@ void Webinterface::execute() {
warnmsg("Web interface: failed to accept network "
"connection: " << strerror(errno) << '\n');
} else {
- serve ();
+ serve();
socketclose(cfd);
}
}
} catch (Error const &e) {
cerr << e.what() << " (webinterface)\n";
- ostringstream m;
- m <<
- "<h1>Web interface error</h1>\n"
- "XR's web interface could not handle your request.<p/>\n"
- "<i>" << e.what() << "</i>\n";
- ostringstream o;
- o <<
- "HTTP/1.0 500 Server Error\r\n"
- "X-Reason: " << e.what() << "\r\n"
- "Content-Length: " << m.str().length() << "\r\n"
- "\r\n" <<
- m.str();
+ if (cfd >= 0) {
+ ostringstream m;
+ m << "<h1>Web interface error</h1>\n"
+ "XR's web interface could not handle your request.<p/>\n"
+ "<i>" << e.what() << "</i>\n";
+ ostringstream o;
+ o << "HTTP/1.0 500 Server Error\r\n"
+ "X-Reason: " << e.what() << "\r\n"
+ "Content-Length: " << m.str().length() << "\r\n"
+ "\r\n" <<
+ m.str();
Netbuffer buf(o.str());
buf.netwrite(cfd, config.client_write_timeout());
+ }
socketclose(cfd);
}
}
diff --git a/xr/webinterface/serve.cc b/xr/webinterface/serve.cc
@@ -1,7 +1,7 @@
#include "webinterface"
void Webinterface::serve () {
- msg("Webinterface serving request on client fd " << cfd << '\n');
+ debugmsg("Webinterface serving request on client fd " << cfd << '\n');
Httpbuffer clientrequest;
clientrequest.netread(cfd, config.client_read_timeout());
diff --git a/xrctl/xrctl b/xrctl/xrctl
@@ -513,6 +513,7 @@ sub xr_cmdarr {
# Handle general flags and boolflags
push (@cmd,
flag($ss, '--web-interface', 'webinterface', ''),
+ flag($ss, '--web-interface-auth', 'webinterfaceauth', ''),
flag($ss, '--dispatch-mode', 'dispatchmode',
$default_dispatchmode),
flag($ss, '--max-connections', 'maxconnections',