package l2disco
import (
"context"
"encoding/binary"
"fmt"
"net"
"slices"
"golang.org/x/sys/unix"
)
var (
arpTemplate = []byte{
0x00, 0x01, // Hardware type
0x08, 0x00, // Protocol
0x06, // Hardware address length
0x04, // IPv4 address length
0x00, 0x01, // ARP request
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Sender MAC
0x00, 0x00, 0x00, 0x00, // Sender IP
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Target MAC (always zeros)
0x00, 0x00, 0x00, 0x00, // Target IP
}
bcastMAC = []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
)
type UnsolARP struct {
pkt []byte
sd int
sa *unix.SockaddrLinklayer
}
// NewUnsolARP returns a pointer to an object that can send unsolicited ARPs on
// the interface with ifIndex, for ip and mac.
func NewUnsolARP(_ context.Context, ip net.IP, mac net.HardwareAddr, ifIndex int) (*UnsolARP, error) {
sd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, 0)
if err != nil {
return nil, fmt.Errorf("create socket: %w", err)
}
pkt := slices.Clone(arpTemplate)
copy(pkt[8:14], mac)
copy(pkt[14:18], ip)
copy(pkt[24:28], ip)
sa := &unix.SockaddrLinklayer{
Protocol: htons(unix.ETH_P_ARP),
Ifindex: ifIndex,
Hatype: unix.ARPHRD_ETHER,
Halen: uint8(len(bcastMAC)),
}
copy(sa.Addr[:], bcastMAC)
return &UnsolARP{
pkt: pkt,
sd: sd,
sa: sa,
}, nil
}
// Send sends an unsolicited ARP message.
func (ua *UnsolARP) Send() error {
return unix.Sendto(ua.sd, ua.pkt, 0, ua.sa)
}
// Close releases resources.
func (ua *UnsolARP) Close() error {
if ua.sd >= 0 {
err := unix.Close(ua.sd)
ua.sd = -1
return err
}
return nil
}
// From https://github.com/mdlayher/packet/blob/f9999b41d9cfb0586e75467db1c81cfde4f965ba/packet_linux.go#L238-L248
func htons(i uint16) uint16 {
var bigEndian [2]byte
binary.BigEndian.PutUint16(bigEndian[:], i)
return binary.NativeEndian.Uint16(bigEndian[:])
}