Skip to content

Commit 2f40eda

Browse files
authored
fix(al2023): dynamic proxy function to bypass default caching by http.ProxyFromEnvironment (#2471)
* fix: dynamic proxy function to bypass default caching by http.ProxyFromEnvironment
1 parent 2797c70 commit 2f40eda

File tree

2 files changed

+124
-0
lines changed

2 files changed

+124
-0
lines changed

nodeadm/internal/aws/imds/imds.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,35 @@ import (
44
"context"
55
"fmt"
66
"io"
7+
"net/http"
8+
"net/url"
79
"path"
810
"time"
911

1012
"github.com/aws/aws-sdk-go-v2/aws/ratelimit"
1113
"github.com/aws/aws-sdk-go-v2/aws/retry"
14+
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
1215
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
1316
)
1417

1518
var _defaultClient *imds.Client
1619

20+
// This function is a wrapper around the default `http.ProxyFromEnvironment` function
21+
// which cachces the proxy variables in environment when first invoked thus, preventing subsequent
22+
// configuration attempts of http proxy via derived from user-data. Since, the first outbound
23+
// API call in nodeadm is to the IMDS for fetching user-data, we bypass caching.
24+
// Ref: https://github.com/golang/go/blob/master/src/net/http/transport.go#L499
25+
func dynamicProxyFunc(req *http.Request) (*url.URL, error) {
26+
// Link-local addresses for IMDS do not need to be going through a proxy
27+
// The IMDS has two endpoints on an instance: IPv4 (169.254.169.254) and IPv6 ([fd00:ec2::254])
28+
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-options.html
29+
if req.URL.Host == "169.254.169.254" || req.URL.Host == "[fd00:ec2::254]" {
30+
return nil, nil
31+
}
32+
33+
return http.ProxyFromEnvironment(req)
34+
}
35+
1736
func init() {
1837
_defaultClient = New(false /* do not retry 404s with default client */)
1938
}
@@ -40,7 +59,13 @@ type IMDSClient interface {
4059
}
4160

4261
func New(retry404s bool, fnOpts ...func(*imds.Options)) *imds.Client {
62+
// Create HTTP client with dynamic proxy function
63+
httpClient := awshttp.NewBuildableClient().WithTransportOptions(func(tr *http.Transport) {
64+
tr.Proxy = dynamicProxyFunc
65+
})
66+
4367
return imds.New(imds.Options{
68+
HTTPClient: httpClient,
4469
DisableDefaultTimeout: true,
4570
Retryer: retry.NewStandard(func(so *retry.StandardOptions) {
4671
so.MaxAttempts = 60
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package imds
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"os"
7+
"os/exec"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestDynamicProxyFuncBehavior(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
envVars map[string]string
17+
testURL string
18+
expectedProxy string
19+
}{
20+
{
21+
name: "external_url_with_proxy",
22+
envVars: map[string]string{
23+
"HTTPS_PROXY": "http://example-proxy:8080",
24+
},
25+
testURL: "https://ec2.amazonaws.com",
26+
expectedProxy: "http://example-proxy:8080",
27+
},
28+
{
29+
name: "imds_ipv4_no_proxy",
30+
envVars: map[string]string{
31+
"HTTPS_PROXY": "http://example-proxy:8080",
32+
},
33+
testURL: "http://169.254.169.254/latest/user-data",
34+
expectedProxy: "",
35+
},
36+
{
37+
name: "imds_ipv6_no_proxy",
38+
envVars: map[string]string{
39+
"HTTPS_PROXY": "http://example-proxy:8080",
40+
},
41+
testURL: "http://[fd00:ec2::254]/latest/user-data",
42+
expectedProxy: "",
43+
},
44+
{
45+
name: "no_env_vars",
46+
envVars: map[string]string{},
47+
testURL: "https://ec2.amazonaws.com",
48+
expectedProxy: "",
49+
},
50+
}
51+
52+
for _, tt := range tests {
53+
t.Run(tt.name, func(t *testing.T) {
54+
// Testing the dynamicProxyFunc can invoke http.ProxyFromEnvironment() function
55+
// which freezes the proxy environment variables for the entire process due to sync.Once caching.
56+
// Hence, each test-case is run in a separate process to avoid caching.
57+
cmd := exec.Command("go", "test", "-run", "TestSingleProxyCase", "./")
58+
59+
cmd.Env = os.Environ()
60+
cmd.Env = append(cmd.Env, fmt.Sprintf("TEST_NAME=%s", tt.name))
61+
cmd.Env = append(cmd.Env, fmt.Sprintf("TEST_URL=%s", tt.testURL))
62+
cmd.Env = append(cmd.Env, fmt.Sprintf("EXPECTED_PROXY=%s", tt.expectedProxy))
63+
64+
// add the env variables for the actual proxy test
65+
for key, value := range tt.envVars {
66+
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", key, value))
67+
}
68+
output, err := cmd.CombinedOutput()
69+
70+
if err != nil {
71+
t.Errorf("Test %s failed: %v\nOutput: %s", tt.name, err, string(output))
72+
}
73+
})
74+
}
75+
}
76+
77+
func TestSingleProxyCase(t *testing.T) {
78+
testName := os.Getenv("TEST_NAME")
79+
if testName == "" {
80+
t.Skip("Not a subprocess test")
81+
}
82+
83+
testURL := os.Getenv("TEST_URL")
84+
expectedProxy := os.Getenv("EXPECTED_PROXY")
85+
86+
req, _ := http.NewRequest("GET", testURL, nil)
87+
proxyURL, err := dynamicProxyFunc(req)
88+
89+
if err != nil {
90+
t.Fatalf("dynamicProxyFunc returned error: %v", err)
91+
}
92+
93+
var actualProxy string
94+
if proxyURL != nil {
95+
actualProxy = proxyURL.String()
96+
}
97+
98+
assert.Equal(t, expectedProxy, actualProxy)
99+
}

0 commit comments

Comments
 (0)