#pragma once #include #include #include #include #include #include #include #include namespace socks5 { struct SocksErrCategory : boost::system::error_category { char const* name() const noexcept override; std::string message(int) const override; }; extern SocksErrCategory const theSocksErrCategory; enum class SocksErrc { // Errors from the server Succeeded = 0, GeneralFailure = 1, NotAllowed = 2, NetworkUnreachable = 3, HostUnreachable = 4, ConnectionRefused = 5, TtlExpired = 6, CommandNotSupported = 7, AddressNotSupported = 8, // Errors from the client WrongVersion = 256, NoAcceptableMethods, AuthenticationFailed, UnsupportedEndpointAddress, DomainTooLong, UsernameTooLong, PasswordTooLong, }; /// Either a hostname or an address. Hostnames are resolved locally on the proxy server using Host = std::variant; struct NoCredential { }; struct UsernamePasswordCredential { std::string_view username; std::string_view password; }; using Auth = std::variant; namespace detail { template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; auto make_socks_error(SocksErrc const err) -> boost::system::error_code; inline auto push_buffer(std::vector& buffer, auto const& thing) -> void { buffer.push_back(thing.size()); buffer.insert(buffer.end(), thing.begin(), thing.end()); } uint8_t const socks_version_tag = 5; uint8_t const auth_version_tag = 1; enum class AuthMethod { NoAuth = 0, Gssapi = 1, UsernamePassword = 2, NoAcceptableMethods = 255, }; enum class Command { Connect = 1, Bind = 2, UdpAssociate = 3, }; enum class AddressType { IPv4 = 1, DomainName = 3, IPv6 = 4, }; /// @brief Encode the given host into the end of the buffer /// @param host host to encode /// @param buffer target to push bytes onto /// @return true for success and false for failure auto push_host(Host const& host, std::vector& buffer) -> void; template struct SocksImplementation { AsyncStream& socket_; Host const host_; boost::endian::big_uint16_t const port_; Auth const auth_; /// buffer used to back async read/write operations std::vector buffer_; // Representations of states in the protocol struct Start { }; struct HelloRecvd { static const std::size_t READ = 2; // version, method }; struct AuthRecvd { static const std::size_t READ = 2; // subversion, status }; struct ReplyRecvd { static const std::size_t READ = 4; // version, reply, reserved, address-tag }; struct FinishIpv4 { static const std::size_t READ = 6; // ipv4 + port = 6 bytes }; struct FinishIpv6 { static const std::size_t READ = 18; // ipv6 + port = 18 bytes }; /// @brief State when the application needs to receive some bytes /// @tparam Next State to transistion to after read is successful template struct Sent { }; /// @brief intermediate completion callback /// @tparam Self type of enclosing intermediate completion handler /// @tparam State protocol state tag type /// @param self enclosing intermediate completion handler /// @param state protocol state tag value /// @param error error code of read/write operation /// @param size bytes read or written /// @param template auto operator()( Self& self, State state = {}, boost::system::error_code const error = {}, std::size_t = 0 ) -> void { if (error) { self.complete(error, {}); } else { step(self, state); } } /// @brief Write the buffer to the socket and then read N bytes back into the buffer /// @tparam Next state to resume after read /// @tparam N number of bytes to read /// @tparam Self type of enclosing intermediate completion handler /// @param self enclosing intermediate completion handler template auto transact(Self& self) -> void { boost::asio::async_write( socket_, boost::asio::buffer(buffer_), [self = std::move(self)](boost::system::error_code const err, std::size_t const n) mutable { self(Sent{}, err, n); } ); } /// @brief Notify the caller of a failure and terminate the protocol /// @param self intermediate completion handler /// @param err error code to return to the caller auto failure(auto& self, SocksErrc const err) -> void { self.complete(make_socks_error(err), {}); } /// @brief Read bytes needed by Next state from the socket and then proceed to Next state /// @tparam Self type of enclosing intermediate completion handler /// @tparam Next state to transition to after read /// @param self enclosing intermediate completion handler /// @param state protocol state tag template auto step(Self& self, Sent) -> void { buffer_.resize(Next::READ); boost::asio::async_read( socket_, boost::asio::buffer(buffer_), [self = std::move(self)](boost::system::error_code const err, std::size_t n) mutable { self(Next{}, err, n); } ); } // Send hello and offer authentication methods template auto step(Self& self, Start) -> void { if (auto const* const host = std::get_if(&host_)) { if (host->size() >= 256) { return failure(self, SocksErrc::DomainTooLong); } } if (auto const* const plain = std::get_if(&auth_)) { if (plain->username.size() >= 256) { return failure(self, SocksErrc::UsernameTooLong); } if (plain->password.size() >= 256) { return failure(self, SocksErrc::PasswordTooLong); } } buffer_ = {socks_version_tag, 1 /* number of methods */, static_cast(method_wanted())}; transact(self); } // Send TCP connection request for the domain name and port template auto step(Self& self, HelloRecvd) -> void { if (socks_version_tag != buffer_[0]) { return failure(self, SocksErrc::WrongVersion); } auto const wanted = method_wanted(); auto const selected = static_cast(buffer_[1]); if (AuthMethod::NoAuth == wanted && wanted == selected) { send_connect(self); } else if (AuthMethod::UsernamePassword == wanted && wanted == selected) { send_usernamepassword(self); } else { failure(self, SocksErrc::NoAcceptableMethods); } } /// @brief Transmit the username and password to the server /// @tparam Self type of enclosing intermediate completion handler /// @param self enclosing intermediate completion handler template auto send_usernamepassword(Self& self) -> void { buffer_ = { auth_version_tag, }; auto const [username, password] = std::get<1>(auth_); push_buffer(buffer_, username); push_buffer(buffer_, password); transact(self); } template auto step(Self& self, AuthRecvd) -> void { if (auth_version_tag != buffer_[0]) { return failure(self, SocksErrc::WrongVersion); } // STATUS zero is success, non-zero is failure if (0 != buffer_[1]) { return failure(self, SocksErrc::AuthenticationFailed); } send_connect(self); } template auto send_connect(Self& self) -> void { buffer_ = { socks_version_tag, static_cast(Command::Connect), 0 /* reserved */, }; push_host(host_, buffer_); buffer_.insert(buffer_.end(), port_.data(), port_.data() + 2); transact(self); } // Waiting on the remaining variable-sized address portion of the response template auto step(Self& self, ReplyRecvd) -> void { if (socks_version_tag != buffer_[0]) { return failure(self, SocksErrc::WrongVersion); } auto const reply = static_cast(buffer_[1]); if (SocksErrc::Succeeded != reply) { return failure(self, reply); } switch (static_cast(buffer_[3])) { case AddressType::IPv4: return step(self, Sent{}); case AddressType::IPv6: return step(self, Sent{}); default: return failure(self, SocksErrc::UnsupportedEndpointAddress); } } // Protocol complete! Return the client's remote endpoint template void step(Self& self, FinishIpv4) { boost::asio::ip::address_v4::bytes_type bytes; boost::endian::big_uint16_t port; std::memcpy(bytes.data(), &buffer_[0], 4); std::memcpy(port.data(), &buffer_[4], 2); self.complete({}, {boost::asio::ip::make_address_v4(bytes), port}); } // Protocol complete! Return the client's remote endpoint template void step(Self& self, FinishIpv6) { boost::asio::ip::address_v6::bytes_type bytes; boost::endian::big_uint16_t port; std::memcpy(bytes.data(), &buffer_[0], 16); std::memcpy(port.data(), &buffer_[16], 2); self.complete({}, {boost::asio::ip::make_address_v6(bytes), port}); } auto method_wanted() const -> AuthMethod { return std::visit( overloaded{ [](NoCredential) { return AuthMethod::NoAuth; }, [](UsernamePasswordCredential) { return AuthMethod::UsernamePassword; }, }, auth_ ); } }; } // namespace detail using Signature = void(boost::system::error_code, boost::asio::ip::tcp::endpoint); /// @brief Asynchronous SOCKS5 connection request /// @tparam AsyncStream Type of socket /// @tparam CompletionToken Token accepting: error_code, address, port /// @param socket Established connection to SOCKS5 server /// @param host Connection target host /// @param port Connection target port /// @param token Completion token /// @return Behavior determined by completion token type template < typename AsyncStream, boost::asio::completion_token_for CompletionToken> auto async_connect( AsyncStream& socket, Host const host, uint16_t const port, Auth const auth, CompletionToken&& token ) { return boost::asio::async_compose(detail::SocksImplementation{socket, host, port, auth, {}}, token, socket); } } // namespace socks5