1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
// Modified by `Vector Contributors <vector@datadoghq.com>`.
// Based on `https://github.com/Dentosal/portpicker-rs` by `Hannes Karppila <hannes.karppila@gmail.com>`.
// `portpicker-rs` LICENSE:
// This is free and unencumbered software released into the public domain.

// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.

// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

// For more information, please refer to <http://unlicense.org>

#![deny(warnings)]

use std::net::{IpAddr, SocketAddr, TcpListener, ToSocketAddrs, UdpSocket};

use rand::{thread_rng, Rng};

pub type Port = u16;

// Try to bind to a socket using UDP
fn test_bind_udp<A: ToSocketAddrs>(addr: A) -> Option<Port> {
    Some(UdpSocket::bind(addr).ok()?.local_addr().ok()?.port())
}

// Try to bind to a socket using TCP
fn test_bind_tcp<A: ToSocketAddrs>(addr: A) -> Option<Port> {
    Some(TcpListener::bind(addr).ok()?.local_addr().ok()?.port())
}

/// Check if a port is free on UDP
pub fn is_free_udp(ip: IpAddr, port: Port) -> bool {
    test_bind_udp(SocketAddr::new(ip, port)).is_some()
}

/// Check if a port is free on TCP
pub fn is_free_tcp(ip: IpAddr, port: Port) -> bool {
    test_bind_tcp(SocketAddr::new(ip, port)).is_some()
}

/// Check if a port is free on both TCP and UDP
pub fn is_free(ip: IpAddr, port: Port) -> bool {
    is_free_tcp(ip, port) && is_free_udp(ip, port)
}

/// Asks the OS for a free port
fn ask_free_tcp_port(ip: IpAddr) -> Option<Port> {
    test_bind_tcp(SocketAddr::new(ip, 0))
}

/// Picks an available port that is available on both TCP and UDP
/// ```rust
/// use portpicker::pick_unused_port;
/// use std::net::{IpAddr, Ipv4Addr};
/// let port: u16 = pick_unused_port(IpAddr::V4(Ipv4Addr::LOCALHOST));
/// ```
pub fn pick_unused_port(ip: IpAddr) -> Port {
    let mut rng = thread_rng();

    loop {
        // Try random port first
        for _ in 0..10 {
            let port = rng.gen_range(15000..25000);
            if is_free(ip, port) {
                return port;
            }
        }

        // Ask the OS for a port
        for _ in 0..10 {
            if let Some(port) = ask_free_tcp_port(ip) {
                // Test that the udp port is free as well
                if is_free_udp(ip, port) {
                    return port;
                }
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};

    use super::pick_unused_port;

    #[test]
    fn ipv4_localhost() {
        pick_unused_port(IpAddr::V4(Ipv4Addr::LOCALHOST));
    }

    #[test]
    fn ipv6_localhost() {
        pick_unused_port(IpAddr::V6(Ipv6Addr::LOCALHOST));
    }
}