boost::asio 是否原生支持通过 socks5 进行代理连接?

Is there a native support for proxy connection via socks5 for boost::asio?

我正在使用 boost::asio 来处理我的程序和远程服务器之间的网络通信。要与服务器建立连接,我执行以下操作顺序:

namespace ba = boost::asio;
boost::shared_ptr<ba::ssl::context> ssl_ctx;
boost::shared_ptr<boost::asio::io_context> ios;  // initialized
boost::shared_ptr<ba::ssl::stream<tcp::socket>> ssl_socket;

ssl_ctx.reset(new ba::ssl::context(boost::asio::ssl::context_base::method::sslv23));
ssl_ctx->set_verify_mode(ba::ssl::verify_peer);
ssl_ctx->add_certificate_authority(ba::buffer(certs.data(), certs.size()));

ssl_socket.reset(new ba::ssl::stream<tcp::socket>(*ios, *ssl_ctx));

auto& socket = ssl_socket->next_layer();

ba::ip::tcp::resolver::iterator ep_iter; // target server
std::future<tcp::resolver::iterator> conn_result = boost::asio::async_connect(socket, ep_iter, boost::asio::use_future);
auto status = conn_result.wait_for(std::chrono::seconds(wait_connection_timeout_sec));
if (status == std::future_status::timeout) {
    socket.cancel();
    throw std::runtime_error("wait on server connection is timed out");
}

conn_result.get(); // if the operation failed, then conn_result.get() will throw an error
if (!socket.is_open()) {
    throw std::runtime_error("can't open socket");
}

socket.set_option(tcp::no_delay(true));

ssl_socket->handshake(ba::ssl::stream<tcp::socket>::handshake_type::client);

此代码完美无缺。连接后,我使用标准方法 write/read 和 ssl_socket。

现在我需要使用 socks5 协议通过代理服务器与我的服务器建立连接。 在boost手册中没有找到解决方案。是否有原生的 socks5 代理支持?我应该在我的代码序列中添加什么,以便 ssl_socket 通过代理服务器连接到我的服务器?

我继续进行手指练习并实施 socks5.hpp 就像我之前对 socks4 所做的那样:socks4 with asynchronous boost::asio

事不宜迟:

tcp::resolver::query target("example.com", "443");

std::future<void> conn_result = socks5::async_proxy_connect(
    socket, target, tcp::endpoint{{}, 1080}, ba::use_future);

或者,当然,只是同步:

socks5::proxy_connect(socket, target, tcp::endpoint{{}, 1080});

