├── .github └── workflows │ ├── ci.yml │ ├── proxy.cfg │ └── proxy_auth.cfg ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE.md ├── LICENSE-MIT.md ├── README.md ├── rustfmt.toml └── src └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions-rs/toolchain@v1 11 | with: 12 | profile: minimal 13 | toolchain: stable 14 | override: true 15 | - uses: actions-rs/cargo@v1 16 | with: 17 | command: generate-lockfile 18 | - name: Cache cargo registry 19 | uses: actions/cache@v1 20 | with: 21 | path: ~/.cargo/registry 22 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 23 | restore-keys: | 24 | ${{ runner.os }}-cargo-registry- 25 | - name: Cache cargo index 26 | uses: actions/cache@v1 27 | with: 28 | path: ~/.cargo/git 29 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-cargo-index- 32 | - name: Cache cargo build 33 | uses: actions/cache@v1 34 | with: 35 | path: target 36 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 37 | restore-keys: | 38 | ${{ runner.os }}-cargo-build-target- 39 | - uses: actions-rs/cargo@v1 40 | with: 41 | command: build 42 | args: --tests 43 | - name: Download 3proxy 44 | run: | 45 | curl https://api.github.com/repos/3proxy/3proxy/releases/latest | 46 | grep -wo "https.*x86_64\.deb" | 47 | xargs curl -L -o 3proxy.deb 48 | - run: sudo dpkg -i 3proxy.deb 49 | - run: sudo apt install -f 50 | - run: nohup cat .github/workflows/proxy.cfg | 3proxy & 51 | - run: nohup cat .github/workflows/proxy_auth.cfg | 3proxy & 52 | - run: RUST_BACKTRACE=1 timeout 10 cargo test --all --all-features -- --test-threads=1 53 | rustfmt: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v1 57 | - name: Install toolchain 58 | uses: actions-rs/toolchain@v1 59 | with: 60 | profile: minimal 61 | toolchain: nightly 62 | override: true 63 | components: rustfmt 64 | - name: cargo fmt 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: fmt 68 | args: --all -- --check 69 | clippy: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v1 73 | - run: rustup component add clippy 74 | - uses: actions-rs/clippy-check@v1 75 | with: 76 | token: ${{ secrets.GITHUB_TOKEN }} 77 | args: --all-targets --all-features -- -D warnings 78 | -------------------------------------------------------------------------------- /.github/workflows/proxy.cfg: -------------------------------------------------------------------------------- 1 | log proxy.log 2 | internal 127.0.0.1 3 | users hyper:CL:proxy 4 | auth none 5 | socks -p1080 6 | -------------------------------------------------------------------------------- /.github/workflows/proxy_auth.cfg: -------------------------------------------------------------------------------- 1 | log proxy_auth.log 2 | internal 127.0.0.1 3 | users hyper:CL:proxy 4 | auth strong 5 | socks -p1081 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.6.0 (2024-02-26) 2 | 3 | Set MSRV to 1.75.0 and remove `async-trait` dependency 4 | 5 | # v0.5.1 (2022-03-03) 6 | 7 | Fix processing of `DOMAINNAME` address type 8 | 9 | # v0.5.0 (2020-12-26) 10 | 11 | Update to tokio 1.0 12 | 13 | # v0.4.0 (2020-10-16) 14 | 15 | Update to tokio 0.3 16 | `send_to` and `recv_from` use `&self` now, so you can use something like `Arc` to make send and receive halves 17 | 18 | # v0.3.2 (2020-08-04) 19 | 20 | Add split API for `SocksDatagram` 21 | 22 | # v0.3.1 (2020-03-18) 23 | 24 | Fix `thiserror` crate incorrectly displaying foreign errors 25 | 26 | # v0.3.0 (2020-01-05) 27 | 28 | Now you can use anything that implement `AsyncRead` and `AsyncWrite` 29 | 30 | # v0.2.1 (2020-01-04) 31 | 32 | Fix futures have no `Send` trait 33 | 34 | # v0.2.0 (2019-12-31) 35 | 36 | * Rename `TargetAddr` to `AddrKind` 37 | * Implement `From<...>` for `AddrKind` and consume `Into` in method arguments 38 | * Update `Error` 39 | * `SocksDatagram::associate` now consume `TcpStream` instead of `ToSocketAddrs` and can access association address 40 | * Add `new` method for `Auth` 41 | 42 | # v0.1.1 (2019-12-25) 43 | 44 | * Increase inner buffer size when receiving UDP datagram 45 | * Fix crate name in documentation 46 | * Set minimum versions of dependencies 47 | 48 | # v0.1.0 (2019-12-21) 49 | 50 | Initial commit 51 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-socks5" 3 | version = "0.6.0" 4 | authors = ["Arsenii Lyashenko ", "Temirkhan Myrzamadi "] 5 | license = "Apache-2.0 OR MIT" 6 | description = "An async/.await SOCKS5 implementation" 7 | repository = "https://github.com/ark0f/async-socks5" 8 | documentation = "https://docs.rs/async-socks5" 9 | readme = "README.md" 10 | keywords = ["tokio", "async", "socks", "proxy"] 11 | categories = ["asynchronous", "authentication", "network-programming"] 12 | include = ["Cargo.toml", "LICENSE-*.md", "src/**/*"] 13 | edition = "2021" 14 | 15 | [badges] 16 | github-actions = { repository = "https://github.com/ark0f/async-socks5", workflow = "CI" } 17 | 18 | [dependencies] 19 | tokio = { version = "1.0", features = ["net", "io-util"] } 20 | thiserror = "1.0.0" 21 | 22 | [dev-dependencies] 23 | tokio = { version = "1.0", features = ["net", "io-util", "rt", "macros"] } 24 | -------------------------------------------------------------------------------- /LICENSE-APACHE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Arsenii "ark0f" Lyashenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

