Skip to content

Commit

Permalink
feat(qa): start validating I/O events (#2)
Browse files Browse the repository at this point in the history
This commit extends `./internal/qa` to start validating I/O events to
make sure the value in their fields is sensible.

While there, declare the expected fields for DNS-over-HTTPS.
  • Loading branch information
bassosimone authored Nov 29, 2024
1 parent 6f94c19 commit 3ffcc5c
Show file tree
Hide file tree
Showing 3 changed files with 230 additions and 1 deletion.
217 changes: 216 additions & 1 deletion internal/qa/event.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

package qa

import "github.com/stretchr/testify/require"
import (
"net"
"strconv"
"time"

"github.com/stretchr/testify/require"
)

// MatchPattern indicates what kinds of messages an
// [*ExpectedEvent] can match.
Expand Down Expand Up @@ -45,7 +51,216 @@ type ExpectedEvent struct {

// Event is an Event emitted by the RBMK tool.
type Event struct {
//
// Core fields
//

// Msg is the event identifier
Msg string `json:"msg"`

// T0 is the optional start timestamp for the event duration
T0 time.Time `json:"t0,omitempty"`

// T is the event timestamp
T time.Time `json:"t,omitempty"`

//
// Network fields
//

// Protocol is the network protocol (e.g., "tcp", "udp").
Protocol string `json:"protocol,omitempty"`

// LocalAddr is the local endpoint address (IP:port).
LocalAddr string `json:"localAddr,omitempty"`

// RemoteAddr is the remote endpoint address (IP:port).
RemoteAddr string `json:"remoteAddr,omitempty"`

//
// Failure
//

// Err is the Go error that occurred.
Err string `json:"err,omitempty"`

//
// I/O operations
//

// Count is the number of bytes read or written.
Count int64 `json:"count,omitempty"`

//
// DNS-specific fields
//

// RawQuery is the raw DNS query message.
RawQuery string `json:"rawQuery,omitempty"`

// RawResponse is the raw DNS response message.
RawResponse string `json:"rawResponse,omitempty"`

// ServerAddr is the DNS server address.
ServerAddr string `json:"serverAddr,omitempty"`

// ServerProtocol is the DNS server protocol.
ServerProtocol string `json:"serverProtocol,omitempty"`

//
// TLS-specific fields
//

// TLSServerName is the TLS server name.
TLSServerName string `json:"tlsServerName,omitempty"`

// TLSSkipVerify is true if the TLS verification was skipped.
TLSSkipVerify bool `json:"tlsSkipVerify,omitempty"`

// TLSCipherSuite is the negotiated TLS cipher suite.
TLSCipherSuite string `json:"tlsCipherSuite,omitempty"`

// TLSNegotiatedProto is the negotiated TLS protocol.
TLSNegotiatedProto string `json:"tlsNegotiatedProtocol,omitempty"`

// TLSVersion is the negotiated TLS version.
TLSVersion string `json:"tlsVersion,omitempty"`

// TLSPeerCerts is the list of TLS peer certificates.
TLSPeerCerts [][]byte `json:"tlsPeerCerts,omitempty"`
}

// VerifyReadWriteClose checks that the current [*Event] matches
// the expectations for a read/write/close operation.
func (ev *Event) VerifyReadWriteClose(t Driver) {
switch ev.Msg {
case "readStart", "writeStart":
ev.verifyStartEventTime(t)
ev.verifyProtocol(t)
ev.verifyEndpoint(t, ev.LocalAddr)
ev.verifyEndpoint(t, ev.RemoteAddr)
ev.verifyErrEmpty(t)
ev.verifyCountPositive(t)

case "closeStart":
ev.verifyStartEventTime(t)
ev.verifyProtocol(t)
ev.verifyEndpoint(t, ev.LocalAddr)
ev.verifyEndpoint(t, ev.RemoteAddr)
ev.verifyErrEmpty(t)
ev.verifyCountZero(t)

case "readDone", "writeDone":
ev.verifyDoneEventTime(t)
ev.verifyProtocol(t)
ev.verifyEndpoint(t, ev.LocalAddr)
ev.verifyEndpoint(t, ev.RemoteAddr)
ev.verifyCountOrErr(t)

case "closeDone":
ev.verifyDoneEventTime(t)
ev.verifyProtocol(t)
ev.verifyEndpoint(t, ev.LocalAddr)
ev.verifyEndpoint(t, ev.RemoteAddr)
// any value of error is okay
ev.verifyCountZero(t)

default:
require.Fail(t, "unexpected message %q", ev.Msg)
}

ev.verifyRawQueryEmpty(t)
ev.verifyRawResponseEmpty(t)
ev.verifyServerAddrEmpty(t)
ev.verifyServerProtocolEmpty(t)
ev.verifyTLSServerNameEmpty(t)
ev.verifyTLSSkipVerifyFalse(t)
ev.verifyTLSCipherSuiteEmpty(t)
ev.verifyTLSNegotiatedProtoEmpty(t)
ev.verifyTLSVersionEmpty(t)
ev.verifyTLSPeerCertsEmpty(t)
}

func (ev *Event) verifyStartEventTime(t Driver) {
require.False(t, ev.T.IsZero(), "expected non-zero t field")
require.True(t, ev.T0.IsZero(), "expected zero t0 field")
}

func (ev *Event) verifyDoneEventTime(t Driver) {
require.False(t, ev.T.IsZero(), "expected non-zero t field")
require.False(t, ev.T0.IsZero(), "expected non-zero t0 field")
require.False(t, ev.T.Before(ev.T0), "expected t >= t0")
}

func (ev *Event) verifyProtocol(t Driver) {
require.True(t,
ev.Protocol == "tcp" || ev.Protocol == "udp",
"expected protocol to be tcp or udp")
}

func (ev *Event) verifyEndpoint(t Driver, epnt string) {
addr, port, err := net.SplitHostPort(epnt)
require.NoError(t, err, "expected valid endpoint")
require.True(t, net.ParseIP(addr) != nil, "expected valid IP address")
pnum, err := strconv.Atoi(port)
require.NoError(t, err, "expected valid port number")
require.True(t, pnum >= 1 && pnum <= 65535, "expected valid port number")
}

func (ev *Event) verifyErrEmpty(t Driver) {
require.Empty(t, ev.Err, "expected empty error field")
}

func (ev *Event) verifyCountPositive(t Driver) {
require.True(t, ev.Count > 0, "expected positive count field")
}

func (ev *Event) verifyCountZero(t Driver) {
require.Zero(t, ev.Count, "expected zero count field")
}

func (ev *Event) verifyCountOrErr(t Driver) {
require.True(t, ev.Count > 0 || ev.Err != "", "expected count or error")
}

func (ev *Event) verifyRawQueryEmpty(t Driver) {
require.Empty(t, ev.RawQuery, "expected empty rawQuery field")
}

func (ev *Event) verifyRawResponseEmpty(t Driver) {
require.Empty(t, ev.RawResponse, "expected empty rawResponse field")
}

func (ev *Event) verifyServerAddrEmpty(t Driver) {
require.Empty(t, ev.ServerAddr, "expected empty serverAddr field")
}

func (ev *Event) verifyServerProtocolEmpty(t Driver) {
require.Empty(t, ev.ServerProtocol, "expected empty serverProtocol field")
}

func (ev *Event) verifyTLSServerNameEmpty(t Driver) {
require.Empty(t, ev.TLSServerName, "expected empty tlsServerName field")
}

func (ev *Event) verifyTLSSkipVerifyFalse(t Driver) {
require.False(t, ev.TLSSkipVerify, "expected false tlsSkipVerify field")
}

func (ev *Event) verifyTLSCipherSuiteEmpty(t Driver) {
require.Empty(t, ev.TLSCipherSuite, "expected empty tlsCipherSuite field")
}

func (ev *Event) verifyTLSNegotiatedProtoEmpty(t Driver) {
require.Empty(t, ev.TLSNegotiatedProto, "expected empty tlsNegotiatedProtocol field")
}

func (ev *Event) verifyTLSVersionEmpty(t Driver) {
require.Empty(t, ev.TLSVersion, "expected empty tlsVersion field")
}

func (ev *Event) verifyTLSPeerCertsEmpty(t Driver) {
require.Empty(t, ev.TLSPeerCerts, "expected empty tlsPeerCerts field")
}

// VerifyEqual checks whether an event is equal to another.
Expand Down
12 changes: 12 additions & 0 deletions internal/qa/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,5 +102,17 @@ var Registry = []ScenarioDescriptor{
"rbmk", "dig", "+logs", "+https", "@8.8.8.8", "A", "www.example.com",
},
ExpectedErr: nil,
ExpectedSeq: []ExpectedEvent{
{Msg: "dnsQuery"},
{Msg: "connectStart"},
{Msg: "connectDone"},
{Msg: "tlsHandshakeStart"},
{Pattern: MatchAnyRead | MatchAnyWrite},
{Msg: "tlsHandshakeDone"},
{Pattern: MatchAnyRead | MatchAnyWrite},
{Pattern: MatchAnyRead | MatchAnyWrite},
{Msg: "dnsResponse"},
{Pattern: MatchAnyRead | MatchAnyWrite | MatchAnyClose},
},
},
}
2 changes: 2 additions & 0 deletions internal/qa/scenario.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ func (desc *ScenarioDescriptor) VerifyEvents(t Driver, r io.Reader) {
case expect.Pattern&MatchAnyClose != 0 && got.Msg == "closeStart":
fallthrough
case expect.Pattern&MatchAnyClose != 0 && got.Msg == "closeDone":
t.Logf("skipping at j=%d: %+v", j, got)
got.VerifyReadWriteClose(t)
j++
continue

Expand Down

0 comments on commit 3ffcc5c

Please sign in to comment.