feat: add VRRP unicast peer support for macvlan and restricted network environments
Release / Build darwin-amd64 (push) Has been cancelled
Release / Build linux-amd64 (push) Has been cancelled
Release / Build darwin-arm64 (push) Has been cancelled
Release / Build linux-arm64 (push) Has been cancelled
Release / Create Release (push) Has been cancelled

- Add unicast_src_ip and unicast_peers config fields
- NewUnicastSocket binds to source IP for proper packet reception
- SendTo() sends VRRP advertisements directly to peer IPs
- Receive loop filters packets from non-peer sources in unicast mode
- Backward compatible: multicast mode used when unicast_peers is empty
- Add unicast config example (etc/config.unicast.yaml)
- Update README with unicast documentation and examples
This commit is contained in:
loveuer
2026-06-07 13:11:34 +08:00
parent 3fc3c860bc
commit 00eba85e39
7 changed files with 281 additions and 32 deletions
+95 -22
View File
@@ -47,29 +47,11 @@ func NewSocket(ifaceName string) (*Socket, error) {
return nil, fmt.Errorf("no IPv4 address found on interface %s", ifaceName)
}
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, VRRPProtocolNumber)
rawConn, packetConn, err := createRawConn()
if err != nil {
return nil, fmt.Errorf("failed to create raw socket: %w", err)
}
if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
syscall.Close(fd)
return nil, fmt.Errorf("failed to set SO_REUSEADDR: %w", err)
}
file := os.NewFile(uintptr(fd), "vrrp-socket")
packetConn, err := net.FilePacketConn(file)
file.Close()
if err != nil {
return nil, fmt.Errorf("failed to create packet connection: %w", err)
}
rawConn, err := ipv4.NewRawConn(packetConn)
if err != nil {
packetConn.Close()
return nil, fmt.Errorf("failed to create raw connection: %w", err)
}
groupIP := net.ParseIP(VRRPMulticastAddr).To4()
if groupIP == nil {
rawConn.Close()
@@ -95,6 +77,71 @@ func NewSocket(ifaceName string) (*Socket, error) {
}, nil
}
func NewUnicastSocket(ifaceName string, srcIP net.IP) (*Socket, error) {
iface, err := net.InterfaceByName(ifaceName)
if err != nil {
return nil, fmt.Errorf("failed to get interface %s: %w", ifaceName, err)
}
rawConn, packetConn, err := createRawConn()
if err != nil {
return nil, err
}
// Bind to the specific interface and source IP so we receive unicast VRRP packets
bindAddr := &syscall.SockaddrInet4{}
copy(bindAddr.Addr[:], srcIP.To4())
fd, _ := rawConn.SyscallConn()
var bindErr error
fd.Control(func(fd uintptr) {
bindErr = syscall.Bind(int(fd), bindAddr)
})
if bindErr != nil {
rawConn.Close()
return nil, fmt.Errorf("failed to bind to %s: %w", srcIP, bindErr)
}
if err := rawConn.SetControlMessage(ipv4.FlagTTL|ipv4.FlagSrc|ipv4.FlagDst|ipv4.FlagInterface, true); err != nil {
rawConn.Close()
return nil, fmt.Errorf("failed to set control message: %w", err)
}
return &Socket{
conn: rawConn,
packetConn: packetConn,
iface: iface,
localIP: srcIP,
groupIP: nil,
}, nil
}
func createRawConn() (*ipv4.RawConn, net.PacketConn, error) {
fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, VRRPProtocolNumber)
if err != nil {
return nil, nil, fmt.Errorf("failed to create raw socket: %w", err)
}
if err := syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1); err != nil {
syscall.Close(fd)
return nil, nil, fmt.Errorf("failed to set SO_REUSEADDR: %w", err)
}
file := os.NewFile(uintptr(fd), "vrrp-socket")
packetConn, err := net.FilePacketConn(file)
file.Close()
if err != nil {
return nil, nil, fmt.Errorf("failed to create packet connection: %w", err)
}
rawConn, err := ipv4.NewRawConn(packetConn)
if err != nil {
packetConn.Close()
return nil, nil, fmt.Errorf("failed to create raw connection: %w", err)
}
return rawConn, packetConn, nil
}
func (s *Socket) Send(pkt *VRRPPacket) error {
data, err := pkt.Marshal()
if err != nil {
@@ -119,6 +166,30 @@ func (s *Socket) Send(pkt *VRRPPacket) error {
return nil
}
func (s *Socket) SendTo(pkt *VRRPPacket, dst net.IP) error {
data, err := pkt.Marshal()
if err != nil {
return fmt.Errorf("failed to marshal packet: %w", err)
}
header := &ipv4.Header{
Version: ipv4.Version,
Len: ipv4.HeaderLen,
TOS: 0xC0,
TotalLen: ipv4.HeaderLen + len(data),
TTL: 255,
Protocol: VRRPProtocolNumber,
Dst: dst,
Src: s.localIP,
}
if err := s.conn.WriteTo(header, data, nil); err != nil {
return fmt.Errorf("failed to send packet to %s: %w", dst, err)
}
return nil
}
func (s *Socket) Receive() (*VRRPPacket, net.IP, error) {
buf := make([]byte, 1500)
@@ -140,8 +211,10 @@ func (s *Socket) SetReadDeadline(t time.Time) error {
}
func (s *Socket) Close() error {
if err := s.conn.LeaveGroup(s.iface, &net.IPAddr{IP: s.groupIP}); err != nil {
return err
if s.groupIP != nil {
if err := s.conn.LeaveGroup(s.iface, &net.IPAddr{IP: s.groupIP}); err != nil {
return err
}
}
return s.conn.Close()
}
}