Skip to content

Commit 371bf72

Browse files
authored
Merge pull request #105 from deploymenttheory/dev
Added experiential icmp based ping functionality
2 parents 120d3a6 + 9ec6a91 commit 371bf72

File tree

3 files changed

+107
-2
lines changed

3 files changed

+107
-2
lines changed

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ require (
77
github.com/google/uuid v1.6.0
88
github.com/stretchr/testify v1.8.1
99
go.uber.org/zap v1.27.0
10+
golang.org/x/net v0.19.0
1011
)
1112

1213
require (
@@ -15,6 +16,6 @@ require (
1516
github.com/pmezard/go-difflib v1.0.0 // indirect
1617
github.com/stretchr/objx v0.5.0 // indirect
1718
go.uber.org/multierr v1.10.0 // indirect
18-
golang.org/x/net v0.19.0 // indirect
19+
golang.org/x/sys v0.15.0 // indirect
1920
gopkg.in/yaml.v3 v3.0.1 // indirect
2021
)

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
4343
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4444
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
4545
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
46+
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
47+
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
4648
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
4749
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
4850
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=

httpclient/httpclient_ping.go

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@ package httpclient
33

44
import (
55
"fmt"
6+
"net"
67
"net/http"
8+
"os"
79
"time"
810

911
"go.uber.org/zap"
12+
"golang.org/x/net/icmp"
13+
"golang.org/x/net/ipv4"
1014
)
1115

1216
// DoPing performs an HTTP "ping" to the specified endpoint using the given HTTP method, body,
@@ -57,7 +61,7 @@ func (c *Client) DoPing(method, endpoint string, body, out interface{}) (*http.R
5761
// Loop until a successful response is received or maximum retries are reached
5862
for retryCount <= maxRetries {
5963
// Use the existing 'do' function for sending the request
60-
resp, err := c.executeRequest(method, endpoint, body, out)
64+
resp, err := c.executeRequestWithRetries(method, endpoint, body, out)
6165

6266
// If request is successful and returns 200 status code, return the response
6367
if err == nil && resp.StatusCode == http.StatusOK {
@@ -78,3 +82,101 @@ func (c *Client) DoPing(method, endpoint string, body, out interface{}) (*http.R
7882
log.Error("Ping failed after maximum retries", zap.String("method", method), zap.String("endpoint", endpoint))
7983
return nil, fmt.Errorf("ping failed after %d retries", maxRetries)
8084
}
85+
86+
// DoPing performs an ICMP "ping" to the specified host. It sends ICMP echo requests and waits for echo replies.
87+
// This function is useful for checking the availability or health of a host, particularly in environments where
88+
// network reliability might be an issue.
89+
90+
// Parameters:
91+
// - host: The target host for the ping request.
92+
// - timeout: The timeout for waiting for a ping response.
93+
94+
// Returns:
95+
// - error: An error object indicating failure during the execution of the ping operation or nil if the ping was successful.
96+
97+
// Usage:
98+
// This function is intended for use in scenarios where it's necessary to confirm the availability or health of a host.
99+
// The caller is responsible for handling the error according to their needs.
100+
101+
// Example:
102+
// err := client.DoPing("www.example.com", 3*time.Second)
103+
// if err != nil {
104+
// // Handle error
105+
// }
106+
107+
func (c *Client) DoPingV2(host string, timeout time.Duration) error {
108+
log := c.Logger
109+
110+
// Listen for ICMP replies
111+
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
112+
if err != nil {
113+
log.Error("Failed to listen for ICMP packets", zap.Error(err))
114+
return fmt.Errorf("failed to listen for ICMP packets: %w", err)
115+
}
116+
defer conn.Close()
117+
118+
// Resolve the IP address of the host
119+
dst, err := net.ResolveIPAddr("ip4", host)
120+
if err != nil {
121+
log.Error("Failed to resolve IP address", zap.String("host", host), zap.Error(err))
122+
return fmt.Errorf("failed to resolve IP address for host %s: %w", host, err)
123+
}
124+
125+
// Create an ICMP Echo Request message
126+
msg := icmp.Message{
127+
Type: ipv4.ICMPTypeEcho, Code: 0,
128+
Body: &icmp.Echo{
129+
ID: os.Getpid() & 0xffff, Seq: 1, // Use PID as ICMP ID
130+
Data: []byte("HELLO"), // Data payload
131+
},
132+
}
133+
134+
// Marshal the message into bytes
135+
msgBytes, err := msg.Marshal(nil)
136+
if err != nil {
137+
log.Error("Failed to marshal ICMP message", zap.Error(err))
138+
return fmt.Errorf("failed to marshal ICMP message: %w", err)
139+
}
140+
141+
// Send the ICMP Echo Request message
142+
if _, err := conn.WriteTo(msgBytes, dst); err != nil {
143+
log.Error("Failed to send ICMP message", zap.Error(err))
144+
return fmt.Errorf("failed to send ICMP message: %w", err)
145+
}
146+
147+
// Set read timeout
148+
if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil {
149+
log.Error("Failed to set read deadline", zap.Error(err))
150+
return fmt.Errorf("failed to set read deadline: %w", err)
151+
}
152+
153+
// Wait for an ICMP Echo Reply message
154+
reply := make([]byte, 1500)
155+
n, _, err := conn.ReadFrom(reply)
156+
if err != nil {
157+
log.Error("Failed to receive ICMP reply", zap.Error(err))
158+
return fmt.Errorf("failed to receive ICMP reply: %w", err)
159+
}
160+
161+
// Parse the ICMP message from the reply
162+
parsedMsg, err := icmp.ParseMessage(1, reply[:n])
163+
if err != nil {
164+
log.Error("Failed to parse ICMP message", zap.Error(err))
165+
return fmt.Errorf("failed to parse ICMP message: %w", err)
166+
}
167+
168+
// Check if the message is an ICMP Echo Reply
169+
if echoReply, ok := parsedMsg.Type.(*ipv4.ICMPType); ok {
170+
if *echoReply != ipv4.ICMPTypeEchoReply {
171+
log.Error("Did not receive ICMP Echo Reply", zap.String("received_type", echoReply.String()))
172+
return fmt.Errorf("did not receive ICMP Echo Reply, received type: %s", echoReply.String())
173+
}
174+
} else {
175+
// Handle the case where the type assertion fails
176+
log.Error("Failed to assert ICMP message type")
177+
return fmt.Errorf("failed to assert ICMP message type")
178+
}
179+
180+
log.Info("Ping successful", zap.String("host", host))
181+
return nil
182+
}

0 commit comments

Comments
 (0)