From d1b06fc36a75f6d700ed5d863a45943fc3021c67 Mon Sep 17 00:00:00 2001 From: Tobias Hintze Date: Thu, 16 Nov 2017 00:50:06 +0100 Subject: [PATCH] [probers] Allow configuration of source addresses. This adds the configuration option source_ip_address to modules ICMP, TCP and DNS which changes the probe's local address. --- CONFIGURATION.md | 9 +++++ config/config.go | 3 ++ example.yml | 1 + prober/dns.go | 17 ++++++++++ prober/icmp.go | 87 ++++++++++++++++++++++++++++++------------------ prober/tcp.go | 11 ++++++ 6 files changed, 95 insertions(+), 33 deletions(-) diff --git a/CONFIGURATION.md b/CONFIGURATION.md index b761603..ed16383 100644 --- a/CONFIGURATION.md +++ b/CONFIGURATION.md @@ -100,6 +100,9 @@ The other placeholders are specified separately. # The preferred IP protocol of the TCP probe (ip4, ip6). [ preferred_ip_protocol: | default = "ip6" ] +# The source IP address. +[ source_ip_address: ] + # The query sent in the TCP probe and the expected associated response. # starttls upgrades TCP connection to TLS. query_response: @@ -125,6 +128,9 @@ tls_config: # The preferred IP protocol of the DNS probe (ip4, ip6). [ preferred_ip_protocol: | default = "ip6" ] +# The source IP address. +[ source_ip_address: ] + [ transport_protocol: | default = "udp" ] # udp, tcp query_name: @@ -168,6 +174,9 @@ validate_additional_rrs: # The preferred IP protocol of the ICMP probe (ip4, ip6). [ preferred_ip_protocol: | default = "ip6" ] +# The source IP address. +[ source_ip_address: ] + # Set the DF-bit in the IP-header. Only works with ip4 and on *nix systems. [ dont_fragment: | default = false ] diff --git a/config/config.go b/config/config.go index 21a1d65..0ecbadc 100644 --- a/config/config.go +++ b/config/config.go @@ -87,6 +87,7 @@ type QueryResponse struct { type TCPProbe struct { PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"` + SourceIPAddress string `yaml:"source_ip_address,omitempty"` QueryResponse []QueryResponse `yaml:"query_response,omitempty"` TLS bool `yaml:"tls,omitempty"` TLSConfig config.TLSConfig `yaml:"tls_config,omitempty"` @@ -97,6 +98,7 @@ type TCPProbe struct { type ICMPProbe struct { PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"` // Defaults to "ip6". + SourceIPAddress string `yaml:"source_ip_address,omitempty"` PayloadSize int `yaml:"payload_size,omitempty"` DontFragment bool `yaml:"dont_fragment,omitempty"` // Catches all undefined fields and must be empty after parsing. @@ -105,6 +107,7 @@ type ICMPProbe struct { type DNSProbe struct { PreferredIPProtocol string `yaml:"preferred_ip_protocol,omitempty"` + SourceIPAddress string `yaml:"source_ip_address,omitempty"` TransportProtocol string `yaml:"transport_protocol,omitempty"` QueryName string `yaml:"query_name,omitempty"` QueryType string `yaml:"query_type,omitempty"` // Defaults to ANY. diff --git a/example.yml b/example.yml index 7d09cf7..96fead0 100644 --- a/example.yml +++ b/example.yml @@ -85,6 +85,7 @@ modules: timeout: 5s icmp: preferred_ip_protocol: "ip4" + source_ip_address: "127.0.0.1" dns_udp_example: prober: dns timeout: 5s diff --git a/prober/dns.go b/prober/dns.go index 6f778cd..94e276d 100644 --- a/prober/dns.go +++ b/prober/dns.go @@ -137,6 +137,23 @@ func ProbeDNS(ctx context.Context, target string, module config.Module, registry client := new(dns.Client) client.Net = dialProtocol + + // Use configured SourceIPAddress. + if len(module.DNS.SourceIPAddress) > 0 { + srcIP := net.ParseIP(module.DNS.SourceIPAddress) + if srcIP == nil { + level.Error(logger).Log("msg", "Error parsing source ip address", "srcIP", module.DNS.SourceIPAddress) + return false + } + level.Info(logger).Log("msg", "Using local address", "srcIP", srcIP) + client.Dialer = &net.Dialer{} + if module.DNS.TransportProtocol == "tcp" { + client.Dialer.LocalAddr = &net.TCPAddr{IP: srcIP} + } else { + client.Dialer.LocalAddr = &net.UDPAddr{IP: srcIP} + } + } + qt := dns.TypeANY if module.DNS.QueryType != "" { var ok bool diff --git a/prober/icmp.go b/prober/icmp.go index c222f72..687ab6c 100644 --- a/prober/icmp.go +++ b/prober/icmp.go @@ -46,6 +46,7 @@ func getICMPSequence() uint16 { func ProbeICMP(ctx context.Context, target string, module config.Module, registry *prometheus.Registry, logger log.Logger) (success bool) { var ( socket net.PacketConn + socket6 *ipv6.PacketConn requestType icmp.Type replyType icmp.Type ) @@ -58,40 +59,44 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr return false } + var srcIP net.IP + if len(module.ICMP.SourceIPAddress) > 0 { + if srcIP = net.ParseIP(module.ICMP.SourceIPAddress); srcIP == nil { + level.Error(logger).Log("msg", "Error parsing source ip address", "srcIP", module.ICMP.SourceIPAddress) + return false + } + level.Info(logger).Log("msg", "Using source address", "srcIP", srcIP) + } + level.Info(logger).Log("msg", "Creating socket") if ip.IP.To4() == nil { requestType = ipv6.ICMPTypeEchoRequest replyType = ipv6.ICMPTypeEchoReply - socket, err = icmp.ListenPacket("ip6:ipv6-icmp", "::") + icmpConn, err := icmp.ListenPacket("ip6:ipv6-icmp", "::") if err != nil { level.Error(logger).Log("msg", "Error listening to socket", "err", err) return } + + socket = icmpConn + socket6 = icmpConn.IPv6PacketConn() } else { requestType = ipv4.ICMPTypeEcho replyType = ipv4.ICMPTypeEchoReply - if !module.ICMP.DontFragment { - socket, err = icmp.ListenPacket("ip4:icmp", "0.0.0.0") - if err != nil { - level.Error(logger).Log("msg", "Error listening to socket", "err", err) - return - } - } else { - s, err := net.ListenPacket("ip4:icmp", "0.0.0.0") - if err != nil { - level.Error(logger).Log("msg", "Error listening to socket", "err", err) - return - } + s, err := net.ListenPacket("ip4:icmp", "0.0.0.0") + if err != nil { + level.Error(logger).Log("msg", "Error listening to socket", "err", err) + return + } - rc, err := ipv4.NewRawConn(s) - if err != nil { - level.Error(logger).Log("msg", "cannot construct raw connection", "err", err) - return - } - socket = &dfConn{c: rc} + rc, err := ipv4.NewRawConn(s) + if err != nil { + level.Error(logger).Log("msg", "Error creating raw connection", "err", err) + return } + socket = &v4Conn{c: rc, df: module.ICMP.DontFragment, src: srcIP} } defer socket.Close() @@ -122,9 +127,18 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr return } level.Info(logger).Log("msg", "Writing out packet") - if _, err = socket.WriteTo(wb, ip); err != nil { - level.Warn(logger).Log("msg", "Error writing to socket", "err", err) - return + if socket6 != nil && srcIP != nil { + // Also set source address for IPv6. + cm := &ipv6.ControlMessage{Src: srcIP} + if _, err = socket6.WriteTo(wb, cm, ip); err != nil { + level.Error(logger).Log("msg", "Error writing to IPv6 socket", "err", err) + return + } + } else { + if _, err = socket.WriteTo(wb, ip); err != nil { + level.Warn(logger).Log("msg", "Error writing to socket", "err", err) + return + } } // Reply should be the same except for the message type. @@ -166,11 +180,14 @@ func ProbeICMP(ctx context.Context, target string, module config.Module, registr } } -type dfConn struct { +type v4Conn struct { c *ipv4.RawConn + + df bool + src net.IP } -func (c *dfConn) ReadFrom(b []byte) (int, net.Addr, error) { +func (c *v4Conn) ReadFrom(b []byte) (int, net.Addr, error) { h, p, _, err := c.c.ReadFrom(b) if err != nil { return 0, nil, err @@ -184,41 +201,45 @@ func (c *dfConn) ReadFrom(b []byte) (int, net.Addr, error) { return n, &net.IPAddr{IP: h.Src}, nil } -func (d *dfConn) WriteTo(b []byte, addr net.Addr) (int, error) { +func (d *v4Conn) WriteTo(b []byte, addr net.Addr) (int, error) { ipAddr, err := net.ResolveIPAddr(addr.Network(), addr.String()) if err != nil { return 0, err } - dfHeader := &ipv4.Header{ + header := &ipv4.Header{ Version: ipv4.Version, Len: ipv4.HeaderLen, Protocol: 1, TotalLen: ipv4.HeaderLen + len(b), - Flags: ipv4.DontFragment, TTL: 64, Dst: ipAddr.IP, + Src: d.src, + } + + if d.df { + header.Flags |= ipv4.DontFragment } - return len(b), d.c.WriteTo(dfHeader, b, nil) + return len(b), d.c.WriteTo(header, b, nil) } -func (d *dfConn) Close() error { +func (d *v4Conn) Close() error { return d.c.Close() } -func (d *dfConn) LocalAddr() net.Addr { +func (d *v4Conn) LocalAddr() net.Addr { return nil } -func (d *dfConn) SetDeadline(t time.Time) error { +func (d *v4Conn) SetDeadline(t time.Time) error { return d.c.SetDeadline(t) } -func (d *dfConn) SetReadDeadline(t time.Time) error { +func (d *v4Conn) SetReadDeadline(t time.Time) error { return d.c.SetReadDeadline(t) } -func (d *dfConn) SetWriteDeadline(t time.Time) error { +func (d *v4Conn) SetWriteDeadline(t time.Time) error { return d.c.SetWriteDeadline(t) } diff --git a/prober/tcp.go b/prober/tcp.go index 7368d41..48b5cb0 100644 --- a/prober/tcp.go +++ b/prober/tcp.go @@ -49,6 +49,17 @@ func dialTCP(ctx context.Context, target string, module config.Module, registry } else { dialProtocol = "tcp4" } + + if len(module.TCP.SourceIPAddress) > 0 { + srcIP := net.ParseIP(module.TCP.SourceIPAddress) + if srcIP == nil { + level.Error(logger).Log("msg", "Error parsing source ip address", "srcIP", module.TCP.SourceIPAddress) + return nil, fmt.Errorf("Error parsing source ip address: %s", module.TCP.SourceIPAddress) + } + level.Info(logger).Log("msg", "Using local address", "srcIP", srcIP) + dialer.LocalAddr = &net.TCPAddr{IP: srcIP} + } + dialTarget = net.JoinHostPort(ip.String(), port) if !module.TCP.TLS { -- 2.25.1