Enable non-localhost connections by specifying allowed IPs/CIDRs in settings (e.g., 100.64.0.0/10 for Tailscale). Server auto-binds to 0.0.0.0 when remote IPs are configured, with three-layer validation (source IP, CORS, host header) plus mandatory Bearer token auth.
142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
/**
|
|
* Unit tests for network-utils
|
|
*/
|
|
|
|
import { parseAllowedIPs, isIPAllowed, AllowedIPEntry } from '../src/utils/network-utils';
|
|
|
|
describe('parseAllowedIPs', () => {
|
|
test('should return empty array for empty string', () => {
|
|
expect(parseAllowedIPs('')).toEqual([]);
|
|
});
|
|
|
|
test('should return empty array for whitespace-only string', () => {
|
|
expect(parseAllowedIPs(' ')).toEqual([]);
|
|
});
|
|
|
|
test('should parse a single IP', () => {
|
|
const result = parseAllowedIPs('192.168.1.1');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('ip');
|
|
});
|
|
|
|
test('should parse multiple comma-separated IPs', () => {
|
|
const result = parseAllowedIPs('192.168.1.1, 10.0.0.5');
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
test('should parse CIDR notation', () => {
|
|
const result = parseAllowedIPs('100.64.0.0/10');
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0].type).toBe('cidr');
|
|
});
|
|
|
|
test('should parse mixed IPs and CIDRs', () => {
|
|
const result = parseAllowedIPs('192.168.1.1, 10.0.0.0/8, 172.16.0.5');
|
|
expect(result).toHaveLength(3);
|
|
expect(result[0].type).toBe('ip');
|
|
expect(result[1].type).toBe('cidr');
|
|
expect(result[2].type).toBe('ip');
|
|
});
|
|
|
|
test('should handle extra whitespace', () => {
|
|
const result = parseAllowedIPs(' 192.168.1.1 , 10.0.0.5 ');
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
test('should skip invalid entries', () => {
|
|
const result = parseAllowedIPs('192.168.1.1, invalid, 10.0.0.5');
|
|
expect(result).toHaveLength(2);
|
|
});
|
|
|
|
test('should skip invalid CIDR prefix', () => {
|
|
const result = parseAllowedIPs('10.0.0.0/33');
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
test('should skip entries with invalid octets', () => {
|
|
const result = parseAllowedIPs('256.0.0.1');
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
|
|
test('should handle trailing commas', () => {
|
|
const result = parseAllowedIPs('192.168.1.1,');
|
|
expect(result).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('isIPAllowed', () => {
|
|
test('should always allow 127.0.0.1 with empty list', () => {
|
|
expect(isIPAllowed('127.0.0.1', [])).toBe(true);
|
|
});
|
|
|
|
test('should always allow localhost with empty list', () => {
|
|
expect(isIPAllowed('localhost', [])).toBe(true);
|
|
});
|
|
|
|
test('should always allow IPv4-mapped localhost', () => {
|
|
expect(isIPAllowed('::ffff:127.0.0.1', [])).toBe(true);
|
|
});
|
|
|
|
test('should reject non-localhost with empty list', () => {
|
|
expect(isIPAllowed('192.168.1.1', [])).toBe(false);
|
|
});
|
|
|
|
test('should match exact IP', () => {
|
|
const allowList = parseAllowedIPs('192.168.1.50');
|
|
expect(isIPAllowed('192.168.1.50', allowList)).toBe(true);
|
|
expect(isIPAllowed('192.168.1.51', allowList)).toBe(false);
|
|
});
|
|
|
|
test('should match CIDR range', () => {
|
|
const allowList = parseAllowedIPs('10.0.0.0/8');
|
|
expect(isIPAllowed('10.0.0.1', allowList)).toBe(true);
|
|
expect(isIPAllowed('10.255.255.255', allowList)).toBe(true);
|
|
expect(isIPAllowed('11.0.0.1', allowList)).toBe(false);
|
|
});
|
|
|
|
test('should match Tailscale CGNAT range (100.64.0.0/10)', () => {
|
|
const allowList = parseAllowedIPs('100.64.0.0/10');
|
|
expect(isIPAllowed('100.64.0.1', allowList)).toBe(true);
|
|
expect(isIPAllowed('100.100.50.25', allowList)).toBe(true);
|
|
expect(isIPAllowed('100.127.255.255', allowList)).toBe(true);
|
|
expect(isIPAllowed('100.128.0.0', allowList)).toBe(false);
|
|
expect(isIPAllowed('100.63.255.255', allowList)).toBe(false);
|
|
});
|
|
|
|
test('should handle IPv4-mapped IPv6 addresses', () => {
|
|
const allowList = parseAllowedIPs('192.168.1.50');
|
|
expect(isIPAllowed('::ffff:192.168.1.50', allowList)).toBe(true);
|
|
expect(isIPAllowed('::ffff:192.168.1.51', allowList)).toBe(false);
|
|
});
|
|
|
|
test('should handle IPv4-mapped IPv6 with CIDR', () => {
|
|
const allowList = parseAllowedIPs('10.0.0.0/8');
|
|
expect(isIPAllowed('::ffff:10.5.3.1', allowList)).toBe(true);
|
|
expect(isIPAllowed('::ffff:11.0.0.1', allowList)).toBe(false);
|
|
});
|
|
|
|
test('should match against multiple entries', () => {
|
|
const allowList = parseAllowedIPs('192.168.1.0/24, 10.0.0.5');
|
|
expect(isIPAllowed('192.168.1.100', allowList)).toBe(true);
|
|
expect(isIPAllowed('10.0.0.5', allowList)).toBe(true);
|
|
expect(isIPAllowed('10.0.0.6', allowList)).toBe(false);
|
|
});
|
|
|
|
test('should handle /32 CIDR as single IP', () => {
|
|
const allowList = parseAllowedIPs('192.168.1.1/32');
|
|
expect(isIPAllowed('192.168.1.1', allowList)).toBe(true);
|
|
expect(isIPAllowed('192.168.1.2', allowList)).toBe(false);
|
|
});
|
|
|
|
test('should handle /0 CIDR as allow-all', () => {
|
|
const allowList = parseAllowedIPs('0.0.0.0/0');
|
|
expect(isIPAllowed('1.2.3.4', allowList)).toBe(true);
|
|
expect(isIPAllowed('255.255.255.255', allowList)).toBe(true);
|
|
});
|
|
|
|
test('should return false for invalid IP input', () => {
|
|
const allowList = parseAllowedIPs('10.0.0.0/8');
|
|
expect(isIPAllowed('not-an-ip', allowList)).toBe(false);
|
|
});
|
|
});
|