diff --git a/README.md b/README.md
index 7795dad..b2187be 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
[](https://opensource.org/licenses/BSD-2)
-
+
[](https://goreportcard.com/report/github.com/cybertec-postgresql/vip-manager)
[](https://github.com/cybertec-postgresql/vip-manager/releases/latest)
[](https://github.com/cybertec-postgresql/vip-manager/releases)
@@ -9,6 +9,7 @@
Manages a virtual IP based on state kept in `etcd`, `Consul` or using `Patroni` REST API
## Table of Contents
+
- [Prerequisites](#prerequisites)
- [Building](#building)
- [Installing from package](#installing-from-package)
@@ -17,7 +18,7 @@ Manages a virtual IP based on state kept in `etcd`, `Consul` or using `Patroni`
- [PostgreSQL prerequisites](#postgresql-prerequisites)
- [Configuration](#configuration)
- [Configuration - Hetzner](#configuration---hetzner)
- - [Credential File - Hetzmer](#credential-file---hetzner)
+ - [Credential File - Hetzmer](#credential-file---hetzner)
- [Debugging](#debugging)
- [Author](#author)
@@ -28,25 +29,34 @@ Manages a virtual IP based on state kept in `etcd`, `Consul` or using `Patroni`
- `goreleaser` (optional)
## Building
+
1. clone this repo
-```
+
+```shell
git clone https://github.com/cybertec-postgresql/vip-manager.git
```
-2. Build the binary using `make` or `go build`.
-5. To build your own packages (.deb, .rpm, .zip, etc.), run
-```
+
+1. Build the binary using `make` or `go build`.
+
+1. To build your own packages (.deb, .rpm, .zip, etc.), run
+
+```shell
make package
```
-or
-```
+
+or
+
+```shell
goreleaser release --snapshot --skip-publish --rm-dist
```
## Installing from package
+
You can download .rpm or .deb packages here, on the [Releases](https://github.com/cybertec-postgresql/vip-manager/releases) page.
On Debian and Ubuntu, the universe repositories should provide you with vip-manager, though the version may be not as recent.
-> **Warning**
-> Our packages are probably not compatible with the one from those repositories, do not try to install them side-by-side.
+
+> [!IMPORTANT]
+Our packages are probably not compatible with the one from those repositories, do not try to install them side-by-side.
## Installing from source
@@ -54,15 +64,15 @@ On Debian and Ubuntu, the universe repositories should provide you with vip-mana
- Run `DESTDIR=/tmp make install` to copy the binary, service files and config file into the destination of your choice.
- Edit config to your needs, then run `systemctl daemon-reload`, then `systemctl start vip-manager`.
-> **Note**
-> systemd will only pick the service files up if you chose a `DESTDIR` so that it can find it. Usually `DESTDIR=''` should work.
+> [!NOTE]
+systemd will only pick the service files up if you chose a `DESTDIR` so that it can find it. Usually `DESTDIR=''` should work.
## Environment prerequisites
When vip-manager is in charge of registering and deregistering the VIP locally, it needs superuser privileges to do so.
This is not required when vip-manager is used to manage a VIP through some API, e.g. Hetzner Robot API or Hetzner Cloud API.
-> **Note**
+> [!NOTE]
> At some point it would be great to reduce this requirement to only the `CAP_NET_RAW` and `CAP_NET_ADMIN` capabilities, which could be added by a superuser to the vip-manager binary _once_.
> Right now, this is not possible since vip-manager launches plain shell commands to register and deregister virtual IP addresses locally (at least on linux), so the whole user would need these privileges.
> When vip-manager is eventually taught to directly use a library that directly uses the Linux kernel's API to register/deregister the VIP, the capabilities set for the binary will suffice.
@@ -74,15 +84,19 @@ to all found network interfaces. So something like `*` or `0.0.0.0` (IPv4 only)
to activate the automatic binding. This again might not be suitable for all use cases where security is paramount for example.
### nonlocal bind
+
If you can't set `listen_addresses` to a wildcard address, you can explicitly specify only those adresses that you want to listen to.
However, if you add the virtual IP to those addresses, PostgreSQL will fail to start when that address is not yet registered on one of the interfaces of the machine.
You need to configure the kernel to allow "nonlocal bind" of IP (v4) addresses:
+
- temporarily:
+
```bash
sysctl -w net.ipv4.ip_nonlocal_bind=1
```
- permanently:
+
```bash
echo "net.ipv4.ip_nonlocal_bind = 1" >> /etc/sysctl.conf
sysctl -p
@@ -92,18 +106,19 @@ sysctl -p
The configuration can be passed to the executable through argument flags, environment variables or through a YAML config file. Run `vip-manager --help` to see the available flags.
-> **Note**
+> [!NOTE]
> The location of the YAML config file can be specified with the --config flag.
> An exemplary config file is installed into `/etc/default/vip-manager.yml` or is available in the vipconfig directory in the repository of the software.
Configuration is now (from release v1.0 on) handled using the [`viper`](https://github.com/spf13/viper) library.
This means that environment variables, command line flags, and config files can be used to configure vip-manager.
When using different configuration sources simultaneously, this is the precedence order:
+
- flag
- env
- config
-> **Note**
+> [!NOTE]
> So flags always overwrite env variables and entries from the config file. Env variables overwrite the config file entries.
All flags and file entries are written in lower case. To make longer multi-word flags and entries readable, they are separated by dashes, e.g. `retry-num`.
@@ -112,38 +127,42 @@ If you put a flag or file entry into uppercase and replace dashes with underscor
This is a list of all avaiable configuration items:
-| flag/yaml key | env notation | required | example | description |
-| ----------------- | --------------------- | --------- | ------------------------- | ----------- |
-`ip` | `VIP_IP` | yes | 10.10.10.123 | The virtual IP address that will be managed.
-`netmask` | `VIP_NETMASK` | yes | 24 | The netmask that is associated with the subnet that the virtual IP `vip` is part of.
-`interface` | `VIP_INTERFACE` | yes | eth0 | A local network interface on the machine that runs vip-manager. Required when using `manager-type=basic`. The vip will be added to and removed from this interface.
-`trigger-key` | `VIP_TRIGGER_KEY` | yes | /service/pgcluster/leader | The key in the DCS or the Patroni REST endpoint (e.g. `/leader`) that will be monitored by vip-manager. Must match `//leader` from Patroni config. When the value returned by the DCS equals `trigger-value`, vip-manager will make sure that the virtual IP is registered to this machine. If it does not match, vip-manager makes sure that the virtual IP is not registered to this machine.
-`trigger-value` | `VIP_TRIGGER_VALUE` | no | pgcluster_member_1 | The value that the DCS' answer for `trigger-key` will be matched to. Must match `` from Patroni config for DCS or the HTTP response for Patroni REST API. This is usually set to the name of the Patroni cluster member that this vip-manager instance is associated with. Defaults to the machine's hostname or to 200 for Patroni.
-`manager-type` | `VIP_MANAGER_TYPE` | no | basic | Either `basic` or `hetzner`. This describes the mechanism that is used to manage the virtual IP. Defaults to `basic`.
-`dcs-type` | `VIP_DCS_TYPE` | no | etcd | The type of DCS that vip-manager will use to monitor the `trigger-key`. Defaults to `etcd`.
-`dcs-endpoints` | `VIP_DCS_ENDPOINTS` | no | http://10.10.11.1:2379 | A url that defines where to reach the DCS or Patroni REST API. Multiple endpoints can be passed to the flag or env variable using a comma-separated-list. In the config file, a list can be specified, see the sample config for an example. Defaults to `http://127.0.0.1:2379` for `dcs-type=etcd`, `http://127.0.0.1:8500` for `dcs-type=consul` and `http://127.0.0.1:8008` for `dcs-type=patroni`.
-`etcd-user` | `VIP_ETCD_USER` | no | patroni | A username that is allowed to look at the `trigger-key` in an etcd DCS. Optional when using `dcs-type=etcd` .
-`etcd-password` | `VIP_ETCD_PASSWORD` | no | snakeoil | The password for `etcd-user`. Optional when using `dcs-type=etcd` . Requires that `etcd-user` is also set.
-`consul-token` | `VIP_CONSUL_TOKEN` | no | snakeoil | A token that can be used with the consul-API for authentication. Optional when using `dcs-type=consul` .
-`interval` | `VIP_INTERVAL` | no | 1000 | The time vip-manager main loop sleeps before checking for changes. Measured in ms. Defaults to `1000`. Doesn't affect etcd checker since v2.3.0.
-`retry-after` | `VIP_RETRY_AFTER` | no | 250 | The time to wait before retrying interactions with components outside of vip-manager. Measured in ms. Defaults to `250`.
-`retry-num` | `VIP_RETRY_NUM` | no | 3 | The number of times interactions with components outside of vip-manager are retried. Defaults to `3`.
-`etcd-ca-file` | `VIP_ETCD_CA_FILE` | no | /etc/etcd/ca.cert.pem | A certificate authority file that can be used to verify the certificate provided by etcd endpoints. Make sure to change `dcs-endpoints` to reflect that `https` is used.
-`etcd-cert-file` | `VIP_ETCD_CERT_FILE` | no | /etc/etcd/client.cert.pem | A client certificate that is used to authenticate against etcd endpoints. Requires `etcd-ca-file` to be set as well.
-`etcd-key-file` | `VIP_ETCD_KEY_FILE` | no | /etc/etcd/client.key.pem | A private key for the client certificate, used to decrypt messages sent by etcd endpoints. Required when `etcd-cert-file` is specified.
-`verbose` | `VIP_VERBOSE` | no | true | Enable more verbose logging. Currently only the manager-type=hetzner provides additional logs.
+| flag/yaml key | env notation | required | example | description |
+| ----------------- | --------------------- | --------- | --------------------------- | ----------- |
+| `ip` | `VIP_IP` | yes | `10.10.10.123` | The virtual IP address that will be managed. |
+| `netmask` | `VIP_NETMASK` | yes | `24` | The netmask that is associated with the subnet that the virtual IP `vip` is part of. |
+| `interface` | `VIP_INTERFACE` | yes | `eth0` | A local network interface on the machine that runs vip-manager. Required when using `manager-type=basic`. The vip will be added to and removed from this interface. |
+| `trigger-key` | `VIP_TRIGGER_KEY` | yes | `/service/pgcluster/leader` | The key in the DCS or the Patroni REST endpoint (e.g. `/leader`) that will be monitored by vip-manager. Must match `//leader` from Patroni config. When the value returned by the DCS equals `trigger-value`, vip-manager will make sure that the virtual IP is registered to this machine. If it does not match, vip-manager makes sure that the virtual IP is not registered to this machine. |
+| `trigger-value` | `VIP_TRIGGER_VALUE` | no | `pgcluster_member_1` | The value that the DCS' answer for `trigger-key` will be matched to. Must match `` from Patroni config for DCS or the HTTP response for Patroni REST API. This is usually set to the name of the Patroni cluster member that this vip-manager instance is associated with. Defaults to the machine's hostname or to 200 for Patroni. |
+| `manager-type` | `VIP_MANAGER_TYPE` | no | `basic` | Either `basic` or `hetzner`. This describes the mechanism that is used to manage the virtual IP. Defaults to `basic`. |
+| `dcs-type` | `VIP_DCS_TYPE` | no | `etcd` | The type of DCS that vip-manager will use to monitor the `trigger-key`. Defaults to `etcd`. |
+| `dcs-endpoints` | `VIP_DCS_ENDPOINTS` | no | `http://10.10.11.1:2379` | A url that defines where to reach the DCS or Patroni REST API. Multiple endpoints can be passed to the flag or env variable using a comma-separated-list. In the config file, a list can be specified, see the sample config for an example. Defaults to `http://127.0.0.1:2379` for `dcs-type=etcd`, `http://127.0.0.1:8500` for `dcs-type=consul` and `http://127.0.0.1:8008` for `dcs-type=patroni`. |
+| `etcd-user` | `VIP_ETCD_USER` | no | `patroni` | A username that is allowed to look at the `trigger-key` in an etcd DCS. Optional when using `dcs-type=etcd` . |
+| `etcd-password` | `VIP_ETCD_PASSWORD` | no | `snakeoil` | The password for `etcd-user`. Optional when using `dcs-type=etcd` . Requires that `etcd-user` is also set. |
+| `consul-token` | `VIP_CONSUL_TOKEN` | no | `snakeoil` | A token that can be used with the consul-API for authentication. Optional when using `dcs-type=consul` . |
+| `interval` | `VIP_INTERVAL` | no | `1000` | The time vip-manager main loop sleeps before checking for changes. Measured in ms. Defaults to `1000`. Doesn't affect etcd checker since v2.3.0. |
+| `retry-after` | `VIP_RETRY_AFTER` | no | `250` | The time to wait before retrying interactions with components outside of vip-manager. Measured in ms. Defaults to `250`. |
+| `retry-num` | `VIP_RETRY_NUM` | no | `3` | The number of times interactions with components outside of vip-manager are retried. Defaults to `3`. |
+| `etcd-ca-le` | `VIP_ETCD_CA_FILE` | no | `/etc/etcd/ca.cert.pem` | A certificate authority file that can be used to verify the certificate provided by etcd endpoints. Make sure to change `dcs-endpoints` to reflect that `https` is used. |
+| `etcd-cert-le` | `VIP_ETCD_CERT_FILE` | no | `/etc/etcd/client.cert.pem` | A client certificate that is used to authenticate against etcd endpoints. Requires `etcd-ca-file` to be set as well. |
+| `etcd-key-le` | `VIP_ETCD_KEY_FILE` | no | `/etc/etcd/client.key.pem` | A private key for the client certificate, used to decrypt messages sent by etcd endpoints. Required when `etcd-cert-file` is specified. |
+| `verbose` | `VIP_VERBOSE` | no | `true` | Enable more verbose logging. Currently only the manager-type=hetzner provides additional logs. |
## Configuration - Patroni REST API
+
To directly use the Patroni REST API, simply set `dcs-type` to `patroni` and `trigger-key` to `/leader`. The defaults for `dcs-endpoints` (`http://127.0.0.1:8008`) and `trigger-value` (200) for the Patroni checker should work in most cases.
## Configuration - Hetzner
-To use vip-manager with Hetzner Robot API you need a Credential file, set `hosting_type` to `hetzner` in `/etc/default/vip-manager.yml`
+
+To use vip-manager with Hetzner Robot API you need a Credential file, set `hosting_type` to `hetzner` in `/etc/default/vip-manager.yml`
and your Floating-IP must be added on all Servers.
The Floating-IP (VIP) will not be added or removed on the current Master node interface, Hetzner will route it to the current one.
### Credential File - Hetzner
+
Add the File `/etc/hetzner` with your Username and Password
-```
+
+```shell
user="myUsername"
pass="myPassword"
```
@@ -152,13 +171,13 @@ pass="myPassword"
Either:
-* run `vip-manager` with `--verbose` flag or
-* set `verbose` to `true` in `/etc/default/vip-manager.yml`
-* set `VIP_VERBOSE=true`
+- run `vip-manager` with `--verbose` flag or
+- set `verbose` to `true` in `/etc/default/vip-manager.yml`
+- set `VIP_VERBOSE=true`
-> **Note**
+> [!NOTE]
> Currently only supported for `hetzner`
## Author
-CYBERTEC PostgreSQL International GmbH, https://www.cybertec-postgresql.com
+CYBERTEC PostgreSQL International GmbH,
diff --git a/go.mod b/go.mod
index c4cb4ac..d8c4406 100644
--- a/go.mod
+++ b/go.mod
@@ -5,8 +5,8 @@ go 1.23.0
toolchain go1.24.1
require (
+ github.com/google/gopacket v1.1.19
github.com/hashicorp/consul/api v1.31.2
- github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875
github.com/spf13/pflag v1.0.6
github.com/spf13/viper v1.20.0
go.etcd.io/etcd/client/v3 v3.5.19
@@ -31,12 +31,8 @@ require (
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v1.0.2 // indirect
github.com/hashicorp/serf v0.10.1 // indirect
- github.com/josharian/native v1.1.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
- github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 // indirect
- github.com/mdlayher/packet v1.1.2 // indirect
- github.com/mdlayher/socket v0.5.1 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
@@ -51,7 +47,6 @@ require (
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
golang.org/x/net v0.36.0 // indirect
- golang.org/x/sync v0.11.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
diff --git a/go.sum b/go.sum
index 265d119..84f7e94 100644
--- a/go.sum
+++ b/go.sum
@@ -51,11 +51,11 @@ github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
+github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/hashicorp/consul/api v1.31.2 h1:NicObVJHcCmyOIl7Z9iHPvvFrocgTYo9cITSGg0/7pw=
github.com/hashicorp/consul/api v1.31.2/go.mod h1:Z8YgY0eVPukT/17ejW+l+C7zJmKwgPHtjU1q16v/Y40=
github.com/hashicorp/consul/sdk v0.16.1 h1:V8TxTnImoPD5cj0U9Spl0TUxcytjcbbJeADFF07KdHg=
@@ -102,9 +102,6 @@ github.com/hashicorp/memberlist v0.5.0 h1:EtYPN8DpAURiapus508I4n9CzHs2W+8NZGbmmR
github.com/hashicorp/memberlist v0.5.0/go.mod h1:yvyXLpo0QaGE59Y7hDTsTzDD25JYBZ4mHgHUZ8lrOI0=
github.com/hashicorp/serf v0.10.1 h1:Z1H2J60yRKvfDYAOZLd2MU0ND4AH/WDz7xYHDWQsIPY=
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
-github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
-github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
-github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@@ -135,16 +132,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875 h1:ql8x//rJsHMjS+qqEag8n3i4azw1QneKh5PieH9UEbY=
-github.com/mdlayher/arp v0.0.0-20220512170110-6706a2966875/go.mod h1:kfOoFJuHWp76v1RgZCb9/gVUc7XdY877S2uVYbNliGc=
-github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE=
-github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og=
-github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU=
-github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
-github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
-github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E=
-github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
-github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=
github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
@@ -239,12 +226,12 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -260,8 +247,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -281,7 +266,6 @@ golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -298,6 +282,7 @@ golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/ipmanager/basicConfigurer.go b/ipmanager/basicConfigurer.go
index cef1085..d5f1a25 100644
--- a/ipmanager/basicConfigurer.go
+++ b/ipmanager/basicConfigurer.go
@@ -5,7 +5,8 @@ import (
"net"
"strings"
- arp "github.com/mdlayher/arp"
+ "github.com/google/gopacket"
+ "github.com/google/gopacket/layers"
)
// BasicConfigurer can be used to enable vip-management on nodes
@@ -16,7 +17,6 @@ import (
// nearby routers and other devices.
type BasicConfigurer struct {
*IPConfiguration
- arpClient *arp.Client
ntecontext uint32 //used by Windows to delete IP address
}
@@ -48,8 +48,43 @@ func (c *BasicConfigurer) queryAddress() bool {
return false
}
-func (c *BasicConfigurer) cleanupArp() {
- if c.arpClient != nil {
- c.arpClient.Close()
+const (
+ MACAddressSize = 6
+ IPv4AddressSize = 4
+)
+
+// createGratuitousARP prepares a packet with a gratuitous ARP request
+func (c *BasicConfigurer) createGratuitousARP() ([]byte, error) {
+ // Create the Ethernet layer
+ ethLayer := &layers.Ethernet{
+ SrcMAC: c.Iface.HardwareAddr,
+ DstMAC: net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, // Broadcast
+ EthernetType: layers.EthernetTypeARP,
}
+
+ // Create the ARP layer
+ arpLayer := &layers.ARP{
+ AddrType: layers.LinkTypeEthernet,
+ Protocol: layers.EthernetTypeIPv4,
+ HwAddressSize: MACAddressSize,
+ ProtAddressSize: IPv4AddressSize,
+ Operation: layers.ARPReply, // Gratuitous ARP is sent as a reply
+ SourceHwAddress: c.Iface.HardwareAddr,
+ SourceProtAddress: c.IPConfiguration.VIP.AsSlice(),
+ DstHwAddress: c.Iface.HardwareAddr, // Gratuitous ARP targets itself
+ DstProtAddress: c.IPConfiguration.VIP.AsSlice(),
+ }
+
+ // Create a packet with the layers
+ buffer := gopacket.NewSerializeBuffer()
+ opts := gopacket.SerializeOptions{
+ FixLengths: true,
+ ComputeChecksums: true,
+ }
+
+ if err := gopacket.SerializeLayers(buffer, opts, ethLayer, arpLayer); err != nil {
+ return nil, err
+ }
+
+ return buffer.Bytes(), nil
}
diff --git a/ipmanager/basicConfigurer_linux.go b/ipmanager/basicConfigurer_linux.go
index 342320b..a18d95c 100644
--- a/ipmanager/basicConfigurer_linux.go
+++ b/ipmanager/basicConfigurer_linux.go
@@ -3,37 +3,46 @@ package ipmanager
import (
"net"
"os/exec"
- "time"
-
- arp "github.com/mdlayher/arp"
+ "syscall"
)
-const (
- arpRequestOp = 1
- arpReplyOp = 2
-)
+// htons converts uint16 to network byte order
+func htons(i uint16) uint16 {
+ return (i<<8)&0xff00 | i>>8
+}
-var (
- ethernetBroadcast = net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
-)
+func sendPacketLinux(iface net.Interface, packetData []byte) error {
+ fd, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_RAW, int(htons(syscall.ETH_P_ALL)))
+ if err != nil {
+ return err
+ }
+ defer syscall.Close(fd)
-// configureAddress assigns virtual IP address
-func (c *BasicConfigurer) configureAddress() bool {
- if c.arpClient == nil {
- if err := c.createArpClient(); err != nil {
- log.Error("Couldn't create an Arp client:", err)
- }
+ var sll syscall.SockaddrLinklayer
+ sll.Protocol = htons(syscall.ETH_P_ARP)
+ sll.Ifindex = iface.Index
+ sll.Hatype = syscall.ARPHRD_ETHER
+ sll.Pkttype = syscall.PACKET_BROADCAST
+
+ if err = syscall.Bind(fd, &sll); err != nil {
+ return err
}
- log.Infof("Configuring address %s on %s", c.getCIDR(), c.Iface.Name)
+ return syscall.Sendto(fd, packetData, 0, &sll)
+}
+// configureAddress assigns virtual IP address
+func (c *BasicConfigurer) configureAddress() bool {
+ log.Infof("Configuring address %s on %s", c.getCIDR(), c.Iface.Name)
result := c.runAddressConfiguration("add")
-
if result {
- // For now it is save to say that also working even if a
- // gratuitous arp message could not be send but logging an
- // errror should be enough.
- _ = c.arpSendGratuitous()
+ if buff, err := c.createGratuitousARP(); err != nil {
+ log.Warn("Failed to compose gratuitous ARP request: ", err)
+ } else {
+ if err := sendPacketLinux(c.Iface, buff); err != nil {
+ log.Warn("Failed to send gratuitous ARP request: ", err)
+ }
+ }
}
return result
@@ -64,96 +73,3 @@ func (c *BasicConfigurer) runAddressConfiguration(action string) bool {
}
return true
}
-
-func (c *BasicConfigurer) createArpClient() (err error) {
- for i := 0; i < c.RetryNum; i++ {
- if c.arpClient, err = arp.Dial(&c.Iface); err == nil {
- return
- }
- log.Infof("Problems with producing the arp client: %s", err)
- time.Sleep(time.Duration(c.RetryAfter) * time.Millisecond)
- }
- return
-}
-
-// sends a gratuitous ARP request and reply
-func (c *BasicConfigurer) arpSendGratuitous() error {
- /* While RFC 2002 does not say whether a gratuitous ARP request or reply is preferred
- * to update ones neighbours' MAC tables, the Wireshark Wiki recommends sending both.
- * https://wiki.wireshark.org/Gratuitous_ARP
- * This site also recommends sending a reply, as requests might be ignored by some hardware:
- * https://support.citrix.com/article/CTX112701
- */
- if c.arpClient == nil {
- log.Info("No arp client available, skip send gratuitous ARP")
- return nil
- }
- gratuitousReplyPackage, err := arp.NewPacket(
- arpReplyOp,
- c.Iface.HardwareAddr,
- c.VIP,
- c.Iface.HardwareAddr,
- c.VIP,
- )
- if err != nil {
- log.Infof("Gratuitous arp reply package is malformed: %s", err)
- return err
- }
-
- /* RFC 2002 specifies (in section 4.6) that a gratuitous ARP request
- * should "not set" the target Hardware Address (THA).
- * Since the arp package offers no option to leave the THA out, we specify the Zero-MAC.
- * If parsing that fails for some reason, we'll just use the local interface's address.
- * The field is probably ignored by the receivers' implementation anyway.
- */
- arpRequestDestMac, err := net.ParseMAC("00:00:00:00:00:00")
- if err != nil {
- // not entirely RFC-2002 conform but better then nothing.
- arpRequestDestMac = c.Iface.HardwareAddr
- }
-
- gratuitousRequestPackage, err := arp.NewPacket(
- arpRequestOp,
- c.Iface.HardwareAddr,
- c.VIP,
- arpRequestDestMac,
- c.VIP,
- )
- if err != nil {
- log.Infof("Gratuitous arp request package is malformed: %s", err)
- return err
- }
-
- for i := 0; i < c.RetryNum; i++ {
- errReply := c.arpClient.WriteTo(gratuitousReplyPackage, ethernetBroadcast)
- if err != nil {
- log.Error("Couldn't write to the arpClient:", errReply)
- } else {
- log.Info("Sent gratuitous ARP reply")
- }
-
- errRequest := c.arpClient.WriteTo(gratuitousRequestPackage, ethernetBroadcast)
- if err != nil {
- log.Error("Couldn't write to the arpClient:", errRequest)
- } else {
- log.Info("Sent gratuitous ARP request")
- }
-
- if errReply != nil || errRequest != nil {
- /* If something went wrong while sending the packages, we'll recreate the ARP client for the next try,
- * to avoid having a stale client that gives "network is down" error.
- */
- err = c.createArpClient()
- } else {
- //TODO: think about whether to leave this out to achieve simple repeat sending of GARP packages
- break
- }
- time.Sleep(time.Duration(c.RetryAfter) * time.Millisecond)
- }
- if err != nil {
- log.Error("Too many retries", err)
- return err
- }
-
- return nil
-}
diff --git a/ipmanager/basicConfigurer_windows.go b/ipmanager/basicConfigurer_windows.go
index fb6bd6f..682b821 100644
--- a/ipmanager/basicConfigurer_windows.go
+++ b/ipmanager/basicConfigurer_windows.go
@@ -7,6 +7,18 @@ import (
"github.com/cybertec-postgresql/vip-manager/iphlpapi"
)
+func sendPacketWindows(iface net.Interface, packetData []byte) error {
+ // Open a raw socket using Winsock
+ conn, err := net.Dial("ip4:ethernet", iface.HardwareAddr.String())
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+ // Send the packet
+ _, err = conn.Write(packetData)
+ return err
+}
+
// configureAddress assigns virtual IP address
func (c *BasicConfigurer) configureAddress() bool {
log.Infof("Configuring address %s on %s", c.getCIDR(), c.Iface.Name)
@@ -25,10 +37,14 @@ func (c *BasicConfigurer) configureAddress() bool {
log.Error("Failed to add address: ", err)
return false
}
- // For now it is save to say that also working even if a
- // gratuitous arp message could not be send but logging an
- // errror should be enough.
- //_ = c.ARPSendGratuitous()
+
+ if buff, err := c.createGratuitousARP(); err != nil {
+ log.Warn("Failed to compose gratuitous ARP request: ", err)
+ } else {
+ if err := sendPacketWindows(c.Iface, buff); err != nil {
+ log.Warn("Failed to send gratuitous ARP request: ", err)
+ }
+ }
return true
}
diff --git a/ipmanager/hetznerConfigurer.go b/ipmanager/hetznerConfigurer.go
index da1d9ce..1c00b99 100644
--- a/ipmanager/hetznerConfigurer.go
+++ b/ipmanager/hetznerConfigurer.go
@@ -275,8 +275,3 @@ func (c *HetznerConfigurer) runAddressConfiguration() bool {
c.cachedState = unknown
return false
}
-
-func (c *HetznerConfigurer) cleanupArp() {
- // dummy function as the usage of interfaces requires us to have this function.
- // It is sufficient for the leader to tell Hetzner to switch the IP, no cleanup needed.
-}
diff --git a/ipmanager/ip_manager.go b/ipmanager/ip_manager.go
index 66f6242..97a4143 100644
--- a/ipmanager/ip_manager.go
+++ b/ipmanager/ip_manager.go
@@ -16,7 +16,6 @@ type ipConfigurer interface {
configureAddress() bool
deconfigureAddress() bool
getCIDR() string
- cleanupArp()
}
var log *zap.SugaredLogger = zap.L().Sugar()
@@ -124,7 +123,6 @@ func (m *IPManager) SyncStates(ctx context.Context, states <-chan bool) {
}
case <-ctx.Done():
m.configurer.deconfigureAddress()
- m.configurer.cleanupArp()
return
}
}