Skip to content

Commit

Permalink
Scan for also unicast addresses among IP neighbors
Browse files Browse the repository at this point in the history
  • Loading branch information
seamlik committed Mar 16, 2024
1 parent b1a6036 commit da0f2ca
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 10 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ Service discovery is mostly implemented by exchanging multicast packets on a ren

When multicast is unavailable, services on other devices are naturally undiscoverable.
However, devices connected directly to the host device are still discoverable as IP neighbors.
Applications can still communicate with each other but over link-local addresses.

This scenario can happen, for example, when 2 Android smartphones are connected via a hotspot or even Wi-Fi Direct.
In this case, no other device exists within the LAN, but applications on the smartphones can still communicate.

This feature relies on the operating system. Current we support these operating systems:
This feature relies on the operating system.
Current we support these operating systems:

- Windows (using [PowerShell](https://learn.microsoft.com/powershell/module/nettcpip/get-netneighbor?view=windowsserver2022-ps))
- Linux (using [iproute2](https://wiki.linuxfoundation.org/networking/iproute2))
Expand Down
4 changes: 2 additions & 2 deletions main/src/network/ip_neighbor/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl IpRoute2IpNeighborScanner {
let neighbors: Vec<_> = neighbors
.into_iter()
.filter(|n| n.state != "FAILED")
.filter(|n| n.ip.segments().starts_with(&[0xFE80, 0, 0, 0]))
.filter(|n| super::is_valid_ip(n.ip))
.filter_map(|n| {
links.get(&n.ifname).map(|l| IpNeighbor {
address: n.ip,
Expand Down Expand Up @@ -109,7 +109,7 @@ mod test {
fe80::1:abcd dev wlan0 lladdr 00:00:00:00:00:01 router REACHABLE
fe80::2:abcd dev eth0 lladdr 00:00:00:00:00:02 router STALE
fe80::3:abcd dev wlan2 lladdr 00:00:00:00:00:03 router STALE
fe02::4:abcd dev wlan0 lladdr 00:00:00:00:00:04 router REACHABLE
ff02::4:abcd dev wlan0 lladdr 00:00:00:00:00:04 router REACHABLE
fe80::5:abcd dev wlan0 lladdr 00:00:00:00:00:05 router FAILED
"#
.trim();
Expand Down
47 changes: 46 additions & 1 deletion main/src/network/ip_neighbor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ pub async fn ip_neighbor_scanner() -> Box<dyn IpNeighborScanner + Send> {
}
}

fn is_valid_ip(ip: Ipv6Addr) -> bool {
!ip.is_loopback() && !ip.is_unspecified() && !ip.is_multicast()
}

#[derive(Error, Debug)]
pub enum IpNeighborScanError {
#[error("Failed in running an external command")]
Expand Down Expand Up @@ -67,7 +71,16 @@ pub struct IpNeighbor {

impl IpNeighbor {
pub fn get_socket_address(&self, port: u16) -> SocketAddrV6 {
SocketAddrV6::new(self.address, port, 0, self.network_interface_index)
let scope = if self.is_link_local() {
self.network_interface_index
} else {
0
};
SocketAddrV6::new(self.address, port, 0, scope)
}

fn is_link_local(&self) -> bool {
self.address.octets().starts_with(&[0xFE, 0x80])
}
}

Expand All @@ -79,4 +92,36 @@ mod test {
async fn find_scanner() {
ip_neighbor_scanner().await.scan().await.unwrap();
}

#[test]
fn get_socket_address_unicast() {
let neighbor = IpNeighbor {
address: "2001::1".parse().unwrap(),
network_interface_index: 123,
};
let port = 10;
let expected_address: SocketAddrV6 = "[2001::1]:10".parse().unwrap();

// When
let actual_address = neighbor.get_socket_address(port);

// Then
assert_eq!(actual_address, expected_address);
}

#[test]
fn get_socket_address_link_local() {
let neighbor = IpNeighbor {
address: "FE80::1".parse().unwrap(),
network_interface_index: 123,
};
let port = 10;
let expected_address: SocketAddrV6 = "[FE80::1%123]:10".parse().unwrap();

// When
let actual_address = neighbor.get_socket_address(port);

// Then
assert_eq!(actual_address, expected_address);
}
}
2 changes: 1 addition & 1 deletion main/src/network/ip_neighbor/windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl PowerShellIpNeighborScanner {
let neighbors: Vec<_> = neighbors
.into_iter()
.filter(|n| n.State != "Unreachable")
.filter(|n| n.IPAddress.segments().starts_with(&[0xFE80, 0, 0, 0]))
.filter(|n| super::is_valid_ip(n.IPAddress))
.map(Into::into)
.collect();
neighbors
Expand Down
9 changes: 5 additions & 4 deletions main/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ async fn announce_to_ip_neighbors(
let tasks = neighbors
.into_iter()
.map(|n| n.get_socket_address(discovery_port))
.inspect(|a| log::debug!("Sending announcement to {}", a))
.map(|a| udp_sender.send_unicast(a, data.clone()));
futures_util::future::try_join_all(tasks)
.map_ok(|_| ())
Expand Down Expand Up @@ -187,11 +188,11 @@ mod test {
let multicast_address = SocketAddrV6::new(crate::get_discovery_ip(), DISCOVERY_PORT, 0, 0);
let ip_neighbors = vec![
IpNeighbor {
address: "::A".parse().unwrap(),
address: "FE80::A".parse().unwrap(),
network_interface_index: 1000,
},
IpNeighbor {
address: "::B".parse().unwrap(),
address: "2001::B".parse().unwrap(),
network_interface_index: 1001,
},
];
Expand Down Expand Up @@ -224,14 +225,14 @@ mod test {
udp_sender
.expect_send_unicast()
.with(
eq("[::A%1000]:50000".parse::<SocketAddrV6>().unwrap()),
eq("[FE80::A%1000]:50000".parse::<SocketAddrV6>().unwrap()),
eq(response_bytes.clone()),
)
.return_once(|_, _| async { Ok(()) }.boxed());
udp_sender
.expect_send_unicast()
.with(
eq("[::B%1001]:50000".parse::<SocketAddrV6>().unwrap()),
eq("[2001::B]:50000".parse::<SocketAddrV6>().unwrap()),
eq(response_bytes.clone()),
)
.return_once(|_, _| async { Ok(()) }.boxed());
Expand Down

0 comments on commit da0f2ca

Please sign in to comment.