Skip to content

Add NO_PROXY support for automatic proxy bypass based on target host#114

Open
ibozas wants to merge 1 commit intorundeck-plugins:masterfrom
ibozas:feature/noproxy-support
Open

Add NO_PROXY support for automatic proxy bypass based on target host#114
ibozas wants to merge 1 commit intorundeck-plugins:masterfrom
ibozas:feature/noproxy-support

Conversation

@ibozas
Copy link

@ibozas ibozas commented Mar 6, 2026

VIDEO TESTING:

REC-20260304175703.mp4

Summary

  • Add a new No Proxy List configuration parameter to all three plugin providers (NodeExecutor, FileCopier, WinRMCheck) that allows specifying hosts/IPs/CIDRs that should bypass the proxy and connect directly
  • Centralize proxy logic in a new configure_proxy() function in common.py that delegates proxy routing to Python's requests library via environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY), enabling automatic per-host proxy decisions
  • Add clear INFO-level log messages indicating the connection path for each execution (DIRECTLY vs via PROXY), so operators can verify routing without enabling debug mode
  • Maintain full backward compatibility: when only Proxy is set (no No Proxy List), the existing explicit proxy behavior is preserved

Changed files

File Changes
contents/common.py Added configure_proxy() function with env-var delegation, should_bypass_proxies check, and INFO-level logging
contents/winrm-exec.py Read RD_CONFIG_WINRMNOPROXY env var, replaced explicit proxy assignment with configure_proxy() call
contents/winrm-filecopier.py Same pattern as winrm-exec.py
contents/winrm-check.py Added --proxy and --noproxy argparse arguments, added import common, integrated configure_proxy() call
plugin.yaml Added winrmnoproxy property to WinRMPython and WinRMcpPython providers; added winrmproxy and winrmnoproxy to WinRMCheck provider; updated WinRMCheck script-args

How it works

When both Proxy and No Proxy List are configured:

  1. The plugin sets HTTP_PROXY, HTTPS_PROXY, and NO_PROXY environment variables instead of passing proxy= directly to winrm.Session()
  2. Python's requests library (used internally by pywinrm) natively evaluates the target host against NO_PROXY patterns
  3. If the target matches NO_PROXY, the connection goes direct; otherwise it routes through the proxy

When only Proxy is configured (no No Proxy List):

  • Legacy behavior is preserved: proxy is passed explicitly to winrm.Session() for all connections

Supported NO_PROXY patterns

  • Exact IP: 192.168.1.50
  • CIDR notation: 192.168.1.0/24, 10.0.0.0/8
  • Domain suffix: .internal.corp
  • Exact hostname: myserver.domain.com
  • Wildcard (bypass all): *
  • Multiple values (comma-separated): 192.168.1.0/24,10.0.0.0/8,.internal.corp

Log output examples

Target matches NO_PROXY (direct connection):

[INFO] Connecting to http://10.0.0.100:5985 DIRECTLY (matched NO_PROXY: 10.0.0.0/24)

Target does NOT match NO_PROXY (proxy connection):

[INFO] Connecting to http://10.0.0.100:5985 via PROXY (http://proxy-server:8080)

Testing

  • ✅ Proxy + No Proxy List matching target IP → connection goes DIRECTLY (bypasses proxy)
  • ✅ Proxy + No Proxy List NOT matching target IP → connection routes via PROXY
  • ✅ INFO-level log messages correctly indicate routing decision
  • ✅ Verified with Rundeck WAR environment on macOS

- Add 'No Proxy List' config parameter to all three providers (NodeExecutor, FileCopier, WinRMCheck)
- Centralize proxy logic in configure_proxy() function in common.py
- Delegate proxy routing to requests library via HTTP_PROXY/HTTPS_PROXY/NO_PROXY env vars
- Add INFO-level log messages showing connection path (DIRECTLY vs via PROXY)
- Maintain backward compatibility: proxy-only config preserves legacy explicit proxy behavior
- Supports exact IPs, CIDR notation, domain suffixes, hostnames, and wildcard patterns
Copilot AI review requested due to automatic review settings March 6, 2026 19:27
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds configurable NO_PROXY behavior to the WinRM Python plugin so proxy routing can be automatically bypassed per target host/IP, with routing decisions delegated to requests via environment variables.

Changes:

  • Added winrmnoproxy config to NodeExecutor/FileCopier providers, and added winrmproxy + winrmnoproxy to the WinRMCheck provider.
  • Centralized proxy/no-proxy handling in contents/common.py via a new configure_proxy() helper and integrated it into exec/filecopier/check flows.
  • Updated WinRMCheck to accept --proxy/--noproxy args and pass them through from plugin.yaml.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
