Skip to content

Commit bb9e281

Browse files
authored
Merge pull request #1 from asokolov365/spec_http_grpc_src_ip
allow configuration of source ip for http and grpc probers
2 parents c382a47 + f0890d1 commit bb9e281

File tree

7 files changed

+150
-6
lines changed

7 files changed

+150
-6
lines changed

CONFIGURATION.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ modules:
153153
[ preferred_ip_protocol: <string> | default = "ip6" ]
154154
[ ip_protocol_fallback: <boolean> | default = true ]
155155

156+
# The source IP address.
157+
[ source_ip_address: <string> ]
158+
156159
# The body of the HTTP request used in probe.
157160
[ body: <string> ]
158161

@@ -310,6 +313,9 @@ validate_additional_rrs:
310313
[ preferred_ip_protocol: <string> ]
311314
[ ip_protocol_fallback: <boolean> | default = true ]
312315
316+
# The source IP address.
317+
[ source_ip_address: <string> ]
318+
313319
# Whether to connect to the endpoint with TLS.
314320
[ tls: <boolean | default = false> ]
315321

config/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ type HTTPProbe struct {
208208
ValidHTTPVersions []string `yaml:"valid_http_versions,omitempty"`
209209
IPProtocol string `yaml:"preferred_ip_protocol,omitempty"`
210210
IPProtocolFallback bool `yaml:"ip_protocol_fallback,omitempty"`
211+
SourceIPAddress string `yaml:"source_ip_address,omitempty"`
211212
SkipResolvePhaseWithProxy bool `yaml:"skip_resolve_phase_with_proxy,omitempty"`
212213
NoFollowRedirects *bool `yaml:"no_follow_redirects,omitempty"`
213214
FailIfSSL bool `yaml:"fail_if_ssl,omitempty"`
@@ -231,6 +232,7 @@ type GRPCProbe struct {
231232
TLSConfig config.TLSConfig `yaml:"tls_config,omitempty"`
232233
IPProtocolFallback bool `yaml:"ip_protocol_fallback,omitempty"`
233234
PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"`
235+
SourceIPAddress string `yaml:"source_ip_address,omitempty"`
234236
}
235237

236238
type HeaderMatch struct {

config/testdata/blackbox-good.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ modules:
33
prober: http
44
timeout: 5s
55
http:
6+
source_ip_address: 127.0.0.1
67
http_post_2xx:
78
prober: http
89
timeout: 5s
@@ -64,6 +65,7 @@ modules:
6465
dns:
6566
query_name: example.com
6667
preferred_ip_protocol: ip4
68+
source_ip_address: 127.0.0.1
6769
ip_protocol_fallback: false
6870
validate_answer_rrs:
6971
fail_if_matches_regexp: [test]

prober/grpc.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ package prober
1515

1616
import (
1717
"context"
18+
"net"
19+
"net/url"
20+
"strings"
21+
"time"
22+
1823
"github.com/go-kit/log"
1924
"github.com/go-kit/log/level"
2025
"github.com/prometheus/blackbox_exporter/config"
@@ -27,10 +32,6 @@ import (
2732
"google.golang.org/grpc/health/grpc_health_v1"
2833
"google.golang.org/grpc/peer"
2934
"google.golang.org/grpc/status"
30-
"net"
31-
"net/url"
32-
"strings"
33-
"time"
3435
)
3536

3637
type GRPCHealthCheck interface {
@@ -167,6 +168,18 @@ func ProbeGRPC(ctx context.Context, target string, module config.Module, registr
167168
}
168169

169170
var opts []grpc.DialOption
171+
if len(module.GRPC.SourceIPAddress) > 0 {
172+
srcIP := net.ParseIP(module.GRPC.SourceIPAddress)
173+
if srcIP == nil {
174+
level.Error(logger).Log("msg", "Error parsing source ip address", "srcIP", module.GRPC.SourceIPAddress)
175+
return false
176+
}
177+
level.Info(logger).Log("msg", "Using local address", "srcIP", srcIP)
178+
opts = append(opts, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
179+
return (&net.Dialer{LocalAddr: &net.TCPAddr{IP: srcIP}}).DialContext(ctx, "tcp", addr)
180+
}))
181+
}
182+
170183
target = targetHost + ":" + targetPort
171184
if !module.GRPC.TLS {
172185
level.Debug(logger).Log("msg", "Dialing GRPC without TLS")

prober/grpc_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,64 @@ func TestGRPCHealthCheckUnimplemented(t *testing.T) {
414414

415415
checkRegistryResults(expectedResults, mfs, t)
416416
}
417+
418+
func TestGrpcSourceIPAddress(t *testing.T) {
419+
420+
ln, err := net.Listen("tcp", "localhost:0")
421+
if err != nil {
422+
t.Fatalf("Error listening on socket: %s", err)
423+
}
424+
defer ln.Close()
425+
426+
_, port, err := net.SplitHostPort(ln.Addr().String())
427+
if err != nil {
428+
t.Fatalf("Error retrieving port for socket: %s", err)
429+
}
430+
s := grpc.NewServer()
431+
healthServer := health.NewServer()
432+
healthServer.SetServingStatus("service", grpc_health_v1.HealthCheckResponse_SERVING)
433+
grpc_health_v1.RegisterHealthServer(s, healthServer)
434+
435+
go func() {
436+
if err := s.Serve(ln); err != nil {
437+
t.Errorf("failed to serve: %v", err)
438+
return
439+
}
440+
}()
441+
defer s.GracefulStop()
442+
443+
ifaces, err := net.Interfaces()
444+
if err != nil {
445+
t.Fatalf("Error retrieving network interfaces: %s", err)
446+
}
447+
for _, iface := range ifaces {
448+
addrs, err := iface.Addrs()
449+
if err != nil {
450+
t.Fatalf("Error retrieving addrs from iface %s: %s", iface.Name, err)
451+
}
452+
for _, addr := range addrs {
453+
var ip net.IP
454+
switch v := addr.(type) {
455+
case *net.IPNet:
456+
ip = v.IP
457+
case *net.IPAddr:
458+
ip = v.IP
459+
}
460+
// Skipping IPv6 addrs
461+
if ip.To4() == nil {
462+
continue
463+
}
464+
registry := prometheus.NewRegistry()
465+
testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
466+
defer cancel()
467+
result := ProbeGRPC(testCTX, "localhost:"+port,
468+
config.Module{Timeout: time.Second, GRPC: config.GRPCProbe{
469+
IPProtocolFallback: false,
470+
SourceIPAddress: ip.String(),
471+
}}, registry, log.NewNopLogger())
472+
if result != true {
473+
t.Fatalf("Test %s had unexpected result", ip.String())
474+
}
475+
}
476+
}
477+
}

prober/http.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -348,14 +348,30 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
348348
}
349349
}
350350
}
351-
client, err := pconfig.NewClientFromConfig(httpClientConfig, "http_probe", pconfig.WithKeepAlivesDisabled())
351+
352+
httpClientOptions := []pconfig.HTTPClientOption{
353+
pconfig.WithKeepAlivesDisabled(),
354+
}
355+
356+
if len(module.HTTP.SourceIPAddress) > 0 {
357+
srcIP := net.ParseIP(module.HTTP.SourceIPAddress)
358+
if srcIP == nil {
359+
level.Error(logger).Log("msg", "Error parsing source ip address", "srcIP", module.HTTP.SourceIPAddress)
360+
return false
361+
}
362+
level.Info(logger).Log("msg", "Using local address", "srcIP", srcIP)
363+
httpClientOptions = append(httpClientOptions,
364+
pconfig.WithDialContextFunc((&net.Dialer{LocalAddr: &net.TCPAddr{IP: srcIP}}).DialContext))
365+
}
366+
367+
client, err := pconfig.NewClientFromConfig(httpClientConfig, "http_probe", httpClientOptions...)
352368
if err != nil {
353369
level.Error(logger).Log("msg", "Error generating HTTP client", "err", err)
354370
return false
355371
}
356372

