I'm creating a tap device using the tun/tap interface by opening /dev/net/tun
. However, when I try to get the MAC address using the SIOCGIFHWADDR
ioctl call I get the wrong MAC address. If I make the same call once or twice more, I'll eventually get the "right" mac address and it will stick to that MAC. A sample output from my program is shown below, as well as the code used to set/get tap interface properties. Is there a proper way to get the device's MAC that I'm not using? Is there a way to know when I got the "right" one? or to be notified when it changes?
As a side note, the order things being called in as as follows:
ip link set dev <dev_name> up
# produced in a tight loop where we read 1 frame from the tap device
# then get the mac address again using get_tap_address
[2025-09-08T03:13:17Z DEBUG teecee] Created new tap device: TapDevice { name: "teecee0", address: ea:dd:cc:40:d1:00 }
[2025-09-08T03:13:17Z DEBUG teecee] Got new frame:
{
dst_mac: 33:33:00:00:00:16,
src_mac: 0e:58:b2:b1:61:35,
ethertype: 0x0086DD,
}
[2025-09-08T03:13:17Z DEBUG teecee] New tap mac: 0e:58:b2:b1:61:35
[2025-09-08T03:13:17Z DEBUG teecee] Got new frame:
{
dst_mac: 33:33:00:00:00:16,
src_mac: 0e:58:b2:b1:61:35,
ethertype: 0x0086DD,
}
[2025-09-08T03:13:17Z DEBUG teecee] New tap mac: 0e:58:b2:b1:61:35
fn set_tap_with_name(fd: libc::c_int, name: &CStr) -> Result<String, TapError> {
let mut ifreq = Self::ifreq_with_params(
name.to_bytes(),
(libc::IFF_TAP | libc::IFF_NO_PI) as _
);
unsafe {
if libc::ioctl(fd, libc::TUNSETIFF, &mut ifreq as *mut _) < 0 {
return Err(TapError::IoctlSet(std::io::Error::last_os_error()));
}
};
let name_bytes = ifreq.ifr_name.iter()
.map_while(|c| {
if *c != 0 {
Some(*c as u8)
}
else {
None
}
})
.collect::<Vec<u8>>();
String::from_utf8(name_bytes).map_err(TapError::NameUtf8)
}
fn get_tap_address(fd: libc::c_int, name: &String) -> Result<MacAddr, TapError> {
let mut ifreq = Self::ifreq_with_params(name.as_bytes(), 0);
unsafe {
if libc::ioctl(fd, libc::SIOCGIFHWADDR, &mut ifreq as *mut _) < 0 {
return Err(TapError::IoctlAddrGet(std::io::Error::last_os_error()));
}
};
let hw_addr = unsafe {
let addr = &ifreq.ifr_ifru.ifru_hwaddr.sa_data[..MacAddr::LEN];
std::slice::from_raw_parts(addr.as_ptr() as _, addr.len())
};
let mut addr = MacAddr::new_zeroed();
addr.as_mut_bytes().copy_from_slice(hw_addr);
Ok(addr)
}
fn ifreq_with_params(name: &[u8], flags: i16) -> libc::ifreq {
const MAX_NAME_LEN: usize = 16;
assert!(name.len() < MAX_NAME_LEN);
let name = unsafe {
std::slice::from_raw_parts(name.as_ptr() as _, name.len())
};
let mut ifreq: libc::ifreq;
unsafe {
ifreq = std::mem::zeroed();
ifreq.ifr_name[..name.len()].copy_from_slice(name);
ifreq.ifr_ifru.ifru_flags = flags;
}
ifreq
}
If you have e.g. systemd automatically applying a "stable" MAC address to every network interface that lacks one, you should wait for the device to be announced via libudev (or via libsystemd sd-device), which happens after udev rules have finished processing. Create a libudev 'monitor' and add a subsystem match on "net"
to watch network interfaces. As soon you receive an event with ACTION==add, SUBSYSTEM==net, NAME==tap123, it should be ready for use. I'm not sure if systemd has a .link file that applies to tap interfaces by default but it is a possibility.
(This is also how a program would automatically pick up new physically connected devices.)
General interface/address/route notifications arrive via Linux Netlink, specifically rtnetlink (rtnl). A live example is ip monitor [link|addr|route]
. The MAC address is a property of a link, so if I remember correctly, the MAC change would arrive as a second RTM_NEWLINK event, after the initial 'interface created' RTM_NEWLINK.
Rtnetlink events differ from udev in that they can happen (or not happen) at any time for any change, whereas udev processing is typically exactly-once-per-device. So it is reasonable to wait for an udev notification before using the device, but no guarantee that a Netlink 'changed MAC' notification will occur. On the other hand, Netlink notifications can occur at some later points e.g. if someone manually brings the interface down or manually deletes it, so they may still be worth watching.
Note that if you monitor both, then the rtnetlink 'new link' message will arrive first – then udev will react to the new device and you will see rtnl 'changed link' messages – and finally the udev 'ACTION=add' uevent will arrive.
Rtnetlink also supports creating and configuring links, and is the preferred replacement for the SIOCGIF*
/SIOCSIF*
ioctls in general (on both Linux and FreeBSD).