repo init
This commit is contained in:
commit
7e44db87b2
48 files changed
+5346
No files matched your search
@@ -0,0 +1,36 @@
|
||||
package checkrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// HTTPS performs a GET against target (expected to be a full URL, e.g.
|
||||
// https://github.com) and reports success for any response with status
|
||||
// < 400. It never follows the check to a different host on a check-type
|
||||
// boundary — redirects within the same request are followed by the
|
||||
// standard http.Client default policy, which is what we want for
|
||||
// reachability checks.
|
||||
func HTTPS(target string, timeout time.Duration) func(ctx context.Context) Result {
|
||||
return run("https", target, func(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("build request: %w", err)
|
||||
}
|
||||
client := &http.Client{Timeout: timeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("unexpected status %d", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package checkrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/icmp"
|
||||
"golang.org/x/net/ipv4"
|
||||
)
|
||||
|
||||
// ICMPEcho sends up to `count` ICMP echo requests to host and reports
|
||||
// success if at least one echo reply is received before timeout. Uses a
|
||||
// raw ICMP socket (ip4:icmp), which requires either running as root or
|
||||
// (on Linux, as deployed here) the CAP_NET_RAW capability — see the
|
||||
// validator-agent and prober systemd units.
|
||||
func ICMPEcho(host string, count int, timeout time.Duration) func(ctx context.Context) Result {
|
||||
return run("icmp", host, func(ctx context.Context) error {
|
||||
conn, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err != nil {
|
||||
return fmt.Errorf("open icmp socket (needs CAP_NET_RAW or root): %w", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
dst, err := net.ResolveIPAddr("ip4", host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("resolve %s: %w", host, err)
|
||||
}
|
||||
|
||||
id := os.Getpid() & 0xffff
|
||||
var lastErr error
|
||||
for seq := 1; seq <= count; seq++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
msg := icmp.Message{
|
||||
Type: ipv4.ICMPTypeEcho,
|
||||
Code: 0,
|
||||
Body: &icmp.Echo{
|
||||
ID: id,
|
||||
Seq: seq,
|
||||
Data: []byte("cloud-ip-validator"),
|
||||
},
|
||||
}
|
||||
wb, err := msg.Marshal(nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal echo request: %w", err)
|
||||
}
|
||||
if _, err := conn.WriteTo(wb, dst); err != nil {
|
||||
lastErr = fmt.Errorf("write echo request: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
perAttempt := timeout / time.Duration(count)
|
||||
if perAttempt <= 0 {
|
||||
perAttempt = timeout
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(perAttempt))
|
||||
rb := make([]byte, 1500)
|
||||
n, _, err := conn.ReadFrom(rb)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("read echo reply: %w", err)
|
||||
continue
|
||||
}
|
||||
rm, err := icmp.ParseMessage(1 /* protocolICMP */, rb[:n])
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("parse reply: %w", err)
|
||||
continue
|
||||
}
|
||||
if rm.Type == ipv4.ICMPTypeEchoReply {
|
||||
return nil
|
||||
}
|
||||
lastErr = fmt.Errorf("unexpected icmp type %v", rm.Type)
|
||||
}
|
||||
if lastErr == nil {
|
||||
lastErr = fmt.Errorf("no reply received")
|
||||
}
|
||||
return lastErr
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package checkrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TCPConnect reports success if a TCP handshake against host:port
|
||||
// completes within timeout. This is the primitive behind the prober's
|
||||
// per-port inbound reachability checks (22/80/443/8080).
|
||||
func TCPConnect(host string, port int, timeout time.Duration) func(ctx context.Context) Result {
|
||||
target := net.JoinHostPort(host, strconv.Itoa(port))
|
||||
checkType := "tcp-" + strconv.Itoa(port)
|
||||
return run(checkType, target, func(ctx context.Context) error {
|
||||
d := net.Dialer{Timeout: timeout}
|
||||
conn, err := d.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return conn.Close()
|
||||
})
|
||||
}
|
||||
|
||||
// SSHBanner performs a TCP connect to host:22 and additionally verifies the
|
||||
// remote sends an "SSH-2.0-" banner, without performing any auth handshake.
|
||||
// Used for the optional ssh check type.
|
||||
func SSHBanner(host string, timeout time.Duration) func(ctx context.Context) Result {
|
||||
target := net.JoinHostPort(host, "22")
|
||||
return run("ssh", target, func(ctx context.Context) error {
|
||||
d := net.Dialer{Timeout: timeout}
|
||||
conn, err := d.DialContext(ctx, "tcp", target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.SetReadDeadline(time.Now().Add(timeout))
|
||||
buf := make([]byte, 8)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read banner: %w", err)
|
||||
}
|
||||
if n < 8 || string(buf[:8]) != "SSH-2.0-" {
|
||||
return fmt.Errorf("unexpected banner prefix %q", string(buf[:n]))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Package checkrunner implements the actual network probes (HTTPS, TCP
|
||||
// connect, ICMP echo, SSH banner) shared by both the validator-agent
|
||||
// (outbound/egress checks) and the prober (inbound/reachability checks).
|
||||
// The two binaries use the same primitives against different targets and
|
||||
// in different directions, but never share process state.
|
||||
package checkrunner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Result is the outcome of a single check, in a form that maps directly
|
||||
// onto a db.Check row (minus the fields the caller already knows: ip_id,
|
||||
// attempt_number, validator_id, source).
|
||||
type Result struct {
|
||||
CheckType string
|
||||
Target string
|
||||
Success bool
|
||||
LatencyMS int64
|
||||
Detail string
|
||||
CheckedAt time.Time
|
||||
}
|
||||
|
||||
func run(checkType, target string, fn func(ctx context.Context) error) func(ctx context.Context) Result {
|
||||
return func(ctx context.Context) Result {
|
||||
start := time.Now()
|
||||
err := fn(ctx)
|
||||
latency := time.Since(start).Milliseconds()
|
||||
res := Result{
|
||||
CheckType: checkType,
|
||||
Target: target,
|
||||
Success: err == nil,
|
||||
LatencyMS: latency,
|
||||
CheckedAt: time.Now().UTC(),
|
||||
}
|
||||
if err != nil {
|
||||
res.Detail = err.Error()
|
||||
} else {
|
||||
res.Detail = "ok"
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user