Repository: TwiN/gatus

Issue:
Feature Request: choose which IP protocol is used
Following the current OVH outage, which only partially affected the services accessible in IPv6, I think it would be a nice feature to be able to distinguish on Gatus the services accessible only in v6 or in v4.

An implementation could be by setting `services[].ip_version` to `inet` or `inet6`:

```yaml
services:
  - name: website IPv4
    url: "https://twin.sh/health"
    ip_version: inet
    conditions:
      - "[STATUS] == 200"

  - name: website IPv6
    url: "https://twin.sh/health"
    ip_version: inet6
    conditions:
      - "[STATUS] == 200"
```

Hints from issue comments:
@TwiN: Fixed by #661 @ribtoks: @TwiN Why this is closed? The issue is about chosing IP protocol in general and the author's use-case clearly indicated the HTTP GET request (**not** ICMP request) to "https://twin.sh/health", while the pull request introduced an unrelated ICMP functionality. Can we reopen it until the network mode for HTTP is implemented?@TwiN: My bad, it was probably a busy day.

Though given nobody pointed this out in over a year,  it seems this feature isn't really useful for most.

Could you create a separate issue? Just FYI, I'd expect the configuration to be under the client configuration, that way, we could make it apply to multiple endpoint types as well.@ribtoks: @TwiN I can create another issue. But just to clarify, applying it to multiple endpoint types is actuall NOT the intended behavior. The intended behavior is to test a single endpoint in two different ways by resolving domain to IPv4 and IPv6 addresses only. This is the actual source of network problems: service was accessible over IPv4 address, but not via IPv6 address which is hard to detect until some customers complain.@TwiN: @ribtoks I'm talking about the endpoint-level client configuration. I'm not suggesting this configuration would be applied to all endpoints at the same time, it will still be configurable per-endpoint.

The goal is simply code re-use. I don't want there to be 50 different ways of configuring the same thing.

Solution patch:
diff --git a/README.md b/README.md
index beff1f26..6c028a54 100644
--- a/README.md
+++ b/README.md
@@ -362,6 +362,7 @@ the client used to send the request.
 | `client.oauth2.scopes[]`               | A list of `scopes` which should be used for the `Client credentials flow`.  | required `[""]` |
 | `client.identity-aware-proxy`          | Google Identity-Aware-Proxy client configuration.                           | `{}`            |
 | `client.identity-aware-proxy.audience` | The Identity-Aware-Proxy audience. (client-id of the IAP oauth2 credential) | required `""`   |
+| `client.network`                       | The network to use for ICMP endpoint client (`ip`, `ip4` or `ip6`).         | `"ip"`          |
 
 > 📝 Some of these parameters are ignored based on the type of endpoint. For instance, there's no certificate involved
 in ICMP requests (ping), therefore, setting `client.insecure` to `true` for an endpoint of that type will not do anything.
diff --git a/client/client.go b/client/client.go
index 6d1d5a49..7a00a767 100644
--- a/client/client.go
+++ b/client/client.go
@@ -235,10 +235,7 @@ func ExecuteSSHCommand(sshClient *ssh.Client, body string, config *Config) (bool
 //
 // Note that this function takes at least 100ms, even if the address is 127.0.0.1
 func Ping(address string, config *Config) (bool, time.Duration) {
-	pinger, err := ping.NewPinger(address)
-	if err != nil {
-		return false, 0
-	}
+	pinger := ping.New(address)
 	pinger.Count = 1
 	pinger.Timeout = config.Timeout
 	// Set the pinger's privileged mode to true for every GOOS except darwin
@@ -247,7 +244,8 @@ func Ping(address string, config *Config) (bool, time.Duration) {
 	// Note that for this to work on Linux, Gatus must run with sudo privileges.
 	// See https://github.com/prometheus-community/pro-bing#linux
 	pinger.SetPrivileged(runtime.GOOS != "darwin")
-	err = pinger.Run()
+	pinger.SetNetwork(config.Network)
+	err := pinger.Run()
 	if err != nil {
 		return false, 0
 	}
diff --git a/client/config.go b/client/config.go
index 4f0ad781..aa514e5a 100644
--- a/client/config.go
+++ b/client/config.go
@@ -30,6 +30,7 @@ var (
 		Insecure:       false,
 		IgnoreRedirect: false,
 		Timeout:        defaultTimeout,
+		Network:        "ip",
 	}
 )
 
@@ -64,6 +65,9 @@ type Config struct {
 	IAPConfig *IAPConfig `yaml:"identity-aware-proxy,omitempty"`
 
 	httpClient *http.Client
+
+	// Network (ip, ip4 or ip6) for the ICMP client
+	Network string `yaml:"network"`
 }
 
 // DNSResolverConfig is the parsed configuration from the DNSResolver config string.



Test patch:
diff --git a/client/client_test.go b/client/client_test.go
index 3e91dda8..3cb117a9 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -83,6 +83,32 @@ func TestPing(t *testing.T) {
 			t.Error("Round-trip time returned on failure should've been 0")
 		}
 	}
+	// Can't perform integration tests (e.g. pinging public targets by single-stacked hostname) here,
+	// because ICMP is blocked in the network of GitHub-hosted runners.
+	if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
+		t.Error("expected true")
+		if rtt == 0 {
+			t.Error("Round-trip time returned on failure should've been 0")
+		}
+	}
+	if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip"}); !success {
+		t.Error("expected true")
+		if rtt == 0 {
+			t.Error("Round-trip time returned on failure should've been 0")
+		}
+	}
+	if success, rtt := Ping("::1", &Config{Timeout: 500 * time.Millisecond, Network: "ip4"}); success {
+		t.Error("expected false, because the IP isn't an IPv4 address")
+		if rtt != 0 {
+			t.Error("Round-trip time returned on failure should've been 0")
+		}
+	}
+	if success, rtt := Ping("127.0.0.1", &Config{Timeout: 500 * time.Millisecond, Network: "ip6"}); success {
+		t.Error("expected false, because the IP isn't an IPv6 address")
+		if rtt != 0 {
+			t.Error("Round-trip time returned on failure should've been 0")
+		}
+	}
 }
 
 func TestCanPerformStartTLS(t *testing.T) {