Files
obsidian-mcp-server/tests/network-utils.test.ts
Bill Ballou e91b9f6025 feat: add configurable IP allow-list for remote MCP access
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.
2026-02-06 20:35:49 -05:00

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);
});
});