Skip to content

Commit 607bd3a

Browse files
authored
Add option for client-token header (#92)
* Add option for client-token header
1 parent 48451ea commit 607bd3a

File tree

6 files changed

+139
-7
lines changed

6 files changed

+139
-7
lines changed

README.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,22 @@ After preparing this directory, point to it when running the local proxy with th
304304
* Consider running the local proxy on separate hosts, containers, sandboxes, chroot jail, or a virtualized environment
305305

306306
#### Access tokens
307-
308-
* Access tokens are not meant to be re-used.
309-
* After localproxy uses an access token, it will no longer be valid.
307+
* After localproxy uses an access token, it will no longer be valid without an accompanying Client Token.
310308
* You can revoke an existing token and get a new valid token by calling [RotateTunnelAccessToken](https://docs.aws.amazon.com/iot/latest/apireference/API_iot-secure-tunneling_RotateTunnelAccessToken.html).
311309
* Refer to the [Developer Guide](https://docs.aws.amazon.com/iot/latest/developerguide/iot-secure-tunneling-troubleshooting.html) for troubleshooting connectivity issues that can be due to an invalid token.
312310

311+
#### Client Tokens
312+
* The client token is an added security layer to protect the tunnel by ensuring that only the agent that generated the client token can use a particular access token to connect to a tunnel.
313+
* Only one client token value may be present in the request. Supplying multiple values will cause the handshake to fail.
314+
* The client token is optional.
315+
* The client token must be unique across all the open tunnels per AWS account
316+
* It's recommended to use a UUID to generate the client token.
317+
* The client token can be any string that matches the regex `^[a-zA-Z0-9-]{32,128}$`
318+
* If a client token is provided, then local proxy needs to pass the same client token for subsequent retries (This is yet to be implemented in the current version of local proxy)
319+
* If a client token is not provided, then the access token will become invalid after a successful handshake, and localproxy won't be able to reconnect using the same access token.
320+
* The Client Token may be passed using the **-i** argument from the command line or setting the **AWSIOT_TUNNEL_CLIENT_TOKEN** environment variable.
321+
322+
313323
### IPv6 support
314324

315325
The local proxy uses IPv4 and IPv6 dynamically based on how addresses are specified directly by the user, or how are they resolved on the system. For example, if 'localhost' resolves to '127.0.0.1' then IPv4 will is being used to connect or as the listening address. If localhost resolves to '::1' then IPv6 will be used.

src/LocalproxyConfig.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ namespace aws {
7171
*/
7272
std::string access_token { };
7373
proxy_mode mode{ proxy_mode::UNKNOWN };
74+
/**
75+
* A unique client-token to ensure only the agent which generated the token may connect to a tunnel
76+
*/
77+
std::string client_token;
7478
/**
7579
* local address to bind to for listening in source mode or a local socket address for destination mode,
7680
* defaults localhost.

src/TcpAdapterProxy.cpp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ namespace aws { namespace iot { namespace securedtunneling {
4242

4343
char const * const PROXY_MODE_QUERY_PARAM = "local-proxy-mode";
4444
char const * const ACCESS_TOKEN_HEADER = "access-token";
45+
char const * const CLIENT_TOKEN_HEADER = "client-token";
4546
char const * const SOURCE_PROXY_MODE = "source";
4647
char const * const DESTINATION_PROXY_MODE = "destination";
4748
char const * const LOCALHOST_IP = "127.0.0.1";
@@ -53,7 +54,6 @@ namespace aws { namespace iot { namespace securedtunneling {
5354
com::amazonaws::iot::securedtunneling::Message_Type_DATA,
5455
com::amazonaws::iot::securedtunneling::Message_Type_STREAM_RESET};
5556

56-
5757
std::string get_region_endpoint(std::string const &region, boost::property_tree::ptree const &settings)
5858
{
5959
boost::optional<std::string> endpoint_override = settings.get_optional<std::string>(
@@ -721,6 +721,10 @@ namespace aws { namespace iot { namespace securedtunneling {
721721
{
722722
request.set(boost::beast::http::field::sec_websocket_protocol, GET_SETTING(settings, WEB_SOCKET_SUBPROTOCOL));
723723
request.set(ACCESS_TOKEN_HEADER, tac.adapter_config.access_token.c_str());
724+
if(!tac.adapter_config.client_token.empty())
725+
{
726+
request.set(CLIENT_TOKEN_HEADER, tac.adapter_config.client_token.c_str());
727+
}
724728
request.set(boost::beast::http::field::user_agent, user_agent_string);
725729
BOOST_LOG_SEV(log, trace) << "Web socket ugprade request(*not entirely final):\n" << get_token_filtered_request(request);
726730
},

src/TcpAdapterProxy.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <boost/asio/ip/tcp.hpp>
2020
#include <boost/format.hpp>
2121
#include <boost/property_tree/ptree.hpp>
22+
#include <boost/uuid/uuid.hpp>
2223
#include "ProxySettings.h"
2324
#include "TcpConnection.h"
2425
#include "TcpServer.h"

src/main.cpp

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@
2020
#include <boost/log/utility/setup/console.hpp>
2121
#include <boost/log/utility/setup/common_attributes.hpp>
2222
#include <boost/log/expressions.hpp>
23+
#include <boost/uuid/uuid.hpp>
24+
25+
#include <boost/lexical_cast.hpp>
26+
#include <boost/regex.hpp>
2327

2428
#include "ProxySettings.h"
2529
#include "TcpAdapterProxy.h"
@@ -47,7 +51,8 @@ using aws::iot::securedtunneling::proxy_mode;
4751
using aws::iot::securedtunneling::get_region_endpoint;
4852
using aws::iot::securedtunneling::settings::apply_region_overrides;
4953

50-
char const * const TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_ACCESS_TOKEN";
54+
char const * const ACCESS_TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_ACCESS_TOKEN";
55+
char const * const CLIENT_TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_CLIENT_TOKEN";
5156
char const * const ENDPOINT_ENV_VARIABLE = "AWSIOT_TUNNEL_ENDPOINT";
5257
char const * const REGION_ENV_VARIABLE = "AWSIOT_TUNNEL_REGION";
5358
char const * const WEB_PROXY_ENV_VARIABLE = "HTTPS_PROXY";
@@ -143,6 +148,7 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings,
143148
cliargs_desc.add_options()
144149
("help,h", "Show help message")
145150
("access-token,t", value<string>()->required(), "Client access token")
151+
("client-token,i", value<string>(), "Optional Client Token")
146152
("proxy-endpoint,e", value<string>(), "Endpoint of proxy server with port (if not default 443). Example: data.tunneling.iot.us-east-1.amazonaws.com:443")
147153
("region,r", value<string>(), "Endpoint region where tunnel exists. Mutually exclusive flag with --proxy-endpoint")
148154
("source-listen-port,s", value<string>(), "Sets the mappings between source listening ports and service identifier. Example: SSH1=5555 or 5555")
@@ -186,8 +192,10 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings,
186192
store(parse_environment(cliargs_desc,
187193
[](std::string name) -> std::string
188194
{
189-
if (name == TOKEN_ENV_VARIABLE)
195+
if (name == ACCESS_TOKEN_ENV_VARIABLE)
190196
return "access-token";
197+
if (name == CLIENT_TOKEN_ENV_VARIABLE)
198+
return "client-token";
191199
if (name == ENDPOINT_ENV_VARIABLE)
192200
return "proxy-endpoint";
193201
if (name == REGION_ENV_VARIABLE)
@@ -212,10 +220,15 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings,
212220
notify(vm);
213221
if (token_cli_warning)
214222
{
215-
BOOST_LOG_TRIVIAL(warning) << "Found access token supplied via CLI arg. Consider using environment variable " << TOKEN_ENV_VARIABLE << " instead";
223+
BOOST_LOG_TRIVIAL(warning) << "Found access token supplied via CLI arg. Consider using environment variable " << ACCESS_TOKEN_ENV_VARIABLE << " instead";
216224
}
217225
cfg.access_token = vm["access-token"].as<string>();
218226

227+
if (vm.count("client-token") != 0)
228+
{
229+
cfg.client_token = vm["client-token"].as<string>();
230+
}
231+
219232
string proxy_endpoint = vm.count("proxy-endpoint") == 1 ? vm["proxy-endpoint"].as<string>() :
220233
get_region_endpoint(vm["region"].as<string>(), settings);
221234

test/AdapterTests.cpp

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,106 @@ TEST_CASE( "Test source mode", "[source]") {
278278
tcp_adapter_thread.join();
279279
}
280280

281+
TEST_CASE( "Test source mode with client token", "[source]") {
282+
using namespace com::amazonaws::iot::securedtunneling;
283+
/**
284+
* Test case set up
285+
* 1. Create tcp socket to acts as destination app.
286+
* 2. Create web socket server to act as secure tunneling service (cloud side).
287+
* 3. Configure adapter config used for the local proxy.
288+
*/
289+
boost::asio::io_context io_ctx{};
290+
tcp::socket client_socket{ io_ctx };
291+
292+
boost::system::error_code ec;
293+
ptree settings;
294+
apply_test_settings(settings);
295+
TestWebsocketServer ws_server(LOCALHOST, settings);
296+
tcp::endpoint ws_address{ws_server.get_endpoint()};
297+
std::cout << "Test server is listening on address: " << ws_address.address() << " and port: " << ws_address.port() << endl;
298+
299+
LocalproxyConfig adapter_cfg;
300+
apply_test_config(adapter_cfg, ws_address);
301+
adapter_cfg.mode = proxy_mode::SOURCE;
302+
adapter_cfg.bind_address = LOCALHOST;
303+
adapter_cfg.access_token = "foobar_token";
304+
adapter_cfg.client_token = "foobar-client-token";
305+
const std::string service_id= "ssh1";
306+
uint16_t adapter_chosen_port = get_available_port(io_ctx);
307+
adapter_cfg.serviceId_to_endpoint_map[service_id] = boost::lexical_cast<std::string>(adapter_chosen_port);
308+
309+
tcp_adapter_proxy proxy{ settings, adapter_cfg };
310+
311+
//start web socket server thread and tcp adapter threads
312+
thread ws_server_thread{[&ws_server]() { ws_server.run(); } };
313+
thread tcp_adapter_thread{[&proxy]() { proxy.run_proxy(); } };
314+
315+
// Verify web socket handshake request from local proxy
316+
this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS));
317+
CHECK( ws_server.get_handshake_request().method() == boost::beast::http::verb::get );
318+
CHECK( ws_server.get_handshake_request().target() == "/tunnel?local-proxy-mode=source" );
319+
CHECK( ws_server.get_handshake_request().base()["sec-websocket-protocol"] == "aws.iot.securetunneling-2.0" );
320+
CHECK( ws_server.get_handshake_request().base()["access-token"] == adapter_cfg.access_token );
321+
CHECK( ws_server.get_handshake_request().base()["client-token"] == adapter_cfg.client_token );
322+
323+
// Simulate cloud side sends control message Message_Type_SERVICE_IDS
324+
message ws_server_message{};
325+
ws_server_message.set_type(Message_Type_SERVICE_IDS);
326+
ws_server_message.add_availableserviceids(service_id);
327+
ws_server_message.set_ignorable(false);
328+
ws_server_message.clear_payload();
329+
330+
ws_server.deliver_message(ws_server_message);
331+
this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS));
332+
333+
// Simulate source app connects to source local proxy
334+
client_socket.connect( tcp::endpoint{boost::asio::ip::make_address(adapter_cfg.bind_address.get()), adapter_chosen_port} );
335+
336+
uint8_t read_buffer[READ_BUFFER_SIZE];
337+
338+
// Simulate sending data messages from source app
339+
for(int i = 0; i < 5; ++i)
340+
{
341+
string const test_string = (boost::format("test message: %1%") % i).str();
342+
client_socket.send(boost::asio::buffer(test_string));
343+
client_socket.read_some(boost::asio::buffer(reinterpret_cast<void *>(read_buffer), READ_BUFFER_SIZE));
344+
CHECK( string(reinterpret_cast<char *>(read_buffer)) == test_string );
345+
}
346+
347+
// Verify local proxy sends Message_Type_STREAM_RESET
348+
ws_server.expect_next_message(
349+
[](message const&msg)
350+
{
351+
return (msg.type() == com::amazonaws::iot::securedtunneling::Message_Type_STREAM_RESET) && msg.streamid() == 1;
352+
});
353+
client_socket.close();
354+
355+
this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS));
356+
357+
// Simulate source app connects to source local proxy
358+
client_socket.connect( tcp::endpoint{boost::asio::ip::make_address(adapter_cfg.bind_address.get()), adapter_chosen_port} );
359+
360+
// Simulate sending data messages from source app
361+
for(int i = 0; i < 5; ++i)
362+
{
363+
string const test_string = (boost::format("test message: %1%") % i).str();
364+
client_socket.send(boost::asio::buffer(test_string));
365+
client_socket.read_some(boost::asio::buffer(reinterpret_cast<void *>(read_buffer), READ_BUFFER_SIZE));
366+
CHECK( string(reinterpret_cast<char *>(read_buffer)) == test_string );
367+
}
368+
369+
//instruct websocket to close on client
370+
ws_server.close_client("test_closure", boost::beast::websocket::internal_error);
371+
//attempt a read on the client which should now see the socket EOF (peer closed) caused by adapter
372+
client_socket.read_some(boost::asio::buffer(reinterpret_cast<void *>(read_buffer), READ_BUFFER_SIZE), ec);
373+
CHECK( ec.value() == BOOST_EC_SOCKET_CLOSED );
374+
375+
client_socket.close();
376+
377+
ws_server_thread.join();
378+
tcp_adapter_thread.join();
379+
}
380+
281381

282382
TEST_CASE( "Test destination mode", "[destination]") {
283383
using namespace com::amazonaws::iot::securedtunneling;

0 commit comments

Comments
 (0)