- "Download the latest version here"
tls_config:
insecure_skip_verify: false
+ protocol: "tcp" # accepts "tcp/tcp4/tcp6", defaults to "tcp"
+ preferred_ip_protocol: "ip4" # used for "tcp", defaults to "ip6"
tcp_connect:
prober: tcp
timeout: 5s
+ tcp:
+ protocol: "tcp"
+ preferred_ip_protocol: "ip4"
pop3s_banner:
prober: tcp
tcp:
icmp:
prober: icmp
timeout: 5s
+ icmp:
+ protocol: "icmp"
+ preferred_ip_protocol: "ip4"
dns_udp:
prober: dns
timeout: 5s
dns_tcp:
prober: dns
dns:
- protocol: "tcp" # can also be something like "udp4" or "tcp6"
+ protocol: "tcp" # accepts "tcp/tcp4/tcp6/udp/udp4/udp6", defaults to "udp"
+ preferred_ip_protocol: "ip4" # used for "udp/tcp", defaults to "ip6"
query_name: "www.prometheus.io"
```
import (
"fmt"
+ "net"
"net/http"
"regexp"
func probeDNS(target string, w http.ResponseWriter, module Module) bool {
var numAnswer, numAuthority, numAdditional int
+ var dialProtocol, fallbackProtocol string
defer func() {
// These metrics can be used to build additional alerting based on the number of replies.
// They should be returned even in case of errors.
fmt.Fprintf(w, "probe_dns_additional_rrs %d\n", numAdditional)
}()
+ if module.DNS.Protocol == "" {
+ module.DNS.Protocol = "udp"
+ }
+
+ if (module.DNS.Protocol == "tcp" || module.DNS.Protocol == "udp") && module.DNS.PreferredIpProtocol == "" {
+ module.DNS.PreferredIpProtocol = "ip6"
+ }
+ if module.DNS.PreferredIpProtocol == "ip6" {
+ fallbackProtocol = "ip4"
+ } else {
+ fallbackProtocol = "ip6"
+ }
+
+ dialProtocol = module.DNS.Protocol
+ if module.DNS.Protocol == "udp" || module.DNS.Protocol == "tcp" {
+ target_address, _, _ := net.SplitHostPort(target)
+ ip, err := net.ResolveIPAddr(module.DNS.PreferredIpProtocol, target_address)
+ if err != nil {
+ ip, err = net.ResolveIPAddr(fallbackProtocol, target_address)
+ if err != nil {
+ return false
+ }
+ }
+
+ if ip.IP.To4() == nil {
+ dialProtocol = module.DNS.Protocol + "6"
+ } else {
+ dialProtocol = module.DNS.Protocol + "4"
+ }
+ }
+
+ if dialProtocol[len(dialProtocol)-1] == '6' {
+ fmt.Fprintf(w, "probe_ip_protocol 6\n")
+ } else {
+ fmt.Fprintf(w, "probe_ip_protocol 4\n")
+ }
+
client := new(dns.Client)
- client.Net = module.DNS.Protocol
+ client.Net = dialProtocol
client.Timeout = module.Timeout
qt := dns.TypeANY
import (
"net"
"net/http/httptest"
+ "runtime"
"strings"
"testing"
"time"
}
}
}
+
+func TestDNSProtocol(t *testing.T) {
+ // This test assumes that listening "tcp" listens both IPv6 and IPv4 traffic and
+ // localhost resolves to both 127.0.0.1 and ::1. we must skip the test if either
+ // of these isn't true. This should be true for modern Linux systems.
+ if runtime.GOOS == "dragonfly" || runtime.GOOS == "openbsd" {
+ t.Skip("IPv6 socket isn't able to accept IPv4 traffic in the system.")
+ }
+ _, err := net.ResolveIPAddr("ip6", "localhost")
+ if err != nil {
+ t.Skip("\"localhost\" doesn't resolve to ::1.")
+ }
+
+ for _, protocol := range PROTOCOLS {
+ server, addr := startDNSServer(protocol, recursiveDNSHandler)
+ defer server.Shutdown()
+
+ _, port, _ := net.SplitHostPort(addr.String())
+
+ // Force IPv4
+ module := Module{
+ Timeout: time.Second,
+ DNS: DNSProbe{
+ QueryName: "example.com",
+ Protocol: protocol + "4",
+ },
+ }
+ recorder := httptest.NewRecorder()
+ result := probeDNS(net.JoinHostPort("localhost", port), recorder, module)
+ body := recorder.Body.String()
+ if !result {
+ t.Fatalf("DNS protocol: \"%v4\" connection test failed, expected success.", protocol)
+ }
+ if !strings.Contains(body, "probe_ip_protocol 4\n") {
+ t.Fatalf("Expected IPv4, got %s", body)
+ }
+
+ // Force IPv6
+ module = Module{
+ Timeout: time.Second,
+ DNS: DNSProbe{
+ QueryName: "example.com",
+ Protocol: protocol + "6",
+ },
+ }
+ recorder = httptest.NewRecorder()
+ result = probeDNS(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("DNS protocol: \"%v6\" connection test failed, expected success.", protocol)
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+
+ // Prefer IPv6
+ module = Module{
+ Timeout: time.Second,
+ DNS: DNSProbe{
+ QueryName: "example.com",
+ Protocol: protocol,
+ PreferredIpProtocol: "ip6",
+ },
+ }
+ recorder = httptest.NewRecorder()
+ result = probeDNS(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("DNS protocol: \"%v\", preferred \"ip6\" connection test failed, expected success.", protocol)
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+
+ // Prefer IPv4
+ module = Module{
+ Timeout: time.Second,
+ DNS: DNSProbe{
+ QueryName: "example.com",
+ Protocol: protocol,
+ PreferredIpProtocol: "ip4",
+ },
+ }
+ recorder = httptest.NewRecorder()
+ result = probeDNS(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("DNS protocol: \"%v\", preferred \"ip4\" connection test failed, expected success.", protocol)
+ }
+ if !strings.Contains(body, "probe_ip_protocol 4\n") {
+ t.Fatalf("Expected IPv4, got %s", body)
+ }
+
+ // Prefer none
+ module = Module{
+ Timeout: time.Second,
+ DNS: DNSProbe{
+ QueryName: "example.com",
+ Protocol: protocol,
+ },
+ }
+ recorder = httptest.NewRecorder()
+ result = probeDNS(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("DNS protocol: \"%v\" connection test failed, expected success.", protocol)
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+
+ // No protocol
+ module = Module{
+ Timeout: time.Second,
+ DNS: DNSProbe{
+ QueryName: "example.com",
+ },
+ }
+ recorder = httptest.NewRecorder()
+ result = probeDNS(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if protocol == "udp" {
+ if !result {
+ t.Fatalf("DNS test connection with protocol unspecified failed, expected success.", protocol)
+ }
+ } else {
+ if result {
+ t.Fatalf("DNS test connection with protocol unspecified succeeded, expected failure.", protocol)
+ }
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+ }
+}
"fmt"
"io"
"io/ioutil"
+ "net"
"net/http"
+ "net/url"
"regexp"
"strings"
func probeHTTP(target string, w http.ResponseWriter, module Module) (success bool) {
var isSSL, redirects int
+ var dialProtocol, fallbackProtocol string
+
config := module.HTTP
+ if module.HTTP.Protocol == "" {
+ module.HTTP.Protocol = "tcp"
+ }
+
+ if module.HTTP.Protocol == "tcp" && module.HTTP.PreferredIpProtocol == "" {
+ module.HTTP.PreferredIpProtocol = "ip6"
+ }
+ if module.HTTP.PreferredIpProtocol == "ip6" {
+ fallbackProtocol = "ip4"
+ } else {
+ fallbackProtocol = "ip6"
+ }
+
+ dialProtocol = module.HTTP.Protocol
+ if module.HTTP.Protocol == "tcp" {
+ target_url, err := url.Parse(target)
+ if err != nil {
+ return false
+ }
+ target_host, _, err := net.SplitHostPort(target_url.Host)
+ // If split fails, assuming it's a hostname without port part
+ if err != nil {
+ target_host = target_url.Host
+ }
+ ip, err := net.ResolveIPAddr(module.HTTP.PreferredIpProtocol, target_host)
+ if err != nil {
+ ip, err = net.ResolveIPAddr(fallbackProtocol, target_host)
+ if err != nil {
+ return false
+ }
+ }
+
+ if ip.IP.To4() == nil {
+ dialProtocol = "tcp6"
+ } else {
+ dialProtocol = "tcp4"
+ }
+ }
+
+ if dialProtocol == "tcp6" {
+ fmt.Fprintf(w, "probe_ip_protocol 6\n")
+ } else {
+ fmt.Fprintf(w, "probe_ip_protocol 4\n")
+ }
+
client := &http.Client{
Timeout: module.Timeout,
}
log.Errorf("Error generating TLS config: %s", err)
return false
}
+ dial := func(network, address string) (net.Conn, error) {
+ return net.Dial(dialProtocol, address)
+ }
client.Transport = &http.Transport{
TLSClientConfig: tlsconfig,
+ Dial: dial,
}
client.CheckRedirect = func(_ *http.Request, via []*http.Request) error {
import (
"bytes"
+ "fmt"
"golang.org/x/net/icmp"
"golang.org/x/net/ipv4"
+ "golang.org/x/net/ipv6"
"net"
"net/http"
"os"
}
func probeICMP(target string, w http.ResponseWriter, module Module) (success bool) {
+ var (
+ socket *icmp.PacketConn
+ requestType icmp.Type
+ replyType icmp.Type
+ fallbackProtocol string
+ )
+
deadline := time.Now().Add(module.Timeout)
- socket, err := icmp.ListenPacket("ip4:icmp", "0.0.0.0")
+
+ // Defaults to IPv4 to be compatible with older versions
+ if module.ICMP.Protocol == "" {
+ module.ICMP.Protocol = "icmp"
+ }
+
+ // In case of ICMP prefer IPv6 by default
+ if module.ICMP.Protocol == "icmp" && module.ICMP.PreferredIpProtocol == "" {
+ module.ICMP.PreferredIpProtocol = "ip6"
+ }
+
+ if module.ICMP.Protocol == "icmp4" {
+ module.ICMP.PreferredIpProtocol = "ip4"
+ fallbackProtocol = ""
+ } else if module.ICMP.Protocol == "icmp6" {
+ module.ICMP.PreferredIpProtocol = "ip6"
+ fallbackProtocol = ""
+ } else if module.ICMP.PreferredIpProtocol == "ip6" {
+ fallbackProtocol = "ip4"
+ } else {
+ fallbackProtocol = "ip6"
+ }
+
+ ip, err := net.ResolveIPAddr(module.ICMP.PreferredIpProtocol, target)
+ if err != nil && fallbackProtocol != "" {
+ ip, err = net.ResolveIPAddr(fallbackProtocol, target)
+ }
+ if err != nil {
+ log.Errorf("Error resolving address %s: %s", target, err)
+ }
+
+ if ip.IP.To4() == nil {
+ requestType = ipv6.ICMPTypeEchoRequest
+ replyType = ipv6.ICMPTypeEchoReply
+ socket, err = icmp.ListenPacket("ip6:ipv6-icmp", "::")
+ fmt.Fprintf(w, "probe_ip_protocol 6\n")
+ } else {
+ requestType = ipv4.ICMPTypeEcho
+ replyType = ipv4.ICMPTypeEchoReply
+ socket, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0")
+ fmt.Fprintf(w, "probe_ip_protocol 4\n")
+ }
+
if err != nil {
log.Errorf("Error listening to socket: %s", err)
return
}
defer socket.Close()
- ip, err := net.ResolveIPAddr("ip4", target)
if err != nil {
log.Errorf("Error resolving address %s: %s", target, err)
return
pid := os.Getpid() & 0xffff
wm := icmp.Message{
- Type: ipv4.ICMPTypeEcho, Code: 0,
+ Type: requestType,
+ Code: 0,
Body: &icmp.Echo{
ID: pid, Seq: int(seq),
Data: []byte("Prometheus Blackbox Exporter"),
},
}
+
wb, err := wm.Marshal(nil)
if err != nil {
log.Errorf("Error marshalling packet for %s: %s", target, err)
}
// Reply should be the same except for the message type.
- wm.Type = ipv4.ICMPTypeEchoReply
+ wm.Type = replyType
wb, err = wm.Marshal(nil)
if err != nil {
log.Errorf("Error marshalling packet for %s: %s", target, err)
if peer.String() != ip.String() {
continue
}
+ if replyType == ipv6.ICMPTypeEchoReply {
+ // Clear checksum to make comparison succeed.
+ rb[2] = 0
+ rb[3] = 0
+ }
if bytes.Compare(rb[:n], wb) == 0 {
success = true
return
FailIfMatchesRegexp []string `yaml:"fail_if_matches_regexp"`
FailIfNotMatchesRegexp []string `yaml:"fail_if_not_matches_regexp"`
TLSConfig config.TLSConfig `yaml:"tls_config"`
+ Protocol string `yaml:"protocol"` // Defaults to "tcp".
+ PreferredIpProtocol string `yaml:"preferred_ip_protocol"` // Defaults to "ip6".
}
type QueryResponse struct {
}
type TCPProbe struct {
- QueryResponse []QueryResponse `yaml:"query_response"`
- TLS bool `yaml:"tls"`
- TLSConfig config.TLSConfig `yaml:"tls_config"`
+ QueryResponse []QueryResponse `yaml:"query_response"`
+ TLS bool `yaml:"tls"`
+ TLSConfig config.TLSConfig `yaml:"tls_config"`
+ Protocol string `yaml:"protocol"` // Defaults to "tcp".
+ PreferredIpProtocol string `yaml:"preferred_ip_protocol"` // Defaults to "ip6".
}
type ICMPProbe struct {
+ Protocol string `yaml:"protocol"` // Defaults to "icmp4".
+ PreferredIpProtocol string `yaml:"preferred_ip_protocol"` // Defaults to "ip6".
}
type DNSProbe struct {
- Protocol string `yaml:"protocol"` // Defaults to "udp".
- QueryName string `yaml:"query_name"`
- QueryType string `yaml:"query_type"` // Defaults to ANY.
- ValidRcodes []string `yaml:"valid_rcodes"` // Defaults to NOERROR.
- ValidateAnswer DNSRRValidator `yaml:"validate_answer_rrs"`
- ValidateAuthority DNSRRValidator `yaml:"validate_authority_rrs"`
- ValidateAdditional DNSRRValidator `yaml:"validate_additional_rrs"`
+ Protocol string `yaml:"protocol"` // Defaults to "udp".
+ QueryName string `yaml:"query_name"`
+ QueryType string `yaml:"query_type"` // Defaults to ANY.
+ ValidRcodes []string `yaml:"valid_rcodes"` // Defaults to NOERROR.
+ ValidateAnswer DNSRRValidator `yaml:"validate_answer_rrs"`
+ ValidateAuthority DNSRRValidator `yaml:"validate_authority_rrs"`
+ ValidateAdditional DNSRRValidator `yaml:"validate_additional_rrs"`
+ PreferredIpProtocol string `yaml:"preferred_ip_protocol"` // Defaults to "ip6".
}
type DNSRRValidator struct {
"github.com/prometheus/common/log"
)
-func dialTCP(target string, module Module) (net.Conn, error) {
+func dialTCP(target string, w http.ResponseWriter, module Module) (net.Conn, error) {
+ var dialProtocol, fallbackProtocol string
+
dialer := &net.Dialer{Timeout: module.Timeout}
+ if module.TCP.Protocol == "" {
+ module.TCP.Protocol = "tcp"
+ }
+ if module.TCP.Protocol == "tcp" && module.TCP.PreferredIpProtocol == "" {
+ module.TCP.PreferredIpProtocol = "ip6"
+ }
+ if module.TCP.PreferredIpProtocol == "ip6" {
+ fallbackProtocol = "ip4"
+ } else {
+ fallbackProtocol = "ip6"
+ }
+
+ dialProtocol = module.TCP.Protocol
+ if module.TCP.Protocol == "tcp" {
+ target_address, _, err := net.SplitHostPort(target)
+ ip, err := net.ResolveIPAddr(module.TCP.PreferredIpProtocol, target_address)
+ if err != nil {
+ ip, err = net.ResolveIPAddr(fallbackProtocol, target_address)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ if ip.IP.To4() == nil {
+ dialProtocol = "tcp6"
+ } else {
+ dialProtocol = "tcp4"
+ }
+ }
+
+ if dialProtocol == "tcp6" {
+ fmt.Fprintf(w, "probe_ip_protocol 6\n")
+ } else {
+ fmt.Fprintf(w, "probe_ip_protocol 4\n")
+ }
+
if !module.TCP.TLS {
- return dialer.Dial("tcp", target)
+ return dialer.Dial(dialProtocol, target)
}
config, err := module.TCP.TLSConfig.GenerateConfig()
if err != nil {
return nil, err
}
- return tls.DialWithDialer(dialer, "tcp", target, config)
+ return tls.DialWithDialer(dialer, dialProtocol, target, config)
}
func probeTCP(target string, w http.ResponseWriter, module Module) bool {
deadline := time.Now().Add(module.Timeout)
- conn, err := dialTCP(target, module)
+ conn, err := dialTCP(target, w, module)
if err != nil {
return false
}
import (
"fmt"
"net"
+ "net/http/httptest"
+ "runtime"
+ "strings"
"testing"
"time"
)
conn.Close()
ch <- struct{}{}
}()
- if !probeTCP(ln.Addr().String(), nil, Module{Timeout: time.Second}) {
+ recorder := httptest.NewRecorder()
+ if !probeTCP(ln.Addr().String(), recorder, Module{Timeout: time.Second}) {
t.Fatalf("TCP module failed, expected success.")
}
<-ch
func TestTCPConnectionFails(t *testing.T) {
// Invalid port number.
- if probeTCP(":0", nil, Module{Timeout: time.Second}) {
+ recorder := httptest.NewRecorder()
+ if probeTCP(":0", recorder, Module{Timeout: time.Second}) {
t.Fatalf("TCP module suceeded, expected failure.")
}
}
conn.Close()
ch <- struct{}{}
}()
- if !probeTCP(ln.Addr().String(), nil, module) {
+ recorder := httptest.NewRecorder()
+ if !probeTCP(ln.Addr().String(), recorder, module) {
t.Fatalf("TCP module failed, expected success.")
}
<-ch
conn.Close()
ch <- struct{}{}
}()
- if probeTCP(ln.Addr().String(), nil, module) {
+ if probeTCP(ln.Addr().String(), recorder, module) {
t.Fatalf("TCP module succeeded, expected failure.")
}
<-ch
conn.Close()
ch <- version
}()
- if !probeTCP(ln.Addr().String(), nil, module) {
+ recorder := httptest.NewRecorder()
+ if !probeTCP(ln.Addr().String(), recorder, module) {
t.Fatalf("TCP module failed, expected success.")
}
if got, want := <-ch, "OpenSSH_6.9p1"; got != want {
t.Fatalf("Read unexpected version: got %q, want %q", got, want)
}
}
+
+func TestTCPConnectionProtocol(t *testing.T) {
+ // This test assumes that listening "tcp" listens both IPv6 and IPv4 traffic and
+ // localhost resolves to both 127.0.0.1 and ::1. we must skip the test if either
+ // of these isn't true. This should be true for modern Linux systems.
+ if runtime.GOOS == "dragonfly" || runtime.GOOS == "openbsd" {
+ t.Skip("IPv6 socket isn't able to accept IPv4 traffic in the system.")
+ }
+ _, err := net.ResolveIPAddr("ip6", "localhost")
+ if err != nil {
+ t.Skip("\"localhost\" doesn't resolve to ::1.")
+ }
+
+ ln, err := net.Listen("tcp", ":0")
+ if err != nil {
+ t.Fatalf("Error listening on socket: %s", err)
+ }
+ defer ln.Close()
+
+ _, port, _ := net.SplitHostPort(ln.Addr().String())
+
+ // Force IPv4
+ module := Module{
+ Timeout: time.Second,
+ TCP: TCPProbe{
+ Protocol: "tcp4",
+ },
+ }
+
+ recorder := httptest.NewRecorder()
+ result := probeTCP(net.JoinHostPort("localhost", port), recorder, module)
+ body := recorder.Body.String()
+ if !result {
+ t.Fatalf("TCP protocol: \"tcp4\" connection test failed, expected success.")
+ }
+ if !strings.Contains(body, "probe_ip_protocol 4\n") {
+ t.Fatalf("Expected IPv4, got %s", body)
+ }
+
+ // Force IPv6
+ module = Module{
+ Timeout: time.Second,
+ TCP: TCPProbe{
+ Protocol: "tcp6",
+ },
+ }
+
+ recorder = httptest.NewRecorder()
+ result = probeTCP(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("TCP protocol: \"tcp6\" connection test failed, expected success.")
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+
+ // Prefer IPv4
+ module = Module{
+ Timeout: time.Second,
+ TCP: TCPProbe{
+ Protocol: "tcp",
+ PreferredIpProtocol: "ip4",
+ },
+ }
+
+ recorder = httptest.NewRecorder()
+ result = probeTCP(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("TCP protocol: \"tcp\", prefer: \"ip4\" connection test failed, expected success.")
+ }
+ if !strings.Contains(body, "probe_ip_protocol 4\n") {
+ t.Fatalf("Expected IPv4, got %s", body)
+ }
+
+ // Prefer IPv6
+ module = Module{
+ Timeout: time.Second,
+ TCP: TCPProbe{
+ Protocol: "tcp",
+ PreferredIpProtocol: "ip6",
+ },
+ }
+
+ recorder = httptest.NewRecorder()
+ result = probeTCP(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("TCP protocol: \"tcp\", prefer: \"ip6\" connection test failed, expected success.")
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+
+ // Prefer nothing
+ module = Module{
+ Timeout: time.Second,
+ TCP: TCPProbe{
+ Protocol: "tcp",
+ },
+ }
+
+ recorder = httptest.NewRecorder()
+ result = probeTCP(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("TCP protocol: \"tcp\" connection test failed, expected success.")
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+
+ // No protocol
+ module = Module{
+ Timeout: time.Second,
+ TCP: TCPProbe{},
+ }
+
+ recorder = httptest.NewRecorder()
+ result = probeTCP(net.JoinHostPort("localhost", port), recorder, module)
+ body = recorder.Body.String()
+ if !result {
+ t.Fatalf("TCP connection test with protocol unspecified failed, expected success.")
+ }
+ if !strings.Contains(body, "probe_ip_protocol 6\n") {
+ t.Fatalf("Expected IPv6, got %s", body)
+ }
+}