diff --git a/changes/bug4700 b/changes/bug4700 new file mode 100644 index 0000000000..3c8d9b19b0 --- /dev/null +++ b/changes/bug4700 @@ -0,0 +1,5 @@ + o Minor features (onion services): + - Version 3 onion services can now use the per-service + HiddenServiceExportCircuitID option to differentiate client circuits by + using the HAProxy proxy protocol which assigns IP addresses to inbound client + circuits. Closes ticket 4700. Patch by Mahrud Sayrafi. diff --git a/doc/tor.1.txt b/doc/tor.1.txt index 37f21742b2..6403c1c3e6 100644 --- a/doc/tor.1.txt +++ b/doc/tor.1.txt @@ -2845,6 +2845,33 @@ 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) +[[HiddenServiceExportCircuitID]] **HiddenServiceExportCircuitID** __protocol__:: + The onion service will use the given protocol to expose the global circuit + identifier of each inbound client circuit via the selected protocol. The only + protocol supported right now \'haproxy\'. This option is only for v3 + services. (Default: none) + + + + The haproxy option works in the following way: when the feature is + enabled, the Tor process will write a header line when a client is connecting + to the onion service. The header will look like this: + + + + "PROXY TCP6 fc00:dead:beef:4dad::ffff:ffff ::1 65535 42\r\n" + + + + We encode the "global circuit identifier" as the last 32-bits of the first + IPv6 address. All other values in the header can safely be ignored. You can + compute the global circuit identifier using the following formula given the + IPv6 address "fc00:dead:beef:4dad::AABB:CCDD": + + + + global_circuit_id = (0xAA << 24) + (0xBB << 16) + (0xCC << 8) + 0xDD; + + + + In the case above, where the last 32-bit is 0xffffffff, the global circuit + identifier would be 4294967295. You can use this value together with Tor's + control port where it is possible to terminate a circuit given the global + circuit identifier. For more information about this see controls-spec.txt. + + + + The HAProxy version 1 proxy protocol is described in detail at + https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt + [[HiddenServiceMaxStreams]] **HiddenServiceMaxStreams** __N__:: The maximum number of simultaneous streams (connections) per rendezvous circuit. The maximum value allowed is 65535. (Setting this to 0 will allow diff --git a/src/app/config/config.c b/src/app/config/config.c index 0b2c391fea..abb3407043 100644 --- a/src/app/config/config.c +++ b/src/app/config/config.c @@ -460,6 +460,7 @@ static config_var_t option_vars_[] = { VAR("HiddenServiceMaxStreams",LINELIST_S, RendConfigLines, NULL), VAR("HiddenServiceMaxStreamsCloseCircuit",LINELIST_S, RendConfigLines, NULL), VAR("HiddenServiceNumIntroductionPoints", LINELIST_S, RendConfigLines, NULL), + VAR("HiddenServiceExportCircuitID", LINELIST_S, RendConfigLines, NULL), VAR("HiddenServiceStatistics", BOOL, HiddenServiceStatistics_option, "1"), V(HidServAuth, LINELIST, NULL), V(ClientOnionAuthDir, FILENAME, NULL), diff --git a/src/core/or/connection_edge.c b/src/core/or/connection_edge.c index 40772670ee..d0fad67009 100644 --- a/src/core/or/connection_edge.c +++ b/src/core/or/connection_edge.c @@ -839,6 +839,46 @@ connected_cell_format_payload(uint8_t *payload_out, return connected_payload_len; } +/* This is an onion service client connection: Export the client circuit ID + * according to the HAProxy proxy protocol. */ +STATIC void +export_hs_client_circuit_id(edge_connection_t *edge_conn, + hs_circuit_id_protocol_t protocol) +{ + /* We only support HAProxy right now. */ + if (protocol != HS_CIRCUIT_ID_PROTOCOL_HAPROXY) + return; + + char *buf = NULL; + const char dst_ipv6[] = "::1"; + /* See RFC4193 regarding fc00::/7 */ + const char src_ipv6_prefix[] = "fc00:dead:beef:4dad:"; + uint16_t dst_port = 0; + uint16_t src_port = 1; /* default value */ + uint32_t gid = 0; /* default value */ + + /* Generate a GID and source port for this client */ + if (edge_conn->on_circuit != NULL) { + gid = TO_ORIGIN_CIRCUIT(edge_conn->on_circuit)->global_identifier; + src_port = gid & 0x0000ffff; + } + + /* Grab the original dest port from the hs ident */ + if (edge_conn->hs_ident) { + dst_port = edge_conn->hs_ident->orig_virtual_port; + } + + /* Build the string */ + tor_asprintf(&buf, "PROXY TCP6 %s:%x:%x %s %d %d\r\n", + src_ipv6_prefix, + gid >> 16, gid & 0x0000ffff, + dst_ipv6, src_port, dst_port); + + connection_buf_add(buf, strlen(buf), TO_CONN(edge_conn)); + + tor_free(buf); +} + /** Connected handler for exit connections: start writing pending * data, deliver 'CONNECTED' relay cells as appropriate, and check * any pending data that may have been received. */ @@ -859,6 +899,7 @@ connection_edge_finished_connecting(edge_connection_t *edge_conn) rep_hist_note_exit_stream_opened(conn->port); conn->state = EXIT_CONN_STATE_OPEN; + connection_watch_events(conn, READ_EVENT); /* stop writing, keep reading */ if (connection_get_outbuf_len(conn)) /* in case there are any queued relay * cells */ @@ -3652,6 +3693,14 @@ handle_hs_exit_conn(circuit_t *circ, edge_connection_t *conn) hs_inc_rdv_stream_counter(origin_circ); + /* If it's an onion service connection, we might want to include the proxy + * protocol header: */ + if (conn->hs_ident) { + hs_circuit_id_protocol_t circuit_id_protocol = + hs_service_exports_circuit_id(&conn->hs_ident->identity_pk); + export_hs_client_circuit_id(conn, circuit_id_protocol); + } + /* Connect tor to the hidden service destination. */ connection_exit_connect(conn); diff --git a/src/core/or/connection_edge.h b/src/core/or/connection_edge.h index 71ea95d6e5..6dee93f1f9 100644 --- a/src/core/or/connection_edge.h +++ b/src/core/or/connection_edge.h @@ -14,6 +14,8 @@ #include "lib/testsupport/testsupport.h" +#include "feature/hs/hs_service.h" + edge_connection_t *TO_EDGE_CONN(connection_t *); entry_connection_t *TO_ENTRY_CONN(connection_t *); entry_connection_t *EDGE_TO_ENTRY_CONN(edge_connection_t *); @@ -260,6 +262,10 @@ STATIC void connection_ap_handshake_rewrite(entry_connection_t *conn, rewrite_result_t *out); STATIC int connection_ap_process_http_connect(entry_connection_t *conn); +STATIC void +export_hs_client_circuit_id(edge_connection_t *edge_conn, + hs_circuit_id_protocol_t protocol); + #endif /* defined(CONNECTION_EDGE_PRIVATE) */ #endif /* !defined(TOR_CONNECTION_EDGE_H) */ diff --git a/src/core/or/or.h b/src/core/or/or.h index 4d8b6d7871..7557c1321e 100644 --- a/src/core/or/or.h +++ b/src/core/or/or.h @@ -26,6 +26,7 @@ #include "lib/cc/compat_compiler.h" #include "lib/cc/torint.h" #include "lib/container/map.h" +#include "lib/container/buffers.h" #include "lib/container/smartlist.h" #include "lib/crypt_ops/crypto_cipher.h" #include "lib/crypt_ops/crypto_rsa.h" diff --git a/src/feature/hs/hs_common.c b/src/feature/hs/hs_common.c index 12405a79cb..c36892e0f8 100644 --- a/src/feature/hs/hs_common.c +++ b/src/feature/hs/hs_common.c @@ -882,6 +882,11 @@ hs_set_conn_addr_port(const smartlist_t *ports, edge_connection_t *conn) smartlist_free(matching_ports); if (chosen_port) { if (!(chosen_port->is_unix_addr)) { + /* save the original destination before we overwrite it */ + if (conn->hs_ident) { + conn->hs_ident->orig_virtual_port = TO_CONN(conn)->port; + } + /* Get a non-AF_UNIX connection ready for connection_exit_connect() */ tor_addr_copy(&TO_CONN(conn)->addr, &chosen_port->real_addr); TO_CONN(conn)->port = chosen_port->real_port; diff --git a/src/feature/hs/hs_config.c b/src/feature/hs/hs_config.c index eaeb58829a..93d7403dfb 100644 --- a/src/feature/hs/hs_config.c +++ b/src/feature/hs/hs_config.c @@ -145,6 +145,34 @@ helper_parse_uint64(const char *opt, const char *value, uint64_t min, return ret; } +/** Helper function: Given a configuration option and its value, parse the + * value as a hs_circuit_id_protocol_t. On success, ok is set to 1 and ret is + * the parse value. On error, ok is set to 0 and the "none" + * hs_circuit_id_protocol_t is returned. This function logs on error. */ +static hs_circuit_id_protocol_t +helper_parse_circuit_id_protocol(const char *key, const char *value, int *ok) +{ + tor_assert(value); + tor_assert(ok); + + hs_circuit_id_protocol_t ret = HS_CIRCUIT_ID_PROTOCOL_NONE; + *ok = 0; + + if (! strcasecmp(value, "haproxy")) { + *ok = 1; + ret = HS_CIRCUIT_ID_PROTOCOL_HAPROXY; + } else if (! strcasecmp(value, "none")) { + *ok = 1; + ret = HS_CIRCUIT_ID_PROTOCOL_NONE; + } else { + log_warn(LD_CONFIG, "%s must be 'haproxy' or 'none'.", key); + goto err; + } + + err: + return ret; +} + /* Return the service version by trying to learn it from the key on disk if * any. If nothing is found, the current service configured version is * returned. */ @@ -188,6 +216,11 @@ config_has_invalid_options(const config_line_t *line_, NULL /* End marker. */ }; + const char *opts_exclude_v2[] = { + "HiddenServiceExportCircuitID", + NULL /* End marker. */ + }; + /* Defining the size explicitly allows us to take advantage of the compiler * which warns us if we ever bump the max version but forget to grow this * array. The plus one is because we have a version 0 :). */ @@ -196,7 +229,7 @@ config_has_invalid_options(const config_line_t *line_, } exclude_lists[HS_VERSION_MAX + 1] = { { NULL }, /* v0. */ { NULL }, /* v1. */ - { NULL }, /* v2 */ + { opts_exclude_v2 }, /* v2 */ { opts_exclude_v3 }, /* v3. */ }; @@ -262,6 +295,7 @@ config_service_v3(const config_line_t *line_, hs_service_config_t *config) { int have_num_ip = 0; + bool export_circuit_id = false; /* just to detect duplicate options */ const char *dup_opt_seen = NULL; const config_line_t *line; @@ -288,6 +322,18 @@ config_service_v3(const config_line_t *line_, have_num_ip = 1; continue; } + if (!strcasecmp(line->key, "HiddenServiceExportCircuitID")) { + config->circuit_id_protocol = + helper_parse_circuit_id_protocol(line->key, line->value, &ok); + if (!ok || export_circuit_id) { + if (export_circuit_id) { + dup_opt_seen = line->key; + } + goto err; + } + export_circuit_id = true; + continue; + } } /* We do not load the key material for the service at this stage. This is diff --git a/src/feature/hs/hs_ident.h b/src/feature/hs/hs_ident.h index 92d15b0523..ab87d16d17 100644 --- a/src/feature/hs/hs_ident.h +++ b/src/feature/hs/hs_ident.h @@ -111,6 +111,10 @@ typedef struct hs_ident_edge_conn_t { * in the onion address. */ ed25519_public_key_t identity_pk; + /* The original virtual port that was used by the client to access the onion + * service, regardless of the internal port forwarding that might have + * happened on the service-side. */ + uint16_t orig_virtual_port; /* XXX: Client authorization. */ } hs_ident_edge_conn_t; diff --git a/src/feature/hs/hs_service.c b/src/feature/hs/hs_service.c index 28e3f73340..643240fb68 100644 --- a/src/feature/hs/hs_service.c +++ b/src/feature/hs/hs_service.c @@ -3767,6 +3767,19 @@ hs_service_set_conn_addr_port(const origin_circuit_t *circ, return -1; } +/** Does the service with identity pubkey pk export the circuit IDs of + * its clients? */ +hs_circuit_id_protocol_t +hs_service_exports_circuit_id(const ed25519_public_key_t *pk) +{ + hs_service_t *service = find_service(hs_service_map, pk); + if (!service) { + return HS_CIRCUIT_ID_PROTOCOL_NONE; + } + + return service->config.circuit_id_protocol; +} + /* Add to file_list every filename used by a configured hidden service, and to * dir_list every directory path used by a configured hidden service. This is * used by the sandbox subsystem to whitelist those. */ diff --git a/src/feature/hs/hs_service.h b/src/feature/hs/hs_service.h index 735266071f..6fb15b9d37 100644 --- a/src/feature/hs/hs_service.h +++ b/src/feature/hs/hs_service.h @@ -161,6 +161,15 @@ typedef struct hs_service_authorized_client_t { curve25519_public_key_t client_pk; } hs_service_authorized_client_t; +/** Which protocol to use for exporting HS client circuit ID. */ +typedef enum { + /** Don't expose the circuit id. */ + HS_CIRCUIT_ID_PROTOCOL_NONE, + + /** Use the HAProxy proxy protocol. */ + HS_CIRCUIT_ID_PROTOCOL_HAPROXY +} hs_circuit_id_protocol_t; + /* Service configuration. The following are set from the torrc options either * set by the configuration file or by the control port. Nothing else should * change those values. */ @@ -210,6 +219,9 @@ typedef struct hs_service_config_t { /* Is this service ephemeral? */ unsigned int is_ephemeral : 1; + + /* Does this service export the circuit ID of its clients? */ + hs_circuit_id_protocol_t circuit_id_protocol; } hs_service_config_t; /* Service state. */ @@ -316,6 +328,9 @@ void hs_service_upload_desc_to_dir(const char *encoded_desc, const ed25519_public_key_t *blinded_pk, const routerstatus_t *hsdir_rs); +hs_circuit_id_protocol_t +hs_service_exports_circuit_id(const ed25519_public_key_t *pk); + #ifdef HS_SERVICE_PRIVATE #ifdef TOR_UNIT_TESTS diff --git a/src/test/test_extorport.c b/src/test/test_extorport.c index 1ac1c68d7e..71be9131c7 100644 --- a/src/test/test_extorport.c +++ b/src/test/test_extorport.c @@ -17,6 +17,7 @@ #include "core/or/or_connection_st.h" #include "test/test.h" +#include "test/test_helpers.h" #ifdef HAVE_SYS_STAT_H #include @@ -89,22 +90,6 @@ connection_write_to_buf_impl_replacement(const char *string, size_t len, buf_add(conn->outbuf, string, len); } -static char * -buf_get_contents(buf_t *buf, size_t *sz_out) -{ - char *out; - *sz_out = buf_datalen(buf); - if (*sz_out >= ULONG_MAX) - return NULL; /* C'mon, really? */ - out = tor_malloc(*sz_out + 1); - if (buf_get_bytes(buf, out, (unsigned long)*sz_out) != 0) { - tor_free(out); - return NULL; - } - out[*sz_out] = '\0'; /* Hopefully gratuitous. */ - return out; -} - static void test_ext_or_write_command(void *arg) { diff --git a/src/test/test_helpers.c b/src/test/test_helpers.c index 0a2da100ba..6ac73bff5c 100644 --- a/src/test/test_helpers.c +++ b/src/test/test_helpers.c @@ -125,6 +125,25 @@ connection_write_to_buf_mock(const char *string, size_t len, buf_add(conn->outbuf, string, len); } +char * +buf_get_contents(buf_t *buf, size_t *sz_out) +{ + tor_assert(buf); + tor_assert(sz_out); + + char *out; + *sz_out = buf_datalen(buf); + if (*sz_out >= ULONG_MAX) + return NULL; /* C'mon, really? */ + out = tor_malloc(*sz_out + 1); + if (buf_get_bytes(buf, out, (unsigned long)*sz_out) != 0) { + tor_free(out); + return NULL; + } + out[*sz_out] = '\0'; /* Hopefully gratuitous. */ + return out; +} + /* Set up a fake origin circuit with the specified number of cells, * Return a pointer to the newly-created dummy circuit */ circuit_t * diff --git a/src/test/test_helpers.h b/src/test/test_helpers.h index 3196c93e6b..72bf7f2f71 100644 --- a/src/test/test_helpers.h +++ b/src/test/test_helpers.h @@ -4,6 +4,8 @@ #ifndef TOR_TEST_HELPERS_H #define TOR_TEST_HELPERS_H +#define BUFFERS_PRIVATE + #include "core/or/or.h" const char *get_yesterday_date_str(void); @@ -18,6 +20,7 @@ void helper_setup_fake_routerlist(void); #define GET(path) "GET " path " HTTP/1.0\r\n\r\n" void connection_write_to_buf_mock(const char *string, size_t len, connection_t *conn, int compressed); +char *buf_get_contents(buf_t *buf, size_t *sz_out); int mock_tor_addr_lookup__fail_on_bad_addrs(const char *name, uint16_t family, tor_addr_t *out); diff --git a/src/test/test_hs_service.c b/src/test/test_hs_service.c index 41ad25d171..334bde3e7b 100644 --- a/src/test/test_hs_service.c +++ b/src/test/test_hs_service.c @@ -10,6 +10,7 @@ #define CIRCUITLIST_PRIVATE #define CONFIG_PRIVATE #define CONNECTION_PRIVATE +#define CONNECTION_EDGE_PRIVATE #define CRYPTO_PRIVATE #define HS_COMMON_PRIVATE #define HS_SERVICE_PRIVATE @@ -33,6 +34,9 @@ #include "core/or/circuitbuild.h" #include "core/or/circuitlist.h" #include "core/or/circuituse.h" +#include "core/mainloop/connection.h" +#include "core/or/connection_edge.h" +#include "core/or/edge_connection_st.h" #include "lib/crypt_ops/crypto_rand.h" #include "lib/fs/dir.h" #include "feature/dirauth/dirvote.h" @@ -2003,6 +2007,96 @@ test_authorized_client_config_equal(void *arg) tor_free(config2); } +/** Test that client circuit ID gets correctly exported */ +static void +test_export_client_circuit_id(void *arg) +{ + origin_circuit_t *or_circ = NULL; + size_t sz; + char *cp1=NULL, *cp2=NULL; + connection_t *conn = NULL; + + (void) arg; + + MOCK(connection_write_to_buf_impl_, connection_write_to_buf_mock); + + hs_service_init(); + + /* Create service */ + hs_service_t *service = helper_create_service(); + /* Check that export circuit ID detection works */ + service->config.circuit_id_protocol = HS_CIRCUIT_ID_PROTOCOL_NONE; + tt_int_op(0, OP_EQ, + hs_service_exports_circuit_id(&service->keys.identity_pk)); + service->config.circuit_id_protocol = HS_CIRCUIT_ID_PROTOCOL_HAPROXY; + tt_int_op(1, OP_EQ, + hs_service_exports_circuit_id(&service->keys.identity_pk)); + + /* Create client connection */ + conn = test_conn_get_connection(AP_CONN_STATE_CIRCUIT_WAIT, CONN_TYPE_AP, 0); + + /* Create client edge conn hs_ident */ + edge_connection_t *edge_conn = TO_EDGE_CONN(conn); + edge_conn->hs_ident = hs_ident_edge_conn_new(&service->keys.identity_pk); + edge_conn->hs_ident->orig_virtual_port = 42; + + /* Create rend circuit */ + or_circ = origin_circuit_new(); + or_circ->base_.purpose = CIRCUIT_PURPOSE_C_REND_JOINED; + edge_conn->on_circuit = TO_CIRCUIT(or_circ); + or_circ->global_identifier = 666; + + /* Export circuit ID */ + export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol); + + /* Check contents */ + cp1 = buf_get_contents(conn->outbuf, &sz); + tt_str_op(cp1, OP_EQ, + "PROXY TCP6 fc00:dead:beef:4dad::0:29a ::1 666 42\r\n"); + + /* Change circ GID and see that the reported circuit ID also changes */ + or_circ->global_identifier = 22; + + /* check changes */ + export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol); + cp2 = buf_get_contents(conn->outbuf, &sz); + tt_str_op(cp1, OP_NE, cp2); + tor_free(cp1); + + /* Check that GID with UINT32_MAX works. */ + or_circ->global_identifier = UINT32_MAX; + + export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol); + cp1 = buf_get_contents(conn->outbuf, &sz); + tt_str_op(cp1, OP_EQ, + "PROXY TCP6 fc00:dead:beef:4dad::ffff:ffff ::1 65535 42\r\n"); + tor_free(cp1); + + /* Check that GID with UINT16_MAX works. */ + or_circ->global_identifier = UINT16_MAX; + + export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol); + cp1 = buf_get_contents(conn->outbuf, &sz); + tt_str_op(cp1, OP_EQ, + "PROXY TCP6 fc00:dead:beef:4dad::0:ffff ::1 65535 42\r\n"); + tor_free(cp1); + + /* Check that GID with UINT16_MAX + 7 works. */ + or_circ->global_identifier = UINT16_MAX + 7; + + export_hs_client_circuit_id(edge_conn, service->config.circuit_id_protocol); + cp1 = buf_get_contents(conn->outbuf, &sz); + tt_str_op(cp1, OP_EQ, "PROXY TCP6 fc00:dead:beef:4dad::1:6 ::1 6 42\r\n"); + + done: + UNMOCK(connection_write_to_buf_impl_); + circuit_free_(TO_CIRCUIT(or_circ)); + connection_free_minimal(conn); + hs_service_free(service); + tor_free(cp1); + tor_free(cp2); +} + struct testcase_t hs_service_tests[] = { { "e2e_rend_circuit_setup", test_e2e_rend_circuit_setup, TT_FORK, NULL, NULL }, @@ -2044,6 +2138,8 @@ struct testcase_t hs_service_tests[] = { NULL, NULL }, { "authorized_client_config_equal", test_authorized_client_config_equal, TT_FORK, NULL, NULL }, + { "export_client_circuit_id", test_export_client_circuit_id, TT_FORK, + NULL, NULL }, END_OF_TESTCASES };