Skip to content

Commit 8b4c6d0

Browse files
tachobitfehler
andcommitted
feat: Implement byte matching in TCP query responses
Currently the exporter only supports lines, which breaks byte-oriented protocols such as the PostgreSQL StartTLS handshake. We also give a working example for Postgres in the sample configuration. Signed-off-by: Stanislav Grozev <[email protected]> Co-authored-by: Conrad Hoffmann <[email protected]>
1 parent 9922360 commit 8b4c6d0

File tree

6 files changed

+100
-6
lines changed

6 files changed

+100
-6
lines changed

CONFIGURATION.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,15 @@ regexp: <regex>,
196196
[ source_ip_address: <string> ]
197197
198198
# The query sent in the TCP probe and the expected associated response.
199-
# "expect" matches a regular expression;
199+
# "expect" matches a regular expression; it is mutually exclusive with "expect_bytes".
200+
# "expect_bytes" does exact byte-by-byte match; it is mutually exclusive with "expect".
200201
# "labels" can define labels which will be exported on metric "probe_expect_info";
201202
# "send" sends some content;
202203
# "send" and "labels.value" can contain values matched by "expect" (such as "${1}");
203204
# "starttls" upgrades TCP connection to TLS.
204205
query_response:
205206
[ - [ [ expect: <string> ],
207+
[ expect_bytes: <string> ],
206208
[ labels:
207209
- [ name: <string>
208210
value: <string>

blackbox.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,10 @@ modules:
6767
enable_http3: true
6868
enable_http2: false
6969
valid_http_versions: ["HTTP/3.0"]
70+
postgresql:
71+
prober: tcp
72+
tcp:
73+
query_response:
74+
- send: !!binary AAAACATSFi8= # 0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F - PostgreSQL SSLRequest
75+
- expect_bytes: S # 0x53 - Reply will be 'S' if SSL is enabled, and 'N' if it is not.
76+
- starttls: true

config/config.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -341,10 +341,11 @@ type Label struct {
341341
}
342342

343343
type QueryResponse struct {
344-
Expect Regexp `yaml:"expect,omitempty"`
345-
Labels []Label `yaml:"labels,omitempty"`
346-
Send string `yaml:"send,omitempty"`
347-
StartTLS bool `yaml:"starttls,omitempty"`
344+
Expect Regexp `yaml:"expect,omitempty"`
345+
ExpectBytes string `yaml:"expect_bytes,omitempty"`
346+
Labels []Label `yaml:"labels,omitempty"`
347+
Send string `yaml:"send,omitempty"`
348+
StartTLS bool `yaml:"starttls,omitempty"`
348349
}
349350

350351
type TCPProbe struct {
@@ -568,7 +569,9 @@ func (s *QueryResponse) UnmarshalYAML(unmarshal func(interface{}) error) error {
568569
if err := unmarshal((*plain)(s)); err != nil {
569570
return err
570571
}
571-
572+
if s.Expect.Regexp != nil && s.ExpectBytes != "" {
573+
return errors.New("expect and expect_bytes are mutually exclusive")
574+
}
572575
return nil
573576
}
574577

example.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,10 @@ modules:
222222
query_response:
223223
- send: "PING"
224224
- expect: "PONG"
225+
postgresql:
226+
prober: tcp
227+
tcp:
228+
query_response:
229+
- send: !!binary AAAACATSFi8= # 0x00, 0x00, 0x00, 0x08, 0x04, 0xD2, 0x16, 0x2F - PostgreSQL SSLRequest
230+
- expect_bytes: S # 0x53 - Reply will be 'S' if SSL is enabled, and 'N' if it is not.
231+
- starttls: true

prober/query_response.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package prober
1515

1616
import (
1717
"bufio"
18+
"bytes"
1819
"context"
1920
"crypto/tls"
2021
"fmt"
@@ -63,7 +64,12 @@ func probeQueryResponses(ctx context.Context, target string, conn net.Conn, modu
6364
Name: "probe_failed_due_to_regex",
6465
Help: "Indicates if probe failed due to regex",
6566
})
67+
probeFailedDueToBytes := prometheus.NewGauge(prometheus.GaugeOpts{
68+
Name: "probe_failed_due_to_bytes",
69+
Help: "Indicates if probe failed due to bytes",
70+
})
6671
registry.MustRegister(probeFailedDueToRegex)
72+
registry.MustRegister(probeFailedDueToBytes)
6773

6874
var queryResponses []config.QueryResponse
6975
var tlsConfig *pconfig.TLSConfig
@@ -125,6 +131,32 @@ func probeQueryResponses(ctx context.Context, target string, conn net.Conn, modu
125131
probeExpectInfo(registry, &qr, scanner.Bytes(), match)
126132
}
127133
}
134+
if qr.ExpectBytes != "" {
135+
expect_bytes := []byte(qr.ExpectBytes)
136+
137+
// Try to read same number of bytes as expected.
138+
data := make([]byte, len(expect_bytes))
139+
n, err := conn.Read(data)
140+
if err != nil {
141+
logger.Error("Error reading from connection", "err", err)
142+
return false
143+
}
144+
145+
logger.Debug("Read bytes", "bytes", data)
146+
147+
if n < len(expect_bytes) {
148+
logger.Error("Read less data than expected", "expected", expect_bytes, "bytes", data)
149+
return false
150+
}
151+
152+
if !bytes.Equal(expect_bytes, data) {
153+
probeFailedDueToRegex.Set(1)
154+
logger.Error("Bytes did not match", "expected", expect_bytes, "bytes", data)
155+
return false
156+
}
157+
logger.Debug("Bytes matched", "expected", expect_bytes, "bytes", data)
158+
probeFailedDueToRegex.Set(0)
159+
}
128160
if send != "" {
129161
logger.Debug("Sending line", "line", send)
130162
if _, err := fmt.Fprintf(conn, "%s\n", send); err != nil {

prober/tcp_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,49 @@ func TestTCPConnectionQueryResponseMatching(t *testing.T) {
580580

581581
}
582582

583+
func TestTCPConnectionQueryResponseByteMode(t *testing.T) {
584+
ln, err := net.Listen("tcp", "localhost:0")
585+
if err != nil {
586+
t.Fatalf("Error listening on socket: %s", err)
587+
}
588+
defer ln.Close()
589+
590+
testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
591+
defer cancel()
592+
module := config.Module{
593+
TCP: config.TCPProbe{
594+
IPProtocolFallback: true,
595+
QueryResponse: []config.QueryResponse{
596+
{
597+
ExpectBytes: "not-a-line",
598+
},
599+
},
600+
},
601+
}
602+
603+
go func() {
604+
conn, err := ln.Accept()
605+
if err != nil {
606+
panic(fmt.Sprintf("Error accepting on socket: %s", err))
607+
}
608+
conn.SetDeadline(time.Now().Add(1 * time.Second))
609+
conn.Write([]byte("not-a-line"))
610+
conn.Close()
611+
}()
612+
registry := prometheus.NewRegistry()
613+
if !ProbeTCP(testCTX, ln.Addr().String(), module, registry, promslog.NewNopLogger()) {
614+
t.Fatalf("TCP module failed, expected success.")
615+
}
616+
mfs, err := registry.Gather()
617+
if err != nil {
618+
t.Fatal(err)
619+
}
620+
expectedResults := map[string]float64{
621+
"probe_failed_due_to_regex": 0,
622+
}
623+
checkRegistryResults(expectedResults, mfs, t)
624+
}
625+
583626
func TestTCPConnectionProtocol(t *testing.T) {
584627
if os.Getenv("CI") == "true" {
585628
t.Skip("skipping; CI is failing on ipv6 dns requests")

0 commit comments

Comments
 (0)