[probers] Allow configuration of source addresses.
authorTobias Hintze <th-git@thzn.de>
Wed, 15 Nov 2017 23:50:06 +0000 (00:50 +0100)
committerTobias Hintze <th-git@thzn.de>
Fri, 26 Jan 2018 10:27:37 +0000 (11:27 +0100)
This adds the configuration option source_ip_address
to modules ICMP, TCP and DNS which changes the
probe's local address.

CONFIGURATION.md
config/config.go
example.yml
prober/dns.go
prober/icmp.go
prober/tcp.go

index b761603305b6eef84a7919b24de717896360e070..ed16383606ae5b63277b82dbe2b29fadabdd001b 100644 (file)
@@ -100,6 +100,9 @@ The other placeholders are specified separately.
 # The preferred IP protocol of the TCP probe (ip4, ip6).
 [ preferred_ip_protocol: <string> | default = "ip6" ]
 
+# The source IP address.
+[ source_ip_address: <string> ]
+
 # 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: <string> | default = "ip6" ]
 
+# The source IP address.
+[ source_ip_address: <string> ]
+
 [ transport_protocol: <string> | default = "udp" ] # udp, tcp
 
 query_name: <string>
@@ -168,6 +174,9 @@ validate_additional_rrs:
 # The preferred IP protocol of the ICMP probe (ip4, ip6).
 [ preferred_ip_protocol: <string> | default = "ip6" ]
 
+# The source IP address.
+[ source_ip_address: <string> ]
+
 # Set the DF-bit in the IP-header. Only works with ip4 and on *nix systems.
 [ dont_fragment: <boolean> | default = false ]
 
index 21a1d65644aaa3c41f85491d4a9c971807e259e8..0ecbadc2941322395439e64d311ecc680f29e125 100644 (file)
@@ -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.
index 7d09cf7161d2719769c5040d372782127adf5308..96fead0026bb6aa732f395ae3e729b7b49097396 100644 (file)
@@ -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
index 6f778cdc2933667018e8d55cd6c912045494cb40..94e276d32f1202b64939e1719c5fb84326fa6f34 100644 (file)
@@ -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
index c222f725c51799d2fb467f647861d17c0a9fe67d..687ab6cbf2672de6c008e8d7bc12bea4b6d51bd5 100644 (file)
@@ -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)
 }
index 7368d41717d533c328baff1322fc1437e64e14f9..48b5cb08132bc5d2e0ac118fa668b1ea213a6963 100644 (file)
@@ -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 {