plugin.yaml Adds new proxy/no-proxy configuration properties and passes proxy args to winrm-check.py.
contents/common.py Introduces configure_proxy() that sets proxy env vars, performs bypass check, and logs routing.
contents/winrm-exec.py Reads RD_CONFIG_WINRMNOPROXY and uses common.configure_proxy() instead of explicit proxy=.
contents/winrm-filecopier.py Reads RD_CONFIG_WINRMNOPROXY and uses common.configure_proxy() instead of explicit proxy=.
contents/winrm-check.py Adds CLI args for proxy/no-proxy and calls common.configure_proxy().

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +81 to +83
if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy):
log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy))
else:
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The log message says "matched NO_PROXY" but prints the entire winrmnoproxy list, not the specific entry/pattern that matched. This is misleading (and differs from the PR description examples). Consider rewording to avoid implying a single match, or implement logic to identify which entry matched before logging it.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

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

I did this intentionally so the customer can now see whether the proxy connection is using DIRECT or PROXY. It's just an INFO log.

Comment on lines +113 to +116
- name: winrmnoproxy
title: No Proxy List
description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`"
type: String
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Grammar/wording: "It can be overwriting at node level" should be "It can be overridden at node level" (and similarly for any other occurrences).

Copilot uses AI. Check for mistakes.
Comment on lines +392 to +395
- name: winrmnoproxy
title: No Proxy List
description: "Comma-separated list of hosts/IPs/CIDRs that bypass the proxy. Supports exact IPs, CIDR notation (192.168.1.0/24), domain suffixes (.internal.corp), and wildcard (*). Requires Proxy to also be set. It can be overwriting at node level using `winrm-noproxy`"
type: String
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Grammar/wording: "It can be overwriting at node level" should be "It can be overridden at node level".

Copilot uses AI. Check for mistakes.
else:
if winrmnoproxy:
log.warning("noproxy is set but no proxy configured; noproxy ignored")
log.info("Connecting to %s DIRECTLY (no proxy configured)" % endpoint)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

This branch logs "DIRECTLY (no proxy configured)", but the request may still go through a proxy if the Rundeck process already has HTTP(S)_PROXY set in its environment (requests/pywinrm will honor env proxies by default). Consider either (a) adjusting the message to clarify it's only about plugin config (not actual routing), or (b) detecting existing env proxies and logging the effective routing more accurately.

Suggested change
log.info("Connecting to %s DIRECTLY (no proxy configured)" % endpoint)
# No proxy configured via plugin; check for environment proxies that requests/pywinrm may still use
http_proxy = os.environ.get('HTTP_PROXY') or os.environ.get('http_proxy')
https_proxy = os.environ.get('HTTPS_PROXY') or os.environ.get('https_proxy')
if http_proxy or https_proxy:
log.info(
"Connecting to %s via PROXY from environment (HTTP_PROXY=%s, HTTPS_PROXY=%s)"
% (endpoint, http_proxy, https_proxy)
)
else:
log.info(
"Connecting to %s DIRECTLY (no proxy configured in plugin or environment)" % endpoint
)

Copilot uses AI. Check for mistakes.
Comment on lines +84 to +88
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
else:
# Legacy: explicit proxy for all connections
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The INFO log message includes the full proxy URL, which may contain embedded credentials (e.g., http://user:pass@host:port per the plugin config). This would leak secrets into Rundeck logs. Consider redacting userinfo before logging (and likewise for any debug logs).

Copilot uses AI. Check for mistakes.
Comment on lines +79 to +88
log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy))

if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy):
log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy))
else:
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
else:
# Legacy: explicit proxy for all connections
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

log.debug("Proxy via env vars: HTTP(S)_PROXY=%s...") can also leak proxy credentials when the proxy URL contains user:pass. Even though it's debug-level, Rundeck debug logs are often collected centrally; please redact credentials (or avoid logging the full URL).

Suggested change
log.debug("Proxy via env vars: HTTP(S)_PROXY=%s, NO_PROXY=%s" % (winrmproxy, winrmnoproxy))
if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy):
log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy))
else:
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
else:
# Legacy: explicit proxy for all connections
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY (%s)" % (endpoint, winrmproxy))
log.debug("Proxy via env vars: HTTP(S)_PROXY set, NO_PROXY=%s" % winrmnoproxy)
if should_bypass_proxies(endpoint, no_proxy=winrmnoproxy):
log.info("Connecting to %s DIRECTLY (matched NO_PROXY: %s)" % (endpoint, winrmnoproxy))
else:
log.info("Connecting to %s via PROXY" % endpoint)
else:
# Legacy: explicit proxy for all connections
arguments["proxy"] = winrmproxy
log.info("Connecting to %s via PROXY" % endpoint)

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants