├── .gitignore ├── examples ├── tcp_mapping.rs ├── udp_mapping.rs ├── async_udp_tokio.rs └── async_udp_asyncstd.rs ├── LICENSE ├── .github └── workflows │ ├── release.yml │ ├── pr.yml │ ├── main.yml │ └── nightly.yml ├── Cargo.toml ├── README.md └── src ├── a_std.rs ├── a_tokio.rs ├── error.rs ├── asynchronous.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .idea/ 5 | .vscode/ 6 | -------------------------------------------------------------------------------- /examples/tcp_mapping.rs: -------------------------------------------------------------------------------- 1 | extern crate natpmp; 2 | 3 | use natpmp::*; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | fn main() -> Result<()> { 8 | let mut n = Natpmp::new()?; 9 | n.send_port_mapping_request(Protocol::TCP, 4020, 4020, 30)?; 10 | // sleep for a while 11 | thread::sleep(Duration::from_millis(100)); 12 | match n.read_response_or_retry() { 13 | Err(e) => match e { 14 | Error::NATPMP_TRYAGAIN => println!("Try again later"), 15 | _ => return Err(e), 16 | }, 17 | Ok(Response::TCP(tr)) => { 18 | assert_eq!(tr.private_port(), 4020); 19 | assert_eq!(tr.public_port(), 4020); // Could be another port chosen by gateway 20 | } 21 | _ => { 22 | panic!("Expecting a tcp response"); 23 | } 24 | } 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /examples/udp_mapping.rs: -------------------------------------------------------------------------------- 1 | extern crate natpmp; 2 | 3 | use natpmp::*; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | fn main() -> Result<()> { 8 | let mut n = Natpmp::new()?; 9 | n.send_port_mapping_request(Protocol::UDP, 4020, 4020, 30)?; 10 | // sleep for a while 11 | thread::sleep(Duration::from_millis(100)); 12 | match n.read_response_or_retry() { 13 | Err(e) => match e { 14 | Error::NATPMP_TRYAGAIN => println!("Try again later"), 15 | _ => return Err(e), 16 | }, 17 | Ok(Response::UDP(ur)) => { 18 | assert_eq!(ur.private_port(), 4020); 19 | assert_eq!(ur.public_port(), 4020); // Could be another port chosen by gateway 20 | } 21 | _ => { 22 | panic!("Expecting a udp response"); 23 | } 24 | } 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Feng Yingcai 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 | -------------------------------------------------------------------------------- /examples/async_udp_tokio.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use natpmp::*; 4 | use std::time::Duration; 5 | 6 | #[tokio::main] 7 | async fn main() -> Result<()> { 8 | let n = Arc::new(new_tokio_natpmp().await?); 9 | 10 | let n_cloned = n.clone(); 11 | tokio::spawn(async { 12 | let n = n_cloned; 13 | loop { 14 | println!("Sending request..."); 15 | if let Err(e) = n 16 | .send_port_mapping_request(Protocol::UDP, 4020, 4020, 30) 17 | .await 18 | { 19 | eprintln!("Sending request err: {}", e); 20 | break; 21 | } 22 | tokio::time::sleep(Duration::from_secs(3)).await; 23 | } 24 | }); 25 | 26 | loop { 27 | println!("Waiting response..."); 28 | match n.read_response_or_retry().await? { 29 | Response::UDP(ur) => { 30 | assert_eq!(ur.private_port(), 4020); 31 | assert_eq!(ur.public_port(), 4020); // Could be another port chosen by gateway 32 | } 33 | _ => { 34 | eprintln!("Expecting a udp response"); 35 | break; 36 | } 37 | } 38 | } 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | build-all: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | # Rust cache 17 | - name: Rust cargo cache 18 | uses: actions/cache@v1 19 | env: 20 | cache-name: cache-rust-cargo 21 | with: 22 | path: | 23 | ~/.cargo 24 | target 25 | key: ${{ runner.os }}-build-${{ env.cache-name }} 26 | 27 | # Rust toolchain 28 | - name: Rust toolchain 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: stable 32 | 33 | # Rust cargo 34 | - run: | 35 | cargo build --verbose --all 36 | cargo test --verbose test::test_ffi 37 | cargo test --verbose test::test_natpmp 38 | cargo doc --verbose 39 | 40 | # Release 41 | - uses: "marvinpinto/action-automatic-releases@latest" 42 | with: 43 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 44 | prerelease: false 45 | draft: false 46 | 47 | # Cargo publish 48 | - name: Cargo publish 49 | env: 50 | CARGO_TOKEN: ${{ secrets.CARGO_TOKEN }} 51 | run: | 52 | cargo publish --token "$CARGO_TOKEN" 53 | 54 | -------------------------------------------------------------------------------- /examples/async_udp_asyncstd.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use natpmp::*; 4 | use std::time::Duration; 5 | 6 | fn main() -> Result<()> { 7 | use async_std::future; 8 | use async_std::task; 9 | 10 | task::block_on(async { 11 | let n = Arc::new(new_async_std_natpmp().await.unwrap()); 12 | 13 | let n_cloned = n.clone(); 14 | task::spawn(async { 15 | let n = n_cloned; 16 | loop { 17 | println!("Sending request..."); 18 | if let Err(e) = n 19 | .send_port_mapping_request(Protocol::UDP, 4020, 4020, 30) 20 | .await 21 | { 22 | eprintln!("Sending request error: {}", e); 23 | break; 24 | } 25 | let _ = future::timeout(Duration::from_secs(3), async {}).await; 26 | } 27 | }); 28 | 29 | loop { 30 | match n.read_response_or_retry().await { 31 | Ok(Response::UDP(ur)) => { 32 | assert_eq!(ur.private_port(), 4020); 33 | assert_eq!(ur.public_port(), 4020); // Could be another port chosen by gateway 34 | } 35 | _ => { 36 | eprintln!("Expecting a udp response"); 37 | break; 38 | } 39 | } 40 | } 41 | }); 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "natpmp" 3 | version = "0.5.0" 4 | authors = ["fengyingcai "] 5 | description = "NAT-PMP client library" 6 | homepage = "https://github.com/fengyc/natpmp" 7 | repository = "https://github.com/fengyc/natpmp" 8 | documentation = "https://docs.rs/natpmp/" 9 | readme = "README.md" 10 | keywords = ["natpmp", "rfc6886", "nat", "portmapping"] 11 | categories = ["network-programming"] 12 | license = "MIT" 13 | edition = "2021" 14 | 15 | [badges] 16 | maintenance = { status = "actively-developed" } 17 | 18 | [[example]] 19 | name = "tcp-mapping" 20 | path = "examples/tcp_mapping.rs" 21 | 22 | [[example]] 23 | name = "udp-mapping" 24 | path = "examples/udp_mapping.rs" 25 | 26 | [[example]] 27 | name = "async-udp-tokio" 28 | path = "examples/async_udp_tokio.rs" 29 | required-features = ["tokio"] 30 | 31 | [[example]] 32 | name = "async-udp-async-std" 33 | path = "examples/async_udp_asyncstd.rs" 34 | required-features = ["async-std"] 35 | 36 | 37 | [features] 38 | default = ["tokio"] 39 | 40 | all = ["tokio", "async-std"] 41 | 42 | tokio = ["dep:tokio"] 43 | async-std = ["dep:async-std"] 44 | 45 | [build-dependencies] 46 | cc = "1" # compile native c 47 | 48 | [dependencies] 49 | async-trait = "0.1" 50 | tokio = { version = "1", features = ["net"], optional = true } 51 | async-std = { version = "1", optional = true } 52 | netdev = "0.31.0" 53 | 54 | [dev-dependencies] 55 | tokio = { version = "1", features = ["full"]} 56 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | natpmp 2 | ====== 3 | 4 | [![Main](https://github.com/fengyc/natpmp/actions/workflows/main.yml/badge.svg)](https://github.com/fengyc/natpmp/actions/workflows/main.yml) 5 | [![Nightly](https://github.com/fengyc/natpmp/actions/workflows/nightly.yml/badge.svg)](https://github.com/fengyc/natpmp/actions/workflows/nightly.yml) 6 | 7 | NAT-PMP client library in rust, a rust implementation of the c library libnatpmp([https://github.com/miniupnp/libnatpmp](https://github.com/miniupnp/libnatpmp)). 8 | 9 | Versions 10 | -------- 11 | 12 | Version 0.2.x supports rust 2018 edition. 13 | 14 | Version 0.3.x+ supports tokio and async-std. 15 | 16 | Example 17 | ------- 18 | 19 | Create a natpmp object with system default gateway: 20 | 21 | use natpmp::* 22 | 23 | let n = Natpmp::new()? 24 | 25 | Or a specified gataway: 26 | 27 | use std::str::FromStr; 28 | use natpmp::*; 29 | 30 | let n = Natpmp::new("192.168.0.1").parse.unwrap())? 31 | 32 | To determine the external address, send a public address request: 33 | 34 | n.send_public_address_request()?; 35 | 36 | To add a port mapping, send a port mapping request: 37 | 38 | n.send_port_mapping_request(Protocol::UDP, 4020, 4020, 30)?; 39 | 40 | And then read response after a few milliseconds: 41 | 42 | use std::thread; 43 | use std::time::Duration; 44 | 45 | thread::sleep(Duration::from_millis(250)); 46 | let response = n.read_response_or_retry()?; 47 | 48 | Check response type and and result: 49 | 50 | match response { 51 | Response::Gateway(gr) => {} 52 | Response::UDP(ur) => {} 53 | Response::TCP(tr) => {} 54 | } 55 | 56 | Async 57 | ------ 58 | 59 | Enable feature `tokio` or `async-std` in Cargo.toml (default feature `tokio`). 60 | 61 | cargo add natpmp --features tokio 62 | 63 | Or 64 | 65 | cargo add natpmp --features async-std 66 | 67 | License 68 | ------- 69 | 70 | MIT -------------------------------------------------------------------------------- /src/a_std.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net::{Ipv4Addr, SocketAddrV4}; 3 | 4 | use async_std::net::UdpSocket; 5 | use async_trait::async_trait; 6 | 7 | use super::*; 8 | use crate::asynchronous::{new_natpmp_async_with, AsyncUdpSocket, NatpmpAsync}; 9 | 10 | #[async_trait] 11 | impl AsyncUdpSocket for UdpSocket { 12 | async fn connect(&self, addr: &str) -> io::Result<()> { 13 | self.connect(addr).await 14 | } 15 | 16 | async fn send(&self, buf: &[u8]) -> io::Result { 17 | self.send(buf).await 18 | } 19 | 20 | async fn recv(&self, buf: &mut [u8]) -> io::Result { 21 | self.recv(buf).await 22 | } 23 | } 24 | 25 | /// Create a async-std NAT-PMP object with default gateway 26 | /// 27 | /// # Errors 28 | /// * [`Error::NATPMP_ERR_SOCKETERROR`](enum.Error.html#variant.NATPMP_ERR_SOCKETERROR) 29 | /// * [`Error::NATPMP_ERR_CONNECTERR`](enum.Error.html#variant.NATPMP_ERR_CONNECTERR) 30 | /// 31 | /// # Examples 32 | /// ``` 33 | /// use natpmp::*; 34 | /// 35 | /// let n = new_async_std_natpmp().await?; 36 | /// ``` 37 | pub async fn new_async_std_natpmp() -> Result> { 38 | let gateway = get_default_gateway()?; 39 | new_async_std_natpmp_with(gateway).await 40 | } 41 | 42 | /// Create a tokio NAT-PMP object with default gateway 43 | /// 44 | /// # Errors 45 | /// * [`Error::NATPMP_ERR_SOCKETERROR`](enum.Error.html#variant.NATPMP_ERR_SOCKETERROR) 46 | /// * [`Error::NATPMP_ERR_CONNECTERR`](enum.Error.html#variant.NATPMP_ERR_CONNECTERR) 47 | /// 48 | /// # Examples 49 | /// ``` 50 | /// use natpmp::*; 51 | /// 52 | /// let gateway = get_default_gateway().unwrap(); 53 | /// let n = new_async_std_natpmp_with(gateway).await?; 54 | /// ``` 55 | pub async fn new_async_std_natpmp_with(gateway: Ipv4Addr) -> Result> { 56 | let s = UdpSocket::bind("0.0.0.0:0") 57 | .await 58 | .map_err(|_e| Error::NATPMP_ERR_SOCKETERROR)?; 59 | let gateway_sockaddr = SocketAddrV4::new(gateway, NATPMP_PORT); 60 | if s.connect(gateway_sockaddr).await.is_err() { 61 | return Err(Error::NATPMP_ERR_CONNECTERR); 62 | } 63 | let n = new_natpmp_async_with(s, gateway); 64 | Ok(n) 65 | } 66 | -------------------------------------------------------------------------------- /src/a_tokio.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net::{Ipv4Addr, SocketAddrV4}; 3 | 4 | use async_trait::async_trait; 5 | use tokio::net::UdpSocket; 6 | 7 | use crate::asynchronous::{new_natpmp_async_with, AsyncUdpSocket, NatpmpAsync}; 8 | use crate::{get_default_gateway, Error, Result, NATPMP_PORT}; 9 | 10 | #[async_trait] 11 | impl AsyncUdpSocket for UdpSocket { 12 | async fn connect(&self, addr: &str) -> io::Result<()> { 13 | self.connect(addr).await 14 | } 15 | 16 | async fn send(&self, buf: &[u8]) -> io::Result { 17 | self.send(buf).await 18 | } 19 | 20 | async fn recv(&self, buf: &mut [u8]) -> io::Result { 21 | self.recv(buf).await 22 | } 23 | } 24 | 25 | /// Create a tokio NAT-PMP object with default gateway 26 | /// 27 | /// # Errors 28 | /// * [`Error::NATPMP_ERR_SOCKETERROR`](enum.Error.html#variant.NATPMP_ERR_SOCKETERROR) 29 | /// * [`Error::NATPMP_ERR_CONNECTERR`](enum.Error.html#variant.NATPMP_ERR_CONNECTERR) 30 | /// 31 | /// # Examples 32 | /// ``` 33 | /// use natpmp::*; 34 | /// 35 | /// let n = new_tokio_natpmp().await?; 36 | /// ``` 37 | pub async fn new_tokio_natpmp() -> Result> { 38 | let gateway = get_default_gateway()?; 39 | new_tokio_natpmp_with(gateway).await 40 | } 41 | 42 | /// Create a tokio NAT-PMP object with specified gateway. 43 | /// 44 | /// # Errors 45 | /// * [`Error::NATPMP_ERR_SOCKETERROR`](enum.Error.html#variant.NATPMP_ERR_SOCKETERROR) 46 | /// * [`Error::NATPMP_ERR_CONNECTERR`](enum.Error.html#variant.NATPMP_ERR_CONNECTERR) 47 | /// 48 | /// # Examples 49 | /// ``` 50 | /// use natpmp::*; 51 | /// 52 | /// let gateway = get_default_gateway().unwrap(); 53 | /// let n = new_tokio_natpmp_with(gateway).await?; 54 | /// ``` 55 | pub async fn new_tokio_natpmp_with(gateway: Ipv4Addr) -> Result> { 56 | let s = UdpSocket::bind("0.0.0.0:0") 57 | .await 58 | .map_err(|_| Error::NATPMP_ERR_SOCKETERROR)?; 59 | let gateway_sockaddr = SocketAddrV4::new(gateway, NATPMP_PORT); 60 | if s.connect(gateway_sockaddr).await.is_err() { 61 | return Err(Error::NATPMP_ERR_CONNECTERR); 62 | } 63 | let n = new_natpmp_async_with(s, gateway); 64 | Ok(n) 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: PR 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | 7 | jobs: 8 | build-linux: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Checkout 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | # Rust cache 16 | - name: Rust cargo cache 17 | uses: actions/cache@v1 18 | env: 19 | cache-name: cache-rust-cargo 20 | with: 21 | path: | 22 | ~/.cargo 23 | target 24 | key: ${{ runner.os }}-build-${{ env.cache-name }} 25 | 26 | # Rust toolchain 27 | - name: Rust toolchain 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: stable 31 | 32 | # Rust cargo 33 | - run: | 34 | cargo build --verbose --all 35 | cargo test --verbose test::test_ffi 36 | cargo test --verbose test::test_natpmp 37 | cargo doc --verbose 38 | 39 | build-windows: 40 | runs-on: windows-latest 41 | steps: 42 | # Checkout 43 | - name: Checkout 44 | uses: actions/checkout@v2 45 | 46 | # Rust cache 47 | - name: Rust cargo cache 48 | uses: actions/cache@v1 49 | env: 50 | cache-name: cache-rust-cargo 51 | with: 52 | path: | 53 | ~/.cargo 54 | target 55 | key: ${{ runner.os }}-build-${{ env.cache-name }} 56 | 57 | # Rust toolchain 58 | - name: Rust toolchain 59 | uses: actions-rs/toolchain@v1 60 | with: 61 | toolchain: stable 62 | 63 | # Rust cargo 64 | - run: | 65 | cargo build --verbose --all 66 | cargo test --verbose test::test_ffi 67 | cargo test --verbose test::test_natpmp 68 | cargo doc --verbose 69 | 70 | build-macos: 71 | runs-on: macos-latest 72 | steps: 73 | # Checkout 74 | - name: Checkout 75 | uses: actions/checkout@v2 76 | 77 | # Rust cache 78 | - name: Rust cargo cache 79 | uses: actions/cache@v1 80 | env: 81 | cache-name: cache-rust-cargo 82 | with: 83 | path: | 84 | ~/.cargo 85 | target 86 | key: ${{ runner.os }}-build-${{ env.cache-name }} 87 | 88 | # Rust toolchain 89 | - name: Rust toolchain 90 | uses: actions-rs/toolchain@v1 91 | with: 92 | toolchain: stable 93 | 94 | # Rust cargo 95 | - run: | 96 | cargo build --verbose --all 97 | cargo test --verbose test::test_ffi 98 | cargo test --verbose test::test_natpmp 99 | cargo doc --verbose 100 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | build-linux: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # Checkout 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | # Rust cache 16 | - name: Rust cargo cache 17 | uses: actions/cache@v1 18 | env: 19 | cache-name: cache-rust-cargo 20 | with: 21 | path: | 22 | ~/.cargo 23 | target 24 | key: ${{ runner.os }}-build-${{ env.cache-name }} 25 | 26 | # Rust toolchain 27 | - name: Rust toolchain 28 | uses: actions-rs/toolchain@v1 29 | with: 30 | toolchain: stable 31 | 32 | # Rust cargo 33 | - run: | 34 | cargo build --verbose --all 35 | cargo test --verbose test::test_ffi 36 | cargo test --verbose test::test_natpmp 37 | cargo doc --verbose 38 | 39 | # Latest build 40 | - uses: "marvinpinto/action-automatic-releases@latest" 41 | with: 42 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 43 | automatic_release_tag: "latest" 44 | prerelease: true 45 | draft: false 46 | title: "Development Build" 47 | 48 | build-windows: 49 | runs-on: windows-latest 50 | steps: 51 | # Checkout 52 | - name: Checkout 53 | uses: actions/checkout@v2 54 | 55 | # Rust cache 56 | - name: Rust cargo cache 57 | uses: actions/cache@v1 58 | env: 59 | cache-name: cache-rust-cargo 60 | with: 61 | path: | 62 | ~/.cargo 63 | target 64 | key: ${{ runner.os }}-build-${{ env.cache-name }} 65 | 66 | # Rust toolchain 67 | - name: Rust toolchain 68 | uses: actions-rs/toolchain@v1 69 | with: 70 | toolchain: stable 71 | 72 | # Rust cargo 73 | - run: | 74 | cargo build --verbose --all 75 | cargo test --verbose test::test_ffi 76 | cargo test --verbose test::test_natpmp 77 | cargo doc --verbose 78 | 79 | build-macos: 80 | runs-on: macos-latest 81 | steps: 82 | # Checkout 83 | - name: Checkout 84 | uses: actions/checkout@v2 85 | 86 | # Rust cache 87 | - name: Rust cargo cache 88 | uses: actions/cache@v1 89 | env: 90 | cache-name: cache-rust-cargo 91 | with: 92 | path: | 93 | ~/.cargo 94 | target 95 | key: ${{ runner.os }}-build-${{ env.cache-name }} 96 | 97 | # Rust toolchain 98 | - name: Rust toolchain 99 | uses: actions-rs/toolchain@v1 100 | with: 101 | toolchain: stable 102 | 103 | # Rust cargo 104 | - run: | 105 | cargo build --verbose --all 106 | cargo test --verbose test::test_ffi 107 | cargo test --verbose test::test_natpmp 108 | cargo doc --verbose 109 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | # Hourly build, cache not allowed 2 | name: Nightly build 3 | 4 | on: 5 | schedule: 6 | - cron: "0 0 * * *" 7 | 8 | jobs: 9 | build-linux: 10 | runs-on: ubuntu-latest 11 | steps: 12 | # Checkout 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | # Rust cache 17 | - name: Rust cargo cache 18 | uses: actions/cache@v1 19 | env: 20 | cache-name: cache-rust-cargo 21 | with: 22 | path: | 23 | ~/.cargo 24 | target 25 | key: ${{ runner.os }}-build-${{ env.cache-name }} 26 | 27 | # Rust toolchain 28 | - name: Rust toolchain 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: stable 32 | 33 | # Rust cargo 34 | - run: | 35 | cargo build --verbose --all 36 | cargo test --verbose test::test_ffi 37 | cargo test --verbose test::test_natpmp 38 | cargo doc --verbose 39 | 40 | # Latest build 41 | - uses: "marvinpinto/action-automatic-releases@latest" 42 | with: 43 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 44 | automatic_release_tag: "latest" 45 | prerelease: true 46 | draft: false 47 | title: "Development Build" 48 | 49 | build-windows: 50 | runs-on: windows-latest 51 | steps: 52 | # Checkout 53 | - name: Checkout 54 | uses: actions/checkout@v2 55 | 56 | # Rust cache 57 | - name: Rust cargo cache 58 | uses: actions/cache@v1 59 | env: 60 | cache-name: cache-rust-cargo 61 | with: 62 | path: | 63 | ~/.cargo 64 | target 65 | key: ${{ runner.os }}-build-${{ env.cache-name }} 66 | 67 | # Rust toolchain 68 | - name: Rust toolchain 69 | uses: actions-rs/toolchain@v1 70 | with: 71 | toolchain: stable 72 | 73 | # Rust cargo 74 | - run: | 75 | cargo build --verbose --all 76 | cargo test --verbose test::test_ffi 77 | cargo test --verbose test::test_natpmp 78 | cargo doc --verbose 79 | 80 | build-macos: 81 | runs-on: macos-latest 82 | steps: 83 | # Checkout 84 | - name: Checkout 85 | uses: actions/checkout@v2 86 | 87 | # Rust cache 88 | - name: Rust cargo cache 89 | uses: actions/cache@v1 90 | env: 91 | cache-name: cache-rust-cargo 92 | with: 93 | path: | 94 | ~/.cargo 95 | target 96 | key: ${{ runner.os }}-build-${{ env.cache-name }} 97 | 98 | # Rust toolchain 99 | - name: Rust toolchain 100 | uses: actions-rs/toolchain@v1 101 | with: 102 | toolchain: stable 103 | 104 | # Rust cargo 105 | - run: | 106 | cargo build --verbose --all 107 | cargo test --verbose test::test_ffi 108 | cargo test --verbose test::test_natpmp 109 | cargo doc --verbose 110 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// NAT-PMP error. 4 | /// 5 | /// # Note 6 | /// 7 | /// These errors are for compatibility only: 8 | /// * [`Error::NATPMP_ERR_INVALIDARGS`](enum.Error.html#variant.NATPMP_ERR_INVALIDARGS) 9 | /// * [`Error::NATPMP_ERR_CLOSEERR`](enum.Error.html#variant.NATPMP_ERR_CLOSEERR) 10 | /// * [`Error::NATPMP_ERR_GETTIMEOFDAYERR`](enum.Error.html#variant.NATPMP_ERR_GETTIMEOFDAYERR) 11 | /// 12 | /// # Examples 13 | /// ``` 14 | /// use natpmp::*; 15 | /// 16 | /// let err = Error::NATPMP_ERR_CANNOTGETGATEWAY; 17 | /// println!("{}", err); 18 | /// ``` 19 | /// 20 | #[allow(non_camel_case_types)] 21 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 22 | pub enum Error { 23 | /// Invalid arguments 24 | NATPMP_ERR_INVALIDARGS, 25 | 26 | /// Failed to create a socket 27 | NATPMP_ERR_SOCKETERROR, 28 | 29 | /// Can not get default gateway address 30 | NATPMP_ERR_CANNOTGETGATEWAY, 31 | 32 | /// Failed to close socket 33 | NATPMP_ERR_CLOSEERR, 34 | 35 | /// Failed to recvfrom socket 36 | NATPMP_ERR_RECVFROM, 37 | 38 | /// No pending request 39 | NATPMP_ERR_NOPENDINGREQ, 40 | 41 | /// Gateway does not support NAT-PMP 42 | NATPMP_ERR_NOGATEWAYSUPPORT, 43 | 44 | /// Failed to connect to the gateway 45 | NATPMP_ERR_CONNECTERR, 46 | 47 | /// Packet not received from the gateway 48 | NATPMP_ERR_WRONGPACKETSOURCE, 49 | 50 | /// Failed to send 51 | NATPMP_ERR_SENDERR, 52 | 53 | /// Failed to set nonblocking 54 | NATPMP_ERR_FCNTLERROR, 55 | 56 | /// Failed to get time 57 | NATPMP_ERR_GETTIMEOFDAYERR, 58 | 59 | /// Unsupported NAT-PMP version 60 | NATPMP_ERR_UNSUPPORTEDVERSION, 61 | 62 | /// Unsupported NAT-PMP opcode 63 | NATPMP_ERR_UNSUPPORTEDOPCODE, 64 | 65 | /// Unknown NAT-PMP error 66 | NATPMP_ERR_UNDEFINEDERROR, 67 | 68 | /// Not authorized 69 | NATPMP_ERR_NOTAUTHORIZED, 70 | 71 | /// Network failure 72 | NATPMP_ERR_NETWORKFAILURE, 73 | 74 | /// NAT-PMP out of resources 75 | NATPMP_ERR_OUTOFRESOURCES, 76 | 77 | /// Try again 78 | NATPMP_TRYAGAIN, 79 | } 80 | 81 | impl fmt::Display for Error { 82 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 83 | match self { 84 | Error::NATPMP_ERR_INVALIDARGS => write!(f, "invalid arguments"), 85 | Error::NATPMP_ERR_SOCKETERROR => write!(f, "socket failed"), 86 | Error::NATPMP_ERR_CANNOTGETGATEWAY => { 87 | write!(f, "cannot get default gateway ip address") 88 | } 89 | Error::NATPMP_ERR_CLOSEERR => write!(f, "close failed"), 90 | Error::NATPMP_ERR_RECVFROM => write!(f, "recvfrom failed"), 91 | Error::NATPMP_ERR_NOPENDINGREQ => write!(f, "no pending request"), 92 | Error::NATPMP_ERR_NOGATEWAYSUPPORT => write!(f, "the gateway does not support nat-pmp"), 93 | Error::NATPMP_ERR_CONNECTERR => write!(f, "connect failed"), 94 | Error::NATPMP_ERR_WRONGPACKETSOURCE => { 95 | write!(f, "packet not received from the gateway") 96 | } 97 | Error::NATPMP_ERR_SENDERR => write!(f, "send failed"), 98 | Error::NATPMP_ERR_FCNTLERROR => write!(f, "fcntl failed"), 99 | Error::NATPMP_ERR_GETTIMEOFDAYERR => write!(f, "get time failed"), 100 | Error::NATPMP_ERR_UNSUPPORTEDVERSION => { 101 | write!(f, "unsupported nat-pmp version error from server") 102 | } 103 | Error::NATPMP_ERR_UNSUPPORTEDOPCODE => { 104 | write!(f, "unsupported nat-pmp opcode error from server") 105 | } 106 | Error::NATPMP_ERR_UNDEFINEDERROR => write!(f, "undefined nat-pmp server error"), 107 | Error::NATPMP_ERR_NOTAUTHORIZED => write!(f, "not authorized"), 108 | Error::NATPMP_ERR_NETWORKFAILURE => write!(f, "network failure"), 109 | Error::NATPMP_ERR_OUTOFRESOURCES => write!(f, "nat-pmp server out of resources"), 110 | Error::NATPMP_TRYAGAIN => write!(f, "try again"), 111 | } 112 | } 113 | } 114 | 115 | impl std::error::Error for Error {} 116 | -------------------------------------------------------------------------------- /src/asynchronous.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net::Ipv4Addr; 3 | use std::time::Duration; 4 | 5 | use async_trait::async_trait; 6 | 7 | use crate::{ 8 | Error, GatewayResponse, MappingResponse, Protocol, Response, Result, NATPMP_MAX_ATTEMPS, 9 | }; 10 | 11 | /// A wrapper trait for async udpsocket. 12 | #[async_trait] 13 | pub trait AsyncUdpSocket { 14 | async fn connect(&self, addr: &str) -> io::Result<()>; 15 | 16 | async fn send(&self, buf: &[u8]) -> io::Result; 17 | 18 | async fn recv(&self, buf: &mut [u8]) -> io::Result; 19 | } 20 | 21 | /// NAT-PMP async client 22 | pub struct NatpmpAsync 23 | where 24 | S: AsyncUdpSocket, 25 | { 26 | s: S, 27 | gateway: Ipv4Addr, 28 | } 29 | 30 | /// Create a NAT-PMP object with async udpsocket and gateway 31 | pub fn new_natpmp_async_with(s: S, gateway: Ipv4Addr) -> NatpmpAsync 32 | where 33 | S: AsyncUdpSocket, 34 | { 35 | NatpmpAsync { s, gateway } 36 | } 37 | 38 | impl NatpmpAsync 39 | where 40 | S: AsyncUdpSocket, 41 | { 42 | /// NAT-PMP gateway address. 43 | pub fn gateway(&self) -> &Ipv4Addr { 44 | &self.gateway 45 | } 46 | 47 | /// Send public address request. 48 | /// 49 | /// # Errors 50 | /// * [`Error::NATPMP_ERR_SENDERR`](enum.Error.html#variant.NATPMP_ERR_SENDERR) 51 | /// 52 | /// # Examples 53 | /// ``` 54 | /// use natpmp::*; 55 | /// 56 | /// let mut n = new_tokio_natpmp().await?; 57 | /// n.send_public_address_request().await?; 58 | /// ``` 59 | pub async fn send_public_address_request(&mut self) -> Result<()> { 60 | let request = [0_u8; 2]; 61 | let n = self 62 | .s 63 | .send(&request[..]) 64 | .await 65 | .map_err(|_| Error::NATPMP_ERR_SENDERR)?; 66 | if n != request.len() { 67 | return Err(Error::NATPMP_ERR_SENDERR); 68 | } 69 | Ok(()) 70 | } 71 | 72 | /// Send port mapping request. 73 | /// 74 | /// # Errors 75 | /// * [`Error::NATPMP_ERR_SENDERR`](enum.Error.html#variant.NATPMP_ERR_SENDERR) 76 | /// 77 | /// # Examples 78 | /// ``` 79 | /// use natpmp::*; 80 | /// 81 | /// let mut n = new_tokio_natpmp().await?; 82 | /// n.send_port_mapping_request(Protocol::UDP, 4020, 4020, 30).await?; 83 | /// ``` 84 | pub async fn send_port_mapping_request( 85 | &self, 86 | protocol: Protocol, 87 | private_port: u16, 88 | public_port: u16, 89 | lifetime: u32, 90 | ) -> Result<()> { 91 | let mut request = [0_u8; 12]; 92 | request[1] = match protocol { 93 | Protocol::UDP => 1, 94 | _ => 2, 95 | }; 96 | request[2] = 0; // reserved 97 | request[3] = 0; 98 | // private port 99 | request[4] = (private_port >> 8 & 0xff) as u8; 100 | request[5] = (private_port & 0xff) as u8; 101 | // public port 102 | request[6] = (public_port >> 8 & 0xff) as u8; 103 | request[7] = (public_port & 0xff) as u8; 104 | // lifetime 105 | request[8] = ((lifetime >> 24) & 0xff) as u8; 106 | request[9] = ((lifetime >> 16) & 0xff) as u8; 107 | request[10] = ((lifetime >> 8) & 0xff) as u8; 108 | request[11] = (lifetime & 0xff) as u8; 109 | 110 | let n = self 111 | .s 112 | .send(&request[..]) 113 | .await 114 | .map_err(|_| Error::NATPMP_ERR_SENDERR)?; 115 | if n != request.len() { 116 | return Err(Error::NATPMP_ERR_SENDERR); 117 | } 118 | Ok(()) 119 | } 120 | 121 | /// Read NAT-PMP response if possible 122 | /// 123 | /// # Errors 124 | /// * [`Error::NATPMP_TRYAGAIN`](enum.Error.html#variant.NATPMP_TRYAGAIN) 125 | /// * [`Error::NATPMP_ERR_NOPENDINGREQ`](enum.Error.html#variant.NATPMP_ERR_NOPENDINGREQ) 126 | /// * [`Error::NATPMP_ERR_NOGATEWAYSUPPORT`](enum.Error.html#variant.NATPMP_ERR_NOGATEWAYSUPPORT) 127 | /// * [`Error::NATPMP_ERR_RECVFROM`](enum.Error.html#variant.NATPMP_ERR_RECVFROM) 128 | /// * [`Error::NATPMP_ERR_WRONGPACKETSOURCE`](enum.Error.html#variant.NATPMP_ERR_WRONGPACKETSOURCE) 129 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDVERSION`](enum.Error.html#variant.NATPMP_ERR_UNSUPPORTEDVERSION) 130 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDOPCODE`](enum.Error.html#variant.NATPMP_ERR_UNSUPPORTEDOPCODE) 131 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDVERSION`](enum.Error.html#variant.NATPMP_ERR_UNSUPPORTEDVERSION) 132 | /// * [`Error::NATPMP_ERR_NOTAUTHORIZED`](enum.Error.html#variant.NATPMP_ERR_NOTAUTHORIZED) 133 | /// * [`Error::NATPMP_ERR_NETWORKFAILURE`](enum.Error.html#variant.NATPMP_ERR_NETWORKFAILURE) 134 | /// * [`Error::NATPMP_ERR_OUTOFRESOURCES`](enum.Error.html#variant.NATPMP_ERR_OUTOFRESOURCES) 135 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDOPCODE`](enum.Error.html#variant.NATPMP_ERR_OUTOFRESOURCES) 136 | /// * [`Error::NATPMP_ERR_UNDEFINEDERROR`](enum.Error.html#variant.NATPMP_ERR_UNDEFINEDERROR) 137 | /// 138 | /// # Examples 139 | /// ``` 140 | /// use natpmp::*; 141 | /// 142 | /// let mut n = new_tokio_natpmp().await?; 143 | /// n.send_public_address_request().await?; 144 | /// let response = n.read_response_or_retry().await?; 145 | /// 146 | /// ``` 147 | pub async fn read_response_or_retry(&self) -> Result { 148 | let mut buf = [0_u8; 16]; 149 | let mut retries = 0; 150 | while retries < NATPMP_MAX_ATTEMPS { 151 | match self.s.recv(&mut buf).await { 152 | Err(_) => retries += 1, 153 | Ok(_) => { 154 | // version 155 | if buf[0] != 0 { 156 | return Err(Error::NATPMP_ERR_UNSUPPORTEDVERSION); 157 | } 158 | // opcode 159 | if buf[1] < 128 || buf[1] > 130 { 160 | return Err(Error::NATPMP_ERR_UNSUPPORTEDOPCODE); 161 | } 162 | // result code 163 | let resultcode = u16::from_be_bytes([buf[2], buf[3]]); 164 | // result 165 | if resultcode != 0 { 166 | return Err(match resultcode { 167 | 1 => Error::NATPMP_ERR_UNSUPPORTEDVERSION, 168 | 2 => Error::NATPMP_ERR_NOTAUTHORIZED, 169 | 3 => Error::NATPMP_ERR_NETWORKFAILURE, 170 | 4 => Error::NATPMP_ERR_OUTOFRESOURCES, 171 | 5 => Error::NATPMP_ERR_UNSUPPORTEDOPCODE, 172 | _ => Error::NATPMP_ERR_UNDEFINEDERROR, 173 | }); 174 | } 175 | // epoch 176 | let epoch = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]); 177 | let rsp_type = buf[1] & 0x7f; 178 | return Ok(match rsp_type { 179 | 0 => Response::Gateway(GatewayResponse { 180 | epoch, 181 | public_address: Ipv4Addr::from(u32::from_be_bytes([ 182 | buf[8], buf[9], buf[10], buf[11], 183 | ])), 184 | }), 185 | _ => { 186 | let private_port = u16::from_be_bytes([buf[8], buf[9]]); 187 | let public_port = u16::from_be_bytes([buf[10], buf[11]]); 188 | let lifetime = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]); 189 | let lifetime = Duration::from_secs(lifetime.into()); 190 | let m = MappingResponse { 191 | epoch, 192 | private_port, 193 | public_port, 194 | lifetime, 195 | }; 196 | if rsp_type == 1 { 197 | Response::UDP(m) 198 | } else { 199 | Response::TCP(m) 200 | } 201 | } 202 | }); 203 | } 204 | } 205 | } 206 | 207 | Err(Error::NATPMP_ERR_RECVFROM) 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # natpmp 2 | //! 3 | //! `natpmp` is a NAT-PMP [IETF RFC 6886](https://tools.ietf.org/html/rfc6886) client library in rust. 4 | //! It is a rust implementation of the c library [natpmp](https://github.com/miniupnp/natpmp). 5 | 6 | use std::io; 7 | use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket}; 8 | use std::ops::Add; 9 | use std::result; 10 | use std::time::{Duration, Instant}; 11 | 12 | use netdev; 13 | 14 | mod asynchronous; 15 | mod error; 16 | 17 | #[cfg(feature = "tokio")] 18 | mod a_tokio; 19 | #[cfg(feature = "tokio")] 20 | pub use a_tokio::*; 21 | 22 | #[cfg(feature = "async-std")] 23 | mod a_std; 24 | #[cfg(feature = "async-std")] 25 | pub use a_std::*; 26 | 27 | pub use crate::error::*; 28 | pub use asynchronous::*; 29 | 30 | /// NAT-PMP mini wait milli-seconds 31 | const NATPMP_MIN_WAIT: u64 = 250; 32 | 33 | /// NAT-PMP max retry 34 | const NATPMP_MAX_ATTEMPS: u32 = 9; 35 | 36 | /// NAT-PMP server port as defined by rfc6886. 37 | pub const NATPMP_PORT: u16 = 5351; 38 | 39 | /// NAT-PMP result. 40 | pub type Result = result::Result; 41 | 42 | /// Get default gateway. 43 | /// 44 | /// # Errors 45 | /// * [`Error::NATPMP_ERR_CANNOTGETGATEWAY`](enum.Error.html#variant.NATPMP_ERR_CANNOTGETGATEWAY) 46 | /// 47 | /// # Examples 48 | /// ``` 49 | /// use natpmp::*; 50 | /// 51 | /// let r = get_default_gateway(); 52 | /// assert_eq!(r.is_ok(), true); 53 | /// ``` 54 | pub fn get_default_gateway() -> Result { 55 | if let Ok(ipv4_addrs) = netdev::get_default_gateway().map(|g| g.ipv4) { 56 | if let Some(gw) = ipv4_addrs.get(0) { 57 | return Ok(gw.clone()); 58 | } 59 | } 60 | Err(Error::NATPMP_ERR_CANNOTGETGATEWAY) 61 | } 62 | 63 | /// NAT-PMP mapping protocol. 64 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 65 | pub enum Protocol { 66 | UDP, 67 | TCP, 68 | } 69 | 70 | /// NAT-PMP response type. 71 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] 72 | pub enum ResponseType { 73 | Gateway, 74 | UDP, 75 | TCP, 76 | } 77 | 78 | /// Gateway response. 79 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 80 | pub struct GatewayResponse { 81 | epoch: u32, 82 | public_address: Ipv4Addr, 83 | } 84 | 85 | impl GatewayResponse { 86 | /// Gateway public/external address. 87 | pub fn public_address(&self) -> &Ipv4Addr { 88 | &self.public_address 89 | } 90 | 91 | /// Seconds since epoch. 92 | /// 93 | /// **Note: May be not accurate.** 94 | pub fn epoch(&self) -> u32 { 95 | self.epoch 96 | } 97 | } 98 | 99 | /// Mapping response. 100 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 101 | pub struct MappingResponse { 102 | epoch: u32, 103 | private_port: u16, 104 | public_port: u16, 105 | lifetime: Duration, 106 | } 107 | 108 | impl MappingResponse { 109 | /// Seconds since epoch. 110 | /// 111 | /// **Note: May be not accurate.** 112 | pub fn epoch(&self) -> u32 { 113 | self.epoch 114 | } 115 | 116 | /// Private/internal port. 117 | pub fn private_port(&self) -> u16 { 118 | self.private_port 119 | } 120 | 121 | /// Public/external port. 122 | pub fn public_port(&self) -> u16 { 123 | self.public_port 124 | } 125 | 126 | /// Mapping lifetime. 127 | pub fn lifetime(&self) -> &Duration { 128 | &self.lifetime 129 | } 130 | } 131 | 132 | /// NAT-PMP response. 133 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 134 | pub enum Response { 135 | Gateway(GatewayResponse), 136 | UDP(MappingResponse), 137 | TCP(MappingResponse), 138 | } 139 | 140 | /// NAT-PMP main struct. 141 | /// 142 | /// # Examples 143 | /// ``` 144 | /// use std::thread; 145 | /// use std::time::Duration; 146 | /// use natpmp::*; 147 | /// 148 | /// # fn main() -> Result<()> { 149 | /// let mut n = Natpmp::new()?; 150 | /// n.send_port_mapping_request(Protocol::UDP, 4020, 4020, 30)?; 151 | /// thread::sleep(Duration::from_millis(100)); 152 | /// let response = n.read_response_or_retry()?; 153 | /// match response { 154 | /// Response::UDP(ur) => { 155 | /// assert_eq!(ur.private_port(), 4020); 156 | /// assert_eq!(ur.public_port(), 4020); 157 | /// } 158 | /// _ => panic!("Not a udp mapping response"), 159 | /// } 160 | /// # Ok(()) 161 | /// # } 162 | /// ``` 163 | #[derive(Debug)] 164 | pub struct Natpmp { 165 | s: UdpSocket, 166 | gateway: Ipv4Addr, 167 | has_pending_request: bool, 168 | pending_request: [u8; 12], 169 | pending_request_len: usize, 170 | try_number: u32, 171 | retry_time: Instant, 172 | } 173 | 174 | impl Natpmp { 175 | /// Create a NAT-PMP object with default gateway. 176 | /// 177 | /// # Errors 178 | /// See [`get_default_gateway`](fn.get_default_gateway.html) and [`Natpmp::new_with`](struct.Natpmp.html#method.new_with). 179 | /// 180 | /// # Examples 181 | /// ``` 182 | /// use natpmp::*; 183 | /// 184 | /// let n = Natpmp::new(); 185 | /// assert_eq!(n.is_ok(), true); 186 | /// ``` 187 | pub fn new() -> Result { 188 | let gateway = get_default_gateway()?; 189 | Natpmp::new_with(gateway) 190 | } 191 | 192 | /// Create a NAT-PMP object with a specified gateway. 193 | /// 194 | /// # Errors 195 | /// * [`Error::NATPMP_ERR_SOCKETERROR`](enum.Error.html#variant.NATPMP_ERR_SOCKETERROR) 196 | /// * [`Error::NATPMP_ERR_FCNTLERROR`](enum.Error.html#variant.NATPMP_ERR_FCNTLERROR) 197 | /// * [`Error::NATPMP_ERR_CONNECTERR`](enum.Error.html#variant.NATPMP_ERR_CONNECTERR) 198 | /// 199 | /// # Examples 200 | /// ``` 201 | /// use std::str::FromStr; 202 | /// use std::net::Ipv4Addr; 203 | /// use natpmp::*; 204 | /// 205 | /// let n = Natpmp::new_with("192.168.0.1".parse().unwrap()).unwrap(); 206 | /// ``` 207 | pub fn new_with(gateway: Ipv4Addr) -> Result { 208 | let s: UdpSocket; 209 | if let Ok(udpsock) = UdpSocket::bind("0.0.0.0:0") { 210 | s = udpsock; 211 | } else { 212 | return Err(Error::NATPMP_ERR_SOCKETERROR); 213 | } 214 | if s.set_nonblocking(true).is_err() { 215 | return Err(Error::NATPMP_ERR_FCNTLERROR); 216 | } 217 | let gateway_sockaddr = SocketAddrV4::new(gateway, NATPMP_PORT); 218 | if s.connect(gateway_sockaddr).is_err() { 219 | return Err(Error::NATPMP_ERR_CONNECTERR); 220 | } 221 | let n = Natpmp { 222 | s, 223 | gateway, 224 | has_pending_request: false, 225 | pending_request: [0u8; 12], 226 | pending_request_len: 0, 227 | try_number: 0, 228 | retry_time: Instant::now(), 229 | }; 230 | Ok(n) 231 | } 232 | 233 | /// NAT-PMP gateway address. 234 | /// 235 | /// # Examples 236 | /// ``` 237 | /// use std::net::Ipv4Addr; 238 | /// use natpmp::*; 239 | /// 240 | /// # fn main() -> Result<()> { 241 | /// let gateway = Ipv4Addr::from([192, 168, 0, 1]); 242 | /// let n = Natpmp::new_with(gateway)?; 243 | /// assert_eq!(n.gateway(), &gateway); 244 | /// # Ok(()) 245 | /// # } 246 | /// ``` 247 | pub fn gateway(&self) -> &Ipv4Addr { 248 | &self.gateway 249 | } 250 | 251 | fn send_pending_request(&self) -> Result<()> { 252 | if let Ok(n) = self 253 | .s 254 | .send(&self.pending_request[0..self.pending_request_len]) 255 | { 256 | if n == self.pending_request_len { 257 | return Ok(()); 258 | } 259 | } 260 | Err(Error::NATPMP_ERR_SENDERR) 261 | } 262 | 263 | fn send_natpmp_request(&mut self) -> Result<()> { 264 | self.has_pending_request = true; 265 | self.try_number = 1; 266 | let result = self.send_pending_request(); 267 | self.retry_time = Instant::now(); 268 | self.retry_time = self.retry_time.add(Duration::from_millis(250)); 269 | result 270 | } 271 | 272 | /// Get timeout duration of the currently pending NAT-PMP request. 273 | /// 274 | /// # Errors: 275 | /// * [`Error::NATPMP_ERR_NOPENDINGREQ`](enum.Error.html#variant.NATPMP_ERR_NOPENDINGREQ) 276 | /// 277 | /// # Examples 278 | /// ``` 279 | /// use std::time::Duration; 280 | /// use natpmp::*; 281 | /// 282 | /// # fn main() -> Result<()> { 283 | /// let mut n = Natpmp::new()?; 284 | /// n.send_public_address_request()?; 285 | /// // do something 286 | /// let duration = n.get_natpmp_request_timeout()?; 287 | /// if duration <= Duration::from_millis(10) { 288 | /// // read response ... 289 | /// } 290 | /// # Ok(()) 291 | /// # } 292 | /// ``` 293 | pub fn get_natpmp_request_timeout(&self) -> Result { 294 | if !self.has_pending_request { 295 | return Err(Error::NATPMP_ERR_NOPENDINGREQ); 296 | } 297 | let now = Instant::now(); 298 | if now > self.retry_time { 299 | return Ok(Duration::from_millis(0)); 300 | } 301 | let duration = self.retry_time - now; 302 | Ok(duration) 303 | } 304 | 305 | /// Send public address request. 306 | /// 307 | /// # Errors 308 | /// * [`Error::NATPMP_ERR_SENDERR`](enum.Error.html#variant.NATPMP_ERR_SENDERR) 309 | /// 310 | /// # Examples 311 | /// ``` 312 | /// use natpmp::*; 313 | /// 314 | /// # fn main() -> Result<()> { 315 | /// let mut n = Natpmp::new()?; 316 | /// n.send_public_address_request()?; 317 | /// // do something then read response 318 | /// # Ok(()) 319 | /// # } 320 | /// ``` 321 | pub fn send_public_address_request(&mut self) -> Result<()> { 322 | self.pending_request[0] = 0; 323 | self.pending_request[1] = 0; 324 | self.pending_request_len = 2; 325 | self.send_natpmp_request() 326 | } 327 | 328 | /// Send new port mapping request. 329 | /// 330 | /// # Errors 331 | /// * [`Error::NATPMP_ERR_SENDERR`](enum.Error.html#variant.NATPMP_ERR_SENDERR) 332 | /// 333 | /// # Examples 334 | /// ``` 335 | /// use natpmp::*; 336 | /// 337 | /// # fn main() -> Result<()> { 338 | /// let mut n = Natpmp::new()?; 339 | /// n.send_port_mapping_request(Protocol::UDP, 4020, 4020, 30)?; 340 | /// // do something then read response 341 | /// # Ok(()) 342 | /// # } 343 | /// ``` 344 | pub fn send_port_mapping_request( 345 | &mut self, 346 | protocol: Protocol, 347 | private_port: u16, 348 | public_port: u16, 349 | lifetime: u32, 350 | ) -> Result<()> { 351 | self.pending_request[0] = 0; 352 | self.pending_request[1] = match protocol { 353 | Protocol::UDP => 1, 354 | _ => 2, 355 | }; 356 | self.pending_request[2] = 0; // reserved 357 | self.pending_request[3] = 0; // reserved 358 | // private port 359 | self.pending_request[4] = (private_port >> 8 & 0xff) as u8; 360 | self.pending_request[5] = (private_port & 0xff) as u8; 361 | // public port 362 | self.pending_request[6] = (public_port >> 8 & 0xff) as u8; 363 | self.pending_request[7] = (public_port & 0xff) as u8; 364 | // lifetime 365 | self.pending_request[8] = ((lifetime >> 24) & 0xff) as u8; 366 | self.pending_request[9] = ((lifetime >> 16) & 0xff) as u8; 367 | self.pending_request[10] = ((lifetime >> 8) & 0xff) as u8; 368 | self.pending_request[11] = (lifetime & 0xff) as u8; 369 | self.pending_request_len = 12; 370 | self.send_natpmp_request() 371 | } 372 | 373 | fn read_response(&self) -> Result { 374 | let mut buf = [0u8; 16]; 375 | match self.s.recv_from(&mut buf) { 376 | Err(e) => match e.kind() { 377 | io::ErrorKind::WouldBlock => return Err(Error::NATPMP_TRYAGAIN), 378 | io::ErrorKind::ConnectionRefused => return Err(Error::NATPMP_ERR_NOGATEWAYSUPPORT), 379 | _ => { 380 | return Err(Error::NATPMP_ERR_RECVFROM); 381 | } 382 | }, 383 | Ok((_, sockaddr)) => { 384 | // check gateway address 385 | if let SocketAddr::V4(s) = sockaddr { 386 | if s.ip() != &self.gateway { 387 | return Err(Error::NATPMP_ERR_WRONGPACKETSOURCE); 388 | } 389 | } 390 | // version 391 | if buf[0] != 0 { 392 | return Err(Error::NATPMP_ERR_UNSUPPORTEDVERSION); 393 | } 394 | // opcode 395 | if buf[1] < 128 || buf[1] > 130 { 396 | return Err(Error::NATPMP_ERR_UNSUPPORTEDOPCODE); 397 | } 398 | // result code 399 | let resultcode = u16::from_be_bytes([buf[2], buf[3]]); 400 | if resultcode != 0 { 401 | return Err(match resultcode { 402 | 1 => Error::NATPMP_ERR_UNSUPPORTEDVERSION, 403 | 2 => Error::NATPMP_ERR_NOTAUTHORIZED, 404 | 3 => Error::NATPMP_ERR_NETWORKFAILURE, 405 | 4 => Error::NATPMP_ERR_OUTOFRESOURCES, 406 | 5 => Error::NATPMP_ERR_UNSUPPORTEDOPCODE, 407 | _ => Error::NATPMP_ERR_UNDEFINEDERROR, 408 | }); 409 | } 410 | // epoch 411 | let epoch = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]); 412 | // result 413 | let rsp_type = buf[1] & 0x7f; 414 | return Ok(match rsp_type { 415 | 0 => Response::Gateway(GatewayResponse { 416 | epoch, 417 | public_address: Ipv4Addr::from(u32::from_be_bytes([ 418 | buf[8], buf[9], buf[10], buf[11], 419 | ])), 420 | }), 421 | _ => { 422 | let private_port = u16::from_be_bytes([buf[8], buf[9]]); 423 | let public_port = u16::from_be_bytes([buf[10], buf[11]]); 424 | let lifetime = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]); 425 | let lifetime = Duration::from_secs(lifetime.into()); 426 | let m = MappingResponse { 427 | epoch, 428 | private_port, 429 | public_port, 430 | lifetime, 431 | }; 432 | if rsp_type == 1 { 433 | Response::UDP(m) 434 | } else { 435 | Response::TCP(m) 436 | } 437 | } 438 | }); 439 | } 440 | } 441 | } 442 | 443 | /// Read NAT-PMP response if possible 444 | /// 445 | /// # Errors 446 | /// * [`Error::NATPMP_TRYAGAIN`](enum.Error.html#variant.NATPMP_TRYAGAIN) 447 | /// * [`Error::NATPMP_ERR_NOPENDINGREQ`](enum.Error.html#variant.NATPMP_ERR_NOPENDINGREQ) 448 | /// * [`Error::NATPMP_ERR_NOGATEWAYSUPPORT`](enum.Error.html#variant.NATPMP_ERR_NOGATEWAYSUPPORT) 449 | /// * [`Error::NATPMP_ERR_RECVFROM`](enum.Error.html#variant.NATPMP_ERR_RECVFROM) 450 | /// * [`Error::NATPMP_ERR_WRONGPACKETSOURCE`](enum.Error.html#variant.NATPMP_ERR_WRONGPACKETSOURCE) 451 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDVERSION`](enum.Error.html#variant.NATPMP_ERR_UNSUPPORTEDVERSION) 452 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDOPCODE`](enum.Error.html#variant.NATPMP_ERR_UNSUPPORTEDOPCODE) 453 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDVERSION`](enum.Error.html#variant.NATPMP_ERR_UNSUPPORTEDVERSION) 454 | /// * [`Error::NATPMP_ERR_NOTAUTHORIZED`](enum.Error.html#variant.NATPMP_ERR_NOTAUTHORIZED) 455 | /// * [`Error::NATPMP_ERR_NETWORKFAILURE`](enum.Error.html#variant.NATPMP_ERR_NETWORKFAILURE) 456 | /// * [`Error::NATPMP_ERR_OUTOFRESOURCES`](enum.Error.html#variant.NATPMP_ERR_OUTOFRESOURCES) 457 | /// * [`Error::NATPMP_ERR_UNSUPPORTEDOPCODE`](enum.Error.html#variant.NATPMP_ERR_OUTOFRESOURCES) 458 | /// * [`Error::NATPMP_ERR_UNDEFINEDERROR`](enum.Error.html#variant.NATPMP_ERR_UNDEFINEDERROR) 459 | /// 460 | /// # Examples 461 | /// ``` 462 | /// use std::thread; 463 | /// use std::time::Duration; 464 | /// use natpmp::*; 465 | /// 466 | /// # fn main() -> Result<()> { 467 | /// let mut n = Natpmp::new()?; 468 | /// n.send_public_address_request()?; 469 | /// thread::sleep(Duration::from_millis(250)); 470 | /// let response = n.read_response_or_retry()?; 471 | /// # Ok(()) 472 | /// # } 473 | /// 474 | /// ``` 475 | pub fn read_response_or_retry(&mut self) -> Result { 476 | if !self.has_pending_request { 477 | return Err(Error::NATPMP_ERR_NOPENDINGREQ); 478 | } 479 | let result = self.read_response(); 480 | if let Err(e) = result { 481 | match e { 482 | Error::NATPMP_TRYAGAIN => { 483 | let now = Instant::now(); 484 | // time to retry or not 485 | if now >= self.retry_time { 486 | if self.try_number >= NATPMP_MAX_ATTEMPS { 487 | return Err(Error::NATPMP_ERR_NOGATEWAYSUPPORT); 488 | } 489 | // double dealy 490 | let delay = NATPMP_MIN_WAIT * (1 << self.try_number); // ms 491 | self.retry_time = self.retry_time.add(Duration::from_millis(delay)); // next time 492 | self.try_number += 1; 493 | self.send_pending_request()?; 494 | } 495 | } 496 | _ => return Err(e), 497 | } 498 | } 499 | result 500 | } 501 | } 502 | 503 | #[cfg(test)] 504 | mod tests { 505 | use std::thread; 506 | use std::time::Duration; 507 | 508 | use super::*; 509 | 510 | #[test] 511 | fn test_ffi() { 512 | assert!(get_default_gateway().is_ok()); 513 | } 514 | 515 | #[test] 516 | fn test_natpmp() -> Result<()> { 517 | assert!(Natpmp::new().is_ok()); 518 | let addr = "192.168.0.1".parse().unwrap(); 519 | let n = Natpmp::new_with(addr)?; 520 | assert_eq!(*n.gateway(), addr); 521 | Ok(()) 522 | } 523 | 524 | #[test] 525 | fn test_get_public_address() -> Result<()> { 526 | let mut n = Natpmp::new()?; 527 | n.send_public_address_request()?; 528 | thread::sleep(Duration::from_millis(250)); 529 | let r = n.read_response_or_retry()?; 530 | match r { 531 | Response::Gateway(_) => {} 532 | _ => panic!("Not a gateway response"), 533 | } 534 | Ok(()) 535 | } 536 | 537 | #[test] 538 | fn test_tcp_mapping() -> Result<()> { 539 | let mut n = Natpmp::new()?; 540 | n.send_port_mapping_request(Protocol::TCP, 14020, 14020, 10)?; 541 | thread::sleep(Duration::from_millis(250)); 542 | let r = n.read_response_or_retry()?; 543 | match r { 544 | Response::TCP(tr) => { 545 | assert_eq!(tr.private_port(), 14020); 546 | assert_eq!(tr.public_port(), 14020); 547 | } 548 | _ => panic!("Not a tcp mapping response"), 549 | } 550 | Ok(()) 551 | } 552 | 553 | #[test] 554 | fn test_udp_mapping() -> Result<()> { 555 | let mut n = Natpmp::new()?; 556 | n.send_port_mapping_request(Protocol::UDP, 14020, 14020, 10)?; 557 | thread::sleep(Duration::from_millis(250)); 558 | let r = n.read_response_or_retry()?; 559 | match r { 560 | Response::UDP(ur) => { 561 | assert_eq!(ur.private_port(), 14020); 562 | assert_eq!(ur.public_port(), 14020); 563 | } 564 | _ => panic!("Not a udp mapping response"), 565 | } 566 | Ok(()) 567 | } 568 | 569 | #[test] 570 | fn test_error() -> Result<()> { 571 | let mut n: Natpmp = Natpmp::new()?; 572 | n.send_port_mapping_request(Protocol::UDP, 14020, 14020, 30)?; 573 | thread::sleep(Duration::from_millis(250)); 574 | n.read_response_or_retry()?; 575 | 576 | n.send_port_mapping_request(Protocol::UDP, 14021, 14020, 10)?; 577 | thread::sleep(Duration::from_millis(250)); 578 | match n.read_response_or_retry() { 579 | Ok(Response::UDP(ur)) => { 580 | assert_ne!(ur.public_port(), 14020); 581 | } 582 | Ok(_) => panic!("Not a udp mapping response!"), 583 | Err(_) => {} 584 | } 585 | Ok(()) 586 | } 587 | } 588 | --------------------------------------------------------------------------------