├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── nat_behavior_discovery.rs ├── stun_client.rs └── udp_hole_punching.rs └── src ├── client.rs ├── error.rs ├── lib.rs ├── message.rs └── nat_behavior_discovery.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Run tests 20 | run: cargo test --verbose 21 | - name: Run examples 22 | run: cargo run --example stun_client --verbose 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stun-client" 3 | license = "MIT" 4 | version = "0.1.4" 5 | authors = ["yoshd "] 6 | edition = "2018" 7 | readme = "README.md" 8 | description = "This is a simple async_std based asynchronous STUN client library." 9 | repository = "https://github.com/yoshd/stun-client.git" 10 | documentation = "https://docs.rs/stun-client/" 11 | categories = ["asynchronous", "network-programming"] 12 | keywords = ["stun", "nat", "p2p", "udp"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | async-macros = "2.0.0" 18 | async-std = "1.9.0" 19 | futures = "0.3.14" 20 | pnet = "0.33" 21 | rand = "0.8.3" 22 | thiserror = "1.0.24" 23 | 24 | [dev-dependencies] 25 | anyhow = "1.0.40" 26 | redis = { version = "0.20.0", features = ["async-std-comp"] } 27 | futures-util = "0.3.14" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 yoshd 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 | ![test](https://github.com/yoshd/stun-client/workflows/Test/badge.svg) 2 | 3 | # stun-client 4 | 5 | This is a simple async_std based asynchronous STUN client library. 6 | At the moment only some features of [RFC8489](https://tools.ietf.org/html/rfc8489) are implemented and only simple binding requests are possible. 7 | 8 | It also supports the OTHER-ADDRESS and CHANGE-REQUEST attributes for [RFC5780](https://tools.ietf.org/html/rfc5780) -based NAT Behavior Discovery. 9 | 10 | [Install](https://crates.io/crates/stun-client) 11 | 12 | [Documentation](https://docs.rs/stun-client/) 13 | 14 | ## Examples 15 | 16 | - [Simple STUN Binding](examples/stun_client.rs) 17 | - [NAT Behavior Discovery](examples/nat_behavior_discovery.rs) 18 | - [UDP Hole Punching](examples/udp_hole_punching.rs) 19 | 20 | # Running on Windows 21 | 22 | Due to the requirements of the dependency library libpnet, additional steps are needed. 23 | Please refer to the following. 24 | 25 | https://github.com/libpnet/libpnet#windows 26 | -------------------------------------------------------------------------------- /examples/nat_behavior_discovery.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | 3 | use anyhow::Error; 4 | use async_std::net::ToSocketAddrs; 5 | use async_std::task; 6 | 7 | use stun_client::*; 8 | 9 | fn main() -> Result<(), Error> { 10 | let args: Vec = env::args().collect(); 11 | if args.len() != 2 { 12 | println!("Usage: cargo run --example nat_behavior_discovery -- "); 13 | panic!("invalid argument"); 14 | } 15 | 16 | let stun_addr = &args[1]; 17 | task::block_on(async { 18 | nat_behavior_discovery(stun_addr).await.unwrap(); 19 | }); 20 | Ok(()) 21 | } 22 | 23 | async fn nat_behavior_discovery(stun_addr: A) -> Result<(), Error> { 24 | let mut client = Client::new("0.0.0.0:0", None).await?; 25 | let result = 26 | stun_client::nat_behavior_discovery::check_nat_mapping_behavior(&mut client, &stun_addr) 27 | .await?; 28 | println!("NAT Mapping Type: {:?}", result.mapping_type); 29 | 30 | let mut client = Client::new("0.0.0.0:0", None).await?; 31 | let result = 32 | stun_client::nat_behavior_discovery::check_nat_filtering_behavior(&mut client, &stun_addr) 33 | .await?; 34 | println!("NAT Filtering Type: {:?}", result.filtering_type); 35 | Ok(()) 36 | } 37 | -------------------------------------------------------------------------------- /examples/stun_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Error}; 2 | use async_std::task; 3 | 4 | use stun_client::*; 5 | 6 | fn main() -> Result<(), Error> { 7 | task::block_on(async { 8 | stun_binding().await.unwrap(); 9 | }); 10 | Ok(()) 11 | } 12 | 13 | async fn stun_binding() -> Result<(), Error> { 14 | let mut client = Client::new("0.0.0.0:0", None).await?; 15 | let res = client 16 | .binding_request("stun.l.google.com:19302", None) 17 | .await?; 18 | 19 | let class = res.get_class(); 20 | match class { 21 | Class::SuccessResponse => { 22 | let xor_mapped_addr = Attribute::get_xor_mapped_address(&res); 23 | println!("XOR-MAPPED-ADDRESS: {}", xor_mapped_addr.unwrap()); 24 | Ok(()) 25 | } 26 | _ => Err(anyhow!(format!("failed to request. class: {:?}", class))), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/udp_hole_punching.rs: -------------------------------------------------------------------------------- 1 | //! This sample is a sample that tries Peer-to-Peer communication with reference to RFC5128. 2 | //! Not all NAT types will succeed. 3 | //! Since Redis is used as a signaling server, please prepare Redis yourself when executing it. 4 | //! Also, since this sample does P2P within the same process, it communicates by hairpinning. 5 | use std::env; 6 | use std::sync::atomic::{AtomicBool, Ordering}; 7 | use std::sync::Arc; 8 | use std::time::Duration; 9 | 10 | use anyhow::{anyhow, Error}; 11 | use async_macros::join; 12 | use async_std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; 13 | use async_std::{future, task}; 14 | use futures::channel::mpsc; 15 | use futures::stream::StreamExt; 16 | use futures::SinkExt; 17 | use redis::AsyncCommands; 18 | 19 | use stun_client::nat_behavior_discovery::*; 20 | use stun_client::*; 21 | 22 | fn main() -> Result<(), Error> { 23 | let args: Vec = env::args().collect(); 24 | if args.len() != 3 { 25 | println!("Usage: cargo run --example udp_hole_punching -- "); 26 | panic!("invalid argument"); 27 | } 28 | 29 | let stun_addr = &args[1]; 30 | let redis_addr = &args[2]; 31 | task::block_on(async { 32 | let p1 = String::from("p1"); 33 | let p2 = String::from("p2"); 34 | let t1 = run( 35 | p1.clone(), 36 | p2.clone(), 37 | redis_addr.clone(), 38 | stun_addr.clone(), 39 | ); 40 | let t2 = run( 41 | p2.clone(), 42 | p1.clone(), 43 | redis_addr.clone(), 44 | stun_addr.clone(), 45 | ); 46 | join!(t1, t2).await; 47 | }); 48 | Ok(()) 49 | } 50 | 51 | async fn run(peer_name: String, opponent_name: String, redis_addr: String, stun_addr: String) { 52 | let t = task::spawn(async move { 53 | let peer = Peer::new(String::from(peer_name), redis_addr.to_string()).await; 54 | let (nmt, nft) = peer.nat_behavior_discovery(stun_addr).await.unwrap(); 55 | println!( 56 | "{:?}: NAT Mapping Type={:?}, NAT Filtering Type={:?}", 57 | peer.get_name(), 58 | nmt.mapping_type, 59 | nft.filtering_type 60 | ); 61 | 62 | let mut addr_candidates = vec![]; 63 | match nmt.mapping_type { 64 | NATMappingType::NoNAT | NATMappingType::EndpointIndependent => { 65 | addr_candidates.push(nmt.test1_xor_mapped_addr.unwrap().to_string()); 66 | } 67 | NATMappingType::AddressDependent => { 68 | let mut candidate = nmt.test2_xor_mapped_addr.unwrap().clone(); 69 | // "N+1" technique 70 | candidate.set_port(candidate.port() + 1); 71 | addr_candidates.push(candidate.to_string()); 72 | } 73 | NATMappingType::AddressAndPortDependent => { 74 | let mut candidate = nmt.test3_xor_mapped_addr.unwrap().clone(); 75 | // // "N+1" technique 76 | candidate.set_port(candidate.port() + 1); 77 | addr_candidates.push(candidate.to_string()); 78 | } 79 | NATMappingType::Unknown => { 80 | panic!("unknown NAT type"); 81 | } 82 | } 83 | 84 | let opponent_candidates = peer 85 | .signalling(String::from(opponent_name), addr_candidates) 86 | .await 87 | .unwrap(); 88 | let opponent_peer = peer.hole_punching(opponent_candidates).await.unwrap(); 89 | println!( 90 | "{}: opponent peer address: {:?}", 91 | peer.get_name(), 92 | opponent_peer 93 | ); 94 | peer.send_message_p2p(opponent_peer).await.unwrap(); 95 | }); 96 | t.await; 97 | } 98 | 99 | struct Peer { 100 | name: String, 101 | socket: Arc, 102 | redis_client: redis::Client, 103 | } 104 | 105 | impl Peer { 106 | pub async fn new(name: String, redis_addr: String) -> Self { 107 | let socket = UdpSocket::bind("0.0.0.0:0").await.unwrap(); 108 | let socket = Arc::new(socket); 109 | let redis_client = redis::Client::open(format!("redis://{}/", redis_addr)).unwrap(); 110 | Peer { 111 | name: name, 112 | socket: socket, 113 | redis_client: redis_client, 114 | } 115 | } 116 | 117 | pub fn get_name(&self) -> String { 118 | self.name.clone() 119 | } 120 | 121 | pub async fn nat_behavior_discovery( 122 | &self, 123 | stun_addr: A, 124 | ) -> Result<(NATMappingTypeResult, NATFilteringTypeResult), Error> { 125 | let mut client = Client::from_socket(self.socket.clone(), None); 126 | // If the Filtering Type is not executed first, the Mapping Type check will create a temporary NAT entry for OTHER-ADDRESS. 127 | let result_ft = stun_client::nat_behavior_discovery::check_nat_filtering_behavior( 128 | &mut client, 129 | &stun_addr, 130 | ) 131 | .await?; 132 | let result_mt = stun_client::nat_behavior_discovery::check_nat_mapping_behavior( 133 | &mut client, 134 | &stun_addr, 135 | ) 136 | .await?; 137 | Ok((result_mt, result_ft)) 138 | } 139 | 140 | pub async fn signalling( 141 | &self, 142 | opponent_peer_channel: String, 143 | addr_candidates: Vec, 144 | ) -> Result, Error> { 145 | let mut publish_conn = self.redis_client.get_async_connection().await?; 146 | let mut pubsub_conn = self 147 | .redis_client 148 | .get_async_connection() 149 | .await? 150 | .into_pubsub(); 151 | pubsub_conn.subscribe(&opponent_peer_channel).await?; 152 | let mut pubsub_stream = pubsub_conn.on_message(); 153 | 154 | loop { 155 | let result: redis::RedisResult<()> = publish_conn.publish(&self.name, "Ready").await; 156 | result.unwrap(); 157 | let msg: String = pubsub_stream.next().await.unwrap().get_payload().unwrap(); 158 | let result: redis::RedisResult<()> = publish_conn.publish(&self.name, "Ready").await; 159 | result.unwrap(); 160 | if msg == "Ready" { 161 | break; 162 | } 163 | } 164 | 165 | for addr in addr_candidates { 166 | let result: redis::RedisResult<()> = publish_conn.publish(&self.name, addr).await; 167 | result.unwrap(); 168 | } 169 | 170 | let result: redis::RedisResult<()> = publish_conn.publish(&self.name, "Finish").await; 171 | result.unwrap(); 172 | 173 | let mut opponent_candidates = vec![]; 174 | loop { 175 | let msg: String = pubsub_stream.next().await.unwrap().get_payload()?; 176 | if msg == "Finish" { 177 | break; 178 | } 179 | 180 | if msg == "Ready" { 181 | continue; 182 | } 183 | 184 | opponent_candidates.push(msg) 185 | } 186 | 187 | Ok(opponent_candidates) 188 | } 189 | 190 | pub async fn hole_punching( 191 | &self, 192 | opponent_candidates: Vec, 193 | ) -> Result { 194 | let (mut tx, mut rx) = mpsc::channel(1); 195 | let sock = self.socket.clone(); 196 | task::spawn(async move { 197 | let mut buf = vec![0u8; 128]; 198 | let (_, peer) = sock.recv_from(&mut buf).await.unwrap(); 199 | tx.send(peer).await.unwrap(); 200 | }); 201 | 202 | let running = Arc::new(AtomicBool::new(true)); 203 | let running_task = running.clone(); 204 | let sock = self.socket.clone(); 205 | task::spawn(async move { 206 | while running_task.load(Ordering::Relaxed) { 207 | for candidate in &opponent_candidates { 208 | sock.send_to("test".as_bytes(), candidate).await.unwrap(); 209 | } 210 | task::sleep(Duration::from_secs(1)).await; 211 | } 212 | }); 213 | 214 | let peer = future::timeout(Duration::from_secs(10), rx.next()) 215 | .await 216 | .map_err(|_| anyhow!("P2P was not established."))? 217 | .unwrap(); 218 | running.store(false, Ordering::Relaxed); 219 | 220 | Ok(peer) 221 | } 222 | 223 | pub async fn send_message_p2p(&self, opponent_peer: SocketAddr) -> Result<(), Error> { 224 | let msg = format!("Hello, I'm {}.", self.name); 225 | for _ in 0i32..10 { 226 | self.socket.send_to(msg.as_bytes(), opponent_peer).await?; 227 | let mut buf = vec![0u8; 128]; 228 | let (n, _) = self.socket.recv_from(&mut buf).await?; 229 | println!("{}", String::from_utf8(buf[..n].to_vec())?); 230 | task::sleep(Duration::from_secs(1)).await; 231 | } 232 | 233 | Ok(()) 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/client.rs: -------------------------------------------------------------------------------- 1 | //! This module is a thread-safe async-std-based asynchronous STUN client. 2 | use std::collections::HashMap; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | use std::sync::{Arc, Mutex}; 5 | use std::time::Duration; 6 | 7 | use async_macros::select; 8 | use async_std::future; 9 | use async_std::net::{SocketAddr, ToSocketAddrs, UdpSocket}; 10 | use async_std::task; 11 | use futures::channel::mpsc; 12 | use futures::stream::StreamExt; 13 | use futures::SinkExt; 14 | 15 | use super::error::*; 16 | use super::message::*; 17 | 18 | const DEFAULT_RECV_TIMEOUT_MS: u64 = 3000; 19 | const DEFAULT_RECV_BUF_SIZE: usize = 1024; 20 | 21 | /// STUN client options. 22 | #[derive(Clone, Debug)] 23 | pub struct Options { 24 | pub recv_timeout_ms: u64, 25 | pub recv_buf_size: usize, 26 | } 27 | 28 | /// STUN client. 29 | /// The transport protocol is UDP only and only supports simple STUN Binding requests. 30 | pub struct Client { 31 | socket: Arc, 32 | recv_timeout_ms: u64, 33 | transactions: Arc, mpsc::Sender>>>>, 34 | running: Arc, 35 | stop_tx: mpsc::Sender, 36 | } 37 | 38 | impl Client { 39 | /// Create a Client. 40 | pub async fn new( 41 | local_bind_addr: A, 42 | opts: Option, 43 | ) -> Result { 44 | let socket = UdpSocket::bind(local_bind_addr) 45 | .await 46 | .map_err(|e| STUNClientError::IOError(e))?; 47 | let socket = Arc::new(socket); 48 | let transactions = Arc::new(Mutex::new(HashMap::new())); 49 | let running = Arc::new(AtomicBool::new(true)); 50 | let (tx, rx) = mpsc::channel(1); 51 | let recv_timeout_ms = opts 52 | .clone() 53 | .map(|o| o.recv_timeout_ms) 54 | .unwrap_or_else(|| DEFAULT_RECV_TIMEOUT_MS); 55 | let client = Client { 56 | socket: socket.clone(), 57 | recv_timeout_ms: recv_timeout_ms, 58 | transactions: transactions.clone(), 59 | running: running.clone(), 60 | stop_tx: tx, 61 | }; 62 | 63 | let recv_buf_size = opts 64 | .map(|o| o.recv_buf_size) 65 | .unwrap_or_else(|| DEFAULT_RECV_BUF_SIZE); 66 | task::spawn(async move { 67 | Self::run_message_receiver(socket, recv_buf_size, running, rx, transactions).await 68 | }); 69 | Ok(client) 70 | } 71 | 72 | /// Create a Client from async_std::net::UdpSocket. 73 | pub fn from_socket(socket: Arc, opts: Option) -> Client { 74 | let transactions = Arc::new(Mutex::new(HashMap::new())); 75 | let running = Arc::new(AtomicBool::new(true)); 76 | let (tx, rx) = mpsc::channel(1); 77 | let recv_timeout_ms = opts 78 | .clone() 79 | .map(|o| o.recv_timeout_ms) 80 | .unwrap_or_else(|| DEFAULT_RECV_TIMEOUT_MS); 81 | let client = Client { 82 | socket: socket.clone(), 83 | recv_timeout_ms: recv_timeout_ms, 84 | transactions: transactions.clone(), 85 | running: running.clone(), 86 | stop_tx: tx, 87 | }; 88 | 89 | let recv_buf_size = opts 90 | .map(|o| o.recv_buf_size) 91 | .unwrap_or_else(|| DEFAULT_RECV_BUF_SIZE); 92 | task::spawn(async move { 93 | Self::run_message_receiver(socket, recv_buf_size, running, rx, transactions).await 94 | }); 95 | client 96 | } 97 | 98 | /// Send STUN Binding request asynchronously. 99 | pub async fn binding_request( 100 | &mut self, 101 | stun_addr: A, 102 | attrs: Option>>, 103 | ) -> Result { 104 | let msg = Message::new(Method::Binding, Class::Request, attrs); 105 | let (tx, mut rx) = mpsc::channel(1); 106 | { 107 | let mut m = self.transactions.lock().unwrap(); 108 | m.insert(msg.get_transaction_id(), tx); 109 | } 110 | let raw_msg = msg.to_raw(); 111 | self.socket 112 | .send_to(&raw_msg, stun_addr) 113 | .await 114 | .map_err(|e| STUNClientError::IOError(e))?; 115 | 116 | let fut = rx.next(); 117 | let res = future::timeout(Duration::from_millis(self.recv_timeout_ms), fut) 118 | .await 119 | .map_err(|_| STUNClientError::TimeoutError())? 120 | .ok_or(STUNClientError::Unknown(String::from( 121 | "Receive stream terminated unintentionally", 122 | )))?; 123 | 124 | { 125 | let mut m = self.transactions.lock().unwrap(); 126 | m.remove(&msg.get_transaction_id()); 127 | } 128 | 129 | res 130 | } 131 | 132 | async fn run_message_receiver( 133 | socket: Arc, 134 | recv_buf_size: usize, 135 | running: Arc, 136 | rx: mpsc::Receiver, 137 | transactions: Arc, mpsc::Sender>>>>, 138 | ) { 139 | let mut rx = rx; 140 | while running.load(Ordering::Relaxed) { 141 | let mut buf = vec![0u8; recv_buf_size]; 142 | let sock_fut = Self::socket_recv(socket.clone(), &mut buf); 143 | let stop_fut = Self::stop_recv(&mut rx); 144 | let result = select!(sock_fut, stop_fut).await; 145 | 146 | let socket_recv_result; 147 | match result { 148 | Event::Stop(_) => return, 149 | Event::Socket(ev) => { 150 | socket_recv_result = ev; 151 | } 152 | } 153 | 154 | let result = socket_recv_result.map_err(|e| STUNClientError::IOError(e)); 155 | match result { 156 | Ok(result) => { 157 | let msg = Message::from_raw(&buf[..result.0]); 158 | match msg { 159 | Ok(msg) => { 160 | let tx: Option>>; 161 | { 162 | // It's a bug if you panic with this unwrap 163 | let transactions = transactions.lock().unwrap(); 164 | tx = transactions 165 | .get(&msg.get_transaction_id()) 166 | .map(|v| v.clone()); 167 | } 168 | if let Some(mut tx) = tx { 169 | tx.send(Ok(msg)).await.ok(); 170 | } 171 | } 172 | Err(e) => { 173 | let transactions_unlocked: Option< 174 | HashMap, mpsc::Sender>>, 175 | >; 176 | { 177 | // It's a bug if you panic with this unwrap 178 | let t = transactions.lock().unwrap(); 179 | transactions_unlocked = Some(t.clone()); 180 | } 181 | if let Some(transactions_unlocked) = transactions_unlocked { 182 | for (_, transaction) in transactions_unlocked.iter() { 183 | let mut transaction = transaction.clone(); 184 | transaction.send(Err(e.clone())).await.ok(); 185 | } 186 | } 187 | } 188 | } 189 | } 190 | Err(e) => { 191 | let transactions_unlocked: Option< 192 | HashMap, mpsc::Sender>>, 193 | >; 194 | { 195 | // It's a bug if you panic with this unwrap 196 | let t = transactions.lock().unwrap(); 197 | transactions_unlocked = Some(t.clone()); 198 | } 199 | if let Some(transactions_unlocked) = transactions_unlocked { 200 | for transaction in transactions_unlocked.iter() { 201 | let mut transaction = transaction.1.clone(); 202 | transaction.send(Err(e.clone())).await.ok(); 203 | } 204 | } 205 | } 206 | } 207 | } 208 | } 209 | 210 | async fn socket_recv(socket: Arc, buf: &mut [u8]) -> Event { 211 | let result = socket.recv_from(buf).await; 212 | Event::Socket(result) 213 | } 214 | 215 | async fn stop_recv(rx: &mut mpsc::Receiver) -> Event { 216 | Event::Stop(rx.next().await.unwrap_or_else(|| true)) 217 | } 218 | } 219 | 220 | impl Drop for Client { 221 | fn drop(&mut self) { 222 | self.running.store(false, Ordering::Relaxed); 223 | let mut tx = self.stop_tx.clone(); 224 | task::spawn(async move { 225 | tx.send(true).await.ok(); 226 | }); 227 | } 228 | } 229 | 230 | enum Event { 231 | Socket(Result<(usize, SocketAddr), std::io::Error>), 232 | Stop(bool), 233 | } 234 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | /// Defines the error used by the stun_client library. 4 | #[derive(Error, Debug)] 5 | pub enum STUNClientError { 6 | #[error("cannot parse as STUN message")] 7 | ParseError(), 8 | #[error(transparent)] 9 | IOError(#[from] std::io::Error), 10 | #[error("not supported by the server: {0}")] 11 | NotSupportedError(String), 12 | #[error("request timeout")] 13 | TimeoutError(), 14 | #[error("unknown error: {0}")] 15 | Unknown(String), 16 | } 17 | 18 | impl Clone for STUNClientError { 19 | fn clone(&self) -> Self { 20 | match self { 21 | Self::ParseError() => Self::ParseError(), 22 | Self::IOError(e) => Self::IOError(std::io::Error::new(e.kind(), e.to_string())), 23 | Self::NotSupportedError(msg) => Self::NotSupportedError(msg.clone()), 24 | Self::TimeoutError() => Self::TimeoutError(), 25 | Self::Unknown(msg) => Self::Unknown(msg.clone()), 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This is a simple async_std based asynchronous STUN client library. 2 | //! At the moment only some features of [RFC8489](https://tools.ietf.org/html/rfc8489) are implemented and only simple binding requests are possible. 3 | //! 4 | //! It also supports the OTHER-ADDRESS and CHANGE-REQUEST attributes for [RFC5780](https://tools.ietf.org/html/rfc5780) -based NAT Behavior Discovery 5 | //! 6 | //! ## Example 7 | //! 8 | //! ``` 9 | //! use async_std::task; 10 | //! use stun_client::*; 11 | //! 12 | //! task::block_on(async { 13 | //! let mut client = Client::new("0.0.0.0:0", None).await.unwrap(); 14 | //! let res = client 15 | //! .binding_request("stun.l.google.com:19302", None) 16 | //! .await 17 | //! .unwrap(); 18 | //! let class = res.get_class(); 19 | //! match class { 20 | //! Class::SuccessResponse => { 21 | //! let xor_mapped_addr = Attribute::get_xor_mapped_address(&res); 22 | //! println!("XOR-MAPPED-ADDRESS: {}", xor_mapped_addr.unwrap()); 23 | //! }, 24 | //! _ => panic!("error"), 25 | //! } 26 | //! }); 27 | //! ``` 28 | 29 | mod client; 30 | mod error; 31 | mod message; 32 | pub mod nat_behavior_discovery; 33 | 34 | pub use client::*; 35 | pub use error::*; 36 | pub use message::*; 37 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | //! This module implements some of the STUN protocol message processing based on RFC 8489 and RFC 5780. 2 | use std::collections::HashMap; 3 | use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; 4 | 5 | use rand::{thread_rng, Rng}; 6 | 7 | use super::error::*; 8 | 9 | /// Magic cookie 10 | pub const MAGIC_COOKIE: u32 = 0x2112A442; 11 | 12 | // Methods 13 | /// Binding method 14 | pub const METHOD_BINDING: u16 = 0x0001; 15 | 16 | // Classes 17 | /// A constant that represents a class request 18 | pub const CLASS_REQUEST: u16 = 0x0000; 19 | /// A constant that represents a class indication 20 | pub const CLASS_INDICATION: u16 = 0x0010; 21 | /// A constant that represents a class success response 22 | pub const CLASS_SUCCESS_RESPONSE: u16 = 0x0100; 23 | /// A constant that represents a class error response 24 | pub const CLASS_ERROR_RESPONSE: u16 = 0x0110; 25 | 26 | /// STUN header size 27 | pub const HEADER_BYTE_SIZE: usize = 20; 28 | 29 | // STUN Attributes 30 | /// MAPPED-ADDRESS attribute 31 | pub const ATTR_MAPPED_ADDRESS: u16 = 0x0001; 32 | /// XOR-MAPPED-ADDRESS attribute 33 | pub const ATTR_XOR_MAPPED_ADDRESS: u16 = 0x0020; 34 | /// ERROR-CODE attribute 35 | pub const ATTR_ERROR_CODE: u16 = 0x0009; 36 | /// SOFTWARE attribute 37 | pub const ATTR_SOFTWARE: u16 = 0x8022; 38 | 39 | // RFC 5780 NAT Behavior Discovery 40 | /// OTHER-ADDRESS attribute 41 | pub const ATTR_OTHER_ADDRESS: u16 = 0x802c; 42 | /// CHANGE-REQUEST attribute 43 | pub const ATTR_CHANGE_REQUEST: u16 = 0x0003; 44 | /// RESPONSE-ORIGIN attribute 45 | pub const ATTR_RESPONSE_ORIGIN: u16 = 0x802b; 46 | 47 | /// The "change IP" flag for the CHANGE-REQUEST attribute. 48 | pub const CHANGE_REQUEST_IP_FLAG: u32 = 0x00000004; 49 | /// The "change port" flag for the CHANGE-REQUEST attribute. 50 | pub const CHANGE_REQUEST_PORT_FLAG: u32 = 0x00000002; 51 | 52 | pub const FAMILY_IPV4: u8 = 0x01; 53 | pub const FAMILY_IPV6: u8 = 0x02; 54 | 55 | /// Enum representing STUN method 56 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 57 | pub enum Method { 58 | Binding, 59 | Unknown(u16), 60 | } 61 | 62 | impl Method { 63 | /// Convert from u16 to Method. 64 | pub fn from_u16(method: u16) -> Self { 65 | match method { 66 | METHOD_BINDING => Self::Binding, 67 | _ => Self::Unknown(method), 68 | } 69 | } 70 | 71 | /// Convert from Method to u16. 72 | pub fn to_u16(&self) -> u16 { 73 | match self { 74 | Self::Binding => METHOD_BINDING, 75 | Self::Unknown(method) => method.clone(), 76 | } 77 | } 78 | } 79 | 80 | /// Enum representing STUN class 81 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 82 | pub enum Class { 83 | Request, 84 | Indication, 85 | SuccessResponse, 86 | ErrorResponse, 87 | Unknown(u16), 88 | } 89 | 90 | impl Class { 91 | /// Convert from u16 to Class. 92 | pub fn from_u16(class: u16) -> Self { 93 | match class { 94 | CLASS_REQUEST => Self::Request, 95 | CLASS_INDICATION => Self::Indication, 96 | CLASS_SUCCESS_RESPONSE => Self::SuccessResponse, 97 | CLASS_ERROR_RESPONSE => Self::ErrorResponse, 98 | _ => Self::Unknown(class), 99 | } 100 | } 101 | 102 | /// Convert from u16 to Class. 103 | pub fn to_u16(&self) -> u16 { 104 | match self { 105 | Self::Request => CLASS_REQUEST, 106 | Self::Indication => CLASS_INDICATION, 107 | Self::SuccessResponse => CLASS_SUCCESS_RESPONSE, 108 | Self::ErrorResponse => CLASS_ERROR_RESPONSE, 109 | Self::Unknown(class) => class.clone(), 110 | } 111 | } 112 | } 113 | 114 | /// Enum representing STUN attribute 115 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 116 | pub enum Attribute { 117 | MappedAddress, 118 | XORMappedAddress, 119 | Software, 120 | OtherAddress, 121 | ChangeRequest, 122 | ResponseOrigin, 123 | ErrorCode, 124 | Unknown(u16), 125 | } 126 | 127 | impl Attribute { 128 | /// Convert from u16 to Attribute. 129 | pub fn from_u16(attribute: u16) -> Self { 130 | match attribute { 131 | ATTR_MAPPED_ADDRESS => Self::MappedAddress, 132 | ATTR_XOR_MAPPED_ADDRESS => Self::XORMappedAddress, 133 | ATTR_SOFTWARE => Self::Software, 134 | ATTR_OTHER_ADDRESS => Self::OtherAddress, 135 | ATTR_CHANGE_REQUEST => Self::ChangeRequest, 136 | ATTR_RESPONSE_ORIGIN => Self::ResponseOrigin, 137 | ATTR_ERROR_CODE => Self::ErrorCode, 138 | _ => Self::Unknown(attribute), 139 | } 140 | } 141 | 142 | /// Convert from u16 to Attribute. 143 | pub fn to_u16(&self) -> u16 { 144 | match self { 145 | Self::MappedAddress => ATTR_MAPPED_ADDRESS, 146 | Self::XORMappedAddress => ATTR_XOR_MAPPED_ADDRESS, 147 | Self::Software => ATTR_SOFTWARE, 148 | Self::OtherAddress => ATTR_OTHER_ADDRESS, 149 | Self::ChangeRequest => ATTR_CHANGE_REQUEST, 150 | Self::ResponseOrigin => ATTR_RESPONSE_ORIGIN, 151 | Self::ErrorCode => ATTR_ERROR_CODE, 152 | Self::Unknown(attribute) => attribute.clone(), 153 | } 154 | } 155 | 156 | /// Gets the value of the MAPPED-ADDRESS attribute from Message. 157 | pub fn get_mapped_address(message: &Message) -> Option { 158 | Self::decode_simple_address_attribute(message, Self::MappedAddress) 159 | } 160 | 161 | /// Gets the value of the XOR-MAPPED-ADDRESS attribute from Message. 162 | pub fn get_xor_mapped_address(message: &Message) -> Option { 163 | let attr_value = message.get_raw_attr_value(Self::XORMappedAddress)?; 164 | let family = attr_value[1]; 165 | // RFC8489: X-Port is computed by XOR'ing the mapped port with the most significant 16 bits of the magic cookie. 166 | let mc_bytes = MAGIC_COOKIE.to_be_bytes(); 167 | let port = u16::from_be_bytes([attr_value[2], attr_value[3]]) 168 | ^ u16::from_be_bytes([mc_bytes[0], mc_bytes[1]]); 169 | match family { 170 | FAMILY_IPV4 => { 171 | // RFC8489: If the IP address family is IPv4, X-Address is computed by XOR'ing the mapped IP address with the magic cookie. 172 | let encoded_ip = &attr_value[4..]; 173 | let b: Vec = encoded_ip 174 | .iter() 175 | .zip(&MAGIC_COOKIE.to_be_bytes()) 176 | .map(|(b, m)| b ^ m) 177 | .collect(); 178 | let ip_addr = bytes_to_ip_addr(family, b)?; 179 | Some(SocketAddr::new(ip_addr, port)) 180 | } 181 | FAMILY_IPV6 => { 182 | // RFC8489: If the IP address family is IPv6, X-Address is computed by XOR'ing the mapped IP address with the concatenation of the magic cookie and the 96-bit transaction ID. 183 | let encoded_ip = &attr_value[4..]; 184 | let mut mc_ti: Vec = vec![]; 185 | mc_ti.extend(&MAGIC_COOKIE.to_be_bytes()); 186 | mc_ti.extend(&message.header.transaction_id); 187 | let b: Vec = encoded_ip.iter().zip(&mc_ti).map(|(b, m)| b ^ m).collect(); 188 | let ip_addr = bytes_to_ip_addr(family, b)?; 189 | Some(SocketAddr::new(ip_addr, port)) 190 | } 191 | _ => None, 192 | } 193 | } 194 | 195 | /// Gets the value of the SOFTWARE attribute from message. 196 | pub fn get_software(message: &Message) -> Option { 197 | let attr_value = message.get_raw_attr_value(Self::Software)?; 198 | String::from_utf8(attr_value).ok() 199 | } 200 | 201 | /// Gets the value of the ERROR-CODE attribute from Message. 202 | pub fn get_error_code(message: &Message) -> Option { 203 | let attr_value = message.get_raw_attr_value(Self::ErrorCode)?; 204 | let class = (attr_value[2] as u16) * 100; 205 | let number = attr_value[3] as u16; 206 | let code = class + number; 207 | let reason = String::from_utf8(attr_value[4..].to_vec()) 208 | .unwrap_or(String::from("cannot parse error reason")); 209 | Some(ErrorCode::from(code, reason)) 210 | } 211 | 212 | /// Gets the value of the OTHER-ADDRESS attribute from Message. 213 | pub fn get_other_address(message: &Message) -> Option { 214 | // RFC5780: it is simply a new name with the same semantics as CHANGED-ADDRESS. 215 | // RCF3489: Its syntax is identical to MAPPED-ADDRESS. 216 | Self::decode_simple_address_attribute(message, Self::OtherAddress) 217 | } 218 | 219 | /// Gets the value of the RESPONSE-ORIGIN attribute from Message. 220 | pub fn get_response_origin(message: &Message) -> Option { 221 | Self::decode_simple_address_attribute(message, Self::ResponseOrigin) 222 | } 223 | 224 | /// Generates a value for the CHANGE-REQUEST attribute. 225 | pub fn generate_change_request_value(change_ip: bool, change_port: bool) -> Vec { 226 | let mut value: u32 = 0; 227 | if change_ip { 228 | value |= CHANGE_REQUEST_IP_FLAG; 229 | } 230 | 231 | if change_port { 232 | value |= CHANGE_REQUEST_PORT_FLAG; 233 | } 234 | 235 | value.to_be_bytes().to_vec() 236 | } 237 | 238 | pub fn decode_simple_address_attribute(message: &Message, attr: Self) -> Option { 239 | let attr_value = message.get_raw_attr_value(attr)?; 240 | let family = attr_value[1]; 241 | let port = u16::from_be_bytes([attr_value[2], attr_value[3]]); 242 | let ip_addr = bytes_to_ip_addr(family, attr_value[4..].to_vec())?; 243 | Some(SocketAddr::new(ip_addr, port)) 244 | } 245 | } 246 | 247 | /// Struct representing STUN message 248 | #[derive(Debug, Eq, PartialEq)] 249 | pub struct Message { 250 | header: Header, 251 | attributes: Option>>, 252 | } 253 | 254 | impl Message { 255 | /// Create a STUN Message. 256 | pub fn new( 257 | method: Method, 258 | class: Class, 259 | attributes: Option>>, 260 | ) -> Message { 261 | let attr_type_byte_size = 2; 262 | let attr_length_byte_size = 2; 263 | let length: u16 = if let Some(attributes) = &attributes { 264 | attributes 265 | .iter() 266 | .map(|e| attr_type_byte_size + attr_length_byte_size + e.1.len() as u16) 267 | .sum() 268 | } else { 269 | 0 270 | }; 271 | 272 | let transaction_id: Vec = thread_rng().gen::<[u8; 12]>().to_vec(); 273 | 274 | Message { 275 | header: Header::new(method, class, length, transaction_id), 276 | attributes: attributes, 277 | } 278 | } 279 | 280 | /// Create a STUN message from raw bytes. 281 | pub fn from_raw(buf: &[u8]) -> Result { 282 | if buf.len() < HEADER_BYTE_SIZE { 283 | return Err(STUNClientError::ParseError()); 284 | } 285 | 286 | let header = Header::from_raw(&buf[..HEADER_BYTE_SIZE])?; 287 | let mut attrs = None; 288 | if buf.len() > HEADER_BYTE_SIZE { 289 | attrs = Some(Message::decode_attrs(&buf[HEADER_BYTE_SIZE..])?); 290 | } 291 | 292 | Ok(Message { 293 | header: header, 294 | attributes: attrs, 295 | }) 296 | } 297 | 298 | /// Converts a Message to a STUN protocol message raw bytes. 299 | pub fn to_raw(&self) -> Vec { 300 | let mut bytes = self.header.to_raw(); 301 | if let Some(attributes) = &self.attributes { 302 | for (k, v) in attributes.iter() { 303 | bytes.extend(&k.to_u16().to_be_bytes()); 304 | bytes.extend(&(v.len() as u16).to_be_bytes()); 305 | bytes.extend(v); 306 | } 307 | } 308 | 309 | bytes 310 | } 311 | 312 | /// Get the method from Message. 313 | pub fn get_method(&self) -> Method { 314 | self.header.method 315 | } 316 | 317 | /// Get the class from Message. 318 | pub fn get_class(&self) -> Class { 319 | self.header.class 320 | } 321 | 322 | /// Get the raw attribute bytes from Message. 323 | pub fn get_raw_attr_value(&self, attr: Attribute) -> Option> { 324 | self.attributes 325 | .as_ref()? 326 | .get(&attr) 327 | .and_then(|v| Some(v.clone())) 328 | } 329 | 330 | /// Get the transaction id from Message. 331 | pub fn get_transaction_id(&self) -> Vec { 332 | self.header.transaction_id.clone() 333 | } 334 | 335 | fn decode_attrs(attrs_buf: &[u8]) -> Result>, STUNClientError> { 336 | let mut attrs_buf = attrs_buf.to_vec(); 337 | let mut attributes = HashMap::new(); 338 | 339 | if attrs_buf.is_empty() { 340 | return Err(STUNClientError::ParseError()); 341 | } 342 | 343 | while !attrs_buf.is_empty() { 344 | if attrs_buf.len() < 4 { 345 | break; 346 | } 347 | 348 | let attribute_type = Attribute::from_u16(u16::from_be_bytes([ 349 | attrs_buf.remove(0), 350 | attrs_buf.remove(0), 351 | ])); 352 | let length = 353 | u16::from_be_bytes([attrs_buf.remove(0), attrs_buf.remove(0)]) as usize; 354 | if attrs_buf.len() < length { 355 | return Err(STUNClientError::ParseError()); 356 | } 357 | 358 | let value: Vec = attrs_buf.drain(..length).collect(); 359 | attributes.insert(attribute_type, value); 360 | } 361 | 362 | Ok(attributes) 363 | } 364 | } 365 | 366 | /// Struct representing STUN header 367 | #[derive(Debug, Eq, PartialEq)] 368 | pub struct Header { 369 | method: Method, 370 | class: Class, 371 | length: u16, 372 | transaction_id: Vec, 373 | } 374 | 375 | impl Header { 376 | /// Create a STUN header. 377 | pub fn new(method: Method, class: Class, length: u16, transaction_id: Vec) -> Header { 378 | Header { 379 | class: class, 380 | method: method, 381 | length: length, 382 | transaction_id: transaction_id, 383 | } 384 | } 385 | 386 | /// Create a STUN header from raw bytes. 387 | pub fn from_raw(buf: &[u8]) -> Result { 388 | let mut buf = buf.to_vec(); 389 | if buf.len() < HEADER_BYTE_SIZE { 390 | return Err(STUNClientError::ParseError()); 391 | } 392 | 393 | let message_type = u16::from_be_bytes([buf.remove(0), buf.remove(0)]); 394 | let class = Header::decode_class(message_type); 395 | let method = Header::decode_method(message_type); 396 | let length = u16::from_be_bytes([buf.remove(0), buf.remove(0)]); 397 | 398 | Ok(Header { 399 | class: class, 400 | method: method, 401 | length: length, 402 | // 0..3 is Magic Cookie 403 | transaction_id: buf[4..].to_vec(), 404 | }) 405 | } 406 | 407 | /// Converts a Header to a STUN protocol header raw bytes. 408 | pub fn to_raw(&self) -> Vec { 409 | let message_type = self.message_type(); 410 | let mut bytes = vec![]; 411 | bytes.extend(&message_type.to_be_bytes()); 412 | bytes.extend(&self.length.to_be_bytes()); 413 | bytes.extend(&MAGIC_COOKIE.to_be_bytes()); 414 | bytes.extend(&self.transaction_id); 415 | bytes 416 | } 417 | 418 | fn message_type(&self) -> u16 { 419 | self.class.to_u16() | self.method.to_u16() 420 | } 421 | 422 | fn decode_method(message_type: u16) -> Method { 423 | // RFC8489: M11 through M0 represent a 12-bit encoding of the method 424 | Method::from_u16(message_type & 0x3EEF) 425 | } 426 | 427 | fn decode_class(message_type: u16) -> Class { 428 | // RFC8489: C1 and C0 represent a 2-bit encoding of the class 429 | Class::from_u16(message_type & 0x0110) 430 | } 431 | } 432 | 433 | fn bytes_to_ip_addr(family: u8, b: Vec) -> Option { 434 | match family { 435 | FAMILY_IPV4 => Some(IpAddr::V4(Ipv4Addr::from([b[0], b[1], b[2], b[3]]))), 436 | FAMILY_IPV6 => Some(IpAddr::V6(Ipv6Addr::from([ 437 | b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], 438 | b[14], b[15], 439 | ]))), 440 | _ => None, 441 | } 442 | } 443 | 444 | /// An enum that defines the type of STUN error code. 445 | #[derive(Clone, Debug, Eq, Hash, PartialEq)] 446 | pub enum ErrorCode { 447 | TryAlternate(String), 448 | BadRequest(String), 449 | Unauthorized(String), 450 | UnknownAttribute(String), 451 | StaleNonce(String), 452 | ServerError(String), 453 | Unknown(String), 454 | } 455 | 456 | impl ErrorCode { 457 | pub fn from(code: u16, reason: String) -> Self { 458 | match code { 459 | 300 => Self::TryAlternate(reason), 460 | 400 => Self::BadRequest(reason), 461 | 401 => Self::Unauthorized(reason), 462 | 420 => Self::UnknownAttribute(reason), 463 | 438 => Self::StaleNonce(reason), 464 | 500 => Self::ServerError(reason), 465 | _ => Self::Unknown(reason), 466 | } 467 | } 468 | } 469 | 470 | #[cfg(test)] 471 | mod tests { 472 | use super::*; 473 | 474 | #[test] 475 | fn message_new_and_message_from_raw_are_equivalent() { 476 | let mut attrs = HashMap::new(); 477 | attrs.insert( 478 | Attribute::ChangeRequest, 479 | Attribute::generate_change_request_value(true, false), 480 | ); 481 | let msg = Message::new(Method::Binding, Class::Request, Some(attrs)); 482 | let re_built_msg = Message::from_raw(&msg.to_raw()).unwrap(); 483 | assert_eq!(msg, re_built_msg); 484 | } 485 | } 486 | -------------------------------------------------------------------------------- /src/nat_behavior_discovery.rs: -------------------------------------------------------------------------------- 1 | //! This module is for NAT Behavior Discovery based on RFC5780. 2 | //! To use this module, the STUN server side must support the OTHER-ADDRESS and CHANGE-REQUEST attributes. 3 | use std::collections::HashMap; 4 | use std::net::IpAddr; 5 | 6 | use async_std::net::{SocketAddr, ToSocketAddrs}; 7 | use pnet::datalink; 8 | use pnet::ipnetwork::IpNetwork; 9 | 10 | use super::client::*; 11 | use super::error::*; 12 | use super::message::*; 13 | 14 | /// Defines a NAT type based on mapping behavior. 15 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 16 | pub enum NATMappingType { 17 | NoNAT, 18 | EndpointIndependent, 19 | AddressDependent, 20 | AddressAndPortDependent, 21 | Unknown, 22 | } 23 | 24 | /// Defines a NAT type based on filtering behavior. 25 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 26 | pub enum NATFilteringType { 27 | EndpointIndependent, 28 | AddressDependent, 29 | AddressAndPortDependent, 30 | Unknown, 31 | } 32 | 33 | /// Results of behavior discovery based on NAT mapping behavior. 34 | #[derive(Clone, Debug, Eq, PartialEq)] 35 | pub struct NATMappingTypeResult { 36 | pub test1_xor_mapped_addr: Option, 37 | pub test2_xor_mapped_addr: Option, 38 | pub test3_xor_mapped_addr: Option, 39 | pub mapping_type: NATMappingType, 40 | } 41 | 42 | /// Results of behavior discovery based on NAT filtering behavior. 43 | #[derive(Clone, Debug, Eq, PartialEq)] 44 | pub struct NATFilteringTypeResult { 45 | pub xor_mapped_addr: Option, 46 | pub filtering_type: NATFilteringType, 47 | } 48 | 49 | /// Check NAT mapping behavior. 50 | pub async fn check_nat_mapping_behavior( 51 | client: &mut Client, 52 | stun_addr: A, 53 | ) -> Result { 54 | let mut result = NATMappingTypeResult { 55 | test1_xor_mapped_addr: None, 56 | test2_xor_mapped_addr: None, 57 | test3_xor_mapped_addr: None, 58 | mapping_type: NATMappingType::Unknown, 59 | }; 60 | 61 | // get NIC IPs 62 | let local_ips: Vec = datalink::interfaces() 63 | .iter() 64 | .flat_map(|i| i.ips.clone()) 65 | .collect(); 66 | 67 | // Test1 68 | // Send a Binding request and check the Endpoint mapped to NAT. 69 | // Compare with the IP of the NIC and check if it is behind the NAT. 70 | let t1_res = client.binding_request(&stun_addr, None).await?; 71 | let other_addr = Attribute::get_other_address(&t1_res).ok_or( 72 | STUNClientError::NotSupportedError(String::from("OTHER-ADDRESS")), 73 | )?; 74 | result.test1_xor_mapped_addr = Some(Attribute::get_xor_mapped_address(&t1_res).ok_or( 75 | STUNClientError::NotSupportedError(String::from("XOR-MAPPED-ADDRESS")), 76 | )?); 77 | match result.test1_xor_mapped_addr.unwrap().ip() { 78 | IpAddr::V4(addr) => { 79 | for local_ip in local_ips { 80 | if let IpNetwork::V4(v4_lip) = local_ip { 81 | if v4_lip.ip() == addr { 82 | result.mapping_type = NATMappingType::NoNAT; 83 | return Ok(result); 84 | } 85 | } 86 | } 87 | } 88 | IpAddr::V6(addr) => { 89 | for local_ip in local_ips { 90 | if let IpNetwork::V6(v6_lip) = local_ip { 91 | if v6_lip.ip() == addr { 92 | result.mapping_type = NATMappingType::NoNAT; 93 | return Ok(result); 94 | } 95 | } 96 | } 97 | return Err(STUNClientError::ParseError()); 98 | } 99 | } 100 | 101 | // Test2 102 | // Send Binding Request to IP:Port of OTHER-ADDRESS. 103 | // Compare Test1 and Test2 XOR-MAPPED-ADDRESS to check if it is EIM-NAT. 104 | let t2_res = client.binding_request(&other_addr, None).await?; 105 | result.test2_xor_mapped_addr = Some(Attribute::get_xor_mapped_address(&t2_res).ok_or( 106 | STUNClientError::NotSupportedError(String::from("XOR-MAPPED-ADDRESS")), 107 | )?); 108 | if result.test1_xor_mapped_addr == result.test2_xor_mapped_addr { 109 | result.mapping_type = NATMappingType::EndpointIndependent; 110 | return Ok(result); 111 | } 112 | 113 | // Test3 114 | // Send a Binding Request to the IP used in Test1 and the Port used in Test2. 115 | // (That is, use the primary IP and secondary Port.) 116 | // Compare Test2 and Test3 XOR-MAPPED-ADDRESS to check if it is ADM-NAT or APDM-NAT. 117 | // stun_addr is a known value, so it's okay to unwrap it. 118 | let mut t3_addr = stun_addr.to_socket_addrs().await.unwrap().next().unwrap(); 119 | t3_addr.set_port(other_addr.port()); 120 | let t3_res = client.binding_request(&t3_addr, None).await?; 121 | result.test3_xor_mapped_addr = Some(Attribute::get_xor_mapped_address(&t3_res).ok_or( 122 | STUNClientError::NotSupportedError(String::from("XOR-MAPPED-ADDRESS")), 123 | )?); 124 | if result.test2_xor_mapped_addr == result.test3_xor_mapped_addr { 125 | result.mapping_type = NATMappingType::AddressDependent; 126 | return Ok(result); 127 | } 128 | 129 | result.mapping_type = NATMappingType::AddressAndPortDependent; 130 | Ok(result) 131 | } 132 | 133 | /// Check NAT filtering behavior. 134 | pub async fn check_nat_filtering_behavior( 135 | client: &mut Client, 136 | stun_addr: A, 137 | ) -> Result { 138 | // Test1 139 | // Send a Binding request and check the Endpoint mapped to NAT. 140 | let t1_res = client.binding_request(&stun_addr, None).await?; 141 | let xor_mapped_addr = Some(Attribute::get_xor_mapped_address(&t1_res).ok_or( 142 | STUNClientError::NotSupportedError(String::from("XOR-MAPPED-ADDRESS")), 143 | )?); 144 | 145 | // Test2 146 | // Send Binding Request with the "change IP" and "change port" flags of CHANGE-REQUEST turned on. 147 | // As a result, the response is sent from IP:Port which is different from the sent IP:Port. 148 | // If the response can be received, it is EIF-NAT. 149 | let mut attrs = HashMap::new(); 150 | let change_request = Attribute::generate_change_request_value(true, true); 151 | attrs.insert(Attribute::ChangeRequest, change_request); 152 | let t2_res = client.binding_request(&stun_addr, Some(attrs)).await; 153 | match t2_res { 154 | Ok(_) => { 155 | return Ok(NATFilteringTypeResult { 156 | xor_mapped_addr: xor_mapped_addr, 157 | filtering_type: NATFilteringType::EndpointIndependent, 158 | }) 159 | } 160 | Err(e) => { 161 | match e { 162 | STUNClientError::TimeoutError() => { /* Run Test3 below */ } 163 | _ => return Err(e), 164 | } 165 | } 166 | } 167 | 168 | // Test3 169 | // Send a binding request with only the "change port" flag in CHANGE-REQUEST turned on. 170 | // As a result, the response is sent from Port which is different from the sent Port.(Same IP address) 171 | // If the response can be received, it is ADF-NAT, and if it cannot be received, it is APDF-NAT. 172 | let mut attrs = HashMap::new(); 173 | let change_request = Attribute::generate_change_request_value(false, true); 174 | attrs.insert(Attribute::ChangeRequest, change_request); 175 | let t3_res = client.binding_request(&stun_addr, Some(attrs)).await; 176 | match t3_res { 177 | Ok(_) => { 178 | return Ok(NATFilteringTypeResult { 179 | xor_mapped_addr: xor_mapped_addr, 180 | filtering_type: NATFilteringType::AddressDependent, 181 | }) 182 | } 183 | Err(e) => match e { 184 | STUNClientError::TimeoutError() => { 185 | return Ok(NATFilteringTypeResult { 186 | xor_mapped_addr: xor_mapped_addr, 187 | filtering_type: NATFilteringType::AddressAndPortDependent, 188 | }) 189 | } 190 | _ => return Err(e), 191 | }, 192 | } 193 | } 194 | --------------------------------------------------------------------------------