Skip to content

Commit

Permalink
Add option for client-token header (#92)
Browse files Browse the repository at this point in the history
* Add option for client-token header
  • Loading branch information
shangabl authored Aug 12, 2022
1 parent 48451ea commit 607bd3a
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 7 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,12 +304,22 @@ After preparing this directory, point to it when running the local proxy with th
* Consider running the local proxy on separate hosts, containers, sandboxes, chroot jail, or a virtualized environment

#### Access tokens

* Access tokens are not meant to be re-used.
* After localproxy uses an access token, it will no longer be valid.
* After localproxy uses an access token, it will no longer be valid without an accompanying Client Token.
* 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).
* 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.

#### Client Tokens
* 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.
* Only one client token value may be present in the request. Supplying multiple values will cause the handshake to fail.
* The client token is optional.
* The client token must be unique across all the open tunnels per AWS account
* It's recommended to use a UUID to generate the client token.
* The client token can be any string that matches the regex `^[a-zA-Z0-9-]{32,128}$`
* 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)
* 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.
* The Client Token may be passed using the **-i** argument from the command line or setting the **AWSIOT_TUNNEL_CLIENT_TOKEN** environment variable.


### IPv6 support

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.
Expand Down
4 changes: 4 additions & 0 deletions src/LocalproxyConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ namespace aws {
*/
std::string access_token { };
proxy_mode mode{ proxy_mode::UNKNOWN };
/**
* A unique client-token to ensure only the agent which generated the token may connect to a tunnel
*/
std::string client_token;
/**
* local address to bind to for listening in source mode or a local socket address for destination mode,
* defaults localhost.
Expand Down
6 changes: 5 additions & 1 deletion src/TcpAdapterProxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ namespace aws { namespace iot { namespace securedtunneling {

char const * const PROXY_MODE_QUERY_PARAM = "local-proxy-mode";
char const * const ACCESS_TOKEN_HEADER = "access-token";
char const * const CLIENT_TOKEN_HEADER = "client-token";
char const * const SOURCE_PROXY_MODE = "source";
char const * const DESTINATION_PROXY_MODE = "destination";
char const * const LOCALHOST_IP = "127.0.0.1";
Expand All @@ -53,7 +54,6 @@ namespace aws { namespace iot { namespace securedtunneling {
com::amazonaws::iot::securedtunneling::Message_Type_DATA,
com::amazonaws::iot::securedtunneling::Message_Type_STREAM_RESET};


std::string get_region_endpoint(std::string const &region, boost::property_tree::ptree const &settings)
{
boost::optional<std::string> endpoint_override = settings.get_optional<std::string>(
Expand Down Expand Up @@ -721,6 +721,10 @@ namespace aws { namespace iot { namespace securedtunneling {
{
request.set(boost::beast::http::field::sec_websocket_protocol, GET_SETTING(settings, WEB_SOCKET_SUBPROTOCOL));
request.set(ACCESS_TOKEN_HEADER, tac.adapter_config.access_token.c_str());
if(!tac.adapter_config.client_token.empty())
{
request.set(CLIENT_TOKEN_HEADER, tac.adapter_config.client_token.c_str());
}
request.set(boost::beast::http::field::user_agent, user_agent_string);
BOOST_LOG_SEV(log, trace) << "Web socket ugprade request(*not entirely final):\n" << get_token_filtered_request(request);
},
Expand Down
1 change: 1 addition & 0 deletions src/TcpAdapterProxy.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <boost/asio/ip/tcp.hpp>
#include <boost/format.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/uuid/uuid.hpp>
#include "ProxySettings.h"
#include "TcpConnection.h"
#include "TcpServer.h"
Expand Down
19 changes: 16 additions & 3 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
#include <boost/log/utility/setup/console.hpp>
#include <boost/log/utility/setup/common_attributes.hpp>
#include <boost/log/expressions.hpp>
#include <boost/uuid/uuid.hpp>

#include <boost/lexical_cast.hpp>
#include <boost/regex.hpp>

#include "ProxySettings.h"
#include "TcpAdapterProxy.h"
Expand Down Expand Up @@ -47,7 +51,8 @@ using aws::iot::securedtunneling::proxy_mode;
using aws::iot::securedtunneling::get_region_endpoint;
using aws::iot::securedtunneling::settings::apply_region_overrides;

char const * const TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_ACCESS_TOKEN";
char const * const ACCESS_TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_ACCESS_TOKEN";
char const * const CLIENT_TOKEN_ENV_VARIABLE = "AWSIOT_TUNNEL_CLIENT_TOKEN";
char const * const ENDPOINT_ENV_VARIABLE = "AWSIOT_TUNNEL_ENDPOINT";
char const * const REGION_ENV_VARIABLE = "AWSIOT_TUNNEL_REGION";
char const * const WEB_PROXY_ENV_VARIABLE = "HTTPS_PROXY";
Expand Down Expand Up @@ -143,6 +148,7 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings,
cliargs_desc.add_options()
("help,h", "Show help message")
("access-token,t", value<string>()->required(), "Client access token")
("client-token,i", value<string>(), "Optional Client Token")
("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")
("region,r", value<string>(), "Endpoint region where tunnel exists. Mutually exclusive flag with --proxy-endpoint")
("source-listen-port,s", value<string>(), "Sets the mappings between source listening ports and service identifier. Example: SSH1=5555 or 5555")
Expand Down Expand Up @@ -186,8 +192,10 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings,
store(parse_environment(cliargs_desc,
[](std::string name) -> std::string
{
if (name == TOKEN_ENV_VARIABLE)
if (name == ACCESS_TOKEN_ENV_VARIABLE)
return "access-token";
if (name == CLIENT_TOKEN_ENV_VARIABLE)
return "client-token";
if (name == ENDPOINT_ENV_VARIABLE)
return "proxy-endpoint";
if (name == REGION_ENV_VARIABLE)
Expand All @@ -212,10 +220,15 @@ bool process_cli(int argc, char ** argv, LocalproxyConfig &cfg, ptree &settings,
notify(vm);
if (token_cli_warning)
{
BOOST_LOG_TRIVIAL(warning) << "Found access token supplied via CLI arg. Consider using environment variable " << TOKEN_ENV_VARIABLE << " instead";
BOOST_LOG_TRIVIAL(warning) << "Found access token supplied via CLI arg. Consider using environment variable " << ACCESS_TOKEN_ENV_VARIABLE << " instead";
}
cfg.access_token = vm["access-token"].as<string>();

if (vm.count("client-token") != 0)
{
cfg.client_token = vm["client-token"].as<string>();
}

string proxy_endpoint = vm.count("proxy-endpoint") == 1 ? vm["proxy-endpoint"].as<string>() :
get_region_endpoint(vm["region"].as<string>(), settings);

Expand Down
100 changes: 100 additions & 0 deletions test/AdapterTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,106 @@ TEST_CASE( "Test source mode", "[source]") {
tcp_adapter_thread.join();
}

TEST_CASE( "Test source mode with client token", "[source]") {
using namespace com::amazonaws::iot::securedtunneling;
/**
* Test case set up
* 1. Create tcp socket to acts as destination app.
* 2. Create web socket server to act as secure tunneling service (cloud side).
* 3. Configure adapter config used for the local proxy.
*/
boost::asio::io_context io_ctx{};
tcp::socket client_socket{ io_ctx };

boost::system::error_code ec;
ptree settings;
apply_test_settings(settings);
TestWebsocketServer ws_server(LOCALHOST, settings);
tcp::endpoint ws_address{ws_server.get_endpoint()};
std::cout << "Test server is listening on address: " << ws_address.address() << " and port: " << ws_address.port() << endl;

LocalproxyConfig adapter_cfg;
apply_test_config(adapter_cfg, ws_address);
adapter_cfg.mode = proxy_mode::SOURCE;
adapter_cfg.bind_address = LOCALHOST;
adapter_cfg.access_token = "foobar_token";
adapter_cfg.client_token = "foobar-client-token";
const std::string service_id= "ssh1";
uint16_t adapter_chosen_port = get_available_port(io_ctx);
adapter_cfg.serviceId_to_endpoint_map[service_id] = boost::lexical_cast<std::string>(adapter_chosen_port);

tcp_adapter_proxy proxy{ settings, adapter_cfg };

//start web socket server thread and tcp adapter threads
thread ws_server_thread{[&ws_server]() { ws_server.run(); } };
thread tcp_adapter_thread{[&proxy]() { proxy.run_proxy(); } };

// Verify web socket handshake request from local proxy
this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS));
CHECK( ws_server.get_handshake_request().method() == boost::beast::http::verb::get );
CHECK( ws_server.get_handshake_request().target() == "/tunnel?local-proxy-mode=source" );
CHECK( ws_server.get_handshake_request().base()["sec-websocket-protocol"] == "aws.iot.securetunneling-2.0" );
CHECK( ws_server.get_handshake_request().base()["access-token"] == adapter_cfg.access_token );
CHECK( ws_server.get_handshake_request().base()["client-token"] == adapter_cfg.client_token );

