Skip to content

Commit 1aa16ad

Browse files
authored
Forward arbitrary TCP traffic using aproxy (#580)
* Forward arbitrary TCP traffic using aproxy * Update integration tests and fix linting tests * Fix linting issue and update integration issues * Fix linting issues * Update e2e test * Add validator to aproxy_redirect_ports and aproxy_exclude_addresses * Use partition instead of split * Simplify _generate_cloud_init * Fix _generate_cloud_init * Simplify _generate_cloud_init * Fix e2e test * Apply suggestions from review comments * Small refactor over the charm_state.py * Fix unit tests * remove test for TCP proxy * Update e2e tests * Always exclude 127.0.0.0/8 * Increase timeout for runner_manager_with_one_runner_fixture
1 parent 43ab437 commit 1aa16ad

File tree

14 files changed

+460
-155
lines changed

14 files changed

+460
-155
lines changed

.github/workflows/e2e_test_run.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ on:
2222
jobs:
2323
e2e-test:
2424
name: End-to-End Test Run
25-
runs-on: [self-hosted, linux, "${{ inputs.runner-tag }}"]
25+
runs-on: [ self-hosted, linux, "${{ inputs.runner-tag }}" ]
2626
steps:
2727
- name: Hostname is set to "github-runner"
2828
run: sudo hostnamectl hostname | grep github-runner
@@ -47,6 +47,14 @@ jobs:
4747
run: |
4848
[[ -z "${http_proxy}" && -z "${HTTP_PROXY}" ]] \
4949
|| cat /home/ubuntu/.docker/config.json | grep httpProxy
50+
- name: test network connectivity
51+
run: |
52+
timeout 60 curl --noproxy "*" http://example.com -svS -o /dev/null
53+
timeout 60 curl --noproxy "*" https://example.com -svS -o /dev/null
54+
- name: test aproxy logs
55+
run: |
56+
sudo snap logs aproxy.aproxy | grep -Fq "example.com:80"
57+
sudo snap logs aproxy.aproxy | grep -Fq "example.com:443"
5058
- name: Install microk8s
5159
run: sudo snap install microk8s --classic
5260
- name: Wait for microk8s
@@ -87,3 +95,9 @@ jobs:
8795
# ~/.local/bin is added to path runner env through in scripts/env.j2
8896
- name: test check-jsonschema
8997
run: check-jsonschema --version
98+
- name: show aproxy logs
99+
if: always()
100+
run: |
101+
sudo snap get aproxy
102+
sudo snap logs aproxy.aproxy -n=all
103+
sudo nft list ruleset

config.yaml

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,26 @@ options:
3737
default: false
3838
description: >-
3939
(Experimental, may be removed) When set to true, aproxy (https://github.com/canonical/aproxy)
40-
will be installed within the runners. It will forward all HTTP(S) traffic to standard ports
41-
(80, 443, 11371) to a proxy server configured by the juju model config 'juju-http-proxy'
40+
will be installed within the runners. It will forward TCP traffic matching the 'aproxy-exclude-addresses'
41+
and 'aproxy-redirect-ports' settings to a proxy server configured by the Juju model config 'juju-http-proxy'
4242
(or, if this is not set, 'juju-https-proxy' will be used).
4343
This is useful when the charm is deployed in a network that requires a proxy to access the
4444
internet.
45-
Note that you should not specify a proxy server listening on port 80 or 443, as all traffic
46-
to these ports is relayed to aproxy, which would cause an infinite loop.
45+
Note that you should carefully choose values for the 'aproxy-exclude-addresses' and
46+
'aproxy-redirect-ports' so that the network traffic from the runner to the HTTP proxy is not
47+
captured by aproxy. The simplest way to achieve this is to add the IP address of the HTTP proxy
48+
to 'aproxy-exclude-addresses' or exclude the HTTP proxy port from 'aproxy-redirect-ports'.
49+
aproxy-exclude-addresses:
50+
type: string
51+
default: "10.0.0.0/8, 171.16.0.0/12, 192.168.0.0/16"
52+
description: >-
53+
A comma-separated list of IP addresses that should be excluded from redirection to aproxy.
54+
127.0.0.0/8 are always excluded so you can omit if from the configuration.
55+
aproxy-redirect-ports:
56+
type: string
57+
default: "80, 443"
58+
description: >-
59+
A comma-separated list of ports or port ranges that should be redirected to aproxy.
4760
group:
4861
type: string
4962
default: "default"

docs/changelog.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
This changelog documents user-relevant changes to the GitHub runner charm.
44

5+
### 2025-06-30
6+
- New configuration options aproxy-exclude-addresses and aproxy-redirect-ports for allowing aproxy to redirect arbitrary TCP traffic
7+
58
## 2025-06-26
69

710
- Fix a process leak internal to the charm.
811

9-
## 2025-06-24
10-
1112
- Fix a bug where deleted GitHub Actions Job would cause an endless loop of retries.
1213

1314
### 2025-06-17

github-runner-manager/src/github_runner_manager/configuration/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ class SupportServiceConfig(BaseModel):
8181
proxy_config: The proxy configuration.
8282
runner_proxy_config: The proxy configuration for the runner.
8383
use_aproxy: Whether aproxy should be used for the runners.
84+
aproxy_exclude_addresses: A list of addresses to exclude from the aproxy proxy.
85+
aproxy_redirect_ports: A list of ports to redirect to the aproxy proxy.
8486
dockerhub_mirror: The dockerhub mirror to use for runners.
8587
ssh_debug_connections: The information on the ssh debug services.
8688
repo_policy_compliance: The configuration of the repo policy compliance service.
@@ -91,6 +93,8 @@ class SupportServiceConfig(BaseModel):
9193
proxy_config: "ProxyConfig | None"
9294
runner_proxy_config: "ProxyConfig | None"
9395
use_aproxy: bool
96+
aproxy_exclude_addresses: list[str] = []
97+
aproxy_redirect_ports: list[str] = []
9498
dockerhub_mirror: str | None
9599
ssh_debug_connections: "list[SSHDebugConnection]"
96100
repo_policy_compliance: "RepoPolicyComplianceConfig | None"

github-runner-manager/src/github_runner_manager/openstack_cloud/openstack_runner_manager.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,15 +282,24 @@ def _generate_cloud_init(self, runner_context: RunnerContext) -> str:
282282

283283
pre_job_contents = jinja.get_template("pre-job.j2").render(pre_job_contents_dict)
284284

285-
aproxy_address = (
286-
service_config.runner_proxy_config.proxy_address if service_config.use_aproxy else None
287-
)
285+
use_aproxy = service_config.use_aproxy
286+
if not service_config.runner_proxy_config.proxy_address:
287+
use_aproxy = False
288+
aproxy_redirect_ports = service_config.aproxy_redirect_ports
289+
if not aproxy_redirect_ports:
290+
use_aproxy = False
291+
aproxy_exclude_ipv4_addresses = [
292+
address for address in service_config.aproxy_exclude_addresses if ":" not in address
293+
]
288294
return jinja.get_template("openstack-userdata.sh.j2").render(
289295
run_script=runner_context.shell_run_script,
290296
env_contents=env_contents,
291297
pre_job_contents=pre_job_contents,
292298
metrics_exchange_path=str(METRICS_EXCHANGE_PATH),
293-
aproxy_address=aproxy_address,
299+
use_aproxy=use_aproxy,
300+
aproxy_address=service_config.runner_proxy_config.proxy_address,
301+
aproxy_exclude_ipv4_addresses=", ".join(aproxy_exclude_ipv4_addresses),
302+
aproxy_redirect_ports=", ".join(aproxy_redirect_ports),
294303
dockerhub_mirror=service_config.dockerhub_mirror,
295304
ssh_debug_info=ssh_debug_info,
296305
runner_proxy_config=service_config.runner_proxy_config,

github-runner-manager/src/github_runner_manager/templates/openstack-userdata.sh.j2

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,26 @@ su - ubuntu -c 'cd ~/actions-runner && echo "{{ env_contents }}" > .env'
1010
snap refresh --hold=48h
1111
snap watch --last=auto-refresh?
1212

13-
{% if aproxy_address %}
13+
{% if use_aproxy %}
1414
snap install aproxy --edge
1515
snap set aproxy proxy={{ aproxy_address }} listen=:54969
1616
cat << EOF > /etc/nftables.conf
17-
define default-ip = $(ip route get $(ip route show 0.0.0.0/0 | grep -oP 'via \K\S+') | grep -oP 'src \K\S+')
18-
define private-ips = { 10.0.0.0/8, 127.0.0.1/8, 172.16.0.0/12, 192.168.0.0/16 }
17+
define default-ipv4 = $(ip route get $(ip route show 0.0.0.0/0 | grep -oP 'via \K\S+') | grep -oP 'src \K\S+')
1918
table ip aproxy
2019
flush table ip aproxy
2120
table ip aproxy {
21+
set exclude {
22+
type ipv4_addr;
23+
flags interval; auto-merge;
24+
elements = { 127.0.0.0/8, {{ aproxy_exclude_ipv4_addresses }} }
25+
}
2226
chain prerouting {
2327
type nat hook prerouting priority dstnat; policy accept;
24-
ip daddr != \$private-ips tcp dport { 80, 443, 11371 } counter dnat to \$default-ip:54969
28+
ip daddr != @exclude tcp dport { {{ aproxy_redirect_ports }} } counter dnat to \$default-ipv4:54969
2529
}
26-
2730
chain output {
2831
type nat hook output priority -100; policy accept;
29-
ip daddr != \$private-ips tcp dport { 80, 443, 11371 } counter dnat to \$default-ip:54969
32+
ip daddr != @exclude tcp dport { {{ aproxy_redirect_ports }} } counter dnat to \$default-ipv4:54969
3033
}
3134
}
3235
EOF

github-runner-manager/tests/unit/openstack_cloud/test_openstack_runner_manager.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
"""Module for unit-testing OpenStack runner manager."""
55
import logging
6+
import textwrap
67
from datetime import datetime, timezone
78
from typing import Iterable
89
from unittest.mock import MagicMock
@@ -77,8 +78,89 @@ def runner_metrics_mock_fixture(monkeypatch: pytest.MonkeyPatch) -> MagicMock:
7778
return runner_metrics_mock
7879

7980

81+
@pytest.mark.parametrize(
82+
"aproxy_redirect_ports, aproxy_exclude_addresses, aproxy_used, except_aproxy_script",
83+
[
84+
pytest.param(
85+
[],
86+
["10.0.0.0/8"],
87+
False,
88+
"",
89+
id="empty aproxy_redirect_ports disables aproxy",
90+
),
91+
pytest.param(
92+
["80", "443"],
93+
["10.0.0.0/8", "192.168.0.0/16"],
94+
True,
95+
"10.0.0.0/8, 192.168.0.0/16",
96+
id="aproxy with custom aproxy_exclude_addresses",
97+
),
98+
pytest.param(
99+
["0-3127", "3129-65535"],
100+
["10.0.0.0/8", "192.168.0.0/16"],
101+
True,
102+
"0-3127, 3129-65535",
103+
id="aproxy with custom aproxy_redirect_ports",
104+
),
105+
pytest.param(
106+
["80", "443"],
107+
["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"],
108+
True,
109+
textwrap.dedent(
110+
"""\
111+
table ip aproxy {
112+
set exclude {
113+
type ipv4_addr;
114+
flags interval; auto-merge;
115+
elements = { 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 }
116+
}
117+
chain prerouting {
118+
type nat hook prerouting priority dstnat; policy accept;
119+
ip daddr != @exclude tcp dport { 80, 443 } counter dnat to \\$default-ipv4:54969
120+
}
121+
chain output {
122+
type nat hook output priority -100; policy accept;
123+
ip daddr != @exclude tcp dport { 80, 443 } counter dnat to \\$default-ipv4:54969
124+
}
125+
}
126+
"""
127+
),
128+
id="aproxy default config",
129+
),
130+
pytest.param(
131+
["80", "443"],
132+
[],
133+
True,
134+
textwrap.dedent(
135+
"""\
136+
table ip aproxy {
137+
set exclude {
138+
type ipv4_addr;
139+
flags interval; auto-merge;
140+
elements = { 127.0.0.0/8, }
141+
}
142+
chain prerouting {
143+
type nat hook prerouting priority dstnat; policy accept;
144+
ip daddr != @exclude tcp dport { 80, 443 } counter dnat to \\$default-ipv4:54969
145+
}
146+
chain output {
147+
type nat hook output priority -100; policy accept;
148+
ip daddr != @exclude tcp dport { 80, 443 } counter dnat to \\$default-ipv4:54969
149+
}
150+
}
151+
"""
152+
),
153+
id="aproxy with no aproxy_exclude_addresses",
154+
),
155+
],
156+
)
80157
def test_create_runner_with_aproxy(
81-
runner_manager: OpenStackRunnerManager, monkeypatch: pytest.MonkeyPatch
158+
aproxy_redirect_ports: list[str],
159+
aproxy_exclude_addresses: list[str],
160+
aproxy_used: str,
161+
except_aproxy_script: str,
162+
runner_manager: OpenStackRunnerManager,
163+
monkeypatch: pytest.MonkeyPatch,
82164
):
83165
"""
84166
arrange: Prepare service config with aproxy enabled and a runner proxy config.
@@ -88,6 +170,8 @@ def test_create_runner_with_aproxy(
88170
# Pending to pass service_config as a dependency instead of mocking it this way.
89171
service_config = runner_manager._config.service_config
90172
service_config.use_aproxy = True
173+
service_config.aproxy_redirect_ports = aproxy_redirect_ports
174+
service_config.aproxy_exclude_addresses = aproxy_exclude_addresses
91175
service_config.runner_proxy_config = ProxyConfig(http="http://proxy.example.com:3128")
92176

93177
prefix = "test"
@@ -101,10 +185,11 @@ def test_create_runner_with_aproxy(
101185

102186
runner_manager.create_runner(identity, runner_context)
103187
openstack_cloud.launch_instance.assert_called_once()
104-
assert (
105-
"snap set aproxy proxy=proxy.example.com:3128"
106-
in openstack_cloud.launch_instance.call_args.kwargs["cloud_init"]
107-
)
188+
189+
cloud_init = openstack_cloud.launch_instance.call_args.kwargs["cloud_init"]
190+
assert ("snap set aproxy proxy=proxy.example.com:3128" in cloud_init) == aproxy_used
191+
if aproxy_used:
192+
assert except_aproxy_script in cloud_init
108193

109194

110195
def test_create_runner_without_aproxy(

0 commit comments

Comments
 (0)