From 1b907d13bb97aba8badcb428623fa13e803b8d92 Mon Sep 17 00:00:00 2001 From: trinity-1686a Date: Sun, 10 Sep 2023 13:13:11 +0200 Subject: [PATCH] add rate limit on BEGIN and RESOLVE cell per circuit --- src/core/or/circuitlist.c | 2 ++ src/core/or/connection_edge.c | 49 ++++++++++++++++++++++++++++++++++- src/core/or/dos.c | 38 ++++++++++++++++++++++++++- src/core/or/dos.h | 4 +++ src/core/or/or_circuit_st.h | 4 +++ src/core/or/relay.c | 3 +-- src/feature/relay/dns.c | 6 +++++ src/feature/relay/dns.h | 2 ++ src/test/test_status.c | 1 + 9 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/core/or/circuitlist.c b/src/core/or/circuitlist.c index b90c7ebb58..643d97b064 100644 --- a/src/core/or/circuitlist.c +++ b/src/core/or/circuitlist.c @@ -65,6 +65,7 @@ #include "core/or/conflux.h" #include "core/or/conflux_pool.h" #include "core/or/crypt_path.h" +#include "core/or/dos.h" #include "core/or/extendinfo.h" #include "core/or/status.h" #include "core/or/trace_probes_circuit.h" @@ -1130,6 +1131,7 @@ or_circuit_new(circid_t p_circ_id, channel_t *p_chan) cell_queue_init(&circ->p_chan_cells); init_circuit_base(TO_CIRCUIT(circ)); + dos_stream_init_circ_tbf(circ); tor_trace(TR_SUBSYS(circuit), TR_EV(new_or), circ); return circ; diff --git a/src/core/or/connection_edge.c b/src/core/or/connection_edge.c index 900d639959..764e1c886b 100644 --- a/src/core/or/connection_edge.c +++ b/src/core/or/connection_edge.c @@ -73,6 +73,7 @@ #include "core/or/conflux_util.h" #include "core/or/circuitstats.h" #include "core/or/connection_or.h" +#include "core/or/dos.h" #include "core/or/extendinfo.h" #include "core/or/policies.h" #include "core/or/reasons.h" @@ -3990,6 +3991,7 @@ connection_exit_begin_conn(cell_t *cell, circuit_t *circ) begin_cell_t bcell; int rv; uint8_t end_reason=0; + dos_stream_defense_type_t dos_defense_type; assert_circuit_ok(circ); if (!CIRCUIT_IS_ORIGIN(circ)) { @@ -4148,6 +4150,35 @@ connection_exit_begin_conn(cell_t *cell, circuit_t *circ) log_debug(LD_EXIT,"about to start the dns_resolve()."); + /* TODO should this be moved higher to protect from a stream DoS on directory + * requests, and possibly against an onion service? (for OS, more changes + * would be required) */ + dos_defense_type = dos_stream_new_begin_or_resolve_cell(or_circ); + switch (dos_defense_type) { + case DOS_STREAM_DEFENSE_NONE: + break; + case DOS_STREAM_DEFENSE_REFUSE_STREAM: + // we don't use END_STREAM_REASON_RESOURCELIMIT because it would make a + // client mark us as non-functional until they get a new consensus. + relay_send_end_cell_from_edge(rh.stream_id, circ, END_STREAM_REASON_MISC, + layer_hint); + connection_free_(TO_CONN(n_stream)); + return 0; + case DOS_STREAM_DEFENSE_CLOSE_CIRCUIT: + connection_free_(TO_CONN(n_stream)); + /* TODO we could return REASON_NONE or REASON_RESOURCELIMIT. When closing + * circuits, you either get: + * - END_CIRC_REASON_NONE: tons of notice level "We tried for 15 + * seconds to connect to 'target' using exit X. Retrying on a new + * circuit." + * - END_CIRC_REASON_RESOURCELIMIT: warn level "Guard X is failing + * to carry an extremely large amount of streams on its circuits" + * + * I'm not sure which one we want + */ + return -END_CIRC_REASON_NONE; + } + /* send it off to the gethostbyname farm */ switch (dns_resolve(n_stream)) { case 1: /* resolve worked; now n_stream is attached to circ. */ @@ -4171,17 +4202,21 @@ connection_exit_begin_conn(cell_t *cell, circuit_t *circ) * Called when we receive a RELAY_COMMAND_RESOLVE cell 'cell' along the * circuit circ; * begin resolving the hostname, and (eventually) reply with a RESOLVED cell. + * + * Return -(some circuit end reason) if we want to tear down circ. + * Else return 0. */ int connection_exit_begin_resolve(cell_t *cell, or_circuit_t *circ) { edge_connection_t *dummy_conn; relay_header_t rh; + dos_stream_defense_type_t dos_defense_type; assert_circuit_ok(TO_CIRCUIT(circ)); relay_header_unpack(&rh, cell->payload); if (rh.length > RELAY_PAYLOAD_SIZE) - return -1; + return 0; /* Note the RESOLVE stream as seen. */ rep_hist_note_exit_stream(RELAY_COMMAND_RESOLVE); @@ -4204,6 +4239,18 @@ connection_exit_begin_resolve(cell_t *cell, or_circuit_t *circ) dummy_conn->on_circuit = TO_CIRCUIT(circ); + dos_defense_type = dos_stream_new_begin_or_resolve_cell(circ); + switch (dos_defense_type) { + case DOS_STREAM_DEFENSE_NONE: + break; + case DOS_STREAM_DEFENSE_REFUSE_STREAM: + dns_send_resolved_error_cell(dummy_conn, RESOLVED_TYPE_ERROR_TRANSIENT); + return 0; + case DOS_STREAM_DEFENSE_CLOSE_CIRCUIT: + /* TODO maybe use REASON_RESOURCELIMIT? See connection_exit_begin_conn() */ + return -END_CIRC_REASON_NONE; + } + /* send it off to the gethostbyname farm */ switch (dns_resolve(dummy_conn)) { case -1: /* Impossible to resolve; a resolved cell was sent. */ diff --git a/src/core/or/dos.c b/src/core/or/dos.c index a47738c906..63cac190fd 100644 --- a/src/core/or/dos.c +++ b/src/core/or/dos.c @@ -325,7 +325,8 @@ get_param_stream_defense_type(const networkstatus_t *ns) } return networkstatus_get_param(ns, "DoSStreamCreationDefenseType", DOS_STREAM_DEFENSE_TYPE_DEFAULT, - DOS_STREAM_DEFENSE_NONE, DOS_STREAM_DEFENSE_MAX); + DOS_STREAM_DEFENSE_NONE, + DOS_STREAM_DEFENSE_MAX); } /* Set circuit creation parameters located in the consensus or their default @@ -836,6 +837,41 @@ dos_conn_addr_get_defense_type(const tor_addr_t *addr) return DOS_CONN_DEFENSE_NONE; } +/* Stream creation public API. */ + +/* Return the action to take against a BEGIN or RESOLVE cell. Return + * DOS_STREAM_DEFENSE_NONE when no action should be taken. + * Increment the appropriate counter when the cell was found to go over a + * limit. */ +dos_stream_defense_type_t +dos_stream_new_begin_or_resolve_cell(or_circuit_t *circ) +{ + if (!dos_stream_enabled || circ == NULL) + return DOS_STREAM_DEFENSE_NONE; + + token_bucket_ctr_refill(&circ->stream_limiter, + (uint32_t) monotime_coarse_absolute_sec()); + + if (token_bucket_ctr_get(&circ->stream_limiter) > 0) { + token_bucket_ctr_dec(&circ->stream_limiter, 1); + return DOS_STREAM_DEFENSE_NONE; + } + /* if defense type is DOS_STREAM_DEFENSE_NONE but DoSStreamEnabled is true, + * we count offending cells as rejected, despite them being actually + * accepted. */ + ++stream_num_rejected; + return dos_stream_defense_type; +} + +/* Initialize the token bucket for stream rate limit on a circuit. */ +void +dos_stream_init_circ_tbf(or_circuit_t *circ) +{ + token_bucket_ctr_init(&circ->stream_limiter, dos_stream_rate, + dos_stream_burst, + (uint32_t) monotime_coarse_absolute_sec()); +} + /* General API */ /* Take any appropriate actions for the given geoip entry that is about to get diff --git a/src/core/or/dos.h b/src/core/or/dos.h index 8b36fe1415..77dce333d1 100644 --- a/src/core/or/dos.h +++ b/src/core/or/dos.h @@ -186,6 +186,10 @@ typedef enum dos_stream_defense_type_t { DOS_STREAM_DEFENSE_MAX = 3, } dos_stream_defense_type_t; +dos_stream_defense_type_t dos_stream_new_begin_or_resolve_cell( + or_circuit_t *circ); +void dos_stream_init_circ_tbf(or_circuit_t *circ); + #ifdef DOS_PRIVATE STATIC uint32_t get_param_conn_max_concurrent_count( diff --git a/src/core/or/or_circuit_st.h b/src/core/or/or_circuit_st.h index d5a7007928..28e357338a 100644 --- a/src/core/or/or_circuit_st.h +++ b/src/core/or/or_circuit_st.h @@ -102,6 +102,10 @@ struct or_circuit_t { * used if this is a service introduction circuit at the intro point * (purpose = CIRCUIT_PURPOSE_INTRO_POINT). */ token_bucket_ctr_t introduce2_bucket; + + /** RELAY_BEGIN and RELAY_RESOLVE cell bucket controlling how much can go on + * this circuit. Only used if this is the end of a circuit on an exit node.*/ + token_bucket_ctr_t stream_limiter; }; #endif /* !defined(OR_CIRCUIT_ST_H) */ diff --git a/src/core/or/relay.c b/src/core/or/relay.c index 6abe802355..0cda6e7bf7 100644 --- a/src/core/or/relay.c +++ b/src/core/or/relay.c @@ -2015,8 +2015,7 @@ handle_relay_cell_command(cell_t *cell, circuit_t *circ, circ->purpose); return 0; } - connection_exit_begin_resolve(cell, TO_OR_CIRCUIT(circ)); - return 0; + return connection_exit_begin_resolve(cell, TO_OR_CIRCUIT(circ)); case RELAY_COMMAND_RESOLVED: if (conn) { log_fn(LOG_PROTOCOL_WARN, domain, diff --git a/src/feature/relay/dns.c b/src/feature/relay/dns.c index f6a020d061..129f6209d7 100644 --- a/src/feature/relay/dns.c +++ b/src/feature/relay/dns.c @@ -563,6 +563,12 @@ send_resolved_cell,(edge_connection_t *conn, uint8_t answer_type, connection_edge_send_command(conn, RELAY_COMMAND_RESOLVED, buf, buflen); } +void +dns_send_resolved_error_cell(edge_connection_t *conn, uint8_t answer_type) +{ + send_resolved_cell(conn, answer_type, NULL); +} + /** Send a response to the RESOLVE request of a connection for an in-addr.arpa * address on connection conn which yielded the result hostname. * The answer type will be RESOLVED_HOSTNAME. diff --git a/src/feature/relay/dns.h b/src/feature/relay/dns.h index 3f8519bd97..b43b42756e 100644 --- a/src/feature/relay/dns.h +++ b/src/feature/relay/dns.h @@ -20,6 +20,8 @@ int dns_reset(void); void connection_dns_remove(edge_connection_t *conn); void assert_connection_edge_not_dns_pending(edge_connection_t *conn); int dns_resolve(edge_connection_t *exitconn); +void dns_send_resolved_error_cell(edge_connection_t *conn, + uint8_t answer_type); int dns_seems_to_be_broken(void); int dns_seems_to_be_broken_for_ipv6(void); void dns_reset_correctness_checks(void); diff --git a/src/test/test_status.c b/src/test/test_status.c index 5873d8e5c3..4ceb81f3a5 100644 --- a/src/test/test_status.c +++ b/src/test/test_status.c @@ -365,6 +365,7 @@ test_status_hb_not_in_consensus(void *arg) "with too many cells, [DoSCircuitCreationEnabled disabled], " "[DoSConnectionEnabled disabled], " "[DoSRefuseSingleHopClientRendezvous disabled], " + "[DoSStreamCreationEnabled disabled], " "0 INTRODUCE2 rejected.\n"); tt_int_op(mock_saved_log_n_entries(), OP_EQ, 6);