Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
123 changes: 122 additions & 1 deletion src/docker-manager.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand } from './docker-manager';
import { generateDockerCompose, subnetsOverlap, writeConfigs, startContainers, stopContainers, cleanup, runAgentCommand, validateIdNotInSystemRange, getSafeHostUid, getSafeHostGid, MIN_REGULAR_UID } from './docker-manager';
import { WrapperConfig } from './types';
import * as fs from 'fs';
import * as path from 'path';
Expand All @@ -10,8 +10,8 @@

// Mock execa module
jest.mock('execa', () => {
const fn = (...args: any[]) => mockExecaFn(...args);

Check warning on line 13 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 13 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 13 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 13 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type
fn.sync = (...args: any[]) => mockExecaSync(...args);

Check warning on line 14 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 14 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 14 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 14 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type
return fn;
});

Expand Down Expand Up @@ -47,6 +47,127 @@
});
});

describe('validateIdNotInSystemRange', () => {
it('should return 1000 for system UIDs (0-999)', () => {
expect(validateIdNotInSystemRange(0)).toBe('1000');
expect(validateIdNotInSystemRange(1)).toBe('1000');
expect(validateIdNotInSystemRange(13)).toBe('1000'); // proxy user
expect(validateIdNotInSystemRange(999)).toBe('1000');
});

it('should return the UID as-is for regular users (>= 1000)', () => {
expect(validateIdNotInSystemRange(1000)).toBe('1000');
expect(validateIdNotInSystemRange(1001)).toBe('1001');
expect(validateIdNotInSystemRange(65534)).toBe('65534'); // nobody user on some systems
});
});

describe('getSafeHostUid', () => {
const originalGetuid = process.getuid;
const originalSudoUid = process.env.SUDO_UID;

afterEach(() => {
process.getuid = originalGetuid;
if (originalSudoUid !== undefined) {
process.env.SUDO_UID = originalSudoUid;
} else {
delete process.env.SUDO_UID;
}
});

it('should return 1000 when SUDO_UID is a system UID', () => {
process.getuid = () => 0; // Running as root
process.env.SUDO_UID = '13'; // proxy user
expect(getSafeHostUid()).toBe('1000');
});

it('should return SUDO_UID when it is a regular user UID', () => {
process.getuid = () => 0; // Running as root
process.env.SUDO_UID = '1001';
expect(getSafeHostUid()).toBe('1001');
});

it('should return 1000 when SUDO_UID is 0', () => {
process.getuid = () => 0; // Running as root
process.env.SUDO_UID = '0';
expect(getSafeHostUid()).toBe('1000');
});

it('should return 1000 when running as root without SUDO_UID', () => {
process.getuid = () => 0;
delete process.env.SUDO_UID;
expect(getSafeHostUid()).toBe('1000');
});

it('should return 1000 for non-root system UID', () => {
process.getuid = () => 13; // proxy user
delete process.env.SUDO_UID;
expect(getSafeHostUid()).toBe('1000');
});

it('should return the UID when running as regular user', () => {
process.getuid = () => 1001;
delete process.env.SUDO_UID;
expect(getSafeHostUid()).toBe('1001');
});
});

describe('getSafeHostGid', () => {
const originalGetgid = process.getgid;
const originalSudoGid = process.env.SUDO_GID;

afterEach(() => {
process.getgid = originalGetgid;
if (originalSudoGid !== undefined) {
process.env.SUDO_GID = originalSudoGid;
} else {
delete process.env.SUDO_GID;
}
});

it('should return 1000 when SUDO_GID is a system GID', () => {
process.getgid = () => 0; // Running as root
process.env.SUDO_GID = '13'; // proxy group
expect(getSafeHostGid()).toBe('1000');
});

it('should return SUDO_GID when it is a regular user GID', () => {
process.getgid = () => 0; // Running as root
process.env.SUDO_GID = '1001';
expect(getSafeHostGid()).toBe('1001');
});

it('should return 1000 when SUDO_GID is 0', () => {
process.getgid = () => 0; // Running as root
process.env.SUDO_GID = '0';
expect(getSafeHostGid()).toBe('1000');
});

it('should return 1000 when running as root without SUDO_GID', () => {
process.getgid = () => 0;
delete process.env.SUDO_GID;
expect(getSafeHostGid()).toBe('1000');
});

it('should return 1000 for non-root system GID', () => {
process.getgid = () => 13; // proxy group
delete process.env.SUDO_GID;
expect(getSafeHostGid()).toBe('1000');
});

it('should return the GID when running as regular user', () => {
process.getgid = () => 1001;
delete process.env.SUDO_GID;
expect(getSafeHostGid()).toBe('1001');
});
});

describe('MIN_REGULAR_UID constant', () => {
it('should be 1000 (standard Linux regular user UID threshold)', () => {
expect(MIN_REGULAR_UID).toBe(1000);
});
});

