Skip to content

Commit

Permalink
Add tftp_rrq check (#47)
Browse files Browse the repository at this point in the history
Signed-off-by: Igor Shishkin <me@teran.dev>
  • Loading branch information
teran authored Jun 9, 2024
1 parent 883bda4 commit 49b8ab9
Show file tree
Hide file tree
Showing 9 changed files with 394 additions and 1 deletion.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ services:
tries: 3
interval: 100ms
timeout: 5s
- kind: tftp_rrq
spec:
url: tftp://127.0.0.1:69/lpxelinux.0
tries: 3
interval: 100ms
timeout: 5s
metrics:
enabled: true
address: 127.0.0.1:9090
Expand All @@ -122,9 +128,10 @@ For now the following checks are available:
* assigned_address - ensures the address is assigned on interface
* dns_lookup - performs DNS lookup
* http_2xx - performs HTTP check and expects 2xx code
* icmp_ping - performs ICMP ping to the specified host
* tftp_get - performs TFTP GET request to specified URL
* tls_certificate - performs TLS certificate validation & provide expiration
date via metrics
* icmp_ping - performs ICMP ping to the specified host

## Metrics

Expand Down
26 changes: 26 additions & 0 deletions checkers/tftp_rrq/spec.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tftp_rrq

import (
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"

"github.com/teran/anycastd/config"
)

type spec struct {
URL string `json:"url"`
ExpectedSHA256 *string `json:"expected_sha256"`
Tries uint8 `json:"tries"`
Interval config.Duration `json:"interval"`
Timeout config.Duration `json:"timeout"`
}

func (s spec) Validate() error {
return validation.ValidateStruct(&s,
validation.Field(&s.URL, validation.Required, is.RequestURI),
validation.Field(&s.ExpectedSHA256, validation.Length(64, 64), is.Hexadecimal),
validation.Field(&s.Tries, validation.Required),
validation.Field(&s.Interval, validation.Required),
validation.Field(&s.Timeout, validation.Required),
)
}
85 changes: 85 additions & 0 deletions checkers/tftp_rrq/spec_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package tftp_rrq

import (
"testing"
"time"

"github.com/pkg/errors"
"github.com/stretchr/testify/require"

"github.com/teran/anycastd/config"
"github.com/teran/go-ptr"
)

func TestSpecValidation(t *testing.T) {
type testCase struct {
name string
in spec
expError error
}

tcs := []testCase{
{
name: "valid spec",
in: spec{
URL: "tftp://127.0.0.1:69/lpxelinux.0",
Tries: 3,
Interval: config.Duration(1 * time.Second),
Timeout: config.Duration(2 * time.Second),
},
},
{
name: "valid spec w/ checksum",
in: spec{
URL: "tftp://127.0.0.1:69/lpxelinux.0",
ExpectedSHA256: ptr.String("09da9c01b6b2a8ccc5d3445c4f364243d8a063bd0bf520643737899e6ce0170f"),
Tries: 3,
Interval: config.Duration(1 * time.Second),
Timeout: config.Duration(2 * time.Second),
},
},
{
name: "empty spec",
in: spec{},
expError: errors.New(
"interval: cannot be blank; timeout: cannot be blank; tries: cannot be blank; url: cannot be blank.",
),
},
{
name: "valid spec w/ invalid checksum (not hex)",
in: spec{
URL: "tftp://127.0.0.1:69/lpxelinux.0",
ExpectedSHA256: ptr.String("09da9c01b6b2a8ccc5d3445c4f364243d8a063bd0bf520643737899e6ce0170s"),
Tries: 3,
Interval: config.Duration(1 * time.Second),
Timeout: config.Duration(2 * time.Second),
},
expError: errors.New("expected_sha256: must be a valid hexadecimal number."),
},
{
name: "valid spec w/ invalid checksum (wrong length)",
in: spec{
URL: "tftp://127.0.0.1:69/lpxelinux.0",
ExpectedSHA256: ptr.String("deadbeef"),
Tries: 3,
Interval: config.Duration(1 * time.Second),
Timeout: config.Duration(2 * time.Second),
},
expError: errors.New("expected_sha256: the length must be exactly 64."),
},
}

for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
r := require.New(t)

err := tc.in.Validate()
if tc.expError == nil {
r.NoError(err)
} else {
r.Error(err)
r.Equal(tc.expError.Error(), err.Error())
}
})
}
}
7 changes: 7 additions & 0 deletions checkers/tftp_rrq/testdata/spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"url": "tftp://127.0.0.1:69/lpxelinux.0",
"expected_sha256": "09da9c01b6b2a8ccc5d3445c4f364243d8a063bd0bf520643737899e6ce0170f",
"tries": 3,
"interval": "100ms",
"timeout": "5s"
}
134 changes: 134 additions & 0 deletions checkers/tftp_rrq/tftp_rrq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package tftp_rrq

import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"net/url"
"time"

"github.com/pin/tftp"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"

"github.com/teran/anycastd/checkers"
)

var _ checkers.Checker = (*tftp_rrq)(nil)

const checkName = "tftp_rrq"

func init() {
checkers.Register(checkName, NewFromSpec)
}

type tftp_rrq struct {
url string
expSHA256 *string
tries uint8
interval time.Duration
timeout time.Duration
}

func New(s spec) (checkers.Checker, error) {
if err := s.Validate(); err != nil {
return nil, err
}

return &tftp_rrq{
url: s.URL,
expSHA256: s.ExpectedSHA256,
tries: s.Tries,
interval: time.Duration(s.Interval),
timeout: time.Duration(s.Timeout),
}, nil
}

func NewFromSpec(in json.RawMessage) (checkers.Checker, error) {
s := spec{}
if err := json.Unmarshal(in, &s); err != nil {
return nil, err
}

return New(s)
}

func (t *tftp_rrq) Kind() string {
return checkName
}

func (t *tftp_rrq) Check(ctx context.Context) error {
var lastErr error
for i := 0; i < int(t.tries); i++ {
log.WithFields(log.Fields{
"check": checkName,
"attempt": i + 1,
}).Tracef("running check")

if err := t.check(ctx); err != nil {
lastErr = err
log.WithFields(log.Fields{
"check": checkName,
"attempt": i + 1,
}).Infof("error received: %s", err)
} else {
return nil
}

time.Sleep(t.interval)
}

if lastErr != nil {
return errors.Errorf(
"check failed: %d tries with %s interval; last error: `%s`",
t.tries, t.interval, lastErr.Error(),
)
}

return nil
}

func (t *tftp_rrq) check(context.Context) error {
url, err := url.Parse(t.url)
if err != nil {
return errors.Wrap(err, "error parsing URL")
}

host := url.Host
path := url.RequestURI()

c, err := tftp.NewClient(host)
if err != nil {
return errors.Wrap(err, "error creating TFTP client")
}

wt, err := c.Receive(path, "octet")
if err != nil {
return errors.Wrap(err, "error receiving file")
}

buf := &bytes.Buffer{}
n, err := wt.WriteTo(buf)
if err != nil {
return errors.Wrap(err, "error filling buffer")
}

log.WithFields(log.Fields{
"check": checkName,
}).Tracef("bytes received: %d", n)

if t.expSHA256 != nil {
exp := *t.expSHA256
act := fmt.Sprintf("%x", sha256.Sum256(buf.Bytes()))
if exp != act {
return errors.Errorf(
"checksum mismatch: expected `%s` != actual `%s`",
exp, act,
)
}
}

return nil
}
Loading

0 comments on commit 49b8ab9

Please sign in to comment.