// Simulate cloud side sends control message Message_Type_SERVICE_IDS
message ws_server_message{};
ws_server_message.set_type(Message_Type_SERVICE_IDS);
ws_server_message.add_availableserviceids(service_id);
ws_server_message.set_ignorable(false);
ws_server_message.clear_payload();

ws_server.deliver_message(ws_server_message);
this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS));

// Simulate source app connects to source local proxy
client_socket.connect( tcp::endpoint{boost::asio::ip::make_address(adapter_cfg.bind_address.get()), adapter_chosen_port} );

uint8_t read_buffer[READ_BUFFER_SIZE];

// Simulate sending data messages from source app
for(int i = 0; i < 5; ++i)
{
string const test_string = (boost::format("test message: %1%") % i).str();
client_socket.send(boost::asio::buffer(test_string));
client_socket.read_some(boost::asio::buffer(reinterpret_cast<void *>(read_buffer), READ_BUFFER_SIZE));
CHECK( string(reinterpret_cast<char *>(read_buffer)) == test_string );
}

// Verify local proxy sends Message_Type_STREAM_RESET
ws_server.expect_next_message(
[](message const&msg)
{
return (msg.type() == com::amazonaws::iot::securedtunneling::Message_Type_STREAM_RESET) && msg.streamid() == 1;
});
client_socket.close();

this_thread::sleep_for(chrono::milliseconds(IO_PAUSE_MS));

// Simulate source app connects to source local proxy
client_socket.connect( tcp::endpoint{boost::asio::ip::make_address(adapter_cfg.bind_address.get()), adapter_chosen_port} );

// Simulate sending data messages from source app
for(int i = 0; i < 5; ++i)
{
string const test_string = (boost::format("test message: %1%") % i).str();
client_socket.send(boost::asio::buffer(test_string));
client_socket.read_some(boost::asio::buffer(reinterpret_cast<void *>(read_buffer), READ_BUFFER_SIZE));
CHECK( string(reinterpret_cast<char *>(read_buffer)) == test_string );
}

//instruct websocket to close on client
ws_server.close_client("test_closure", boost::beast::websocket::internal_error);
//attempt a read on the client which should now see the socket EOF (peer closed) caused by adapter
client_socket.read_some(boost::asio::buffer(reinterpret_cast<void *>(read_buffer), READ_BUFFER_SIZE), ec);
CHECK( ec.value() == BOOST_EC_SOCKET_CLOSED );

client_socket.close();

ws_server_thread.join();
tcp_adapter_thread.join();
}


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

0 comments on commit 607bd3a

Please sign in to comment.