describe('generateDockerCompose', () => {
const mockConfig: WrapperConfig = {
allowedDomains: ['github.com', 'npmjs.org'],
Expand Down Expand Up @@ -660,8 +781,8 @@
});

it('should remove existing containers before starting', async () => {
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 784 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 784 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 784 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 784 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 785 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 785 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 785 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 785 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type

await startContainers(testDir, ['github.com']);

Expand All @@ -673,8 +794,8 @@
});

it('should run docker compose up', async () => {
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 797 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 797 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 797 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 797 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 798 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 798 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 798 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 798 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type

await startContainers(testDir, ['github.com']);

Expand All @@ -686,7 +807,7 @@
});

it('should handle docker compose failure', async () => {
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 810 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 810 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 810 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 810 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type
mockExecaFn.mockRejectedValueOnce(new Error('Docker compose failed'));

await expect(startContainers(testDir, ['github.com'])).rejects.toThrow('Docker compose failed');
Expand All @@ -701,7 +822,7 @@
'1760994429.358 172.30.0.20:36274 blocked.com:443 -:- 1.1 CONNECT 403 TCP_DENIED:HIER_NONE blocked.com:443 "curl/7.81.0"\n'
);

mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 825 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 825 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 825 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 825 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type
mockExecaFn.mockRejectedValueOnce(new Error('is unhealthy'));

await expect(startContainers(testDir, ['github.com'])).rejects.toThrow();
Expand Down Expand Up @@ -729,7 +850,7 @@
});

it('should run docker compose down when keepContainers is false', async () => {
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 853 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 853 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 853 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 853 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type

await stopContainers(testDir, false);

Expand Down Expand Up @@ -763,7 +884,7 @@

it('should return exit code from container', async () => {
// Mock docker logs -f
mockExecaFn.mockResolvedValueOnce({ stdout: '', stderr: '', exitCode: 0 } as any);

Check warning on line 887 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / ESLint

Unexpected any. Specify a different type

Check warning on line 887 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 18)

Unexpected any. Specify a different type

Check warning on line 887 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 22)

Unexpected any. Specify a different type

Check warning on line 887 in src/docker-manager.test.ts

View workflow job for this annotation

GitHub Actions / Build and Lint (Node 20)

Unexpected any. Specify a different type
// Mock docker wait
mockExecaFn.mockResolvedValueOnce({ stdout: '0', stderr: '', exitCode: 0 } as any);

Expand Down
53 changes: 41 additions & 12 deletions src/docker-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,70 @@ const SQUID_PORT = 3128;
const SQUID_INTERCEPT_PORT = 3129; // Port for transparently intercepted traffic

/**
* Gets the host user's UID, with fallback to 1000 if unavailable or root (0).
* Minimum UID/GID value for regular users.
* UIDs 0-999 are reserved for system users on most Linux distributions.
*/
export const MIN_REGULAR_UID = 1000;

/**
* Validates that a UID/GID value is safe for use (not in system range).
* Returns the value if valid, or the default (1000) if in system range.
* @internal Exported for testing
*/
export function validateIdNotInSystemRange(id: number): string {
// Reject system UIDs/GIDs (0-999) - use default unprivileged user instead
if (id < MIN_REGULAR_UID) {
return MIN_REGULAR_UID.toString();
}
return id.toString();
}

/**
* Gets the host user's UID, with fallback to 1000 if unavailable, root (0),
* or in the system UID range (0-999).
* When running with sudo, uses SUDO_UID to get the actual user's UID.
* @internal Exported for testing
*/
function getSafeHostUid(): string {
export function getSafeHostUid(): string {
const uid = process.getuid?.();

// When running as root (sudo), try to get the original user's UID
if (!uid || uid === 0) {
const sudoUid = process.env.SUDO_UID;
if (sudoUid && sudoUid !== '0') {
return sudoUid;
if (sudoUid) {
const parsedUid = parseInt(sudoUid, 10);
if (!isNaN(parsedUid)) {
return validateIdNotInSystemRange(parsedUid);
}
}
return '1000';
return MIN_REGULAR_UID.toString();
}

return uid.toString();
return validateIdNotInSystemRange(uid);
}

/**
* Gets the host user's GID, with fallback to 1000 if unavailable or root (0).
* Gets the host user's GID, with fallback to 1000 if unavailable, root (0),
* or in the system GID range (0-999).
* When running with sudo, uses SUDO_GID to get the actual user's GID.
* @internal Exported for testing
*/
function getSafeHostGid(): string {
export function getSafeHostGid(): string {
const gid = process.getgid?.();

// When running as root (sudo), try to get the original user's GID
if (!gid || gid === 0) {
const sudoGid = process.env.SUDO_GID;
if (sudoGid && sudoGid !== '0') {
return sudoGid;
if (sudoGid) {
const parsedGid = parseInt(sudoGid, 10);
if (!isNaN(parsedGid)) {
return validateIdNotInSystemRange(parsedGid);
}
}
return '1000';
return MIN_REGULAR_UID.toString();
}

return gid.toString();
return validateIdNotInSystemRange(gid);
}

/**
Expand Down
Loading