Compare commits
4 Commits
b1701865ab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e6393d9645 | |||
| e9584929a4 | |||
| e91b9f6025 | |||
| 8e97c2fef0 |
14
CHANGELOG.md
14
CHANGELOG.md
@@ -6,6 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
|
||||
---
|
||||
|
||||
## [1.3.0] - 2026-02-06
|
||||
|
||||
### Added
|
||||
- **Remote IP access**: New `allowedIPs` setting accepts comma-separated IPs and CIDR ranges (e.g., `100.64.0.0/10` for Tailscale) to allow non-localhost connections
|
||||
- Server automatically binds to `0.0.0.0` when remote IPs are configured, otherwise stays on `127.0.0.1`
|
||||
- Three-layer network validation: source IP check, CORS origin check, and host header validation
|
||||
- Bearer token authentication remains mandatory for all connections
|
||||
- Localhost is always implicitly allowed — cannot lock out local access
|
||||
- IPv4-mapped IPv6 addresses (`::ffff:x.x.x.x`) handled transparently
|
||||
- New `network-utils` module with CIDR parsing and IP matching (no external dependencies)
|
||||
- Security warning displayed in settings when remote access is enabled
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] - 2026-01-31
|
||||
|
||||
### Added
|
||||
|
||||
87
docs/plans/2026-02-06-remote-ip-access-design.md
Normal file
87
docs/plans/2026-02-06-remote-ip-access-design.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Remote IP access for MCP server
|
||||
|
||||
Date: 2026-02-06
|
||||
|
||||
## Problem
|
||||
|
||||
The MCP server is hardcoded to localhost-only access (bind address, CORS, host header validation). This prevents use cases where Obsidian runs in a Docker container or remote machine and MCP clients connect over a Tailscale VPN or other private network.
|
||||
|
||||
## Design
|
||||
|
||||
### New setting: `allowedIPs`
|
||||
|
||||
A comma-separated string of IPs and CIDR ranges. Default: `""` (empty).
|
||||
|
||||
- When empty: server behaves exactly as today (binds to `127.0.0.1`, localhost-only)
|
||||
- When populated: server binds to `0.0.0.0` and allows connections from listed IPs/CIDRs
|
||||
- Localhost (`127.0.0.1`) is always implicitly allowed regardless of the setting
|
||||
- Examples: `100.64.0.0/10`, `192.168.1.50`, `10.0.0.0/8`
|
||||
|
||||
### Middleware changes (src/server/middleware.ts)
|
||||
|
||||
Three layers are updated:
|
||||
|
||||
1. **Source IP validation (new)** - Checks `req.socket.remoteAddress` against the allow-list before auth. Rejects connections from unlisted IPs with 403. Localhost always passes.
|
||||
|
||||
2. **CORS policy update** - Extends the origin check to allow origins whose hostname matches the allow-list, in addition to the existing localhost regex.
|
||||
|
||||
3. **Host header validation update** - Extends to accept Host headers matching allowed IPs, in addition to localhost.
|
||||
|
||||
All three use a shared `isIPAllowed()` utility.
|
||||
|
||||
### Server bind (src/server/mcp-server.ts)
|
||||
|
||||
The `start()` method computes bind address dynamically:
|
||||
- `allowedIPs` non-empty (trimmed) -> bind `0.0.0.0`
|
||||
- `allowedIPs` empty -> bind `127.0.0.1` (current behavior)
|
||||
|
||||
### Network utilities (src/utils/network-utils.ts)
|
||||
|
||||
New file (~40 lines) exporting:
|
||||
|
||||
- `parseAllowedIPs(setting: string): AllowedIPEntry[]` - Parses comma-separated string into structured list of individual IPs and CIDR ranges
|
||||
- `isIPAllowed(ip: string, allowList: AllowedIPEntry[]): boolean` - Checks if an IP matches any entry. Handles IPv4-mapped IPv6 addresses (`::ffff:x.x.x.x`) that Node.js uses for `req.socket.remoteAddress`
|
||||
|
||||
CIDR matching is standard bit arithmetic, no external dependencies needed.
|
||||
|
||||
### Settings UI (src/settings.ts)
|
||||
|
||||
New text field below the Port setting:
|
||||
- Name: "Allowed IPs"
|
||||
- Description: "Comma-separated IPs or CIDR ranges allowed to connect (e.g., 100.64.0.0/10, 192.168.1.50). Leave empty for localhost only. Restart required."
|
||||
- Placeholder: `100.64.0.0/10, 192.168.1.0/24`
|
||||
- Shows restart warning when changed while server is running
|
||||
- Shows security note when non-empty: "Server is accessible from non-localhost IPs. Ensure your API key is kept secure."
|
||||
|
||||
Status display updates to show actual bind address (`0.0.0.0` vs `127.0.0.1`).
|
||||
|
||||
Generated client configs (Windsurf/Claude Code) stay as `127.0.0.1` - users adjust manually for remote access.
|
||||
|
||||
### Settings type (src/types/settings-types.ts)
|
||||
|
||||
Add `allowedIPs: string` to `MCPServerSettings` with default `""`.
|
||||
|
||||
## Security model
|
||||
|
||||
- **Auth is still mandatory.** IP allow-list is defense-in-depth, not a replacement for Bearer token authentication.
|
||||
- **Localhost always allowed.** Cannot accidentally lock out local access.
|
||||
- **Empty default = current behavior.** Zero-change upgrade for existing users. Feature is opt-in.
|
||||
- **Three-layer validation:** Source IP check + CORS + Host header validation + Bearer auth.
|
||||
|
||||
## Testing
|
||||
|
||||
New file `tests/network-utils.test.ts`:
|
||||
- Individual IP match/mismatch
|
||||
- CIDR range matching (e.g., `100.64.0.0/10` matches `100.100.1.1`)
|
||||
- IPv4-mapped IPv6 handling (`::ffff:192.168.1.1`)
|
||||
- Edge cases: empty string, malformed entries, extra whitespace
|
||||
- Localhost always allowed regardless of list contents
|
||||
|
||||
## Files changed
|
||||
|
||||
1. `src/types/settings-types.ts` - Add `allowedIPs` field
|
||||
2. `src/utils/network-utils.ts` - New file: CIDR parsing + IP matching
|
||||
3. `src/server/middleware.ts` - Update CORS, host validation, add source IP check
|
||||
4. `src/server/mcp-server.ts` - Dynamic bind address
|
||||
5. `src/settings.ts` - New text field + security note
|
||||
6. `tests/network-utils.test.ts` - New test file
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "mcp-server",
|
||||
"name": "MCP Server",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Exposes vault operations via Model Context Protocol (MCP) over HTTP.",
|
||||
"author": "William Ballou",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mcp-server",
|
||||
"version": "1.2.1",
|
||||
"version": "1.3.0",
|
||||
"description": "MCP (Model Context Protocol) server plugin - exposes vault operations via HTTP",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -101,7 +101,8 @@ export class MCPServer {
|
||||
public async start(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
this.server = this.app.listen(this.settings.port, '127.0.0.1', () => {
|
||||
const bindAddress = this.settings.allowedIPs?.trim() ? '0.0.0.0' : '127.0.0.1';
|
||||
this.server = this.app.listen(this.settings.port, bindAddress, () => {
|
||||
resolve();
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,24 @@ import express from 'express';
|
||||
import cors from 'cors';
|
||||
import { MCPServerSettings } from '../types/settings-types';
|
||||
import { ErrorCodes, JSONRPCResponse } from '../types/mcp-types';
|
||||
import { parseAllowedIPs, isIPAllowed } from '../utils/network-utils';
|
||||
|
||||
export function setupMiddleware(app: Express, settings: MCPServerSettings, createErrorResponse: (id: string | number | null, code: number, message: string) => JSONRPCResponse): void {
|
||||
const allowList = parseAllowedIPs(settings.allowedIPs);
|
||||
|
||||
// Parse JSON bodies
|
||||
app.use(express.json());
|
||||
|
||||
// CORS configuration - Always enabled with fixed localhost-only policy
|
||||
// Source IP validation - reject connections from unlisted IPs before any other checks
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
const remoteAddress = req.socket.remoteAddress;
|
||||
if (remoteAddress && !isIPAllowed(remoteAddress, allowList)) {
|
||||
return res.status(403).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Connection from this IP is not allowed'));
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// CORS configuration
|
||||
const corsOptions = {
|
||||
origin: (origin: string | undefined, callback: (err: Error | null, allow?: boolean) => void) => {
|
||||
// Allow requests with no origin (like CLI clients, curl, MCP SDKs)
|
||||
@@ -19,10 +31,22 @@ export function setupMiddleware(app: Express, settings: MCPServerSettings, creat
|
||||
// Allow localhost and 127.0.0.1 on any port, both HTTP and HTTPS
|
||||
const localhostRegex = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/;
|
||||
if (localhostRegex.test(origin)) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
return callback(null, true);
|
||||
}
|
||||
|
||||
// Check if origin hostname is in the allow-list
|
||||
if (allowList.length > 0) {
|
||||
try {
|
||||
const url = new URL(origin);
|
||||
if (isIPAllowed(url.hostname, allowList)) {
|
||||
return callback(null, true);
|
||||
}
|
||||
} catch {
|
||||
// Invalid origin URL, fall through to reject
|
||||
}
|
||||
}
|
||||
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
},
|
||||
credentials: true
|
||||
};
|
||||
@@ -44,15 +68,27 @@ export function setupMiddleware(app: Express, settings: MCPServerSettings, creat
|
||||
next();
|
||||
});
|
||||
|
||||
// Origin validation for security (DNS rebinding protection)
|
||||
// Host header validation for security (DNS rebinding protection)
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
const host = req.headers.host;
|
||||
|
||||
// Only allow localhost connections
|
||||
if (host && !host.startsWith('localhost') && !host.startsWith('127.0.0.1')) {
|
||||
return res.status(403).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Only localhost connections allowed'));
|
||||
if (!host) {
|
||||
return next();
|
||||
}
|
||||
|
||||
next();
|
||||
// Strip port from host header
|
||||
const hostname = host.split(':')[0];
|
||||
|
||||
// Always allow localhost
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Check against allow-list
|
||||
if (allowList.length > 0 && isIPAllowed(hostname, allowList)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
return res.status(403).json(createErrorResponse(null, ErrorCodes.InvalidRequest, 'Connection from this host is not allowed'));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -138,9 +138,10 @@ export class MCPServerSettingTab extends PluginSettingTab {
|
||||
const statusEl = containerEl.createEl('div', {cls: 'mcp-server-status'});
|
||||
const isRunning = this.plugin.mcpServer?.isRunning() ?? false;
|
||||
|
||||
const bindAddress = this.plugin.settings.allowedIPs?.trim() ? '0.0.0.0' : '127.0.0.1';
|
||||
statusEl.createEl('p', {
|
||||
text: isRunning
|
||||
? `✅ Running on http://127.0.0.1:${this.plugin.settings.port}/mcp`
|
||||
? `✅ Running on http://${bindAddress}:${this.plugin.settings.port}/mcp`
|
||||
: '⭕ Stopped'
|
||||
});
|
||||
|
||||
@@ -203,6 +204,29 @@ export class MCPServerSettingTab extends PluginSettingTab {
|
||||
}
|
||||
}));
|
||||
|
||||
// Allowed IPs setting
|
||||
new Setting(containerEl)
|
||||
.setName('Allowed IPs')
|
||||
.setDesc('Comma-separated IPs or CIDR ranges allowed to connect remotely (e.g., 100.64.0.0/10, 192.168.1.50). Leave empty for localhost only. Restart required.')
|
||||
.addText(text => text
|
||||
.setPlaceholder('100.64.0.0/10, 192.168.1.0/24')
|
||||
.setValue(this.plugin.settings.allowedIPs)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.allowedIPs = value;
|
||||
await this.plugin.saveSettings();
|
||||
if (this.plugin.mcpServer?.isRunning()) {
|
||||
new Notice('⚠️ Server restart required for allowed IPs changes to take effect');
|
||||
}
|
||||
}));
|
||||
|
||||
// Security note when remote access is enabled
|
||||
if (this.plugin.settings.allowedIPs?.trim()) {
|
||||
const securityNote = containerEl.createEl('div', {cls: 'mcp-security-note'});
|
||||
securityNote.createEl('p', {
|
||||
text: '⚠️ Server is accessible from non-localhost IPs. Ensure your API key is kept secure.'
|
||||
});
|
||||
}
|
||||
|
||||
// Authentication (Always Enabled)
|
||||
const authDetails = containerEl.createEl('details', {cls: 'mcp-auth-section'});
|
||||
const authSummary = authDetails.createEl('summary', {cls: 'mcp-auth-summary'});
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface MCPServerSettings {
|
||||
port: number;
|
||||
apiKey: string; // Now required, not optional
|
||||
enableAuth: boolean; // Will be removed in future, kept for migration
|
||||
allowedIPs: string; // Comma-separated IPs/CIDRs allowed to connect remotely
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
@@ -20,6 +21,7 @@ export const DEFAULT_SETTINGS: MCPPluginSettings = {
|
||||
port: 3000,
|
||||
apiKey: '', // Will be auto-generated on first load
|
||||
enableAuth: true, // Always true now
|
||||
allowedIPs: '', // Empty = localhost only
|
||||
autoStart: false,
|
||||
// Notification defaults
|
||||
notificationsEnabled: false,
|
||||
|
||||
84
src/utils/network-utils.ts
Normal file
84
src/utils/network-utils.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
export interface AllowedIPEntry {
|
||||
type: 'ip' | 'cidr';
|
||||
ip: number; // 32-bit numeric IPv4
|
||||
mask: number; // 32-bit subnet mask (only for CIDR)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert dotted IPv4 string to 32-bit number.
|
||||
* Returns null if invalid.
|
||||
*/
|
||||
function ipToNumber(ip: string): number | null {
|
||||
const parts = ip.split('.');
|
||||
if (parts.length !== 4) return null;
|
||||
let num = 0;
|
||||
for (const part of parts) {
|
||||
const octet = parseInt(part, 10);
|
||||
if (isNaN(octet) || octet < 0 || octet > 255) return null;
|
||||
num = (num << 8) | octet;
|
||||
}
|
||||
return num >>> 0; // Ensure unsigned
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip IPv4-mapped IPv6 prefix (::ffff:) if present.
|
||||
*/
|
||||
function normalizeIP(ip: string): string {
|
||||
if (ip.startsWith('::ffff:')) {
|
||||
return ip.slice(7);
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a comma-separated string of IPs and CIDRs into structured entries.
|
||||
* Invalid entries are silently skipped.
|
||||
*/
|
||||
export function parseAllowedIPs(setting: string): AllowedIPEntry[] {
|
||||
if (!setting || !setting.trim()) return [];
|
||||
|
||||
const entries: AllowedIPEntry[] = [];
|
||||
for (const raw of setting.split(',')) {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
if (trimmed.includes('/')) {
|
||||
const [ipStr, prefixStr] = trimmed.split('/');
|
||||
const ip = ipToNumber(ipStr);
|
||||
const prefix = parseInt(prefixStr, 10);
|
||||
if (ip === null || isNaN(prefix) || prefix < 0 || prefix > 32) continue;
|
||||
const mask = prefix === 0 ? 0 : (~0 << (32 - prefix)) >>> 0;
|
||||
entries.push({ type: 'cidr', ip: (ip & mask) >>> 0, mask });
|
||||
} else {
|
||||
const ip = ipToNumber(trimmed);
|
||||
if (ip === null) continue;
|
||||
entries.push({ type: 'ip', ip, mask: 0xFFFFFFFF });
|
||||
}
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an IP address is allowed by the given allow-list.
|
||||
* Localhost (127.0.0.1) is always allowed.
|
||||
*/
|
||||
export function isIPAllowed(ip: string, allowList: AllowedIPEntry[]): boolean {
|
||||
const normalized = normalizeIP(ip);
|
||||
|
||||
// Localhost is always allowed
|
||||
if (normalized === '127.0.0.1' || normalized === 'localhost') return true;
|
||||
|
||||
if (allowList.length === 0) return false;
|
||||
|
||||
const num = ipToNumber(normalized);
|
||||
if (num === null) return false;
|
||||
|
||||
for (const entry of allowList) {
|
||||
if (entry.type === 'ip') {
|
||||
if (num === entry.ip) return true;
|
||||
} else {
|
||||
if (((num & entry.mask) >>> 0) === entry.ip) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
141
tests/network-utils.test.ts
Normal file
141
tests/network-utils.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
@@ -4,5 +4,7 @@
|
||||
"1.1.0": "0.15.0",
|
||||
"1.1.1": "0.15.0",
|
||||
"1.1.2": "0.15.0",
|
||||
"1.1.3": "0.15.0"
|
||||
"1.1.3": "0.15.0",
|
||||
"1.2.0": "0.15.0",
|
||||
"1.3.0": "0.15.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user