Skip to content

Commit 9e620eb

Browse files
authored
feat: harden egress IP address validation (#5604)
* feat: Have a single dialer dependency Consolodate down to a single implementation of Dialer in the dependencies. * feat: use a validating dialer to connect to prometheus In the standardlib prometheus scrape function use the HTTP client from the injected dependencies to provide IP validation at connection time. * feat: expand PrivateIPValidator's block list Extend the PrivateIPValidator to include additional IP ranges that are defined as not being "Globally Reachable" by IANA.
1 parent 0228b18 commit 9e620eb

File tree

9 files changed

+131
-71
lines changed

9 files changed

+131
-71
lines changed

dependencies.go

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@ package flux
33
import (
44
"context"
55
"net"
6-
"syscall"
7-
"time"
86

97
"github.com/influxdata/flux/codes"
8+
"github.com/influxdata/flux/dependencies/dialer"
109
"github.com/influxdata/flux/dependencies/filesystem"
1110
"github.com/influxdata/flux/dependencies/http"
1211
"github.com/influxdata/flux/dependencies/secret"
@@ -126,22 +125,5 @@ func GetDialer(ctx context.Context) (*net.Dialer, error) {
126125
if err != nil {
127126
return nil, err
128127
}
129-
130-
// Control is called after DNS lookup, but before the
131-
// network connection is initiated.
132-
control := func(network, address string, c syscall.RawConn) error {
133-
host, _, err := net.SplitHostPort(address)
134-
if err != nil {
135-
return err
136-
}
137-
138-
ip := net.ParseIP(host)
139-
return url.ValidateIP(ip)
140-
}
141-
142-
return &net.Dialer{
143-
Timeout: 30 * time.Second,
144-
KeepAlive: 30 * time.Second,
145-
Control: control,
146-
}, nil
128+
return dialer.New(url), nil
147129
}

dependencies/bigtable/bigtable.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type Dependency struct {
2727
Provider Provider
2828
}
2929

30-
// Inject will inject the Dialer into the dependency chain.
30+
// Inject will inject the Provider into the dependency chain.
3131
func (d Dependency) Inject(ctx context.Context) context.Context {
3232
return Inject(ctx, d.Provider)
3333
}

dependencies/dialer/dialer.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package dialer
2+
3+
import (
4+
"context"
5+
"net"
6+
"strings"
7+
"syscall"
8+
"time"
9+
10+
"github.com/influxdata/flux/codes"
11+
"github.com/influxdata/flux/dependencies/url"
12+
"github.com/influxdata/flux/internal/errors"
13+
)
14+
15+
// Create a new *net.Dialer that uses the provided url.Validator to
16+
// validate the destination OP address before connecting.
17+
func New(urlValidator url.Validator) *net.Dialer {
18+
// ControlContext is called after DNS lookup, but before the network
19+
// connection is initiated.
20+
controlContext := func(ctx context.Context, network, address string, c syscall.RawConn) error {
21+
host, _, err := net.SplitHostPort(address)
22+
if err != nil {
23+
return err
24+
}
25+
// Remove any zone from the host.
26+
host, _, _ = strings.Cut(host, "%")
27+
ip := net.ParseIP(host)
28+
if ip == nil {
29+
return errors.New(codes.Invalid, "no such host")
30+
}
31+
return urlValidator.ValidateIP(ip)
32+
}
33+
34+
return &net.Dialer{
35+
Timeout: 30 * time.Second,
36+
KeepAlive: 30 * time.Second,
37+
ControlContext: controlContext,
38+
}
39+
}

dependencies/http/http.go

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@ package http
33
import (
44
"crypto/tls"
55
"io"
6-
"net"
76
"net/http"
8-
"strings"
9-
"syscall"
107
"time"
118

129
"github.com/influxdata/flux/codes"
10+
"github.com/influxdata/flux/dependencies/dialer"
1311
"github.com/influxdata/flux/dependencies/url"
1412
"github.com/influxdata/flux/internal/errors"
1513
)
@@ -64,28 +62,7 @@ func (l roundTripLimiter) RoundTrip(r *http.Request) (*http.Response, error) {
6462

6563
// NewDefaultClient creates a client with sane defaults.
6664
func NewDefaultClient(urlValidator url.Validator) *http.Client {
67-
// Control is called after DNS lookup, but before the network connection is
68-
// initiated.
69-
control := func(network, address string, c syscall.RawConn) error {
70-
host, _, err := net.SplitHostPort(address)
71-
if err != nil {
72-
return err
73-
}
74-
// Remove any zone from the host.
75-
host, _, _ = strings.Cut(host, "%")
76-
ip := net.ParseIP(host)
77-
if ip == nil {
78-
return errors.New(codes.Invalid, "no such host")
79-
}
80-
return urlValidator.ValidateIP(ip)
81-
}
82-
83-
dialer := &net.Dialer{
84-
Timeout: 30 * time.Second,
85-
KeepAlive: 30 * time.Second,
86-
Control: control,
87-
// DualStack is deprecated
88-
}
65+
dialer := dialer.New(urlValidator)
8966

9067
// These defaults are copied from http.DefaultTransport.
9168
return &http.Client{

dependencies/url/validator.go

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,51 @@ var privateIPBlocks []*net.IPNet
6262

6363
func init() {
6464
for _, cidr := range []string{
65-
"0.0.0.0/32", // Linux treats 0.0.0.0 as 127.0.0.1
66-
"127.0.0.0/8", // IPv4 loopback
67-
"10.0.0.0/8", // RFC1918
68-
"172.16.0.0/12", // RFC1918
69-
"192.168.0.0/16", // RFC1918
70-
"169.254.0.0/16", // RFC3927
71-
"::1/128", // IPv6 loopback
72-
"fe80::/10", // IPv6 link-local
73-
"fc00::/7", // IPv6 unique local addr
65+
// IPv4 Special-Purpose Address Space
66+
// Address ranges taken from https://www.iana.org/assignments/iana-ipv4-special-registry/
67+
// that have the "Globally Reachable" flag marked as "False".
68+
"0.0.0.0/8", // "This network" [RFC791], Section 3.2
69+
"10.0.0.0/8", // Private-Use [RFC1918]
70+
"100.64.0.0/10", // Shared Address Space [RFC6598]
71+
"127.0.0.0/8", // Loopback [RFC1122], Section 3.2.1.3
72+
"169.254.0.0/16", // Link Local [RFC3927]
73+
"172.16.0.0/12", // Private-Use [RFC1918]
74+
// The 192.0.0.0/24 block does include the addresses 192.0.0.9 &
75+
// 192.0.0.10, these are marked as "Globally Reachable" but are
76+
// for such specific protocols that blocking them will not
77+
// affect flux scripts.
78+
"192.0.0.0/24", // IETF Protocol Assignments [RFC6890], Section 2.1
79+
"192.0.2.0/24", // Documentation (TEST-NET-1) [RFC5737]
80+
"192.88.99.2/32", // 6a44-relay anycast address [RFC6751]
81+
"192.168.0.0/16", // Private-Use [RFC1918]
82+
"198.18.0.0/15", // Benchmarking [RFC2544]
83+
"198.51.100.0/24", // Documentation (TEST-NET-2) [RFC5737]
84+
"203.0.113.0/24", // Documentation (TEST-NET-3) [RFC5737]
85+
"240.0.0.0/4", // Reserved [RFC1112], Section 4
86+
"255.255.255.255/32", // Limited Broadcast [RFC8190] [RFC919], Section 7
87+
88+
// IPv6 Special-Purpose Address Space
89+
// Address ranges taken from https://www.iana.org/assignments/iana-ipv6-special-registry/
90+
// that have the "Globally Reachable" flag marked as "False".
91+
"::1/128", // Loopback Address [RFC4291]
92+
"::/128", // Unspecified Address [RFC4291]
93+
// The IPv4-mapped Address block is marked as not being globally
94+
// reachable, but is also how Go stores IPv4 Addresses, so
95+
// adding the range causes all IPv4 addresses to be blocked.
96+
// "::ffff:0:0/96", IPv4-mapped Address [RFC4291]
97+
"64:ff9b:1::/48", // IPv4-IPv6 Translat. [RFC8215]
98+
"100::/64", // Discard-Only Address Block [RFC6666]
99+
"100:0:0:1::/64", // Dummy IPv6 Prefix [RFC9780]
100+
// The 2001::/23 block includes a number of ranges which are
101+
// marked as "Globally Reachable" but are for such specific
102+
// protocols that blocking them will not affect flux scripts.
103+
"2001::/23", // IETF Protocol Assignments [RFC2928]
104+
"2001:db8::/32", // Documentation [RFC3849]
105+
"2002::/16", // 6to4 [RFC3056]
106+
"3fff::/20", // Documentation [RFC9637]
107+
"5f00::/16", // Segment Routing (SRv6) SIDs [RFC9602]
108+
"fc00::/7", // Unique-Local [RFC4193] [RFC8190]
109+
"fe80::/10", // Link-Local Unicast [RFC4291]
74110
} {
75111
_, block, err := net.ParseCIDR(cidr)
76112
if err != nil {

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ require (
3636
github.com/mattn/go-sqlite3 v1.14.18
3737
github.com/microsoft/go-mssqldb v1.9.4
3838
github.com/opentracing/opentracing-go v1.2.0
39-
github.com/prometheus/client_model v0.6.1
39+
github.com/prometheus/client_model v0.6.2
4040
github.com/prometheus/common v0.53.0
4141
github.com/segmentio/kafka-go v0.4.50
4242
github.com/spf13/cobra v0.0.3
@@ -50,7 +50,7 @@ require (
5050
gonum.org/v1/gonum v0.15.1
5151
google.golang.org/api v0.214.0
5252
google.golang.org/grpc v1.71.0
53-
google.golang.org/protobuf v1.36.5
53+
google.golang.org/protobuf v1.36.6
5454
gopkg.in/yaml.v2 v2.4.0
5555
gopkg.in/yaml.v3 v3.0.1 // indirect
5656
)

go.sum

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
379379
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
380380
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
381381
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
382-
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
383-
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
382+
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
383+
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
384384
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
385385
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
386386
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -685,8 +685,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
685685
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
686686
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
687687
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
688-
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
689-
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
688+
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
689+
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
690690
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
691691
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
692692
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

stdlib/experimental/prometheus/prometheus_test.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import (
55
"fmt"
66
"net/http"
77
"net/http/httptest"
8+
"strings"
89
"testing"
910
"time"
1011

1112
flux "github.com/influxdata/flux"
1213
"github.com/influxdata/flux/dependencies/dependenciestest"
14+
fhttp "github.com/influxdata/flux/dependencies/http"
15+
"github.com/influxdata/flux/dependencies/url"
1316
"github.com/influxdata/flux/dependency"
1417
"github.com/influxdata/flux/execute"
1518
"github.com/influxdata/flux/execute/executetest"
@@ -230,7 +233,9 @@ func testSourceDecoder(p *PrometheusIterator, t *testing.T) *executetest.Result
230233
results := &executetest.Result{}
231234
runOnce := true
232235

233-
ctx, deps := dependency.Inject(context.Background(), dependenciestest.Default())
236+
dependencies := dependenciestest.Default()
237+
dependencies.Deps.Deps.HTTPClient = fhttp.NewDefaultClient(url.PassValidator{})
238+
ctx, deps := dependency.Inject(context.Background(), dependencies)
234239
defer deps.Finish()
235240

236241
err := p.Connect(ctx)
@@ -271,3 +276,28 @@ func testSourceDecoder(p *PrometheusIterator, t *testing.T) *executetest.Result
271276
}
272277
return results
273278
}
279+
280+
func TestValidation(t *testing.T) {
281+
dependencies := dependenciestest.Default()
282+
dependencies.Deps.Deps.HTTPClient = fhttp.NewDefaultClient(url.PrivateIPValidator{})
283+
ctx, deps := dependency.Inject(context.Background(), dependencies)
284+
defer deps.Finish()
285+
286+
spec := &ScrapePrometheusProcedureSpec{URL: "http://[::1]"}
287+
admin := &mock.Administration{}
288+
c := execute.NewTableBuilderCache(admin.Allocator())
289+
timestamp := time.Now()
290+
p := &PrometheusIterator{
291+
NowFn: func() time.Time { return timestamp },
292+
spec: spec,
293+
administration: admin,
294+
cache: c,
295+
}
296+
err := p.Connect(ctx)
297+
if err == nil {
298+
t.Fatal("expected connection error")
299+
}
300+
if !strings.HasSuffix(err.Error(), "no such host") {
301+
t.Fatalf("unexpected error: %s", err)
302+
}
303+
}

stdlib/experimental/prometheus/scrape.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"math"
1111
"mime"
1212
"net/http"
13-
"net/url"
1413
"time"
1514

1615
"github.com/influxdata/flux/runtime"
@@ -135,23 +134,20 @@ func (p *PrometheusIterator) Connect(ctx context.Context) error {
135134
p.now = time.Now()
136135
}
137136

138-
u, err := url.Parse(p.url)
137+
// Get URL validating HTTP client.
138+
client, err := flux.GetDependencies(ctx).HTTPClient()
139139
if err != nil {
140140
return err
141141
}
142142

143-
// Validate url
144-
deps := flux.GetDependencies(ctx)
145-
validator, err := deps.URLValidator()
143+
// Create request
144+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, p.url, nil)
146145
if err != nil {
147146
return err
148147
}
149-
if err := validator.Validate(u); err != nil {
150-
return err
151-
}
152148

153149
// Get response
154-
resp, err := http.Get(p.url)
150+
resp, err := client.Do(req)
155151
if err != nil {
156152
return err
157153
}

0 commit comments

Comments
 (0)