Skip to content

Commit b6dec95

Browse files
committed
[!] add cross-platform support for ARP gratuitous requests, fixes #288
1 parent 26cd6d3 commit b6dec95

File tree

8 files changed

+178
-238
lines changed

8 files changed

+178
-238
lines changed

README.md

Lines changed: 61 additions & 42 deletions
Large diffs are not rendered by default.

go.mod

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module github.com/cybertec-postgresql/vip-manager
33
go 1.23.0
44

55
require (
6+
github.com/google/gopacket v1.1.19
67
github.com/hashicorp/consul/api v1.31.0
7-
github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875
88
github.com/spf13/pflag v1.0.6
99
github.com/spf13/viper v1.19.0
1010
go.etcd.io/etcd/client/v3 v3.5.18
@@ -13,11 +13,11 @@ require (
1313
)
1414

1515
require (
16-
github.com/armon/go-metrics v0.5.3 // indirect
16+
github.com/armon/go-metrics v0.5.4 // indirect
1717
github.com/coreos/go-semver v0.3.1 // indirect
1818
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
19-
github.com/fatih/color v1.17.0 // indirect
20-
github.com/fsnotify/fsnotify v1.7.0 // indirect
19+
github.com/fatih/color v1.18.0 // indirect
20+
github.com/fsnotify/fsnotify v1.8.0 // indirect
2121
github.com/gogo/protobuf v1.3.2 // indirect
2222
github.com/golang/protobuf v1.5.4 // indirect
2323
github.com/hashicorp/errwrap v1.1.0 // indirect
@@ -29,34 +29,29 @@ require (
2929
github.com/hashicorp/golang-lru v1.0.2 // indirect
3030
github.com/hashicorp/hcl v1.0.0 // indirect
3131
github.com/hashicorp/serf v0.10.1 // indirect
32-
github.com/josharian/native v1.1.0 // indirect
33-
github.com/magiconair/properties v1.8.7 // indirect
34-
github.com/mattn/go-colorable v0.1.13 // indirect
32+
github.com/magiconair/properties v1.8.9 // indirect
33+
github.com/mattn/go-colorable v0.1.14 // indirect
3534
github.com/mattn/go-isatty v0.0.20 // indirect
36-
github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect
37-
github.com/mdlayher/packet v1.1.2 // indirect
38-
github.com/mdlayher/socket v0.5.1 // indirect
3935
github.com/mitchellh/go-homedir v1.1.0 // indirect
4036
github.com/mitchellh/mapstructure v1.5.0 // indirect
4137
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
4238
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
43-
github.com/sagikazarmark/locafero v0.6.0 // indirect
39+
github.com/sagikazarmark/locafero v0.7.0 // indirect
4440
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
4541
github.com/sourcegraph/conc v0.3.0 // indirect
46-
github.com/spf13/afero v1.11.0 // indirect
47-
github.com/spf13/cast v1.7.0 // indirect
42+
github.com/spf13/afero v1.12.0 // indirect
43+
github.com/spf13/cast v1.7.1 // indirect
4844
github.com/subosito/gotenv v1.6.0 // indirect
4945
go.etcd.io/etcd/api/v3 v3.5.18 // indirect
5046
go.etcd.io/etcd/client/pkg/v3 v3.5.18 // indirect
5147
go.uber.org/multierr v1.11.0 // indirect
52-
golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
48+
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
5349
golang.org/x/net v0.34.0 // indirect
54-
golang.org/x/sync v0.10.0 // indirect
5550
golang.org/x/text v0.21.0 // indirect
56-
google.golang.org/genproto/googleapis/api v0.0.0-20240823204242-4ba0660f739c // indirect
57-
google.golang.org/genproto/googleapis/rpc v0.0.0-20240823204242-4ba0660f739c // indirect
58-
google.golang.org/grpc v1.65.0 // indirect
59-
google.golang.org/protobuf v1.34.2 // indirect
51+
google.golang.org/genproto/googleapis/api v0.0.0-20250106144421-5f5ef82da422 // indirect
52+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect
53+
google.golang.org/grpc v1.69.4 // indirect
54+
google.golang.org/protobuf v1.36.2 // indirect
6055
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
6156
gopkg.in/ini.v1 v1.67.0 // indirect
6257
gopkg.in/yaml.v3 v3.0.1 // indirect

go.sum

Lines changed: 47 additions & 48 deletions
Large diffs are not rendered by default.

ipmanager/basicConfigurer.go

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import (
55
"net"
66
"strings"
77

8-
arp "github.com/mdlayher/arp"
8+
"github.com/google/gopacket"
9+
"github.com/google/gopacket/layers"
10+
"github.com/google/gopacket/pcap"
911
)
1012

1113
// BasicConfigurer can be used to enable vip-management on nodes
@@ -16,7 +18,6 @@ import (
1618
// nearby routers and other devices.
1719
type BasicConfigurer struct {
1820
*IPConfiguration
19-
arpClient *arp.Client
2021
ntecontext uint32 //used by Windows to delete IP address
2122
}
2223

@@ -48,8 +49,51 @@ func (c *BasicConfigurer) queryAddress() bool {
4849
return false
4950
}
5051

51-
func (c *BasicConfigurer) cleanupArp() {
52-
if c.arpClient != nil {
53-
c.arpClient.Close()
52+
const (
53+
MACAddressSize = 6
54+
IPv4AddressSize = 4
55+
)
56+
57+
// arpSendGratuitous is a function that sends gratuitous ARP requests
58+
func (c *BasicConfigurer) arpSendGratuitous() error {
59+
// Open the network interface for sending
60+
handle, err := pcap.OpenLive(c.Iface.Name, 65536, false, pcap.BlockForever)
61+
if err != nil {
62+
return err
63+
}
64+
defer handle.Close()
65+
66+
// Create the Ethernet layer
67+
ethLayer := &layers.Ethernet{
68+
SrcMAC: c.Iface.HardwareAddr,
69+
DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // Broadcast
70+
EthernetType: layers.EthernetTypeARP,
71+
}
72+
73+
// Create the ARP layer
74+
arpLayer := &layers.ARP{
75+
AddrType: layers.LinkTypeEthernet,
76+
Protocol: layers.EthernetTypeIPv4,
77+
HwAddressSize: MACAddressSize,
78+
ProtAddressSize: IPv4AddressSize,
79+
Operation: layers.ARPReply, // Gratuitous ARP is sent as a reply
80+
SourceHwAddress: c.Iface.HardwareAddr,
81+
SourceProtAddress: c.IPConfiguration.VIP.AsSlice(),
82+
DstHwAddress: c.Iface.HardwareAddr, // Gratuitous ARP targets itself
83+
DstProtAddress: c.IPConfiguration.VIP.AsSlice(),
84+
}
85+
86+
// Create a packet with the layers
87+
buffer := gopacket.NewSerializeBuffer()
88+
opts := gopacket.SerializeOptions{
89+
FixLengths: true,
90+
ComputeChecksums: true,
5491
}
92+
err = gopacket.SerializeLayers(buffer, opts, ethLayer, arpLayer)
93+
if err != nil {
94+
return err
95+
}
96+
97+
// Send the packet
98+
return handle.WritePacketData(buffer.Bytes())
5599
}

ipmanager/basicConfigurer_linux.go

Lines changed: 3 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,22 @@
11
package ipmanager
22

33
import (
4-
"net"
54
"os/exec"
6-
"time"
7-
8-
arp "github.com/mdlayher/arp"
95
)
106

117
const (
128
arpRequestOp = 1
139
arpReplyOp = 2
1410
)
1511

16-
var (
17-
ethernetBroadcast = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
18-
)
19-
2012
// configureAddress assigns virtual IP address
2113
func (c *BasicConfigurer) configureAddress() bool {
22-
if c.arpClient == nil {
23-
if err := c.createArpClient(); err != nil {
24-
log.Error("Couldn't create an Arp client:", err)
25-
}
26-
}
27-
2814
log.Infof("Configuring address %s on %s", c.getCIDR(), c.Iface.Name)
29-
3015
result := c.runAddressConfiguration("add")
31-
3216
if result {
33-
// For now it is save to say that also working even if a
34-
// gratuitous arp message could not be send but logging an
35-
// errror should be enough.
36-
_ = c.arpSendGratuitous()
17+
if err := c.arpSendGratuitous(); err != nil {
18+
log.Error("Failed to send gratuitous ARP: ", err)
19+
}
3720
}
3821

3922
return result
@@ -64,96 +47,3 @@ func (c *BasicConfigurer) runAddressConfiguration(action string) bool {
6447
}
6548
return true
6649
}
67-
68-
func (c *BasicConfigurer) createArpClient() (err error) {
69-
for i := 0; i < c.RetryNum; i++ {
70-
if c.arpClient, err = arp.Dial(&c.Iface); err == nil {
71-
return
72-
}
73-
log.Infof("Problems with producing the arp client: %s", err)
74-
time.Sleep(time.Duration(c.RetryAfter) * time.Millisecond)
75-
}
76-
return
77-
}
78-
79-
// sends a gratuitous ARP request and reply
80-
func (c *BasicConfigurer) arpSendGratuitous() error {
81-
/* While RFC 2002 does not say whether a gratuitous ARP request or reply is preferred
82-
* to update ones neighbours' MAC tables, the Wireshark Wiki recommends sending both.
83-
* https://wiki.wireshark.org/Gratuitous_ARP
84-
* This site also recommends sending a reply, as requests might be ignored by some hardware:
85-
* https://support.citrix.com/article/CTX112701
86-
*/
87-
if c.arpClient == nil {
88-
log.Info("No arp client available, skip send gratuitous ARP")
89-
return nil
90-
}
91-
gratuitousReplyPackage, err := arp.NewPacket(
92-
arpReplyOp,
93-
c.Iface.HardwareAddr,
94-
c.VIP,
95-
c.Iface.HardwareAddr,
96-
c.VIP,
97-
)
98-
if err != nil {
99-
log.Infof("Gratuitous arp reply package is malformed: %s", err)
100-
return err
101-
}
102-
103-
/* RFC 2002 specifies (in section 4.6) that a gratuitous ARP request
104-
* should "not set" the target Hardware Address (THA).
105-
* Since the arp package offers no option to leave the THA out, we specify the Zero-MAC.
106-
* If parsing that fails for some reason, we'll just use the local interface's address.
107-
* The field is probably ignored by the receivers' implementation anyway.
108-
*/
109-
arpRequestDestMac, err := net.ParseMAC("00:00:00:00:00:00")
110-
if err != nil {
111-
// not entirely RFC-2002 conform but better then nothing.
112-
arpRequestDestMac = c.Iface.HardwareAddr
113-
}
114-
115-
gratuitousRequestPackage, err := arp.NewPacket(
116-
arpRequestOp,
117-
c.Iface.HardwareAddr,
118-
c.VIP,
119-
arpRequestDestMac,
120-
c.VIP,
121-
)
122-
if err != nil {
123-
log.Infof("Gratuitous arp request package is malformed: %s", err)
124-
return err
125-
}
126-
127-
for i := 0; i < c.RetryNum; i++ {
128-
errReply := c.arpClient.WriteTo(gratuitousReplyPackage, ethernetBroadcast)
129-
if err != nil {
130-
log.Error("Couldn't write to the arpClient:", errReply)
131-
} else {
132-
log.Info("Sent gratuitous ARP reply")
133-
}
134-
135-
errRequest := c.arpClient.WriteTo(gratuitousRequestPackage, ethernetBroadcast)
136-
if err != nil {
137-
log.Error("Couldn't write to the arpClient:", errRequest)
138-
} else {
139-
log.Info("Sent gratuitous ARP request")
140-
}
141-
142-
if errReply != nil || errRequest != nil {
143-
/* If something went wrong while sending the packages, we'll recreate the ARP client for the next try,
144-
* to avoid having a stale client that gives "network is down" error.
145-
*/
146-
err = c.createArpClient()
147-
} else {
148-
//TODO: think about whether to leave this out to achieve simple repeat sending of GARP packages
149-
break
150-
}
151-
time.Sleep(time.Duration(c.RetryAfter) * time.Millisecond)
152-
}
153-
if err != nil {
154-
log.Error("Too many retries", err)
155-
return err
156-
}
157-
158-
return nil
159-
}

ipmanager/basicConfigurer_windows.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ func (c *BasicConfigurer) configureAddress() bool {
2525
log.Error("Failed to add address: ", err)
2626
return false
2727
}
28-
// For now it is save to say that also working even if a
29-
// gratuitous arp message could not be send but logging an
30-
// errror should be enough.
31-
//_ = c.ARPSendGratuitous()
28+
29+
if err := c.arpSendGratuitous(); err != nil {
30+
log.Error("Failed to send gratuitous ARP: ", err)
31+
}
3232
return true
3333
}
3434

ipmanager/hetznerConfigurer.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,3 @@ func (c *HetznerConfigurer) runAddressConfiguration() bool {
275275
c.cachedState = unknown
276276
return false
277277
}
278-
279-
func (c *HetznerConfigurer) cleanupArp() {
280-
// dummy function as the usage of interfaces requires us to have this function.
281-
// It is sufficient for the leader to tell Hetzner to switch the IP, no cleanup needed.
282-
}

ipmanager/ip_manager.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ type ipConfigurer interface {
1616
configureAddress() bool
1717
deconfigureAddress() bool
1818
getCIDR() string
19-
cleanupArp()
2019
}
2120

2221
var log *zap.SugaredLogger = zap.L().Sugar()
@@ -124,7 +123,6 @@ func (m *IPManager) SyncStates(ctx context.Context, states <-chan bool) {
124123
}
125124
case <-ctx.Done():
126125
m.configurer.deconfigureAddress()
127-
m.configurer.cleanupArp()
128126
return
129127
}
130128
}

0 commit comments

Comments
 (0)