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. 18
      docs/content/guides/clients.md
  2. 22
      src/server/utils/firewall.ts
  3. 8
      src/server/utils/types.ts

18
docs/content/guides/clients.md

@ -23,9 +23,12 @@ 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.
Unlike "Allowed IPs" which only controls routing on the client side, these rules are enforced by the server using iptables/ip6tables and cannot be bypassed by the client. Unlike "Allowed IPs" which only controls routing on the client side, these rules are enforced by the server using iptables/ip6tables and cannot be bypassed by the client.
@ -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