357373
httpClientConfig.TLSConfig.ServerName = ""
358-
noServerName, err := pconfig.NewRoundTripperFromConfig(httpClientConfig, "http_probe", pconfig.WithKeepAlivesDisabled())
374+
noServerName, err := pconfig.NewRoundTripperFromConfig(httpClientConfig, "http_probe", httpClientOptions...)
359375
if err != nil {
360376
level.Error(logger).Log("msg", "Error generating HTTP client without ServerName", "err", err)
361377
return false

prober/http_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1544,3 +1544,47 @@ func TestBody(t *testing.T) {
15441544
}
15451545
}
15461546
}
1547+
1548+
func TestHttpSourceIPAddress(t *testing.T) {
1549+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1550+
w.WriteHeader(http.StatusOK)
1551+
}))
1552+
defer ts.Close()
1553+
1554+
ifaces, err := net.Interfaces()
1555+
if err != nil {
1556+
t.Fatalf("Error retrieving network interfaces: %s", err)
1557+
}
1558+
for _, iface := range ifaces {
1559+
addrs, err := iface.Addrs()
1560+
if err != nil {
1561+
t.Fatalf("Error retrieving addrs from iface %s: %s", iface.Name, err)
1562+
}
1563+
for _, addr := range addrs {
1564+
var ip net.IP
1565+
switch v := addr.(type) {
1566+
case *net.IPNet:
1567+
ip = v.IP
1568+
case *net.IPAddr:
1569+
ip = v.IP
1570+
}
1571+
// Skipping IPv6 addrs
1572+
if ip.To4() == nil {
1573+
continue
1574+
}
1575+
registry := prometheus.NewRegistry()
1576+
recorder := httptest.NewRecorder()
1577+
testCTX, cancel := context.WithTimeout(context.Background(), 10*time.Second)
1578+
defer cancel()
1579+
result := ProbeHTTP(testCTX, ts.URL,
1580+
config.Module{Timeout: time.Second, HTTP: config.HTTPProbe{
1581+
IPProtocolFallback: true,
1582+
SourceIPAddress: ip.String(),
1583+
}}, registry, log.NewNopLogger())
1584+
body := recorder.Body.String()
1585+
if result != true {
1586+
t.Fatalf("Test %s had unexpected result: %s", ip.String(), body)
1587+
}
1588+
}
1589+
}
1590+
}

0 commit comments

Comments
 (0)