Browse Source

Document all allowed IP/cidr/port/proto combinations that are allowed

and check on save
pull/2418/head
Ian Foster 6 months ago
parent
commit
2df1f38b1a
  1. 20
      docs/content/guides/clients.md
  2. 22
      src/server/utils/firewall.ts
  3. 8
      src/server/utils/types.ts

20
docs/content/guides/clients.md

@ -23,8 +23,11 @@ Use the Firewall Allowed IPs feature to prevent access to IP ranges that the use
## Firewall Allowed IPs ## Firewall Allowed IPs
!!! note /// note | Attention
This field only appears when **Per-Client Firewall** is enabled in the Admin Panel → Interface settings.
This field only appears when **Per-Client Firewall** is enabled in the Admin Panel → Interface settings.
///
Server-side firewall rules that restrict which destinations the client can access, regardless of their local configuration. Server-side firewall rules that restrict which destinations the client can access, regardless of their local configuration.
@ -37,7 +40,20 @@ Unlike "Allowed IPs" which only controls routing on the client side, these rules
- `192.168.1.5:443` - Allow access to specific port (TCP+UDP) - `192.168.1.5:443` - Allow access to specific port (TCP+UDP)
- `192.168.1.5:443/tcp` - Allow access to specific port (TCP only) - `192.168.1.5:443/tcp` - Allow access to specific port (TCP only)
- `192.168.1.5:443/udp` - Allow access to specific port (UDP only) - `192.168.1.5:443/udp` - Allow access to specific port (UDP only)
- `10.10.0.0/24:443` - Allow access to an entire subnet on a specific port (TCP+UDP)
- `10.10.0.0/24:443/tcp` - Allow access to an entire subnet on a specific port (TCP only)
- `10.10.0.0/24:443/udp` - Allow access to an entire subnet on a specific port (UDP only)
- `[2001:db8::1]:443` - IPv6 address with port (brackets required) - `[2001:db8::1]:443` - IPv6 address with port (brackets required)
- `[2001:db8::/32]:443/tcp` - IPv6 CIDR with port and protocol
/// warning | Invalid Formats
Protocol specifiers (`/tcp` or `/udp`) require a port number. The following formats are **not supported** and will result in an error:
- `10.10.0.3/tcp` (use `10.10.0.3:443/tcp` instead)
- `10.10.0.0/24/udp` (use `10.10.0.0/24:53/udp` instead)
///
**Behavior:** **Behavior:**

22
src/server/utils/firewall.ts

@ -29,6 +29,13 @@ type ParsedEntry = {
* - CIDR: "10.0.0.0/24" or "2001:db8::/32" * - CIDR: "10.0.0.0/24" or "2001:db8::/32"
* - IP:port: "10.0.0.1:443" or "[2001:db8::1]:443" * - IP:port: "10.0.0.1:443" or "[2001:db8::1]:443"
* - IP:port/proto: "10.0.0.1:443/tcp" or "10.0.0.1:53/udp" * - IP:port/proto: "10.0.0.1:443/tcp" or "10.0.0.1:53/udp"
* - CIDR:port: "10.0.0.0/24:443"
* - CIDR:port/proto: "10.0.0.0/24:443/tcp" or "10.0.0.0/24:53/udp"
*
* Note: Protocol (/tcp or /udp) requires a port. "IP/tcp" or "CIDR/tcp" without
* a port is invalid and will throw an error.
*
* @throws {Error} If protocol is specified without a port
*/ */
function parseFirewallEntry(entry: string): ParsedEntry { function parseFirewallEntry(entry: string): ParsedEntry {
// Extract protocol suffix first: /tcp or /udp // Extract protocol suffix first: /tcp or /udp
@ -52,8 +59,18 @@ function parseFirewallEntry(entry: string): ParsedEntry {
// Just bracketed IPv6 without port // Just bracketed IPv6 without port
const ipMatch = remaining.match(/^\[(.+)\]$/); const ipMatch = remaining.match(/^\[(.+)\]$/);
if (ipMatch) { if (ipMatch) {
if (proto) {
throw new Error(
`Invalid firewall entry "${entry}": Protocol (/${proto}) requires a port. Use format like "[${ipMatch[1]}]:443/${proto}"`
);
}
return { ip: ipMatch[1] }; return { ip: ipMatch[1] };
} }
if (proto) {
throw new Error(
`Invalid firewall entry "${entry}": Protocol (/${proto}) requires a port`
);
}
return { ip: remaining }; return { ip: remaining };
} }
@ -75,6 +92,11 @@ function parseFirewallEntry(entry: string): ParsedEntry {
} }
// Plain IP or CIDR (IPv4 or IPv6) // Plain IP or CIDR (IPv4 or IPv6)
if (proto) {
throw new Error(
`Invalid firewall entry "${entry}": Protocol (/${proto}) requires a port. Use format like "${remaining}:443/${proto}"`
);
}
return { ip: remaining }; return { ip: remaining };
} }

8
src/server/utils/types.ts

@ -99,7 +99,8 @@ const FirewallIpEntrySchema = z
.min(1, { message: t('zod.client.firewallIps') }) .min(1, { message: t('zod.client.firewallIps') })
.refine( .refine(
(entry) => { (entry) => {
// Remove protocol suffix if present (/tcp, /udp) // Check if protocol suffix is present
const hasProto = /\/(tcp|udp)$/i.test(entry);
const entryWithoutProto = entry.replace(/\/(tcp|udp)$/i, ''); const entryWithoutProto = entry.replace(/\/(tcp|udp)$/i, '');
// Check if it's IP:port format // Check if it's IP:port format
@ -115,6 +116,11 @@ const FirewallIpEntrySchema = z
return (isIP(cleanIp) || isCidr(cleanIp)) && port >= 1 && port <= 65535; return (isIP(cleanIp) || isCidr(cleanIp)) && port >= 1 && port <= 65535;
} }
// If protocol was specified without a port, it's invalid
if (hasProto) {
return false;
}
// Check if it's just IP or CIDR // Check if it's just IP or CIDR
return isIP(entryWithoutProto) || isCidr(entryWithoutProto); return isIP(entryWithoutProto) || isCidr(entryWithoutProto);
}, },

Loading…
Cancel
Save