Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions containers/agent/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,23 @@ if [ "${AWF_CHROOT_ENABLED}" = "true" ]; then
fi
fi

# Inject host.docker.internal into chroot's /etc/hosts when host access is enabled
# Docker adds this to the container's /etc/hosts via extra_hosts, but the chroot
# uses a separate hosts file that doesn't have it. The container's /etc/hosts has
# the correct mapping, so we copy it to the chroot's /etc/hosts.
HOSTS_MODIFIED=false
if [ "${AWF_ENABLE_HOST_ACCESS}" = "1" ]; then
HOST_DOCKER_ENTRY=$(grep "host.docker.internal" /etc/hosts 2>/dev/null | head -1 || true)
if [ -n "$HOST_DOCKER_ENTRY" ] && ! grep -q "host.docker.internal" /host/etc/hosts 2>/dev/null; then
if echo "$HOST_DOCKER_ENTRY" >> /host/etc/hosts 2>/dev/null; then
HOSTS_MODIFIED=true
echo "[entrypoint] Added host.docker.internal to chroot /etc/hosts"
else
echo "[entrypoint][WARN] Could not add host.docker.internal to chroot /etc/hosts"
fi
fi
fi

# Determine working directory inside the chroot
# AWF_WORKDIR is set by docker-manager.ts (containerWorkDir or HOME)
# For chroot mode, paths like /home/user stay the same (no /host prefix)
Expand Down Expand Up @@ -309,6 +326,12 @@ AWFEOF
CLEANUP_CMD="${CLEANUP_CMD}; rm -f /etc/resolv.conf 2>/dev/null || true"
echo "[entrypoint] DNS configuration will be removed on exit"
fi
if [ "$HOSTS_MODIFIED" = "true" ]; then
# Remove the specific host.docker.internal line we added (runs inside chroot perspective)
# Use a precise pattern to avoid accidentally removing unrelated entries
CLEANUP_CMD="${CLEANUP_CMD}; sed -i '/^[0-9.]\\+[[:space:]]\\+host\\.docker\\.internal\$/d' /etc/hosts 2>/dev/null || true"
echo "[entrypoint] host.docker.internal will be removed from /etc/hosts on exit"
fi

exec chroot /host /bin/bash -c "
cd '${CHROOT_WORKDIR}' 2>/dev/null || cd /
Expand Down
36 changes: 36 additions & 0 deletions src/docker-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,42 @@ describe('docker-manager', () => {
expect(volumes).toContain('/etc/nsswitch.conf:/host/etc/nsswitch.conf:ro');
});

it('should mount writable chroot-hosts when enableChroot and enableHostAccess are true', () => {
// Ensure workDir exists for chroot-hosts file creation
fs.mkdirSync(mockConfig.workDir, { recursive: true });
try {
const config = {
...mockConfig,
enableChroot: true,
enableHostAccess: true
};
const result = generateDockerCompose(config, mockNetworkConfig);
const agent = result.services.agent;
const volumes = agent.volumes as string[];

// Should mount a writable copy of /etc/hosts (not the read-only original)
const hostsVolume = volumes.find((v: string) => v.includes('/host/etc/hosts'));
expect(hostsVolume).toBeDefined();
expect(hostsVolume).toContain('chroot-hosts:/host/etc/hosts');
expect(hostsVolume).not.toContain(':ro');
} finally {
fs.rmSync(mockConfig.workDir, { recursive: true, force: true });
}
});

it('should mount read-only /etc/hosts when enableChroot is true but enableHostAccess is false', () => {
const config = {
...mockConfig,
enableChroot: true,
enableHostAccess: false
};
const result = generateDockerCompose(config, mockNetworkConfig);
const agent = result.services.agent;
const volumes = agent.volumes as string[];

expect(volumes).toContain('/etc/hosts:/host/etc/hosts:ro');
});

it('should use GHCR image when enableChroot is true with default preset (GHCR)', () => {
const configWithChroot = {
...mockConfig,
Expand Down
21 changes: 20 additions & 1 deletion src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,9 +485,28 @@
'/etc/passwd:/host/etc/passwd:ro', // User database (needed for getent/user lookup)
'/etc/group:/host/etc/group:ro', // Group database (needed for getent/group lookup)
'/etc/nsswitch.conf:/host/etc/nsswitch.conf:ro', // Name service switch config
'/etc/hosts:/host/etc/hosts:ro', // Host name resolution (localhost, etc.)
);

// Mount /etc/hosts for host name resolution inside chroot
// When BOTH chroot and host access are enabled, we mount a writable COPY so the entrypoint
// can inject host.docker.internal (Docker only adds it to the container's
// /etc/hosts via extra_hosts, but chroot uses the host's /etc/hosts)
if (config.enableHostAccess) {
const chrootHostsPath = path.join(config.workDir, 'chroot-hosts');
try {
fs.copyFileSync('/etc/hosts', chrootHostsPath);
fs.chmodSync(chrootHostsPath, 0o644);
logger.debug(`Copied /etc/hosts to ${chrootHostsPath} for chroot host access`);
} catch {
// Fall back to empty file if host /etc/hosts is not readable
fs.writeFileSync(chrootHostsPath, '127.0.0.1 localhost\n', { mode: 0o644 });
Comment thread Fixed
logger.debug('Created minimal chroot-hosts (could not read host /etc/hosts)');
}
agentVolumes.push(`${chrootHostsPath}:/host/etc/hosts`);
} else {
agentVolumes.push('/etc/hosts:/host/etc/hosts:ro');
}
Comment on lines +495 to +520
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

There is no test coverage for the combination of enableChroot and enableHostAccess being both true. The existing tests at line 528-565 test chroot mode, and tests at line 975-1027 test host access, but none test them together.

This new functionality that creates a writable copy of /etc/hosts specifically when both flags are enabled should be covered by a test. The test should verify:

  1. When both enableChroot: true and enableHostAccess: true, a writable mount is created (no :ro suffix)
  2. The mount path includes chroot-hosts
  3. When only enableChroot: true (without host access), the mount is read-only with :ro suffix
  4. The file system operation (copyFileSync) is properly mocked to avoid side effects

Consider adding a test case like:

it('should mount writable /etc/hosts copy when both chroot and host access are enabled', () => {
  const config = { ...mockConfig, enableChroot: true, enableHostAccess: true };
  const result = generateDockerCompose(config, mockNetworkConfig);
  const volumes = result.services.agent.volumes as string[];
  
  // Should mount writable chroot-hosts file (not read-only)
  const hostsMount = volumes.find(v => v.includes('/host/etc/hosts'));
  expect(hostsMount).toBeDefined();
  expect(hostsMount).toMatch(/chroot-hosts:\/host\/etc\/hosts$/);
  expect(hostsMount).not.toContain(':ro');
});

Copilot uses AI. Check for mistakes.

// SECURITY: Hide Docker socket to prevent firewall bypass via 'docker run'
// An attacker could otherwise spawn a new container without network restrictions
agentVolumes.push('/dev/null:/host/var/run/docker.sock:ro');
Expand Down
Loading