async-socks5

3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | An `async`/`.await` [SOCKS5](https://tools.ietf.org/html/rfc1928) implementation. 21 |
22 | 23 | ## Examples 24 | Connect to `google.com:80` through `my-proxy-server.com:54321`: 25 | 26 | ```rust 27 | use tokio::net::TcpStream; 28 | use tokio::io::BufStream; 29 | use async_socks5::{connect, Result}; 30 | 31 | #[tokio::main] 32 | async fn main() -> Result<()> { 33 | let stream = TcpStream::connect("my-proxy-server.com:54321").await?; 34 | let mut stream = BufStream::new(stream); 35 | connect(&mut stream, ("google.com", 80), None).await?; 36 | } 37 | ``` 38 | 39 | [More examples](https://docs.rs/async-socks5). 40 | 41 | # [Changelog](https://github.com/ark0f/async-socks5/blob/master/CHANGELOG.md) 42 | 43 | # License 44 | async-socks5 under either of: 45 | 46 | * [Apache License 2.0](https://github.com/ark0f/async-socks5/blob/master/LICENSE-APACHE.md) 47 | * [MIT](https://github.com/ark0f/async-socks5/blob/master/LICENSE-MIT.md) 48 | 49 | at your option. 50 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_code_in_doc_comments = true 2 | wrap_comments = true 3 | format_strings = true 4 | imports_granularity = "Crate" 5 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An `async`/`.await` [SOCKS5] implementation. 2 | //! 3 | //! [SOCKS5]: https://tools.ietf.org/html/rfc1928 4 | 5 | #![deny(missing_debug_implementations)] 6 | 7 | use std::{ 8 | fmt::Debug, 9 | io::Cursor, 10 | net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, SocketAddrV4, SocketAddrV6}, 11 | string::FromUtf8Error, 12 | }; 13 | use tokio::{ 14 | io, 15 | io::{AsyncReadExt, AsyncWriteExt}, 16 | net::UdpSocket, 17 | }; 18 | 19 | // Error and Result 20 | // ***************************************************************************** 21 | 22 | /// The library's error type. 23 | #[derive(Debug, thiserror::Error)] 24 | pub enum Error { 25 | #[error("{0}")] 26 | Io( 27 | #[from] 28 | #[source] 29 | io::Error, 30 | ), 31 | #[error("{0}")] 32 | FromUtf8( 33 | #[from] 34 | #[source] 35 | FromUtf8Error, 36 | ), 37 | #[error("Invalid SOCKS version: {0:x}")] 38 | InvalidVersion(u8), 39 | #[error("Invalid command: {0:x}")] 40 | InvalidCommand(u8), 41 | #[error("Invalid address type: {0:x}")] 42 | InvalidAtyp(u8), 43 | #[error("Invalid reserved bytes: {0:x}")] 44 | InvalidReserved(u8), 45 | #[error("Invalid authentication status: {0:x}")] 46 | InvalidAuthStatus(u8), 47 | #[error("Invalid authentication version of subnegotiation: {0:x}")] 48 | InvalidAuthSubnegotiation(u8), 49 | #[error("Invalid fragment id: {0:x}")] 50 | InvalidFragmentId(u8), 51 | #[error("Invalid authentication method: {0:?}")] 52 | InvalidAuthMethod(AuthMethod), 53 | #[error("SOCKS version is 4 when 5 is expected")] 54 | WrongVersion, 55 | #[error("No acceptable methods")] 56 | NoAcceptableMethods, 57 | #[error("Unsuccessful reply: {0:?}")] 58 | Response(UnsuccessfulReply), 59 | #[error("{0:?} length is more than 255 bytes")] 60 | TooLongString(StringKind), 61 | } 62 | 63 | /// Required to mark which string is too long. 64 | /// See [`Error::TooLongString`]. 65 | /// 66 | /// [`Error::TooLongString`]: enum.Error.html#variant.TooLongString 67 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] 68 | pub enum StringKind { 69 | Domain, 70 | Username, 71 | Password, 72 | } 73 | 74 | /// The library's `Result` type alias. 75 | pub type Result = std::result::Result; 76 | 77 | // Utilities 78 | // ***************************************************************************** 79 | 80 | trait ReadExt: AsyncReadExt + Unpin { 81 | async fn read_version(&mut self) -> Result<()> { 82 | let value = self.read_u8().await?; 83 | 84 | match value { 85 | 0x04 => Err(Error::WrongVersion), 86 | 0x05 => Ok(()), 87 | _ => Err(Error::InvalidVersion(value)), 88 | } 89 | } 90 | 91 | async fn read_method(&mut self) -> Result { 92 | let value = self.read_u8().await?; 93 | 94 | let method = match value { 95 | 0x00 => AuthMethod::None, 96 | 0x01 => AuthMethod::GssApi, 97 | 0x02 => AuthMethod::UsernamePassword, 98 | 0x03..=0x7f => AuthMethod::IanaReserved(value), 99 | 0x80..=0xfe => AuthMethod::Private(value), 100 | 0xff => return Err(Error::NoAcceptableMethods), 101 | }; 102 | 103 | Ok(method) 104 | } 105 | 106 | async fn read_command(&mut self) -> Result { 107 | let value = self.read_u8().await?; 108 | 109 | let command = match value { 110 | 0x01 => Command::Connect, 111 | 0x02 => Command::Bind, 112 | 0x03 => Command::UdpAssociate, 113 | _ => return Err(Error::InvalidCommand(value)), 114 | }; 115 | 116 | Ok(command) 117 | } 118 | 119 | async fn read_atyp(&mut self) -> Result { 120 | let value = self.read_u8().await?; 121 | let atyp = match value { 122 | 0x01 => Atyp::V4, 123 | 0x03 => Atyp::Domain, 124 | 0x04 => Atyp::V6, 125 | _ => return Err(Error::InvalidAtyp(value)), 126 | }; 127 | Ok(atyp) 128 | } 129 | 130 | async fn read_reserved(&mut self) -> Result<()> { 131 | let value = self.read_u8().await?; 132 | 133 | match value { 134 | 0x00 => Ok(()), 135 | _ => Err(Error::InvalidReserved(value)), 136 | } 137 | } 138 | 139 | async fn read_fragment_id(&mut self) -> Result<()> { 140 | let value = self.read_u8().await?; 141 | 142 | if value == 0x00 { 143 | Ok(()) 144 | } else { 145 | Err(Error::InvalidFragmentId(value)) 146 | } 147 | } 148 | 149 | async fn read_reply(&mut self) -> Result<()> { 150 | let value = self.read_u8().await?; 151 | 152 | let reply = match value { 153 | 0x00 => return Ok(()), 154 | 0x01 => UnsuccessfulReply::GeneralFailure, 155 | 0x02 => UnsuccessfulReply::ConnectionNotAllowedByRules, 156 | 0x03 => UnsuccessfulReply::NetworkUnreachable, 157 | 0x04 => UnsuccessfulReply::HostUnreachable, 158 | 0x05 => UnsuccessfulReply::ConnectionRefused, 159 | 0x06 => UnsuccessfulReply::TtlExpired, 160 | 0x07 => UnsuccessfulReply::CommandNotSupported, 161 | 0x08 => UnsuccessfulReply::AddressTypeNotSupported, 162 | _ => UnsuccessfulReply::Unassigned(value), 163 | }; 164 | 165 | Err(Error::Response(reply)) 166 | } 167 | 168 | async fn read_target_addr(&mut self) -> Result { 169 | let atyp: Atyp = self.read_atyp().await?; 170 | 171 | let addr = match atyp { 172 | Atyp::V4 => { 173 | let mut ip = [0; 4]; 174 | self.read_exact(&mut ip).await?; 175 | let port = self.read_u16().await?; 176 | AddrKind::Ip(SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::from(ip), port))) 177 | } 178 | Atyp::V6 => { 179 | let mut ip = [0; 16]; 180 | self.read_exact(&mut ip).await?; 181 | let port = self.read_u16().await?; 182 | AddrKind::Ip(SocketAddr::V6(SocketAddrV6::new( 183 | Ipv6Addr::from(ip), 184 | port, 185 | 0, 186 | 0, 187 | ))) 188 | } 189 | Atyp::Domain => { 190 | let str = self.read_string().await?; 191 | let port = self.read_u16().await?; 192 | AddrKind::Domain(str, port) 193 | } 194 | }; 195 | 196 | Ok(addr) 197 | } 198 | 199 | async fn read_string(&mut self) -> Result { 200 | let len = self.read_u8().await?; 201 | let mut str = vec![0; len as usize]; 202 | self.read_exact(&mut str).await?; 203 | let str = String::from_utf8(str)?; 204 | Ok(str) 205 | } 206 | 207 | async fn read_auth_version(&mut self) -> Result<()> { 208 | let value = self.read_u8().await?; 209 | 210 | if value != 0x01 { 211 | return Err(Error::InvalidAuthSubnegotiation(value)); 212 | } 213 | 214 | Ok(()) 215 | } 216 | 217 | async fn read_auth_status(&mut self) -> Result<()> { 218 | let value = self.read_u8().await?; 219 | 220 | if value != 0x00 { 221 | return Err(Error::InvalidAuthStatus(value)); 222 | } 223 | 224 | Ok(()) 225 | } 226 | 227 | async fn read_selection_msg(&mut self) -> Result { 228 | self.read_version().await?; 229 | self.read_method().await 230 | } 231 | 232 | async fn read_final(&mut self) -> Result { 233 | self.read_version().await?; 234 | self.read_reply().await?; 235 | self.read_reserved().await?; 236 | let addr = self.read_target_addr().await?; 237 | Ok(addr) 238 | } 239 | } 240 | 241 | impl ReadExt for T {} 242 | 243 | trait WriteExt: AsyncWriteExt + Unpin { 244 | async fn write_version(&mut self) -> Result<()> { 245 | self.write_u8(0x05).await?; 246 | Ok(()) 247 | } 248 | 249 | async fn write_method(&mut self, method: AuthMethod) -> Result<()> { 250 | let value = match method { 251 | AuthMethod::None => 0x00, 252 | AuthMethod::GssApi => 0x01, 253 | AuthMethod::UsernamePassword => 0x02, 254 | AuthMethod::IanaReserved(value) => value, 255 | AuthMethod::Private(value) => value, 256 | }; 257 | self.write_u8(value).await?; 258 | Ok(()) 259 | } 260 | 261 | async fn write_command(&mut self, command: Command) -> Result<()> { 262 | self.write_u8(command as u8).await?; 263 | Ok(()) 264 | } 265 | 266 | async fn write_atyp(&mut self, atyp: Atyp) -> Result<()> { 267 | self.write_u8(atyp as u8).await?; 268 | Ok(()) 269 | } 270 | 271 | async fn write_reserved(&mut self) -> Result<()> { 272 | self.write_u8(0x00).await?; 273 | Ok(()) 274 | } 275 | 276 | async fn write_fragment_id(&mut self) -> Result<()> { 277 | self.write_u8(0x00).await?; 278 | Ok(()) 279 | } 280 | 281 | async fn write_target_addr(&mut self, target_addr: &AddrKind) -> Result<()> { 282 | match target_addr { 283 | AddrKind::Ip(SocketAddr::V4(addr)) => { 284 | self.write_atyp(Atyp::V4).await?; 285 | self.write_all(&addr.ip().octets()).await?; 286 | self.write_u16(addr.port()).await?; 287 | } 288 | AddrKind::Ip(SocketAddr::V6(addr)) => { 289 | self.write_atyp(Atyp::V6).await?; 290 | self.write_all(&addr.ip().octets()).await?; 291 | self.write_u16(addr.port()).await?; 292 | } 293 | AddrKind::Domain(domain, port) => { 294 | self.write_atyp(Atyp::Domain).await?; 295 | self.write_string(domain, StringKind::Domain).await?; 296 | self.write_u16(*port).await?; 297 | } 298 | } 299 | Ok(()) 300 | } 301 | 302 | async fn write_string(&mut self, string: &str, kind: StringKind) -> Result<()> { 303 | let bytes = string.as_bytes(); 304 | if bytes.len() > 255 { 305 | return Err(Error::TooLongString(kind)); 306 | } 307 | self.write_u8(bytes.len() as u8).await?; 308 | self.write_all(bytes).await?; 309 | Ok(()) 310 | } 311 | 312 | async fn write_auth_version(&mut self) -> Result<()> { 313 | self.write_u8(0x01).await?; 314 | Ok(()) 315 | } 316 | 317 | async fn write_methods(&mut self, methods: &[AuthMethod]) -> Result<()> { 318 | self.write_u8(methods.len() as u8).await?; 319 | for method in methods { 320 | self.write_method(*method).await?; 321 | } 322 | Ok(()) 323 | } 324 | 325 | async fn write_selection_msg(&mut self, methods: &[AuthMethod]) -> Result<()> { 326 | self.write_version().await?; 327 | self.write_methods(methods).await?; 328 | self.flush().await?; 329 | Ok(()) 330 | } 331 | 332 | async fn write_final(&mut self, command: Command, addr: &AddrKind) -> Result<()> { 333 | self.write_version().await?; 334 | self.write_command(command).await?; 335 | self.write_reserved().await?; 336 | self.write_target_addr(addr).await?; 337 | self.flush().await?; 338 | Ok(()) 339 | } 340 | } 341 | 342 | impl WriteExt for T {} 343 | 344 | async fn username_password_auth(stream: &mut S, auth: Auth) -> Result<()> 345 | where 346 | S: WriteExt + ReadExt + Send, 347 | { 348 | stream.write_auth_version().await?; 349 | stream 350 | .write_string(&auth.username, StringKind::Username) 351 | .await?; 352 | stream 353 | .write_string(&auth.password, StringKind::Password) 354 | .await?; 355 | stream.flush().await?; 356 | 357 | stream.read_auth_version().await?; 358 | stream.read_auth_status().await 359 | } 360 | 361 | async fn init( 362 | stream: &mut S, 363 | command: Command, 364 | addr: A, 365 | auth: Option, 366 | ) -> Result 367 | where 368 | S: WriteExt + ReadExt + Send, 369 | A: Into, 370 | { 371 | let addr: AddrKind = addr.into(); 372 | 373 | let mut methods = Vec::with_capacity(2); 374 | methods.push(AuthMethod::None); 375 | if auth.is_some() { 376 | methods.push(AuthMethod::UsernamePassword); 377 | } 378 | stream.write_selection_msg(&methods).await?; 379 | 380 | let method: AuthMethod = stream.read_selection_msg().await?; 381 | match method { 382 | AuthMethod::None => {} 383 | // FIXME: until if let in match is stabilized 384 | AuthMethod::UsernamePassword if auth.is_some() => { 385 | username_password_auth(stream, auth.unwrap()).await?; 386 | } 387 | _ => return Err(Error::InvalidAuthMethod(method)), 388 | } 389 | 390 | stream.write_final(command, &addr).await?; 391 | stream.read_final().await 392 | } 393 | 394 | // Types 395 | // ***************************************************************************** 396 | 397 | /// Required for a username + password authentication. 398 | #[derive(Debug, Eq, PartialEq, Clone, Hash)] 399 | pub struct Auth { 400 | pub username: String, 401 | pub password: String, 402 | } 403 | 404 | impl Auth { 405 | /// Constructs `Auth` with the specified username and a password. 406 | pub fn new(username: U, password: P) -> Self 407 | where 408 | U: Into, 409 | P: Into, 410 | { 411 | Self { 412 | username: username.into(), 413 | password: password.into(), 414 | } 415 | } 416 | } 417 | 418 | /// A proxy authentication method. 419 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] 420 | pub enum AuthMethod { 421 | /// No authentication required. 422 | None, 423 | /// GSS API. 424 | GssApi, 425 | /// A username + password authentication. 426 | UsernamePassword, 427 | /// IANA reserved. 428 | IanaReserved(u8), 429 | /// A private authentication method. 430 | Private(u8), 431 | } 432 | 433 | enum Command { 434 | Connect = 0x01, 435 | Bind = 0x02, 436 | UdpAssociate = 0x03, 437 | } 438 | 439 | enum Atyp { 440 | V4 = 0x01, 441 | Domain = 0x03, 442 | V6 = 0x4, 443 | } 444 | 445 | /// An unsuccessful reply from a proxy server. 446 | #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] 447 | pub enum UnsuccessfulReply { 448 | GeneralFailure, 449 | ConnectionNotAllowedByRules, 450 | NetworkUnreachable, 451 | HostUnreachable, 452 | ConnectionRefused, 453 | TtlExpired, 454 | CommandNotSupported, 455 | AddressTypeNotSupported, 456 | Unassigned(u8), 457 | } 458 | 459 | /// Either [`SocketAddr`] or a domain and a port. 460 | /// 461 | /// [`SocketAddr`]: https://doc.rust-lang.org/std/net/enum.SocketAddr.html 462 | #[derive(Debug, Eq, PartialEq, Clone, Hash)] 463 | pub enum AddrKind { 464 | Ip(SocketAddr), 465 | Domain(String, u16), 466 | } 467 | 468 | impl AddrKind { 469 | const MAX_SIZE: usize = 1 // atyp 470 | + 1 // domain len 471 | + 255 // domain 472 | + 2; // port 473 | 474 | // FIXME: until ToSocketAddrs is allowed to implement 475 | fn to_socket_addr(&self) -> String { 476 | match self { 477 | AddrKind::Ip(addr) => addr.to_string(), 478 | AddrKind::Domain(domain, port) => format!("{}:{}", domain, port), 479 | } 480 | } 481 | 482 | fn size(&self) -> usize { 483 | 1 + // atyp 484 | 2 + // port 485 | match self { 486 | AddrKind::Ip(SocketAddr::V4(_)) => 4, 487 | AddrKind::Ip(SocketAddr::V6(_)) => 16, 488 | AddrKind::Domain(domain, _) => 489 | 1 // string len 490 | + domain.len(), 491 | } 492 | } 493 | } 494 | 495 | impl From<(IpAddr, u16)> for AddrKind { 496 | fn from(value: (IpAddr, u16)) -> Self { 497 | Self::Ip(value.into()) 498 | } 499 | } 500 | 501 | impl From<(Ipv4Addr, u16)> for AddrKind { 502 | fn from(value: (Ipv4Addr, u16)) -> Self { 503 | Self::Ip(value.into()) 504 | } 505 | } 506 | 507 | impl From<(Ipv6Addr, u16)> for AddrKind { 508 | fn from(value: (Ipv6Addr, u16)) -> Self { 509 | Self::Ip(value.into()) 510 | } 511 | } 512 | 513 | impl From<(String, u16)> for AddrKind { 514 | fn from((domain, port): (String, u16)) -> Self { 515 | Self::Domain(domain, port) 516 | } 517 | } 518 | 519 | impl From<(&'_ str, u16)> for AddrKind { 520 | fn from((domain, port): (&'_ str, u16)) -> Self { 521 | Self::Domain(domain.to_owned(), port) 522 | } 523 | } 524 | 525 | impl From for AddrKind { 526 | fn from(value: SocketAddr) -> Self { 527 | Self::Ip(value) 528 | } 529 | } 530 | 531 | impl From for AddrKind { 532 | fn from(value: SocketAddrV4) -> Self { 533 | Self::Ip(value.into()) 534 | } 535 | } 536 | 537 | impl From for AddrKind { 538 | fn from(value: SocketAddrV6) -> Self { 539 | Self::Ip(value.into()) 540 | } 541 | } 542 | 543 | // Public API 544 | // ***************************************************************************** 545 | 546 | /// Proxifies a TCP connection. Performs the [`CONNECT`] command under the hood. 547 | /// 548 | /// [`CONNECT`]: https://tools.ietf.org/html/rfc1928#page-6 549 | /// 550 | /// ```no_run 551 | /// # use async_socks5::Result; 552 | /// # #[tokio::main(flavor = "current_thread")] 553 | /// # async fn main() -> Result<()> { 554 | /// use async_socks5::connect; 555 | /// use tokio::{io::BufStream, net::TcpStream}; 556 | /// 557 | /// let stream = TcpStream::connect("my-proxy-server.com:54321").await?; 558 | /// let mut stream = BufStream::new(stream); 559 | /// connect(&mut stream, ("google.com", 80), None).await?; 560 | /// 561 | /// # Ok(()) 562 | /// # } 563 | /// ``` 564 | pub async fn connect(socket: &mut S, addr: A, auth: Option) -> Result 565 | where 566 | S: AsyncWriteExt + AsyncReadExt + Send + Unpin, 567 | A: Into, 568 | { 569 | init(socket, Command::Connect, addr, auth).await 570 | } 571 | 572 | /// A listener that accepts TCP connections through a proxy. 573 | /// 574 | /// ```no_run 575 | /// # use async_socks5::Result; 576 | /// # #[tokio::main(flavor = "current_thread")] 577 | /// # async fn main() -> Result<()> { 578 | /// use async_socks5::SocksListener; 579 | /// use tokio::{io::BufStream, net::TcpStream}; 580 | /// 581 | /// let stream = TcpStream::connect("my-proxy-server.com:54321").await?; 582 | /// let mut stream = BufStream::new(stream); 583 | /// let (stream, addr) = SocksListener::bind(stream, ("ftp-server.org", 21), None) 584 | /// .await? 585 | /// .accept() 586 | /// .await?; 587 | /// 588 | /// # Ok(()) 589 | /// # } 590 | /// ``` 591 | #[derive(Debug)] 592 | pub struct SocksListener { 593 | stream: S, 594 | proxy_addr: AddrKind, 595 | } 596 | 597 | impl SocksListener 598 | where 599 | S: AsyncWriteExt + AsyncReadExt + Send + Unpin, 600 | { 601 | /// Creates `SocksListener`. Performs the [`BIND`] command under the hood. 602 | /// 603 | /// [`BIND`]: https://tools.ietf.org/html/rfc1928#page-6 604 | pub async fn bind(mut stream: S, addr: A, auth: Option) -> Result 605 | where 606 | A: Into, 607 | { 608 | let addr = init(&mut stream, Command::Bind, addr, auth).await?; 609 | Ok(Self { 610 | stream, 611 | proxy_addr: addr, 612 | }) 613 | } 614 | 615 | pub fn proxy_addr(&self) -> &AddrKind { 616 | &self.proxy_addr 617 | } 618 | 619 | pub async fn accept(mut self) -> Result<(S, AddrKind)> { 620 | let addr = self.stream.read_final().await?; 621 | Ok((self.stream, addr)) 622 | } 623 | } 624 | 625 | /// A UDP socket that sends packets through a proxy. 626 | #[derive(Debug)] 627 | pub struct SocksDatagram { 628 | socket: UdpSocket, 629 | proxy_addr: AddrKind, 630 | stream: S, 631 | } 632 | 633 | impl SocksDatagram 634 | where 635 | S: AsyncWriteExt + AsyncReadExt + Send + Unpin, 636 | { 637 | /// Creates `SocksDatagram`. Performs [`UDP ASSOCIATE`] under the hood. 638 | /// 639 | /// [`UDP ASSOCIATE`]: https://tools.ietf.org/html/rfc1928#page-7 640 | pub async fn associate( 641 | mut proxy_stream: S, 642 | socket: UdpSocket, 643 | auth: Option, 644 | association_addr: Option, 645 | ) -> Result 646 | where 647 | A: Into, 648 | { 649 | let addr = association_addr 650 | .map(Into::into) 651 | .unwrap_or_else(|| AddrKind::Ip(SocketAddr::new(IpAddr::from([0, 0, 0, 0]), 0))); 652 | let proxy_addr = init(&mut proxy_stream, Command::UdpAssociate, addr, auth).await?; 653 | socket.connect(proxy_addr.to_socket_addr()).await?; 654 | Ok(Self { 655 | socket, 656 | proxy_addr, 657 | stream: proxy_stream, 658 | }) 659 | } 660 | 661 | pub fn proxy_addr(&self) -> &AddrKind { 662 | &self.proxy_addr 663 | } 664 | 665 | pub fn get_ref(&self) -> &UdpSocket { 666 | &self.socket 667 | } 668 | 669 | pub fn get_mut(&mut self) -> &mut UdpSocket { 670 | &mut self.socket 671 | } 672 | 673 | pub fn into_inner(self) -> (S, UdpSocket) { 674 | (self.stream, self.socket) 675 | } 676 | 677 | async fn write_request(buf: &[u8], addr: AddrKind) -> Result> { 678 | let bytes_size = Self::get_buf_size(addr.size(), buf.len()); 679 | let bytes = Vec::with_capacity(bytes_size); 680 | 681 | let mut cursor = Cursor::new(bytes); 682 | cursor.write_reserved().await?; 683 | cursor.write_reserved().await?; 684 | cursor.write_fragment_id().await?; 685 | cursor.write_target_addr(&addr).await?; 686 | cursor.write_all(buf).await?; 687 | 688 | let bytes = cursor.into_inner(); 689 | Ok(bytes) 690 | } 691 | 692 | pub async fn send_to(&self, buf: &[u8], addr: A) -> Result 693 | where 694 | A: Into, 695 | { 696 | let addr: AddrKind = addr.into(); 697 | let bytes = Self::write_request(buf, addr).await?; 698 | Ok(self.socket.send(&bytes).await?) 699 | } 700 | 701 | async fn read_response( 702 | len: usize, 703 | buf: &mut [u8], 704 | bytes: &mut [u8], 705 | ) -> Result<(usize, AddrKind)> { 706 | let mut cursor = Cursor::new(bytes); 707 | cursor.read_reserved().await?; 708 | cursor.read_reserved().await?; 709 | cursor.read_fragment_id().await?; 710 | let addr = cursor.read_target_addr().await?; 711 | let header_len = cursor.position() as usize; 712 | cursor.read_exact(buf).await?; 713 | Ok((len - header_len, addr)) 714 | } 715 | 716 | pub async fn recv_from(&self, buf: &mut [u8]) -> Result<(usize, AddrKind)> { 717 | let bytes_size = Self::get_buf_size(AddrKind::MAX_SIZE, buf.len()); 718 | let mut bytes = vec![0; bytes_size]; 719 | 720 | let len = self.socket.recv(&mut bytes).await?; 721 | let (read, addr) = Self::read_response(len, buf, &mut bytes).await?; 722 | Ok((read, addr)) 723 | } 724 | 725 | fn get_buf_size(addr_size: usize, buf_len: usize) -> usize { 726 | 2 // reserved 727 | + 1 // fragment id 728 | + addr_size 729 | + buf_len 730 | } 731 | } 732 | 733 | // Tests 734 | // ***************************************************************************** 735 | 736 | #[cfg(test)] 737 | mod tests { 738 | use super::*; 739 | use std::sync::Arc; 740 | use tokio::{io::BufStream, net::TcpStream}; 741 | 742 | const PROXY_ADDR: &str = "127.0.0.1:1080"; 743 | const PROXY_AUTH_ADDR: &str = "127.0.0.1:1081"; 744 | const DATA: &[u8] = b"Hello, world!"; 745 | 746 | async fn connect(addr: &str, auth: Option) { 747 | let socket = TcpStream::connect(addr).await.unwrap(); 748 | let mut socket = BufStream::new(socket); 749 | super::connect( 750 | &mut socket, 751 | AddrKind::Domain("google.com".to_string(), 80), 752 | auth, 753 | ) 754 | .await 755 | .unwrap(); 756 | } 757 | 758 | #[tokio::test] 759 | async fn connect_auth() { 760 | connect(PROXY_AUTH_ADDR, Some(Auth::new("hyper", "proxy"))).await; 761 | } 762 | 763 | #[tokio::test] 764 | async fn connect_no_auth() { 765 | connect(PROXY_ADDR, None).await; 766 | } 767 | 768 | #[should_panic = "ConnectionNotAllowedByRules"] 769 | #[tokio::test] 770 | async fn connect_no_auth_panic() { 771 | connect(PROXY_AUTH_ADDR, None).await; 772 | } 773 | 774 | #[tokio::test] 775 | async fn bind() { 776 | let server_addr = AddrKind::Domain("127.0.0.1".to_string(), 80); 777 | 778 | let client = TcpStream::connect(PROXY_ADDR).await.unwrap(); 779 | let client = BufStream::new(client); 780 | let client = SocksListener::bind(client, server_addr, None) 781 | .await 782 | .unwrap(); 783 | 784 | let server_addr = client.proxy_addr.to_socket_addr(); 785 | let mut server = TcpStream::connect(&server_addr).await.unwrap(); 786 | 787 | let (mut client, _) = client.accept().await.unwrap(); 788 | 789 | server.write_all(DATA).await.unwrap(); 790 | 791 | let mut buf = [0; DATA.len()]; 792 | client.read_exact(&mut buf).await.unwrap(); 793 | assert_eq!(buf, DATA); 794 | } 795 | 796 | type TestStream = BufStream; 797 | type TestDatagram = SocksDatagram; 798 | type TestHalves = (Arc, Arc); 799 | 800 | trait UdpClient { 801 | async fn send_to(&mut self, buf: &[u8], addr: A) -> Result 802 | where 803 | A: Into + Send; 804 | 805 | async fn recv_from(&mut self, buf: &mut [u8]) -> Result<(usize, AddrKind)>; 806 | } 807 | 808 | impl UdpClient for TestDatagram { 809 | async fn send_to(&mut self, buf: &[u8], addr: A) -> Result 810 | where 811 | A: Into + Send, 812 | { 813 | SocksDatagram::send_to(self, buf, addr).await 814 | } 815 | 816 | async fn recv_from(&mut self, buf: &mut [u8]) -> Result<(usize, AddrKind), Error> { 817 | SocksDatagram::recv_from(self, buf).await 818 | } 819 | } 820 | 821 | impl UdpClient for TestHalves { 822 | async fn send_to(&mut self, buf: &[u8], addr: A) -> Result 823 | where 824 | A: Into + Send, 825 | { 826 | self.1.send_to(buf, addr).await 827 | } 828 | 829 | async fn recv_from(&mut self, buf: &mut [u8]) -> Result<(usize, AddrKind), Error> { 830 | self.0.recv_from(buf).await 831 | } 832 | } 833 | 834 | const CLIENT_ADDR: &str = "127.0.0.1:2345"; 835 | const SERVER_ADDR: &str = "127.0.0.1:23456"; 836 | 837 | async fn create_client() -> TestDatagram { 838 | let proxy = TcpStream::connect(PROXY_ADDR).await.unwrap(); 839 | let proxy = BufStream::new(proxy); 840 | let client = UdpSocket::bind(CLIENT_ADDR).await.unwrap(); 841 | SocksDatagram::associate(proxy, client, None, None::) 842 | .await 843 | .unwrap() 844 | } 845 | 846 | struct UdpTest { 847 | client: C, 848 | server: UdpSocket, 849 | server_addr: AddrKind, 850 | } 851 | 852 | impl UdpTest { 853 | async fn test(mut self) { 854 | let mut buf = vec![0; DATA.len()]; 855 | self.client.send_to(DATA, self.server_addr).await.unwrap(); 856 | let (len, addr) = self.server.recv_from(&mut buf).await.unwrap(); 857 | assert_eq!(len, buf.len()); 858 | assert_eq!(buf.as_slice(), DATA); 859 | 860 | let mut buf = vec![0; DATA.len()]; 861 | self.server.send_to(DATA, addr).await.unwrap(); 862 | let (len, _) = self.client.recv_from(&mut buf).await.unwrap(); 863 | assert_eq!(len, buf.len()); 864 | assert_eq!(buf.as_slice(), DATA); 865 | } 866 | } 867 | 868 | impl UdpTest { 869 | async fn datagram() -> Self { 870 | let client = create_client().await; 871 | 872 | let server_addr: SocketAddr = SERVER_ADDR.parse().unwrap(); 873 | let server = UdpSocket::bind(server_addr).await.unwrap(); 874 | let server_addr = AddrKind::Ip(server_addr); 875 | 876 | Self { 877 | client, 878 | server, 879 | server_addr, 880 | } 881 | } 882 | } 883 | 884 | impl UdpTest { 885 | async fn halves() -> Self { 886 | let this = UdpTest::::datagram().await; 887 | let client = Arc::new(this.client); 888 | Self { 889 | client: (client.clone(), client), 890 | server: this.server, 891 | server_addr: this.server_addr, 892 | } 893 | } 894 | } 895 | 896 | #[tokio::test] 897 | async fn udp_associate() { 898 | UdpTest::datagram().await.test().await 899 | } 900 | 901 | #[tokio::test] 902 | async fn udp_datagram_halves() { 903 | UdpTest::halves().await.test().await 904 | } 905 | } 906 | --------------------------------------------------------------------------------