Initial commit

This commit is contained in:
nemunaire 2026-04-08 01:46:37 +07:00
commit a2ebf17774
15 changed files with 1041 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
checker-ping
*.so

14
Dockerfile Normal file
View file

@ -0,0 +1,14 @@
FROM golang:1.25-alpine AS builder
ARG CHECKER_VERSION=custom-build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-X main.Version=${CHECKER_VERSION}" -o /checker-ping .
FROM scratch
COPY --from=builder /checker-ping /checker-ping
EXPOSE 8080
ENTRYPOINT ["/checker-ping"]

25
Makefile Normal file
View file

@ -0,0 +1,25 @@
CHECKER_NAME := checker-ping
CHECKER_IMAGE := happydomain/$(CHECKER_NAME)
CHECKER_VERSION ?= custom-build
CHECKER_SOURCES := main.go $(wildcard checker/*.go)
GO_LDFLAGS := -X main.Version=$(CHECKER_VERSION)
.PHONY: all plugin docker clean
all: $(CHECKER_NAME)
$(CHECKER_NAME): $(CHECKER_SOURCES)
go build -ldflags "$(GO_LDFLAGS)" -o $@ .
plugin: $(CHECKER_NAME).so
$(CHECKER_NAME).so: $(CHECKER_SOURCES) $(wildcard plugin/*.go)
go build -buildmode=plugin -ldflags "$(GO_LDFLAGS)" -o $@ ./plugin/
docker:
docker build --build-arg CHECKER_VERSION=$(CHECKER_VERSION) -t $(CHECKER_IMAGE) .
clean:
rm -f $(CHECKER_NAME) $(CHECKER_NAME).so

32
NOTICE Normal file
View file

@ -0,0 +1,32 @@
checker-ping
Copyright (c) 2020-2026 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This product is currently licensed under the GNU Affero General Public
License v3.0 (see LICENSE), because it imports types from the happyDomain
server module which is itself licensed under AGPL-3.0.
A relicensing to the MIT License is planned once that dependency has been
removed. See README.md for the licensing roadmap.
-------------------------------------------------------------------------------
Third-party notices
-------------------------------------------------------------------------------
This product includes software developed as part of the checker-sdk-go
project (https://git.happydns.org/happyDomain/checker-sdk-go), licensed
under the Apache License, Version 2.0:
checker-sdk-go
Copyright 2020-2026 The happyDomain Authors
This product includes software developed as part of the happyDomain
project (https://happydomain.org).
Portions of this code were originally written for the happyDomain
server (licensed under AGPL-3.0 and a commercial license) and are
made available there under the Apache License, Version 2.0 to enable
a permissively licensed ecosystem of checker plugins.
You may obtain a copy of the Apache License 2.0 at:
http://www.apache.org/licenses/LICENSE-2.0

124
README.md Normal file
View file

@ -0,0 +1,124 @@
# checker-ping
ICMP ping checker for [happyDomain](https://www.happydomain.org/).
Checks reachability, round-trip time, and packet loss for IP addresses associated with a domain's services.
## Usage
### Standalone HTTP server
```bash
# Build and run
make
./checker-ping -listen :8080
# With privileged ICMP (requires CAP_NET_RAW or root)
./checker-ping -listen :8080 -privileged
```
The server exposes:
- `GET /health` — health check
- `POST /collect` — collect ping observations (happyDomain external checker protocol)
### Docker
```bash
make docker
docker run -p 8080:8080 happydomain/checker-ping
# With privileged ICMP
docker run --cap-add NET_RAW -p 8080:8080 happydomain/checker-ping -privileged
```
### happyDomain plugin
```bash
make plugin
# produces checker-ping.so, loadable by happyDomain as a Go plugin
```
The plugin exposes a `NewCheckerPlugin` symbol returning the checker
definition and observation provider, which happyDomain registers in its
global registries at load time.
### Versioning
The binary, plugin, and Docker image embed a version string overridable
at build time:
```bash
make CHECKER_VERSION=1.2.3
make plugin CHECKER_VERSION=1.2.3
make docker CHECKER_VERSION=1.2.3
```
### happyDomain remote endpoint
Set the `endpoint` admin option for the ping checker to the URL of the running checker-ping server (e.g., `http://checker-ping:8080`). happyDomain will delegate observation collection to this endpoint.
## Protocol
### POST /collect
Request:
```json
{
"key": "ping",
"target": {"userId": "...", "domainId": "..."},
"options": {
"addresses": ["1.2.3.4", "2001:db8::1"],
"count": 5
}
}
```
Response:
```json
{
"data": {
"targets": [
{
"address": "1.2.3.4",
"rtt_min": 1.2,
"rtt_avg": 3.4,
"rtt_max": 5.6,
"packet_loss": 0,
"sent": 5,
"received": 5
}
]
}
}
```
## License & licensing roadmap
This project is currently licensed under the **GNU Affero General Public
License v3.0** (see `LICENSE`), because it still imports
`happydns.ServiceMessage` and `abstract.Server` from the happyDomain
server module (`git.happydns.org/happyDomain/model` and
`git.happydns.org/happyDomain/services/abstract`), which are themselves
distributed under AGPL-3.0 and a commercial license.
The core checker types (`CheckerOptions`, `CheckerDefinition`,
`ObservationProvider`, `CheckRule`, …) have already been migrated to
[`checker-sdk-go`](https://git.happydns.org/checker-sdk-go); only the
service-message types remain on the AGPL side.
**Planned relicensing:** as soon as the remaining `ServiceMessage` /
`abstract.Server` dependency has been removed (moved into a dedicated
permissively licensed module), this project will be relicensed under the
**MIT License**, in line with the rest of the happyDomain checker
ecosystem (see `checker-dummy` for the target shape).
**Contributors notice:** by submitting a contribution to this repository,
you accept that your contribution will be relicensed from AGPL-3.0 to MIT
at the time of the relicensing described above. If you do not agree with
this, please do not submit contributions until the relicensing has taken
place.
The third-party Apache-2.0 attributions for `checker-sdk-go` are recorded
in `NOTICE` and must accompany any binary or source redistribution of this
project.

159
checker/collect.go Normal file
View file

@ -0,0 +1,159 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checker
import (
"context"
"encoding/json"
"fmt"
"net"
"strings"
"time"
probing "github.com/prometheus-community/pro-bing"
sdk "git.happydns.org/checker-sdk-go/checker"
happydns "git.happydns.org/happyDomain/model"
"git.happydns.org/happyDomain/services/abstract"
)
// Collect performs ICMP ping and returns PingData.
// Addresses are resolved from opts: "addresses" ([]string), "address" (string),
// or "service" (*ServiceMessage of type abstract.Server).
func (p *pingProvider) Collect(ctx context.Context, opts sdk.CheckerOptions) (any, error) {
addresses, err := resolveAddresses(opts)
if err != nil {
return nil, err
}
count := sdk.GetIntOption(opts, "count", 5)
if count < 1 {
count = 1
}
if count > 20 {
count = 20
}
data := &PingData{}
var errs []string
for _, addr := range addresses {
pinger, err := probing.NewPinger(addr)
if err != nil {
errs = append(errs, fmt.Sprintf("failed to create pinger for %s: %v", addr, err))
continue
}
pinger.Count = count
pinger.Timeout = time.Duration(count)*time.Second + 5*time.Second
if p.Privileged {
pinger.SetPrivileged(true)
}
if err = pinger.RunWithContext(ctx); err != nil {
errs = append(errs, fmt.Sprintf("ping failed for %s: %v", addr, err))
continue
}
stats := pinger.Statistics()
data.Targets = append(data.Targets, PingTargetResult{
Address: addr,
RTTMin: float64(stats.MinRtt.Microseconds()) / 1000.0,
RTTAvg: float64(stats.AvgRtt.Microseconds()) / 1000.0,
RTTMax: float64(stats.MaxRtt.Microseconds()) / 1000.0,
PacketLoss: stats.PacketLoss,
Sent: stats.PacketsSent,
Received: stats.PacketsRecv,
})
}
if len(data.Targets) == 0 {
return nil, fmt.Errorf("all pings failed: %s", strings.Join(errs, "; "))
}
return data, nil
}
// resolveAddresses extracts target IP addresses from the options.
func resolveAddresses(opts sdk.CheckerOptions) ([]string, error) {
// Direct addresses (from HTTP server).
if v, ok := opts["addresses"]; ok {
switch addrs := v.(type) {
case []any:
var result []string
for _, a := range addrs {
if s, ok := a.(string); ok && s != "" {
result = append(result, s)
}
}
if len(result) > 0 {
return result, nil
}
case []string:
if len(addrs) > 0 {
return addrs, nil
}
}
}
// Single address.
if v, ok := opts["address"]; ok {
if s, ok := v.(string); ok && s != "" {
return []string{s}, nil
}
}
// From auto-filled service (plugin provider path or HTTP JSON).
if svc, ok := sdk.GetOption[happydns.ServiceMessage](opts, "service"); ok {
if svc.Type != "abstract.Server" {
return nil, fmt.Errorf("service is %s, expected abstract.Server", svc.Type)
}
ips := ipsFromService(&svc)
if len(ips) > 0 {
addrs := make([]string, len(ips))
for i, ip := range ips {
addrs[i] = ip.String()
}
return addrs, nil
}
return nil, fmt.Errorf("no IP addresses found in the service")
}
return nil, fmt.Errorf("no addresses provided: set 'addresses', 'address', or 'service' in options")
}
func ipsFromService(svc *happydns.ServiceMessage) []net.IP {
var server abstract.Server
if err := json.Unmarshal(svc.Service, &server); err != nil {
return nil
}
var ips []net.IP
if server.A != nil && len(server.A.A) > 0 {
ips = append(ips, server.A.A)
}
if server.AAAA != nil && len(server.AAAA.AAAA) > 0 {
ips = append(ips, server.AAAA.AAAA)
}
return ips
}

102
checker/definition.go Normal file
View file

@ -0,0 +1,102 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checker
import (
"time"
happydns "git.happydns.org/checker-sdk-go/checker"
)
// Version is the checker version reported in CheckerDefinition.Version.
//
// It defaults to "built-in", which is appropriate when the checker package is
// imported directly (built-in or plugin mode). Standalone binaries (like
// main.go) should override this from their own Version variable at the start
// of main(), which makes it easy for CI to inject a version with a single
// -ldflags "-X main.Version=..." flag instead of targeting the nested
// package path.
var Version = "built-in"
// Definition returns the CheckerDefinition for the ping checker.
func Definition() *happydns.CheckerDefinition {
return &happydns.CheckerDefinition{
ID: "ping",
Name: "Ping (ICMP)",
Version: Version,
Availability: happydns.CheckerAvailability{
ApplyToService: true,
LimitToServices: []string{"abstract.Server"},
},
ObservationKeys: []happydns.ObservationKey{ObservationKeyPing},
Options: happydns.CheckerOptionsDocumentation{
UserOpts: []happydns.CheckerOptionDocumentation{
{
Id: "warningRTT",
Type: "number",
Label: "Warning RTT threshold (ms)",
Default: float64(100),
},
{
Id: "criticalRTT",
Type: "number",
Label: "Critical RTT threshold (ms)",
Default: float64(500),
},
{
Id: "warningPacketLoss",
Type: "number",
Label: "Warning packet loss threshold (%)",
Default: float64(10),
},
{
Id: "criticalPacketLoss",
Type: "number",
Label: "Critical packet loss threshold (%)",
Default: float64(50),
},
{
Id: "count",
Type: "uint",
Label: "Number of pings to send",
Default: float64(5),
},
},
ServiceOpts: []happydns.CheckerOptionDocumentation{
{
Id: "service",
Label: "Service",
AutoFill: happydns.AutoFillService,
},
},
},
Rules: []happydns.CheckRule{
Rule(),
},
Interval: &happydns.CheckIntervalSpec{
Min: 1 * time.Minute,
Max: 1 * time.Hour,
Default: 5 * time.Minute,
},
HasMetrics: true,
}
}

89
checker/evaluate.go Normal file
View file

@ -0,0 +1,89 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checker
import (
"fmt"
"strings"
"time"
)
// Metrics extracts time-series metrics from ping data.
func Metrics(data *PingData, collectedAt time.Time) []Metric {
var metrics []Metric
for _, t := range data.Targets {
labels := map[string]string{"address": t.Address}
metrics = append(metrics,
Metric{Name: "ping_rtt_avg", Value: t.RTTAvg, Unit: "ms", Labels: labels, Timestamp: collectedAt},
Metric{Name: "ping_rtt_min", Value: t.RTTMin, Unit: "ms", Labels: labels, Timestamp: collectedAt},
Metric{Name: "ping_rtt_max", Value: t.RTTMax, Unit: "ms", Labels: labels, Timestamp: collectedAt},
Metric{Name: "ping_packet_loss", Value: t.PacketLoss, Unit: "%", Labels: labels, Timestamp: collectedAt},
Metric{Name: "ping_packets_sent", Value: float64(t.Sent), Unit: "count", Labels: labels, Timestamp: collectedAt},
Metric{Name: "ping_packets_received", Value: float64(t.Received), Unit: "count", Labels: labels, Timestamp: collectedAt},
)
}
return metrics
}
// Metric represents a single time-series metric.
type Metric struct {
Name string `json:"name"`
Value float64 `json:"value"`
Unit string `json:"unit,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// EvaluateResult holds the evaluation outcome.
type EvaluateResult struct {
Status int `json:"status"`
Message string `json:"message"`
Code string `json:"code"`
}
const (
StatusOK = 1
StatusWarn = 3
StatusCrit = 4
)
// Evaluate checks the ping data against the given thresholds.
func Evaluate(data *PingData, warningRTT, criticalRTT, warningPacketLoss, criticalPacketLoss float64) EvaluateResult {
overallStatus := StatusOK
var summaryParts []string
for _, target := range data.Targets {
if target.PacketLoss >= criticalPacketLoss || target.RTTAvg >= criticalRTT {
overallStatus = StatusCrit
} else if (target.PacketLoss >= warningPacketLoss || target.RTTAvg >= warningRTT) && overallStatus < StatusWarn {
overallStatus = StatusWarn
}
summaryParts = append(summaryParts, fmt.Sprintf("%s: %.1fms avg, %.0f%% loss", target.Address, target.RTTAvg, target.PacketLoss))
}
return EvaluateResult{
Status: overallStatus,
Message: strings.Join(summaryParts, " | "),
Code: "ping_result",
}
}

74
checker/provider.go Normal file
View file

@ -0,0 +1,74 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checker
import (
"encoding/json"
"time"
happydns "git.happydns.org/checker-sdk-go/checker"
)
// Provider returns a new ping observation provider for local execution.
func Provider() happydns.ObservationProvider {
return &pingProvider{}
}
// ProviderWithPrivileged returns a provider with privileged ICMP mode enabled.
func ProviderWithPrivileged(privileged bool) happydns.ObservationProvider {
return &pingProvider{Privileged: privileged}
}
type pingProvider struct {
// Privileged controls whether raw ICMP sockets are used (requires CAP_NET_RAW or root).
Privileged bool
}
func (p *pingProvider) Key() happydns.ObservationKey {
return ObservationKeyPing
}
// Definition implements happydns.CheckerDefinitionProvider.
func (p *pingProvider) Definition() *happydns.CheckerDefinition {
return Definition()
}
// ExtractMetrics implements happydns.CheckerMetricsReporter.
func (p *pingProvider) ExtractMetrics(raw json.RawMessage, collectedAt time.Time) ([]happydns.CheckMetric, error) {
var data PingData
if err := json.Unmarshal(raw, &data); err != nil {
return nil, err
}
metrics := Metrics(&data, collectedAt)
result := make([]happydns.CheckMetric, len(metrics))
for i, m := range metrics {
result[i] = happydns.CheckMetric{
Name: m.Name,
Value: m.Value,
Unit: m.Unit,
Labels: m.Labels,
Timestamp: m.Timestamp,
}
}
return result, nil
}

146
checker/rule.go Normal file
View file

@ -0,0 +1,146 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checker
import (
"context"
"fmt"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Rule returns a new ping check rule for local evaluation.
func Rule() sdk.CheckRule {
return &pingRule{}
}
type pingRule struct{}
func (r *pingRule) Name() string { return "ping_check" }
func (r *pingRule) Description() string {
return "Checks ICMP ping reachability, round-trip time, and packet loss"
}
func (r *pingRule) ValidateOptions(opts sdk.CheckerOptions) error {
warningRTT := float64(100)
criticalRTT := float64(500)
warningPacketLoss := float64(10)
criticalPacketLoss := float64(50)
if v, ok := opts["warningRTT"]; ok {
d, ok := v.(float64)
if !ok {
return fmt.Errorf("warningRTT must be a number")
}
if d <= 0 {
return fmt.Errorf("warningRTT must be positive")
}
warningRTT = d
}
if v, ok := opts["criticalRTT"]; ok {
d, ok := v.(float64)
if !ok {
return fmt.Errorf("criticalRTT must be a number")
}
if d <= 0 {
return fmt.Errorf("criticalRTT must be positive")
}
criticalRTT = d
}
if v, ok := opts["warningPacketLoss"]; ok {
d, ok := v.(float64)
if !ok {
return fmt.Errorf("warningPacketLoss must be a number")
}
if d < 0 || d > 100 {
return fmt.Errorf("warningPacketLoss must be between 0 and 100")
}
warningPacketLoss = d
}
if v, ok := opts["criticalPacketLoss"]; ok {
d, ok := v.(float64)
if !ok {
return fmt.Errorf("criticalPacketLoss must be a number")
}
if d < 0 || d > 100 {
return fmt.Errorf("criticalPacketLoss must be between 0 and 100")
}
criticalPacketLoss = d
}
if v, ok := opts["count"]; ok {
d, ok := v.(float64)
if !ok {
return fmt.Errorf("count must be a number")
}
if d < 1 || d > 20 {
return fmt.Errorf("count must be between 1 and 20")
}
}
if criticalRTT <= warningRTT {
return fmt.Errorf("criticalRTT (%v) must be greater than warningRTT (%v)", criticalRTT, warningRTT)
}
if criticalPacketLoss <= warningPacketLoss {
return fmt.Errorf("criticalPacketLoss (%v) must be greater than warningPacketLoss (%v)", criticalPacketLoss, warningPacketLoss)
}
return nil
}
func (r *pingRule) Evaluate(ctx context.Context, obs sdk.ObservationGetter, opts sdk.CheckerOptions) sdk.CheckState {
var data PingData
if err := obs.Get(ctx, ObservationKeyPing, &data); err != nil {
return sdk.CheckState{
Status: sdk.StatusError,
Message: fmt.Sprintf("Failed to get ping data: %v", err),
Code: "ping_error",
}
}
warningRTT := sdk.GetFloatOption(opts, "warningRTT", 100)
criticalRTT := sdk.GetFloatOption(opts, "criticalRTT", 500)
warningPacketLoss := sdk.GetFloatOption(opts, "warningPacketLoss", 10)
criticalPacketLoss := sdk.GetFloatOption(opts, "criticalPacketLoss", 50)
result := Evaluate(&data, warningRTT, criticalRTT, warningPacketLoss, criticalPacketLoss)
var status sdk.Status
switch result.Status {
case StatusOK:
status = sdk.StatusOK
case StatusWarn:
status = sdk.StatusWarn
case StatusCrit:
status = sdk.StatusCrit
default:
status = sdk.StatusUnknown
}
return sdk.CheckState{
Status: status,
Message: result.Message,
Code: result.Code,
Meta: map[string]any{
"targets": data.Targets,
},
}
}

41
checker/types.go Normal file
View file

@ -0,0 +1,41 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package checker
// ObservationKeyPing is the observation key for ICMP ping data.
const ObservationKeyPing = "ping"
// PingData holds the collected ping results for all targets.
type PingData struct {
Targets []PingTargetResult `json:"targets"`
}
// PingTargetResult contains the ping statistics for a single IP address.
type PingTargetResult struct {
Address string `json:"address"`
RTTMin float64 `json:"rtt_min"`
RTTAvg float64 `json:"rtt_avg"`
RTTMax float64 `json:"rtt_max"`
PacketLoss float64 `json:"packet_loss"`
Sent int `json:"sent"`
Received int `json:"received"`
}

47
go.mod Normal file
View file

@ -0,0 +1,47 @@
module git.happydns.org/checker-ping
go 1.25.0
require (
git.happydns.org/happyDomain v0.7.0
github.com/prometheus-community/pro-bing v0.8.0
)
require (
git.happydns.org/checker-sdk-go v0.0.1
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/gin-gonic/gin v1.12.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.24.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.42.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

106
go.sum Normal file
View file

@ -0,0 +1,106 @@
git.happydns.org/checker-sdk-go v0.0.1 h1:4RxCJr73HWKxjOyU/6NJMO8lXJmH0gMLA68EzTqLbQI=
git.happydns.org/checker-sdk-go v0.0.1/go.mod h1:aNAcfYFfbhvH9kJhE0Njp5GX0dQbxdRB0rJ0KvSC5nI=
git.happydns.org/happyDomain v0.7.0 h1:NV82/NbcSeRm0+IUZqaK3Vu9Ovl5+vv4AigUJZMdwws=
git.happydns.org/happyDomain v0.7.0/go.mod h1:5tgkmqFE65kK359rY49V++49wgZ0gco+Gh9X6tbL+bY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 h1:9Nu54bhS/H/Kgo2/7xNSUuC5G28VR8ljfrLKU2G4IjU=
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12/go.mod h1:TBzl5BIHNXfS9+C35ZyJaklL7mLDbgUkcgXzSLa8Tk0=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus-community/pro-bing v0.8.0 h1:CEY/g1/AgERRDjxw5P32ikcOgmrSuXs7xon7ovx6mNc=
github.com/prometheus-community/pro-bing v0.8.0/go.mod h1:Idyxz8raDO6TgkUN6ByiEGvWJNyQd40kN9ZUeho3lN0=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

54
main.go Normal file
View file

@ -0,0 +1,54 @@
// This file is part of the happyDomain (R) project.
// Copyright (c) 2020-2026 happyDomain
// Authors: Pierre-Olivier Mercier, et al.
//
// This program is offered under a commercial and under the AGPL license.
// For commercial licensing, contact us at <contact@happydomain.org>.
//
// For AGPL licensing:
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"flag"
"log"
ping "git.happydns.org/checker-ping/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
)
var (
listenAddr = flag.String("listen", ":8080", "HTTP listen address")
privileged = flag.Bool("privileged", false, "Use privileged ICMP (requires CAP_NET_RAW or root)")
)
// Version is the standalone binary's version. It defaults to "custom-build"
// and is meant to be overridden by the CI at link time:
//
// go build -ldflags "-X main.Version=1.2.3" .
var Version = "custom-build"
func main() {
flag.Parse()
// Propagate the binary version to the checker package so it shows up in
// CheckerDefinition.Version.
ping.Version = Version
server := sdk.NewServer(ping.ProviderWithPrivileged(*privileged))
if err := server.ListenAndServe(*listenAddr); err != nil {
log.Fatalf("server error: %v", err)
}
}

26
plugin/plugin.go Normal file
View file

@ -0,0 +1,26 @@
// Command plugin is the happyDomain plugin entrypoint for the ping checker.
//
// It is built as a Go plugin (`go build -buildmode=plugin`) and loaded at
// runtime by happyDomain.
package main
import (
ping "git.happydns.org/checker-ping/checker"
sdk "git.happydns.org/checker-sdk-go/checker"
)
// Version is the plugin's version. It defaults to "custom-build" and is
// meant to be overridden by the CI at link time:
//
// go build -buildmode=plugin -ldflags "-X main.Version=1.2.3" -o checker-ping.so ./plugin
var Version = "custom-build"
// NewCheckerPlugin is the symbol resolved by happyDomain when loading the
// .so file. It returns the checker definition and the observation provider
// that the host will register in its global registries.
func NewCheckerPlugin() (*sdk.CheckerDefinition, sdk.ObservationProvider, error) {
// Propagate the plugin's version to the checker package so it shows up
// in CheckerDefinition.Version.
ping.Version = Version
return ping.Definition(), ping.Provider(), nil
}