Skip to content

Commit cad7b8c

Browse files
authored
Merge pull request #1201 from nginx/add-explicit-forward-proxy
Explicit forward proxy support for HTTP with Basic Auth
2 parents 08eee77 + 5bf3520 commit cad7b8c

File tree

9 files changed

+758
-31
lines changed

9 files changed

+758
-31
lines changed

internal/collector/otel_collector_plugin.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"log/slog"
1212
"net"
13+
"net/url"
1314
"os"
1415
"strings"
1516
"sync"
@@ -221,7 +222,7 @@ func (oc *Collector) processReceivers(ctx context.Context, receivers map[string]
221222
}
222223
}
223224

224-
//nolint:revive // cognitive complexity is 13
225+
//nolint:revive,cyclop // cognitive complexity is 13
225226
func (oc *Collector) bootup(ctx context.Context) error {
226227
errChan := make(chan error)
227228

@@ -231,6 +232,10 @@ func (oc *Collector) bootup(ctx context.Context) error {
231232
return
232233
}
233234

235+
if oc.config.IsCommandServerProxyConfigured() {
236+
oc.setProxyIfNeeded(ctx)
237+
}
238+
234239
appErr := oc.service.Run(ctx)
235240
if appErr != nil {
236241
errChan <- appErr
@@ -394,6 +399,10 @@ func (oc *Collector) restartCollector(ctx context.Context) {
394399
}
395400
oc.service = oTelCollector
396401

402+
if oc.config.IsCommandServerProxyConfigured() {
403+
oc.setProxyIfNeeded(ctx)
404+
}
405+
397406
var runCtx context.Context
398407
runCtx, oc.cancel = context.WithCancel(ctx)
399408

@@ -409,6 +418,14 @@ func (oc *Collector) restartCollector(ctx context.Context) {
409418
}
410419
}
411420

421+
func (oc *Collector) setProxyIfNeeded(ctx context.Context) {
422+
if oc.config.Collector.Exporters.OtlpExporters != nil ||
423+
oc.config.Collector.Exporters.PrometheusExporter != nil {
424+
// Set proxy env vars for OTLP exporter if proxy is configured.
425+
oc.setExporterProxyEnvVars(ctx)
426+
}
427+
}
428+
412429
func (oc *Collector) checkForNewReceivers(ctx context.Context, nginxConfigContext *model.NginxConfigContext) bool {
413430
nginxReceiverFound, reloadCollector := oc.updateExistingNginxPlusReceiver(nginxConfigContext)
414431

@@ -744,3 +761,59 @@ func escapeString(input string) string {
744761

745762
return output
746763
}
764+
765+
func (oc *Collector) setExporterProxyEnvVars(ctx context.Context) {
766+
proxy := oc.config.Command.Server.Proxy
767+
proxyURL := proxy.URL
768+
parsedProxyURL, err := url.Parse(proxyURL)
769+
if err != nil {
770+
slog.ErrorContext(ctx, "Malformed proxy URL, unable to configure proxy for OTLP exporter",
771+
"url", proxyURL, "error", err)
772+
773+
return
774+
}
775+
776+
if parsedProxyURL.Scheme == "https" {
777+
slog.ErrorContext(ctx, "HTTPS protocol not supported by OTLP exporter, unable to configure proxy for "+
778+
"OTLP exporter", "url", proxyURL)
779+
}
780+
781+
auth := ""
782+
if proxy.AuthMethod != "" && strings.TrimSpace(proxy.AuthMethod) != "" {
783+
auth = strings.TrimSpace(proxy.AuthMethod)
784+
}
785+
786+
// Use the standalone setProxyWithBasicAuth function
787+
if auth == "" {
788+
setProxyEnvs(ctx, proxyURL, "Setting Proxy from command.Proxy (no auth)")
789+
return
790+
}
791+
authLower := strings.ToLower(auth)
792+
if authLower == "basic" {
793+
setProxyWithBasicAuth(ctx, proxy, parsedProxyURL)
794+
} else {
795+
slog.ErrorContext(ctx, "Unknown auth type for proxy, unable to configure proxy for OTLP exporter",
796+
"auth", auth, "url", proxyURL)
797+
}
798+
}
799+
800+
// setProxyEnvs sets the HTTP_PROXY and HTTPS_PROXY environment variables and logs the action.
801+
func setProxyEnvs(ctx context.Context, proxyEnvURL, msg string) {
802+
slog.DebugContext(ctx, msg, "url", proxyEnvURL)
803+
if setenvErr := os.Setenv("HTTPS_PROXY", proxyEnvURL); setenvErr != nil {
804+
slog.ErrorContext(ctx, "Failed to set OTLP exporter proxy environment variables", "error", setenvErr)
805+
}
806+
}
807+
808+
// setProxyWithBasicAuth sets the proxy environment variables with basic auth credentials.
809+
func setProxyWithBasicAuth(ctx context.Context, proxy *config.Proxy, parsedProxyURL *url.URL) {
810+
username := proxy.Username
811+
password := proxy.Password
812+
if username == "" || password == "" {
813+
slog.ErrorContext(ctx, "Unable to configure OTLP exporter proxy, username or password missing for basic auth")
814+
return
815+
}
816+
parsedProxyURL.User = url.UserPassword(username, password)
817+
proxyURL := parsedProxyURL.String()
818+
setProxyEnvs(ctx, proxyURL, "Setting Proxy with basic auth")
819+
}

internal/collector/otel_collector_plugin_test.go

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"context"
1010
"errors"
1111
"net"
12+
"net/url"
13+
"os"
1214
"path/filepath"
1315
"testing"
1416

@@ -246,7 +248,11 @@ func TestCollector_ProcessNginxConfigUpdateTopic(t *testing.T) {
246248

247249
conf := types.OTelConfig(t)
248250

249-
conf.Command = nil
251+
conf.Command = &config.Command{
252+
Server: &config.ServerConfig{
253+
Proxy: &config.Proxy{},
254+
},
255+
}
250256

251257
conf.Collector.Log.Path = ""
252258
conf.Collector.Receivers.HostMetrics = nil
@@ -782,6 +788,132 @@ func TestCollector_updateNginxAppProtectTcplogReceivers(t *testing.T) {
782788
})
783789
}
784790

791+
func Test_setProxyEnvs(t *testing.T) {
792+
ctx := context.Background()
793+
proxyURL := "http://localhost:8080"
794+
msg := "Setting test proxy"
795+
796+
// Unset first to ensure clean state
797+
_ = os.Unsetenv("HTTPS_PROXY")
798+
799+
setProxyEnvs(ctx, proxyURL, msg)
800+
801+
httpProxy := os.Getenv("HTTPS_PROXY")
802+
assert.Equal(t, proxyURL, httpProxy)
803+
}
804+
805+
func Test_setProxyWithBasicAuth(t *testing.T) {
806+
ctx := context.Background()
807+
u, _ := url.Parse("http://localhost:8080")
808+
proxy := &config.Proxy{
809+
URL: "http://localhost:8080",
810+
Username: "user",
811+
Password: "pass",
812+
}
813+
814+
// Unset first to ensure clean state
815+
_ = os.Unsetenv("HTTPS_PROXY")
816+
817+
setProxyWithBasicAuth(ctx, proxy, u)
818+
819+
proxyURL := u.String()
820+
httpProxy := os.Getenv("HTTPS_PROXY")
821+
assert.Equal(t, proxyURL, httpProxy)
822+
823+
logBuf := &bytes.Buffer{}
824+
stub.StubLoggerWith(logBuf)
825+
// Test missing username/password
826+
proxyMissing := &config.Proxy{URL: "http://localhost:8080"}
827+
setProxyWithBasicAuth(ctx, proxyMissing, u)
828+
helpers.ValidateLog(t, "Unable to configure OTLP exporter proxy, "+
829+
"username or password missing for basic auth", logBuf)
830+
}
831+
832+
//nolint:contextcheck // Can not update the "OTelConfig" function definition
833+
func TestSetExporterProxyEnvVars(t *testing.T) {
834+
ctx := context.Background()
835+
logBuf := &bytes.Buffer{}
836+
stub.StubLoggerWith(logBuf)
837+
838+
tests := []struct {
839+
name string
840+
proxy *config.Proxy
841+
expectedLog string
842+
setEnv bool
843+
}{
844+
{
845+
name: "Test 1: No proxy config",
846+
proxy: nil,
847+
expectedLog: "Proxy configuration is not setup. Unable to configure proxy for OTLP exporter",
848+
setEnv: false,
849+
},
850+
{
851+
name: "Test 2: Malformed proxy URL",
852+
proxy: &config.Proxy{URL: "://bad_url"},
853+
expectedLog: "Malformed proxy URL, unable to configure proxy for OTLP exporter",
854+
setEnv: false,
855+
},
856+
{
857+
name: "Test 3: No auth, valid URL",
858+
proxy: &config.Proxy{URL: "http://proxy.example.com:8080"},
859+
expectedLog: "Setting Proxy from command.Proxy (no auth)",
860+
setEnv: true,
861+
},
862+
{
863+
name: "Basic auth, valid URL",
864+
proxy: &config.Proxy{
865+
URL: "http://proxy.example.com:8080",
866+
AuthMethod: "basic",
867+
Username: "user",
868+
Password: "pass",
869+
},
870+
expectedLog: "Setting Proxy with basic auth",
871+
setEnv: true,
872+
},
873+
{
874+
name: "Unknown auth method",
875+
proxy: &config.Proxy{URL: "http://proxy.example.com:8080", AuthMethod: "digest"},
876+
expectedLog: "Unknown auth type for proxy, unable to configure proxy for OTLP exporter",
877+
setEnv: false,
878+
},
879+
}
880+
881+
for _, tt := range tests {
882+
t.Run(tt.name, func(t *testing.T) {
883+
logBuf.Reset()
884+
885+
_ = os.Unsetenv("HTTPS_PROXY")
886+
887+
tmpDir := t.TempDir()
888+
cfg := types.OTelConfig(t)
889+
cfg.Collector.Log.Path = filepath.Join(tmpDir, "otel-collector-test.log")
890+
cfg.Command.Server.Proxy = tt.proxy
891+
892+
// If the proxy is nil, the production code would never call the setter functions.
893+
// added this check to prevent the panic error in UT.
894+
if cfg.Command.Server.Proxy == nil {
895+
// For the nil proxy case, we expect nothing to happen.
896+
assert.Empty(t, os.Getenv("HTTPS_PROXY"))
897+
898+
return
899+
}
900+
901+
collector, err := NewCollector(cfg)
902+
require.NoError(t, err)
903+
904+
collector.setExporterProxyEnvVars(ctx)
905+
906+
helpers.ValidateLog(t, tt.expectedLog, logBuf)
907+
908+
if tt.setEnv {
909+
assert.NotEmpty(t, os.Getenv("HTTPS_PROXY"))
910+
} else {
911+
assert.Empty(t, os.Getenv("HTTPS_PROXY"))
912+
}
913+
})
914+
}
915+
}
916+
785917
func TestCollector_findAvailableSyslogServers(t *testing.T) {
786918
ctx := context.Background()
787919
conf := types.OTelConfig(t)

internal/config/config.go

Lines changed: 107 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,65 @@ func registerCommandFlags(fs *flag.FlagSet) {
633633
DefCommandTLServerNameKey,
634634
"Specifies the name of the server sent in the TLS configuration.",
635635
)
636+
fs.Duration(
637+
CommandServerProxyTimeoutKey,
638+
DefCommandServerProxyTimeoutKey,
639+
"The explicit forward proxy HTTP Timeout, value in seconds")
640+
fs.String(
641+
CommandServerProxyURLKey,
642+
DefCommandServerProxyURlKey,
643+
"The Proxy URL to use for explicit forward proxy.",
644+
)
645+
fs.String(
646+
CommandServerProxyNoProxyKey,
647+
DefCommandServerProxyNoProxyKey,
648+
"The No-Proxy URL to use for explicit forward proxy.",
649+
)
650+
fs.String(
651+
CommandServerProxyAuthMethodKey,
652+
DefCommandServerProxyAuthMethodKey,
653+
"The Authentication method used for explicit forward proxy.",
654+
)
655+
fs.String(
656+
CommandServerProxyUsernameKey,
657+
DefCommandServerProxyUsernameKey,
658+
"The Username used for basic authentication for explicit forward proxy.",
659+
)
660+
fs.String(
661+
CommandServerProxyPasswordKey,
662+
DefCommandServerProxyPasswordKey,
663+
"The Password used for basic authentication for explicit forward proxy.",
664+
)
665+
fs.String(
666+
CommandServerProxyTokenKey,
667+
DefCommandServerProxyTokenKey,
668+
"The bearer token used for authentication for explicit forward proxy.",
669+
)
670+
fs.String(
671+
CommandServerProxyTLSCertKey,
672+
DefCommandServerProxyTLSCertKey,
673+
"The path to the certificate file to use for TLS communication with the command server.",
674+
)
675+
fs.String(
676+
CommandServerProxyTLSKeyKey,
677+
DefCommandServerProxyTLSKeyKey,
678+
"The path to the certificate key file to use for TLS communication with the command server.",
679+
)
680+
fs.String(
681+
CommandServerProxyTLSCaKey,
682+
DefCommandServerProxyTLSCaKey,
683+
"The path to CA certificate file to use for TLS communication with the command server.",
684+
)
685+
fs.Bool(
686+
CommandServerProxyTLSSkipVerifyKey,
687+
DefCommandServerProxyTLSSkipVerifyKey,
688+
"Testing only. Skip verify controls client verification of a server's certificate chain and host name.",
689+
)
690+
fs.String(
691+
CommandServerProxyTLSServerNameKey,
692+
DefCommandServerProxyTLServerNameKey,
693+
"Specifies the name of the server sent in the TLS configuration.",
694+
)
636695
}
637696

638697
func registerAuxiliaryCommandFlags(fs *flag.FlagSet) {
@@ -1240,9 +1299,10 @@ func resolveCommand() *Command {
12401299

12411300
command := &Command{
12421301
Server: &ServerConfig{
1243-
Host: viperInstance.GetString(CommandServerHostKey),
1244-
Port: viperInstance.GetInt(CommandServerPortKey),
1245-
Type: serverType,
1302+
Host: viperInstance.GetString(CommandServerHostKey),
1303+
Port: viperInstance.GetInt(CommandServerPortKey),
1304+
Type: serverType,
1305+
Proxy: resolveProxy(),
12461306
},
12471307
}
12481308

@@ -1370,3 +1430,47 @@ func resolveMapStructure(key string, object any) error {
13701430

13711431
return nil
13721432
}
1433+
1434+
func resolveProxy() *Proxy {
1435+
proxy := &Proxy{
1436+
Timeout: viperInstance.GetDuration(CommandServerProxyTimeoutKey),
1437+
URL: viperInstance.GetString(CommandServerProxyURLKey),
1438+
NoProxy: viperInstance.GetString(CommandServerProxyNoProxyKey),
1439+
Username: viperInstance.GetString(CommandServerProxyUsernameKey),
1440+
Password: viperInstance.GetString(CommandServerProxyPasswordKey),
1441+
Token: viperInstance.GetString(CommandServerProxyTokenKey),
1442+
AuthMethod: viperInstance.GetString(CommandServerProxyAuthMethodKey),
1443+
}
1444+
1445+
if areCommandServerProxyTLSSettingsSet() {
1446+
proxy.TLS = &TLSConfig{
1447+
Cert: viperInstance.GetString(CommandServerProxyTLSCertKey),
1448+
Key: viperInstance.GetString(CommandServerProxyTLSKeyKey),
1449+
Ca: viperInstance.GetString(CommandServerProxyTLSCaKey),
1450+
SkipVerify: viperInstance.GetBool(CommandServerProxyTLSSkipVerifyKey),
1451+
ServerName: viperInstance.GetString(CommandServerProxyTLSServerNameKey),
1452+
}
1453+
}
1454+
1455+
// If all fields are zero/nil/empty, return nil
1456+
if proxy.TLS == nil &&
1457+
proxy.Timeout == 0 &&
1458+
proxy.URL == "" &&
1459+
proxy.NoProxy == "" &&
1460+
proxy.AuthMethod == "" &&
1461+
proxy.Username == "" &&
1462+
proxy.Password == "" &&
1463+
proxy.Token == "" {
1464+
return nil
1465+
}
1466+
1467+
return proxy
1468+
}
1469+
1470+
func areCommandServerProxyTLSSettingsSet() bool {
1471+
return viperInstance.IsSet(CommandServerProxyTLSCertKey) ||
1472+
viperInstance.IsSet(CommandServerProxyTLSKeyKey) ||
1473+
viperInstance.IsSet(CommandServerProxyTLSCaKey) ||
1474+
viperInstance.IsSet(CommandServerProxyTLSSkipVerifyKey) ||
1475+
viperInstance.IsSet(CommandServerProxyTLSServerNameKey)
1476+
}

0 commit comments

Comments
 (0)