现场演示

  • 文件socks5.hpp

     #include <boost/asio.hpp>
     #include <boost/endian/arithmetic.hpp>
    
     namespace socks5 { // threw in the kitchen sink for error codes
     #ifdef STANDALONE_ASIO
         using std::error_category;
         using std::error_code;
         using std::error_condition;
         using std::system_error;
     #else
         namespace asio = boost::asio;
         using boost::system::error_category;
         using boost::system::error_code;
         using boost::system::error_condition;
         using boost::system::system_error;
     #endif
    
         enum class result_code {
             ok                         = 0,
             invalid_version            = 1,
             disallowed                 = 2,
             auth_method_rejected       = 3,
             network_unreachable        = 4,
             host_unreachable           = 5,
             connection_refused         = 6,
             ttl_expired                = 7,
             command_not_supported      = 8,
             address_type_not_supported = 9,
             //
             failed = 99,
         };
    
         auto const& get_result_category() {
           struct impl : error_category {
             const char* name() const noexcept override { return "result_code"; }
             std::string message(int ev) const override {
               switch (static_cast<result_code>(ev)) {
               case result_code::ok:                         return "Success";
               case result_code::invalid_version:            return "SOCKS5 invalid reply version";
               case result_code::disallowed:                 return "SOCKS5 disallowed";
               case result_code::auth_method_rejected:       return "SOCKS5 no accepted authentication method";
               case result_code::network_unreachable:        return "SOCKS5 network unreachable";
               case result_code::host_unreachable:           return "SOCKS5 host unreachable";
               case result_code::connection_refused:         return "SOCKS5 connection refused";
               case result_code::ttl_expired:                return "SOCKS5 TTL expired";
               case result_code::command_not_supported:      return "SOCKS5 command not supported";
               case result_code::address_type_not_supported: return "SOCKS5 address type not supported";
               case result_code::failed:                     return "SOCKS5 general unexpected failure";
               default:                                      return "unknown error";
               }
             }
             error_condition
             default_error_condition(int ev) const noexcept override {
                 return error_condition{ev, *this};
             }
             bool equivalent(int ev, error_condition const& condition)
                 const noexcept override {
                 return condition.value() == ev && &condition.category() == this;
             }
             bool equivalent(error_code const& error,
                             int ev) const noexcept override {
                 return error.value() == ev && &error.category() == this;
             }
           } const static instance;
           return instance;
         }
    
         error_code make_error_code(result_code se) {
             return error_code{
                 static_cast<std::underlying_type<result_code>::type>(se),
                 get_result_category()};
         }
     } // namespace socks5
    
     template <>
     struct boost::system::is_error_code_enum<socks5::result_code>
         : std::true_type {};
    
     namespace socks5 {
         using namespace std::placeholders;
    
         template <typename Proto> struct core_t {
             using Endpoint = typename Proto::endpoint;
             using Query    = typename boost::asio::ip::basic_resolver<Proto>::query;
             Endpoint _proxy;
    
             core_t(Query const& target, Endpoint proxy)
                 : _proxy(proxy)
                 , _request(target)
             {
             }
             core_t(Endpoint const& target, Endpoint proxy)
                 : _proxy(proxy)
                 , _request(target)
             {
             }
    
     #pragma pack(push)
     #pragma pack(1)
             enum class addr_type : uint8_t { IPv4 = 0x01, Domain = 0x03, IPv6 = 0x04 };
             enum class auth_method : uint8_t {
                 none   = 0x00, // No authentication
                 gssapi = 0x01, // GSSAPI (RFC 1961
                 basic  = 0x02, // Username/password (RFC 1929)
                 // 0x03–0x7F methods assigned by IANA[11]
                 challenge_handshake = 0x03, // Challenge-Handshake Authentication Protocol
                 challenge_response = 0x05,  // Challenge-Response Authentication Method
                 ssl  = 0x06, // Secure Sockets Layer
                 nds  = 0x07, // NDS Authentication
                 maf  = 0x08, // Multi-Authentication Framework
                 json = 0x09, // JSON Parameter Block
             };
             enum class version : uint8_t {
                 none   = 0x00,
                 socks4 = 0x04,
                 socks5 = 0x05,
             };
             enum class proxy_command : uint8_t {
                 connect       = 0x01,
                 bind          = 0x02,
                 udp_associate = 0x03,
             };
             enum class proxy_reply : uint8_t {
                 succeeded                  = 0x00,
                 general_failure            = 0x01,
                 disallowed                 = 0x02,
                 network_unreachable        = 0x03,
                 host_unreachable           = 0x04,
                 connection_refused         = 0x05,
                 ttl_expired                = 0x06,
                 command_not_supported      = 0x07,
                 address_type_not_supported = 0x08,
             };
    
             using ipv4_octets = boost::asio::ip::address_v4::bytes_type;
             using ipv6_octets = boost::asio::ip::address_v6::bytes_type;
             using net_short   = boost::endian::big_uint16_t;
    
             struct {
                 version     ver       = version::socks5;
                 uint8_t     nmethods  = 0x01;
                 auth_method method[1] = {auth_method::none};
             } _greeting;
    
             struct {
                 version reply_version;
                 uint8_t cauth;
             } _greeting_response;
    
             struct wire_address {
                 addr_type type{};
                 union {
                     ipv4_octets              ipv4;
                     ipv6_octets              ipv6;
                     std::array<uint8_t, 256> domain{0}; // length prefixed
                 } payload;
    
                 size_t var_length() const {
                     return sizeof(type) + payload_length();
                 }
    
                 size_t payload_length() const
                 {
                     switch (type) {
                     case addr_type::IPv4: return sizeof(payload.ipv4);
                     case addr_type::IPv6: return sizeof(payload.ipv6);
                     case addr_type::Domain:
                         assert(payload.domain[0] < payload.domain.max_size());
                         return 1 + payload.domain[0];
                     }
                     return 0;
                 }
             };
    
             struct request_t {
                 version       ver      = version::socks5;
                 proxy_command cmd      = proxy_command::connect;
                 uint8_t       reserved = 0;
                 wire_address  var_address;
                 net_short     port;
    
                 // constructors
                 request_t(Endpoint const& ep) : port(ep.port())
                 {
                     auto& addr = ep.address();
                     if (addr.is_v4()) {
                         var_address.type         = addr_type::IPv4;
                         var_address.payload.ipv4 = addr.to_v4().to_bytes();
                     } else {
                         var_address.type         = addr_type::IPv6;
                         var_address.payload.ipv6 = addr.to_v6().to_bytes();
                     }
                 }
    
                 request_t(Query const& q) : port(std::stoi(q.service_name())) {
                     std::string const domain = q.host_name();
                     var_address.type         = addr_type::Domain;
    
                     auto len = std::min(var_address.payload.domain.max_size() - 1,
                                         domain.length());
                     assert(len == domain.length() || "domain truncated");
                     var_address.payload.domain[0] = len;
                     std::copy_n(domain.data(), len,
                                 var_address.payload.domain.data() + 1);
                 }
    
                 auto buffers() const {
                     return std::array {
                         boost::asio::buffer(this, offsetof(request_t, var_address)),
                         boost::asio::buffer(&var_address, var_address.var_length()),
                         boost::asio::buffer(&port, sizeof(port)),
                     };
                 }
             } _request;
    
             struct response_t {
                 version      reply_version;
                 proxy_reply  reply;
                 uint8_t      reserved = 0x0;
                 wire_address var_address {addr_type::IPv4};
                 net_short    port;
    
                 auto head_buffers() {
                     return std::array{
                         boost::asio::buffer(this, offsetof(response_t, var_address) + sizeof(addr_type)),
                     };
                 }
    
                 auto tail_buffers() { // depends on head_buffers being correctly received!
                     return std::array{
                         boost::asio::buffer(&var_address.payload, var_address.payload_length()),
                         boost::asio::buffer(&port, sizeof(port)),
                     };
                 }
             } _response;
     #pragma pack(pop)
    
             using const_buffer   = boost::asio::const_buffer;
             using mutable_buffer = boost::asio::mutable_buffer;
    
             auto greeting_buffers() const {
                 return boost::asio::buffer(&_greeting, sizeof(_greeting));
             }
    
             auto greeting_response_buffers() {
                 return boost::asio::buffer(&_greeting_response, sizeof(_greeting_response));
             }
    
             auto request_buffers() const { return _request.buffers(); }
             auto response_head_buffers() { return _response.head_buffers(); }
             auto response_tail_buffers() { return _response.tail_buffers(); }
    
             error_code get_greeting_result(error_code ec = {}) const {
                 if (ec)
                     return ec;
                 if (_greeting_response.reply_version != version::socks5)
                     return result_code::invalid_version;
    
                 if (_greeting_response.cauth != 0) {
                     return result_code::auth_method_rejected;
                 }
    
                 return result_code::ok;
             }
    
             error_code get_result(error_code ec = {}) const {
                 if (ec)
                     return ec;
                 if (_response.reply_version != version::socks5)
                     return result_code::invalid_version;
    
                 switch (_response.reply) {
                 case proxy_reply::succeeded:                  return result_code::ok;
                 case proxy_reply::disallowed:                 return result_code::disallowed;
                 case proxy_reply::network_unreachable:        return result_code::network_unreachable;
                 case proxy_reply::host_unreachable:           return result_code::host_unreachable;
                 case proxy_reply::connection_refused:         return result_code::connection_refused;
                 case proxy_reply::ttl_expired:                return result_code::ttl_expired;
                 case proxy_reply::command_not_supported:      return result_code::command_not_supported;
                 case proxy_reply::address_type_not_supported: return result_code::address_type_not_supported;
                 case proxy_reply::general_failure: break;
                 }
                 return result_code::failed;
             };
    
         };
    
         template <typename Socket, typename Completion>
         struct async_proxy_connect_op {
             using Proto         = typename Socket::protocol_type;
             using Endpoint      = typename Proto::endpoint;
             using executor_type = typename Socket::executor_type;
             auto get_executor() { return _socket.get_executor(); }
    
           private:
             core_t<Proto> _core;
             Socket&       _socket;
             Completion    _handler;
    
           public:
             template <typename EndpointOrQuery>
             async_proxy_connect_op(Completion handler, Socket& s,
                                    EndpointOrQuery target, Endpoint proxy)
                 : _core(target, proxy)
                 , _socket(s)
                 , _handler(std::move(handler))
             {
             }
    
             using Self = std::unique_ptr<async_proxy_connect_op>;
             void init(Self&& self) { operator()(self, INIT{}); }
    
           private:
             // states
             struct INIT{};
             struct CONNECT{};
             struct GREETING_SENT{};
             struct ONGREETING_RESPONSE{};
             struct REQUEST_SENT{};
             struct ON_RESPONSE_HEAD{};
             struct ON_RESPONSE_TAIL{};
    
             struct Binder {
                 Self _self;
                 template <typename... Args>
                 decltype(auto) operator()(Args&&... args) {
                     return (*_self)(_self, std::forward<Args>(args)...);
                 }
             };
    
             void operator()(Self& self, INIT) {
                 _socket.async_connect(_core._proxy,
                    std::bind(Binder{std::move(self)}, CONNECT{}, _1));
             }
    
             void operator()(Self& self, CONNECT, error_code ec) {
                 if (ec) return _handler(ec);
                 boost::asio::async_write(
                     _socket,
                     _core.greeting_buffers(),
                     std::bind(Binder{std::move(self)}, GREETING_SENT{}, _1, _2));
             }
    
             void operator()(Self& self, GREETING_SENT, error_code ec, size_t xfer) {
                 if (ec) return _handler(ec);
                 auto buf = _core.greeting_response_buffers();
                 boost::asio::async_read(
                     _socket, buf, boost::asio::transfer_exactly(buffer_size(buf)),
                     std::bind(Binder{std::move(self)}, ONGREETING_RESPONSE{}, _1, _2));
             }
    
             void operator()(Self& self, ONGREETING_RESPONSE, error_code ec, size_t xfer) {
                 ec = _core.get_greeting_result(ec);
                 if (ec) return _handler(ec);
    
                 boost::asio::async_write(
                     _socket, _core.request_buffers(),
                     std::bind(Binder{std::move(self)}, REQUEST_SENT{}, _1, _2));
             }
    
             void operator()(Self& self, REQUEST_SENT, error_code ec, size_t xfer) {
                 if (ec) return _handler(ec);
                 auto buf = _core.response_head_buffers();
                 boost::asio::async_read(
                     _socket, buf, boost::asio::transfer_exactly(buffer_size(buf)),
                     std::bind(Binder{std::move(self)}, ON_RESPONSE_HEAD{}, _1, _2));
             }
    
             void operator()(Self& self, ON_RESPONSE_HEAD, error_code ec, size_t xfer) {
                 if (ec) return _handler(ec);
                 auto buf = _core.response_tail_buffers();
                 boost::asio::async_read(
                     _socket, buf, boost::asio::transfer_exactly(buffer_size(buf)),
                     std::bind(Binder{std::move(self)}, ON_RESPONSE_TAIL{}, _1, _2));
             }
    
             void operator()(Self& self, ON_RESPONSE_TAIL, error_code ec, size_t xfer) {
                 _handler(_core.get_result(ec));
             }
         };
    
         template <typename Socket, typename EndpointOrQuery,
                   typename Endpoint = typename Socket::protocol_type::endpoint>
         error_code proxy_connect(Socket& s, EndpointOrQuery target, Endpoint proxy,
                                  error_code& ec)
         {
             core_t<typename Socket::protocol_type> core(target, proxy);
             ec.clear();
    
             s.connect(core._proxy, ec);
    
             if (!ec)
                 boost::asio::write(s, core.greeting_buffers(), ec);
    
             using boost::asio::transfer_exactly;
             if (!ec) {
                 auto buf = core.greeting_response_buffers();
                 boost::asio::read(s, buf, transfer_exactly(buffer_size(buf)), ec);
             }
             ec = core.get_greeting_result(ec);
    
             if (!ec) {
                 boost::asio::write(s, core.request_buffers(), ec);
             }
    
             if (!ec) {
                 auto buf = core.response_head_buffers();
                 boost::asio::read(s, buf, transfer_exactly(buffer_size(buf)), ec);
             }
             if (!ec) {
                 auto buf = core.response_tail_buffers();
                 boost::asio::read(s, buf, transfer_exactly(buffer_size(buf)), ec);
             }
    
             return ec = core.get_result(ec);
         }
    
         template <typename Socket, typename EndpointOrQuery,
                   typename Endpoint = typename Socket::protocol_type::endpoint>
         void proxy_connect(Socket& s, EndpointOrQuery target, Endpoint proxy)
         {
             error_code ec;
             if (proxy_connect(s, target, proxy, ec))
                 throw system_error(ec);
         }
    
         template <typename Socket, typename Token, typename EndpointOrQuery,
                   typename Endpoint = typename Socket::protocol_type::endpoint>
         auto async_proxy_connect(Socket& s, EndpointOrQuery target, Endpoint proxy,
                                  Token&& token)
         {
             using Result = asio::async_result<std::decay_t<Token>, void(error_code)>;
             using Completion = typename Result::completion_handler_type;
    
             Completion completion(std::forward<Token>(token));
             Result     result(completion);
    
             using Op = async_proxy_connect_op<Socket, Completion>;
             // make an owning self ptr, to serve a unique async chain
             auto self = std::make_unique<Op>(completion, s, target, proxy);
             self->init(std::move(self));
             return result.get();
         }
     } // namespace socks5
    
  • 文件test.cpp

     #include "socks5.hpp"
     ///////////
     #include <boost/asio/ssl.hpp>
     #include <boost/beast.hpp>
     #include <boost/beast/http.hpp>
     #include <boost/make_shared.hpp>
     #include <iostream>
    
     namespace ba   = boost::asio;
     namespace http = boost::beast::http;
     namespace ssl  = boost::asio::ssl;
     using namespace std::chrono_literals;
     using ba::ip::tcp;
    
     static constexpr auto connection_timeout = 1s;
    
     int main()
     {
         auto ios = boost::make_shared<ba::io_context>();
    
         auto ssl_ctx =
             boost::make_shared<ssl::context>(ssl::context_base::method::sslv23);
         ssl_ctx->set_verify_mode(ssl::verify_peer);
         //ssl_ctx->add_certificate_authority(ba::buffer(certs.data(), certs.size()));
         ssl_ctx->set_default_verify_paths(); // FOR DEMO
    
         auto ssl_socket =
             boost::make_shared<ssl::stream<tcp::socket>>(*ios, *ssl_ctx);
    
         auto& socket = ssl_socket->next_layer();
    
         tcp::resolver::query target("example.com", "443");
    
     #if 1
         std::future<void> conn_result = socks5::async_proxy_connect(
             socket, target, tcp::endpoint{{}, 1080}, ba::use_future);
    
         std::thread th([ios] { ios->run(); });
    
         if (conn_result.wait_for(connection_timeout) ==
                 std::future_status::timeout) {
             socket.cancel();
             // no need to throw, `conn_result.get()` will give operation_aborted
         }
    
         conn_result.get(); // may throw error
     #else // synchronously as well:
         socks5::proxy_connect(socket, target, tcp::endpoint{{}, 1080});
     #endif
    
         socket.set_option(tcp::no_delay(true));
    
         ssl_socket->handshake(ssl::stream_base::handshake_type::client);
    
         {
             http::request<http::empty_body> req(http::verb::get, "/", 11);
             req.set(http::field::host, "example.com");
             req.prepare_payload();
    
             http::write(*ssl_socket, req);
         }
         {
             http::response<http::string_body> res;
             boost::beast::flat_buffer buf;
             http::read(*ssl_socket, buf, res);
    
             std::cout << res;
         }
    
         th.join();
     }
    

我的系统使用 ssh SOCKS 服务器测试正确: