-
Notifications
You must be signed in to change notification settings - Fork 4.3k
chore: add integ test that validates full proxy traversal #33092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { promises as fs } from 'fs'; | ||
| import * as querystring from 'node:querystring'; | ||
| import * as os from 'os'; | ||
| import * as path from 'path'; | ||
| import * as mockttp from 'mockttp'; | ||
| import { CompletedRequest } from 'mockttp'; | ||
|
|
||
| export async function startProxyServer(certDirRoot?: string): Promise<ProxyServer> { | ||
| const certDir = await fs.mkdtemp(path.join(certDirRoot ?? os.tmpdir(), 'cdk-')); | ||
| const certPath = path.join(certDir, 'cert.pem'); | ||
| const keyPath = path.join(certDir, 'key.pem'); | ||
|
|
||
| // Set up key and certificate | ||
| const { key, cert } = await mockttp.generateCACertificate(); | ||
| await fs.writeFile(keyPath, key); | ||
| await fs.writeFile(certPath, cert); | ||
|
|
||
| const server = mockttp.getLocal({ | ||
| https: { keyPath: keyPath, certPath: certPath }, | ||
| }); | ||
|
|
||
| // We don't need to modify any request, so the proxy | ||
| // passes through all requests to the target host. | ||
| const endpoint = await server | ||
| .forAnyRequest() | ||
| .thenPassThrough(); | ||
|
|
||
| server.enableDebug(); | ||
| await server.start(); | ||
|
|
||
| return { | ||
| certPath, | ||
| keyPath, | ||
| server, | ||
| url: server.url, | ||
| port: server.port, | ||
| getSeenRequests: () => endpoint.getSeenRequests(), | ||
| async stop() { | ||
| await server.stop(); | ||
| await fs.rm(certDir, { recursive: true, force: true }); | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| export interface ProxyServer { | ||
| readonly certPath: string; | ||
| readonly keyPath: string; | ||
| readonly server: mockttp.Mockttp; | ||
| readonly url: string; | ||
| readonly port: number; | ||
|
|
||
| getSeenRequests(): Promise<CompletedRequest[]>; | ||
| stop(): Promise<void>; | ||
| } | ||
|
|
||
| export function awsActionsFromRequests(requests: CompletedRequest[]): string[] { | ||
| return [...new Set(requests | ||
| .map(req => req.body.buffer.toString('utf-8')) | ||
| .map(body => querystring.decode(body)) | ||
| .map(x => x.Action as string) | ||
| .filter(action => action != null))]; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
125 changes: 125 additions & 0 deletions
125
packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/proxy.integtest.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import { promises as fs } from 'fs'; | ||
| import * as path from 'path'; | ||
| import { integTest } from '../../lib/integ-test'; | ||
| import { startProxyServer } from '../../lib/proxy'; | ||
| import { TestFixture, withDefaultFixture } from '../../lib/with-cdk-app'; | ||
|
|
||
| const docker = process.env.CDK_DOCKER ?? 'docker'; | ||
|
|
||
| integTest( | ||
| 'deploy in isolated container', | ||
| withDefaultFixture(async (fixture) => { | ||
| // Find the 'cdk' command and make sure it is mounted into the container | ||
| const cdkFullpath = (await fixture.shell(['which', 'cdk'])).trim(); | ||
| const cdkTop = topLevelDirectory(cdkFullpath); | ||
|
|
||
| // Run a 'cdk deploy' inside the container | ||
| const commands = [ | ||
| `env ${renderEnv(fixture.cdkShellEnv())} ${cdkFullpath} ${fixture.cdkDeployCommandLine('test-2', { verbose: true }).join(' ')}`, | ||
| ]; | ||
|
|
||
| await runInIsolatedContainer(fixture, [cdkTop], commands); | ||
| }), | ||
| ); | ||
|
|
||
| async function runInIsolatedContainer(fixture: TestFixture, pathsToMount: string[], testCommands: string[]) { | ||
| pathsToMount.push( | ||
| `${process.env.HOME}`, | ||
| fixture.integTestDir, | ||
| ); | ||
|
|
||
| const proxy = await startProxyServer(fixture.integTestDir); | ||
| try { | ||
| const proxyPort = proxy.port; | ||
|
|
||
| const setupCommands = [ | ||
| 'apt-get update -qq', | ||
| 'apt-get install -qqy nodejs > /dev/null', | ||
| ...isolatedDockerCommands(proxyPort, proxy.certPath), | ||
| ]; | ||
|
|
||
| const scriptName = path.join(fixture.integTestDir, 'script.sh'); | ||
|
|
||
| // Write a script file | ||
| await fs.writeFile(scriptName, [ | ||
| '#!/bin/bash', | ||
| 'set -x', | ||
| 'set -eu', | ||
| ...setupCommands, | ||
| ...testCommands, | ||
| ].join('\n'), 'utf-8'); | ||
|
|
||
| await fs.chmod(scriptName, 0o755); | ||
|
|
||
| // Run commands in a Docker shell | ||
| await fixture.shell([ | ||
| docker, 'run', '--net=bridge', '--rm', | ||
| ...pathsToMount.flatMap(p => ['-v', `${p}:${p}`]), | ||
| ...['HOME', 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'].flatMap(e => ['-e', e]), | ||
| '-w', fixture.integTestDir, | ||
| '--cap-add=NET_ADMIN', | ||
| 'ubuntu:latest', | ||
| `${scriptName}`, | ||
| ], { | ||
| stdio: 'inherit', | ||
| }); | ||
| } finally { | ||
| await proxy.stop(); | ||
| } | ||
| } | ||
|
|
||
| function topLevelDirectory(dir: string) { | ||
| while (true) { | ||
| let parent = path.dirname(dir); | ||
| if (parent === '/') { | ||
| return dir; | ||
| } | ||
| dir = parent; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Return the commands necessary to isolate the inside of the container from the internet, | ||
| * except by going through the proxy | ||
| */ | ||
| function isolatedDockerCommands(proxyPort: number, caBundlePath: string) { | ||
| return [ | ||
| 'echo Working...', | ||
| 'apt-get install -qqy curl net-tools iputils-ping dnsutils iptables > /dev/null', | ||
| '', | ||
| 'gateway=$(dig +short host.docker.internal)', | ||
| '', | ||
| '# Some iptables manipulation; there might be unnecessary commands in here, not an expert', | ||
| 'iptables -F', | ||
| 'iptables -X', | ||
| 'iptables -P INPUT DROP', | ||
| 'iptables -P OUTPUT DROP', | ||
| 'iptables -P FORWARD DROP', | ||
| 'iptables -A INPUT -i lo -j ACCEPT', | ||
| 'iptables -A OUTPUT -o lo -j ACCEPT', | ||
| 'iptables -A OUTPUT -d $gateway -j ACCEPT', | ||
| 'iptables -A INPUT -s $gateway -j ACCEPT', | ||
| '', | ||
| '', | ||
| `if [[ ! -f ${caBundlePath} ]]; then`, | ||
| ` echo "Could not find ${caBundlePath}, this will probably not go well. Exiting." >&2`, | ||
| ' exit 1', | ||
| 'fi', | ||
| '', | ||
| '# Configure a bunch of tools to work with the proxy', | ||
| 'echo "+-------------------------------------------------------------------------------------+"', | ||
| 'echo "| Direct network traffic has been blocked, everything must go through the proxy. |"', | ||
| 'echo "+-------------------------------------------------------------------------------------+"', | ||
| `export HTTP_PROXY=http://$gateway:${proxyPort}/`, | ||
| `export HTTPS_PROXY=http://$gateway:${proxyPort}/`, | ||
| `export NODE_EXTRA_CA_CERTS=${caBundlePath}`, | ||
| `export AWS_CA_BUNDLE=${caBundlePath}`, | ||
| `export SSL_CERT_FILE=${caBundlePath}`, | ||
| 'echo "Acquire::http::proxy \"$HTTP_PROXY\";" >> /etc/apt/apt.conf.d/95proxies', | ||
| 'echo "Acquire::https::proxy \"$HTTPS_PROXY\";" >> /etc/apt/apt.conf.d/95proxies', | ||
| ]; | ||
| } | ||
|
|
||
| function renderEnv(env: Record<string, string>) { | ||
| return Object.entries(env).map(([k, v]) => `${k}='${v}'`).join(' '); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose the answer is yes, but does this work in an ECS container? Do we need any special setup?