Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(inputs.http_response): Fix for IPv4 and IPv6 addresses when interface is set #15496

Merged
merged 4 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,8 @@ following works:
- github.com/russross/blackfriday [BSD 2-Clause "Simplified" License](https://github.com/russross/blackfriday/blob/master/LICENSE.txt)
- github.com/safchain/ethtool [Apache License 2.0](https://github.com/safchain/ethtool/blob/master/LICENSE)
- github.com/samber/lo [MIT License](https://github.com/samber/lo/blob/master/LICENSE)
- github.com/seancfoley/bintree [Apache License 2.0](https://github.com/seancfoley/bintree/blob/master/LICENSE)
- github.com/seancfoley/ipaddress-go [Apache License 2.0](https://github.com/seancfoley/ipaddress-go/blob/master/LICENSE)
- github.com/shirou/gopsutil [BSD 3-Clause Clear License](https://github.com/shirou/gopsutil/blob/master/LICENSE)
- github.com/shoenig/go-m1cpu [Mozilla Public License 2.0](https://github.com/shoenig/go-m1cpu/blob/main/LICENSE)
- github.com/shopspring/decimal [MIT License](https://github.com/shopspring/decimal/blob/master/LICENSE)
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ require (
github.com/robinson/gos7 v0.0.0-20240315073918-1f14519e4846
github.com/safchain/ethtool v0.3.0
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
github.com/seancfoley/ipaddress-go v1.6.0
github.com/sensu/sensu-go/api/core/v2 v2.16.0
github.com/shirou/gopsutil/v3 v3.24.4
github.com/showwin/speedtest-go v1.7.7
Expand Down Expand Up @@ -436,6 +437,7 @@ require (
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/seancfoley/bintree v1.3.1 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/signalfx/com_signalfx_metrics_protobuf v0.0.3 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,10 @@ github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21 h1:yWfiTPwYxB0l5fGMhl/G+liULu
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.21/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/seancfoley/bintree v1.3.1 h1:cqmmQK7Jm4aw8gna0bP+huu5leVOgHGSJBEpUx3EXGI=
github.com/seancfoley/bintree v1.3.1/go.mod h1:hIUabL8OFYyFVTQ6azeajbopogQc2l5C/hiXMcemWNU=
github.com/seancfoley/ipaddress-go v1.6.0 h1:9z7yGmOnV4P2ML/dlR/kCJiv5tp8iHOOetJvxJh/R5w=
github.com/seancfoley/ipaddress-go v1.6.0/go.mod h1:TQRZgv+9jdvzHmKoPGBMxyiaVmoI0rYpfEk8Q/sL/Iw=
github.com/sensu/sensu-go/api/core/v2 v2.16.0 h1:HOq4rFkQ1S5ZjxmMTLc5J5mAbECrnKWvtXXbMqr3j9s=
github.com/sensu/sensu-go/api/core/v2 v2.16.0/go.mod h1:MjM7+MCGEyTAgaZ589SiGHwYiaYF7N/58dU0J070u/0=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
Expand Down
129 changes: 84 additions & 45 deletions plugins/inputs/http_response/http_response.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"unicode/utf8"

"github.com/benbjohnson/clock"
"github.com/seancfoley/ipaddress-go/ipaddr"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/internal"
Expand All @@ -36,40 +38,45 @@ const (

// HTTPResponse struct
type HTTPResponse struct {
Address string `toml:"address" deprecated:"1.12.0;1.35.0;use 'urls' instead"`
URLs []string `toml:"urls"`
HTTPProxy string `toml:"http_proxy"`
Body string
Address string `toml:"address" deprecated:"1.12.0;1.35.0;use 'urls' instead"`
URLs []string `toml:"urls"`
HTTPProxy string `toml:"http_proxy"`
Body string `toml:"body"`
BodyForm map[string][]string `toml:"body_form"`
Method string
ResponseTimeout config.Duration
HTTPHeaderTags map[string]string `toml:"http_header_tags"`
Headers map[string]string
FollowRedirects bool
Method string `toml:"method"`
ResponseTimeout config.Duration `toml:"response_timeout"`
HTTPHeaderTags map[string]string `toml:"http_header_tags"`
Headers map[string]string `toml:"headers"`
FollowRedirects bool `toml:"follow_redirects"`
// Absolute path to file with Bearer token
BearerToken string `toml:"bearer_token"`
ResponseBodyField string `toml:"response_body_field"`
ResponseBodyMaxSize config.Size `toml:"response_body_max_size"`
ResponseStringMatch string
ResponseStatusCode int
Interface string
ResponseStringMatch string `toml:"response_string_match"`
ResponseStatusCode int `toml:"response_status_code"`
Interface string `toml:"interface"`
// HTTP Basic Auth Credentials
Username config.Secret `toml:"username"`
Password config.Secret `toml:"password"`
tls.ClientConfig
cookie.CookieAuthConfig

Log telegraf.Logger
Log telegraf.Logger `toml:"-"`

compiledStringMatch *regexp.Regexp
client httpClient
clients []client
}

type client struct {
httpClient httpClient
address string
}

type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}

// Set the proxy. A configured proxy overwrites the system wide proxy.
// Set the proxy. A configured proxy overwrites the system-wide proxy.
func getProxyFunc(httpProxy string) func(*http.Request) (*url.URL, error) {
if httpProxy == "" {
return http.ProxyFromEnvironment
Expand All @@ -85,9 +92,9 @@ func getProxyFunc(httpProxy string) func(*http.Request) (*url.URL, error) {
}
}

// createHTTPClient creates an http client which will timeout at the specified
// createHTTPClient creates an http client which will time out at the specified
// timeout period and can follow redirects if specified
func (h *HTTPResponse) createHTTPClient() (*http.Client, error) {
func (h *HTTPResponse) createHTTPClient(address url.URL) (*http.Client, error) {
tlsCfg, err := h.ClientConfig.TLSConfig()
if err != nil {
return nil, err
Expand All @@ -96,7 +103,7 @@ func (h *HTTPResponse) createHTTPClient() (*http.Client, error) {
dialer := &net.Dialer{}

if h.Interface != "" {
dialer.LocalAddr, err = localAddress(h.Interface)
dialer.LocalAddr, err = localAddress(h.Interface, address)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -127,7 +134,7 @@ func (h *HTTPResponse) createHTTPClient() (*http.Client, error) {
return client, nil
}

func localAddress(interfaceName string) (net.Addr, error) {
func localAddress(interfaceName string, address url.URL) (net.Addr, error) {
i, err := net.InterfaceByName(interfaceName)
if err != nil {
return nil, err
Expand All @@ -138,14 +145,43 @@ func localAddress(interfaceName string) (net.Addr, error) {
return nil, err
}

urlInIPv6, zone := isURLInIPv6(address)
for _, addr := range addrs {
if naddr, ok := addr.(*net.IPNet); ok {
// leaving port set to zero to let kernel pick
return &net.TCPAddr{IP: naddr.IP}, nil
ipNetInIPv6 := isIPNetInIPv6(naddr)

// choose interface address in the same format as server address
if ipNetInIPv6 == urlInIPv6 {
// leaving port set to zero to let kernel pick, but set zone
return &net.TCPAddr{IP: naddr.IP, Zone: zone}, nil
}
}
}

return nil, fmt.Errorf("cannot create local address for interface %q and server address %q", interfaceName, address.String())
Copy link
Contributor

@Mic92 Mic92 Jul 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to break the case when the address is a hostname (in my case pointing to an ipv6 address) instead of an ip address:

Error running agent: could not initialize input inputs.http_response: cannot create local address for interface "tinc.retiolum" and server address "http://transmission.r/transmission/web/"

}

// isURLInIPv6 returns (true, zoneName) only when URL is in IPv6 format.
// For other cases (host part of url cannot be successfully validated, doesn't contain address at all or is in IPv4 format), it returns (false, "").
func isURLInIPv6(address url.URL) (bool, string) {
host := ipaddr.NewHostName(address.Host)
if err := host.Validate(); err != nil {
return false, ""
}
if hostAddr := host.AsAddress(); hostAddr != nil {
if ipv6 := hostAddr.ToIPv6(); ipv6 != nil {
return true, ipv6.GetZone().String()
}
}

return nil, fmt.Errorf("cannot create local address for interface %q", interfaceName)
return false, ""
}

// isIPNetInIPv6 returns true only when IPNet can be represented in IPv6 format.
// For other cases (address cannot be successfully parsed or is in IPv4 format), it returns false.
func isIPNetInIPv6(address *net.IPNet) bool {
ipAddr, err := ipaddr.NewIPAddressFromNetIPNet(address)
return err == nil && ipAddr.ToIPv6() != nil
}

func setResult(resultString string, fields map[string]interface{}, tags map[string]string) {
Expand Down Expand Up @@ -196,10 +232,10 @@ func setError(err error, fields map[string]interface{}, tags map[string]string)
}

// HTTPGather gathers all fields and returns any errors it encounters
func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]string, error) {
func (h *HTTPResponse) httpGather(cl client) (map[string]interface{}, map[string]string, error) {
// Prepare fields and tags
fields := make(map[string]interface{})
tags := map[string]string{"server": u, "method": h.Method}
tags := map[string]string{"server": cl.address, "method": h.Method}

var body io.Reader
if h.Body != "" {
Expand All @@ -214,7 +250,7 @@ func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]
body = strings.NewReader(values.Encode())
}

request, err := http.NewRequest(h.Method, u, body)
request, err := http.NewRequest(h.Method, cl.address, body)
if err != nil {
return nil, nil, err
}
Expand Down Expand Up @@ -245,14 +281,14 @@ func (h *HTTPResponse) httpGather(u string) (map[string]interface{}, map[string]

// Start Timer
start := time.Now()
resp, err := h.client.Do(request)
resp, err := cl.httpClient.Do(request)
responseTime := time.Since(start).Seconds()

// If an error in returned, it means we are dealing with a network error, as
// HTTP error codes do not generate errors in the net/http library
if err != nil {
// Log error
h.Log.Debugf("Network error while polling %s: %s", u, err.Error())
h.Log.Debugf("Network error while polling %s: %s", cl.address, err.Error())

// Get error details
if setError(err, fields, tags) == nil {
Expand Down Expand Up @@ -352,10 +388,9 @@ func (*HTTPResponse) SampleConfig() string {
return sampleConfig
}

// Gather gets all metric fields and tags and returns any errors it encounters
func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
// Compile the body regex if it exist
if h.compiledStringMatch == nil {
func (h *HTTPResponse) Init() error {
// Compile the body regex if it exists
if h.ResponseStringMatch != "" {
var err error
h.compiledStringMatch, err = regexp.Compile(h.ResponseStringMatch)
if err != nil {
Expand All @@ -367,7 +402,6 @@ func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
if h.ResponseTimeout < config.Duration(time.Second) {
h.ResponseTimeout = config.Duration(time.Second * 5)
}
// Check send and expected string
if h.Method == "" {
h.Method = "GET"
}
Expand All @@ -380,32 +414,37 @@ func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
}
}

if h.client == nil {
client, err := h.createHTTPClient()
if err != nil {
return err
}
h.client = client
}

h.clients = make([]client, 0, len(h.URLs))
for _, u := range h.URLs {
addr, err := url.Parse(u)
if err != nil {
acc.AddError(err)
continue
return fmt.Errorf("%q is not a valid address: %w", u, err)
}

if addr.Scheme != "http" && addr.Scheme != "https" {
acc.AddError(errors.New("only http and https are supported"))
continue
return fmt.Errorf("%q is not a valid address: only http and https types are supported", u)
}

cl, err := h.createHTTPClient(*addr)
if err != nil {
return err
}

h.clients = append(h.clients, client{httpClient: cl, address: u})
}

return nil
}

// Gather gets all metric fields and tags and returns any errors it encounters
func (h *HTTPResponse) Gather(acc telegraf.Accumulator) error {
for _, c := range h.clients {
// Prepare data
var fields map[string]interface{}
var tags map[string]string

// Gather data
fields, tags, err = h.httpGather(u)
fields, tags, err := h.httpGather(c)
if err != nil {
acc.AddError(err)
continue
Expand Down
Loading
Loading