diff --git a/changes/feature16052 b/changes/feature16052 new file mode 100644 index 0000000000..cd09b58867 --- /dev/null +++ b/changes/feature16052 @@ -0,0 +1,5 @@ + o Minor features (hidden service): + - Add the new options "HiddenServiceMaxStreams" and + "HiddenServiceMaxStreamsCloseCircuit" to allow hidden services to limit + the maximum number of simultaneous streams per circuit, and optionally + tear down the circuit when the limit is exceeded. Part of ticket 16052. diff --git a/doc/tor.1.txt b/doc/tor.1.txt index 2bb5f947ef..13f2bdd60c 100644 --- a/doc/tor.1.txt +++ b/doc/tor.1.txt @@ -2149,6 +2149,16 @@ The following options are used to configure a hidden service. not an authorization mechanism; it is instead meant to be a mild inconvenience to port-scanners.) (Default: 0) +[[HiddenServiceMaxStreams]] **HiddenServiceMaxStreams** __N__:: + The maximum number of simultaneous streams (connections) per rendezvous + circuit. (Setting this to 0 will allow an unlimited number of simultanous + streams.) (Default: 0) + +[[HiddenServiceMaxStreamsCloseCircuit]] **HiddenServiceMaxStreamsCloseCircuit** **0**|**1**:: + If set to 1, then exceeding **HiddenServiceMaxStreams** will cause the + offending rendezvous circuit to be torn down, as opposed to stream creation + requests that exceed the limit being silently ignored. (Default: 0) + [[RendPostPeriod]] **RendPostPeriod** __N__ **seconds**|**minutes**|**hours**|**days**|**weeks**:: Every time the specified period elapses, Tor uploads any rendezvous service descriptors to the directory servers. This information is also diff --git a/src/or/circuituse.c b/src/or/circuituse.c index b54a4d2a7f..a429a7d053 100644 --- a/src/or/circuituse.c +++ b/src/or/circuituse.c @@ -1189,17 +1189,28 @@ circuit_detach_stream(circuit_t *circ, edge_connection_t *conn) if (CIRCUIT_IS_ORIGIN(circ)) { origin_circuit_t *origin_circ = TO_ORIGIN_CIRCUIT(circ); + int removed = 0; if (conn == origin_circ->p_streams) { origin_circ->p_streams = conn->next_stream; - return; + removed = 1; + } else { + for (prevconn = origin_circ->p_streams; + prevconn && prevconn->next_stream && prevconn->next_stream != conn; + prevconn = prevconn->next_stream) + ; + if (prevconn && prevconn->next_stream) { + prevconn->next_stream = conn->next_stream; + removed = 1; + } } - - for (prevconn = origin_circ->p_streams; - prevconn && prevconn->next_stream && prevconn->next_stream != conn; - prevconn = prevconn->next_stream) - ; - if (prevconn && prevconn->next_stream) { - prevconn->next_stream = conn->next_stream; + if (removed) { + /* If the stream was removed, and it was a rend stream, decrement the + * number of streams on the circuit associated with the rend service. + */ + if (circ->purpose == CIRCUIT_PURPOSE_S_REND_JOINED) { + tor_assert(origin_circ->rend_data); + origin_circ->rend_data->nr_streams--; + } return; } } else { diff --git a/src/or/config.c b/src/or/config.c index 10304482e8..1c04578893 100644 --- a/src/or/config.c +++ b/src/or/config.c @@ -286,6 +286,8 @@ static config_var_t option_vars_[] = { VAR("HiddenServiceVersion",LINELIST_S, RendConfigLines, NULL), VAR("HiddenServiceAuthorizeClient",LINELIST_S,RendConfigLines, NULL), VAR("HiddenServiceAllowUnknownPorts",LINELIST_S, RendConfigLines, NULL), + VAR("HiddenServiceMaxStreams",LINELIST_S, RendConfigLines, NULL), + VAR("HiddenServiceMaxStreamsCloseCircuit",LINELIST_S, RendConfigLines, NULL), V(HiddenServiceStatistics, BOOL, "0"), V(HidServAuth, LINELIST, NULL), V(CloseHSClientCircuitsImmediatelyOnTimeout, BOOL, "0"), diff --git a/src/or/connection_edge.c b/src/or/connection_edge.c index cc6e3d7c58..c63c350fd8 100644 --- a/src/or/connection_edge.c +++ b/src/or/connection_edge.c @@ -2860,6 +2860,8 @@ connection_exit_begin_conn(cell_t *cell, circuit_t *circ) origin_circ->p_streams = n_stream; assert_circuit_ok(circ); + origin_circ->rend_data->nr_streams++; + connection_exit_connect(n_stream); /* For path bias: This circuit was used successfully */ diff --git a/src/or/control.c b/src/or/control.c index 4eeb897afa..746dfff921 100644 --- a/src/or/control.c +++ b/src/or/control.c @@ -3566,9 +3566,12 @@ handle_control_add_onion(control_connection_t *conn, smartlist_t *port_cfgs = smartlist_new(); int discard_pk = 0; int detach = 0; + int max_streams = 0; + int max_streams_close_circuit = 0; for (size_t i = 1; i < arg_len; i++) { static const char *port_prefix = "Port="; static const char *flags_prefix = "Flags="; + static const char *max_s_prefix = "MaxStreams="; const char *arg = smartlist_get(args, i); if (!strcasecmpstart(arg, port_prefix)) { @@ -3582,15 +3585,27 @@ handle_control_add_onion(control_connection_t *conn, goto out; } smartlist_add(port_cfgs, cfg); + } else if (!strcasecmpstart(arg, max_s_prefix)) { + /* "MaxStreams=[0..65535]". */ + const char *max_s_str = arg + strlen(max_s_prefix); + int ok = 0; + max_streams = (int)tor_parse_long(max_s_str, 10, 0, 65535, &ok, NULL); + if (!ok) { + connection_printf_to_buf(conn, "512 Invalid MaxStreams\r\n"); + goto out; + } } else if (!strcasecmpstart(arg, flags_prefix)) { /* "Flags=Flag[,Flag]", where Flag can be: * * 'DiscardPK' - If tor generates the keypair, do not include it in * the response. * * 'Detach' - Do not tie this onion service to any particular control * connection. + * * 'MaxStreamsCloseCircuit' - Close the circuit if MaxStreams is + * exceeded. */ static const char *discard_flag = "DiscardPK"; static const char *detach_flag = "Detach"; + static const char *max_s_close_flag = "MaxStreamsCloseCircuit"; smartlist_t *flags = smartlist_new(); int bad = 0; @@ -3607,6 +3622,8 @@ handle_control_add_onion(control_connection_t *conn, discard_pk = 1; } else if (!strcasecmp(flag, detach_flag)) { detach = 1; + } else if (!strcasecmp(flag, max_s_close_flag)) { + max_streams_close_circuit = 1; } else { connection_printf_to_buf(conn, "512 Invalid 'Flags' argument: %s\r\n", @@ -3652,7 +3669,9 @@ handle_control_add_onion(control_connection_t *conn, * regardless of success/failure. */ char *service_id = NULL; - int ret = rend_service_add_ephemeral(pk, port_cfgs, &service_id); + int ret = rend_service_add_ephemeral(pk, port_cfgs, max_streams, + max_streams_close_circuit, + &service_id); port_cfgs = NULL; /* port_cfgs is now owned by the rendservice code. */ switch (ret) { case RSAE_OKAY: diff --git a/src/or/or.h b/src/or/or.h index 5bb080fdc5..af3496765e 100644 --- a/src/or/or.h +++ b/src/or/or.h @@ -818,6 +818,9 @@ typedef struct rend_data_t { /** List of HSDir fingerprints on which this request has been sent to. * This contains binary identity digest of the directory. */ smartlist_t *hsdirs_fp; + + /** Number of streams associated with this rendezvous circuit. */ + int nr_streams; } rend_data_t; /** Time interval for tracking replays of DH public keys received in diff --git a/src/or/rendservice.c b/src/or/rendservice.c index daca4ccda4..0329d70924 100644 --- a/src/or/rendservice.c +++ b/src/or/rendservice.c @@ -147,6 +147,13 @@ typedef struct rend_service_t { /** If true, we don't close circuits for making requests to unsupported * ports. */ int allow_unknown_ports; + /** The maximum number of simultanious streams-per-circuit that are allowed + * to be established, or 0 if no limit is set. + */ + int max_streams_per_circuit; + /** If true, we close circuits that exceed the max_streams_per_circuit + * limit. */ + int max_streams_close_circuit; } rend_service_t; /** Returns a escaped string representation of the service, s. @@ -259,6 +266,23 @@ rend_add_service(rend_service_t *service) service->intro_nodes = smartlist_new(); + if (service->max_streams_per_circuit < 0) { + log_warn(LD_CONFIG, "Hidden service (%s) configured with negative max " + "streams per circuit; ignoring.", + rend_service_escaped_dir(service)); + rend_service_free(service); + return -1; + } + + if (service->max_streams_close_circuit < 0 || + service->max_streams_close_circuit > 1) { + log_warn(LD_CONFIG, "Hidden service (%s) configured with invalid " + "max streams handling; ignoring.", + rend_service_escaped_dir(service)); + rend_service_free(service); + return -1; + } + if (service->auth_type != REND_NO_AUTH && smartlist_len(service->clients) == 0) { log_warn(LD_CONFIG, "Hidden service (%s) with client authorization but no " @@ -539,6 +563,33 @@ rend_config_services(const or_options_t *options, int validate_only) log_info(LD_CONFIG, "HiddenServiceDirGroupReadable=%d for %s", service->dir_group_readable, service->directory); + } else if (!strcasecmp(line->key, "HiddenServiceMaxStreams")) { + service->max_streams_per_circuit = (int)tor_parse_long(line->value, + 10, 0, 65535, &ok, NULL); + if (!ok) { + log_warn(LD_CONFIG, + "HiddenServiceMaxStreams should be between 0 and %d, not %s", + 65535, line->value); + rend_service_free(service); + return -1; + } + log_info(LD_CONFIG, + "HiddenServiceMaxStreams=%d for %s", + service->max_streams_per_circuit, service->directory); + } else if (!strcasecmp(line->key, "HiddenServiceMaxStreamsCloseCircuit")) { + service->max_streams_close_circuit = (int)tor_parse_long(line->value, + 10, 0, 1, &ok, NULL); + if (!ok) { + log_warn(LD_CONFIG, + "HiddenServiceMaxStreamsCloseCircuit should be 0 or 1, not %s", + line->value); + rend_service_free(service); + return -1; + } + log_info(LD_CONFIG, + "HiddenServiceMaxStreamsCloseCircuit=%d for %s", + (int)service->max_streams_close_circuit, service->directory); + } else if (!strcasecmp(line->key, "HiddenServiceAuthorizeClient")) { /* Parse auth type and comma-separated list of client names and add a * rend_authorized_client_t for each client to the service's list @@ -758,7 +809,10 @@ rend_config_services(const or_options_t *options, int validate_only) return 0; } -/** Add the ephemeral service pk/ports if possible. +/** Add the ephemeral service pk/ports if possible, with + * max_streams_per_circuit streams allowed per rendezvous circuit, + * and circuit closure on max streams being exceeded set by + * max_streams_close_circuit. * * Regardless of sucess/failure, callers should not touch pk/ports after * calling this routine, and may assume that correct cleanup has been done @@ -769,6 +823,8 @@ rend_config_services(const or_options_t *options, int validate_only) rend_service_add_ephemeral_status_t rend_service_add_ephemeral(crypto_pk_t *pk, smartlist_t *ports, + int max_streams_per_circuit, + int max_streams_close_circuit, char **service_id_out) { *service_id_out = NULL; @@ -782,6 +838,8 @@ rend_service_add_ephemeral(crypto_pk_t *pk, s->ports = ports; s->intro_period_started = time(NULL); s->n_intro_points_wanted = NUM_INTRO_POINTS_DEFAULT; + s->max_streams_per_circuit = max_streams_per_circuit; + s->max_streams_close_circuit = max_streams_close_circuit; if (rend_service_derive_key_digests(s) < 0) { rend_service_free(s); return RSAE_BADPRIVKEY; @@ -3795,6 +3853,25 @@ rend_service_set_connection_addr_port(edge_connection_t *conn, serviceid, (unsigned)circ->base_.n_circ_id); return -2; } + if (service->max_streams_per_circuit > 0) { + /* Enforce the streams-per-circuit limit, and refuse to provide a + * mapping if this circuit will exceed the limit. */ +#define MAX_STREAM_WARN_INTERVAL 600 + static struct ratelim_t stream_ratelim = + RATELIM_INIT(MAX_STREAM_WARN_INTERVAL); + if (circ->rend_data->nr_streams >= service->max_streams_per_circuit) { + log_fn_ratelim(&stream_ratelim, LOG_WARN, LD_REND, + "Maximum streams per circuit limit reached on rendezvous " + "circuit %u; %s. Circuit has %d out of %d streams.", + (unsigned)circ->base_.n_circ_id, + service->max_streams_close_circuit ? + "closing circuit" : + "ignoring open stream request", + circ->rend_data->nr_streams, + service->max_streams_per_circuit); + return service->max_streams_close_circuit ? -2 : -1; + } + } matching_ports = smartlist_new(); SMARTLIST_FOREACH(service->ports, rend_service_port_config_t *, p, { diff --git a/src/or/rendservice.h b/src/or/rendservice.h index ec783d53c9..b540d2c8ad 100644 --- a/src/or/rendservice.h +++ b/src/or/rendservice.h @@ -117,6 +117,8 @@ typedef enum { } rend_service_add_ephemeral_status_t; rend_service_add_ephemeral_status_t rend_service_add_ephemeral(crypto_pk_t *pk, smartlist_t *ports, + int max_streams_per_circuit, + int max_streams_close_circuit, char **service_id_out); int rend_service_del_ephemeral(const char *service_id);