diff --git a/src/app/(dashboard)/peer/page.tsx b/src/app/(dashboard)/peer/page.tsx index c9e10e1f7..88f1a361f 100644 --- a/src/app/(dashboard)/peer/page.tsx +++ b/src/app/(dashboard)/peer/page.tsx @@ -64,7 +64,7 @@ import { usePermissions } from "@/contexts/PermissionsProvider"; import RoutesProvider from "@/contexts/RoutesProvider"; import { useHasChanges } from "@/hooks/useHasChanges"; import type { Group } from "@/interfaces/Group"; -import type { Peer } from "@/interfaces/Peer"; +import { type Peer, peerMacAddresses } from "@/interfaces/Peer"; import type { User } from "@/interfaces/User"; import PageContainer from "@/layouts/PageContainer"; import useGroupHelper from "@/modules/groups/useGroupHelper"; @@ -678,6 +678,24 @@ function PeerInformationCard({ peer }: Readonly<{ peer: Peer }>) { /> )} + {peerMacAddresses(peer).length > 0 && ( + + + MAC Address + + } + className={ + peerMacAddresses(peer).length > 1 ? "items-start" : "" + } + value={peerMacAddresses(peer)[0]} + extraText={peerMacAddresses(peer).slice(1)} + /> + )} + {peer.created_at && ( diff --git a/src/interfaces/Peer.ts b/src/interfaces/Peer.ts index fe1e56767..2300558a7 100644 --- a/src/interfaces/Peer.ts +++ b/src/interfaces/Peer.ts @@ -33,6 +33,26 @@ export interface Peer { serial_number: string; ephemeral: boolean; local_flags?: PeerLocalFlags; + network_addresses?: NetworkAddress[]; +} + +export interface NetworkAddress { + net_ip: string; + mac: string; +} + +// peerMacAddresses returns the unique, non-empty MAC addresses reported by a +// peer's network interfaces. A peer reports the same MAC for several IPs +// (e.g. IPv4 + IPv6 of one NIC), so we de-duplicate. +export function peerMacAddresses(peer: Peer): string[] { + if (!peer.network_addresses) return []; + return Array.from( + new Set( + peer.network_addresses + .map((address) => address.mac) + .filter((mac) => !!mac), + ), + ); } export interface PeerLocalFlags { diff --git a/src/modules/peers/PeersTable.tsx b/src/modules/peers/PeersTable.tsx index 9d791caa1..c17096cd9 100644 --- a/src/modules/peers/PeersTable.tsx +++ b/src/modules/peers/PeersTable.tsx @@ -50,7 +50,7 @@ import { useLocalStorage } from "@/hooks/useLocalStorage"; import { getOperatingSystem } from "@/hooks/useOperatingSystem"; import { Group } from "@/interfaces/Group"; import { OperatingSystem } from "@/interfaces/OperatingSystem"; -import { Peer } from "@/interfaces/Peer"; +import { Peer, peerMacAddresses } from "@/interfaces/Peer"; import PeerActionCell from "@/modules/peers/PeerActionCell"; import PeerAddressCell from "@/modules/peers/PeerAddressCell"; import PeerGroupCell from "@/modules/peers/PeerGroupCell"; @@ -262,6 +262,10 @@ const PeersTableColumns: ColumnDef[] = [ id: "ipv6", accessorFn: (row) => row.ipv6, }, + { + id: "mac", + accessorFn: (peer) => peerMacAddresses(peer).join(", "), + }, ]; export type PeersTableKind = "users" | "servers"; @@ -480,7 +484,7 @@ export default function PeersTable({ showResetFilterButton={false} columns={PeersTableColumns} data={showBrowserPeers ? browserPeers : regularPeers} - searchPlaceholder={"Search by name, IP, owner or group..."} + searchPlaceholder={"Search by name, IP, MAC, owner or group..."} columnVisibility={{ select: permission.groups.read, connected: false, @@ -497,6 +501,7 @@ export default function PeersTable({ os: false, os_kind: false, ipv6: false, + mac: false, }} isLoading={isLoading} getStartedCard={