|
|
|
@ -16,6 +16,11 @@ let rebuildQueued = false; |
|
|
|
// Cache iptables availability check result
|
|
|
|
let iptablesAvailable: boolean | null = null; |
|
|
|
|
|
|
|
// Cache whether iptables supports the comment module (xt_comment kernel module).
|
|
|
|
// Some systems (e.g. Synology DSM) ship iptables without the xt_comment kernel
|
|
|
|
// module, which makes `-m comment` fail at rule insertion. See #2545.
|
|
|
|
let commentSupported: boolean | null = null; |
|
|
|
|
|
|
|
type ParsedEntry = { |
|
|
|
ip: string; |
|
|
|
port?: number; |
|
|
|
@ -160,6 +165,81 @@ function generateRuleArgs( |
|
|
|
return rules; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Probe whether the given iptables binary can actually insert a rule that uses |
|
|
|
* the comment module. The userspace extension (libxt_comment) may be present |
|
|
|
* while the xt_comment kernel module is missing, in which case parsing succeeds |
|
|
|
* but rule insertion fails. We therefore probe by inserting a throwaway rule |
|
|
|
* into a dedicated probe chain rather than relying on `--help`. |
|
|
|
*/ |
|
|
|
async function probeComment(bin: 'iptables' | 'ip6tables'): Promise<boolean> { |
|
|
|
const probeChain = `${CHAIN_NAME}_PROBE`; |
|
|
|
try { |
|
|
|
await exec( |
|
|
|
`${bin} -N ${probeChain} 2>/dev/null || ${bin} -F ${probeChain}` |
|
|
|
); |
|
|
|
await exec( |
|
|
|
`${bin} -A ${probeChain} -m comment --comment "wg-easy comment probe" -j RETURN` |
|
|
|
); |
|
|
|
return true; |
|
|
|
} catch { |
|
|
|
return false; |
|
|
|
} finally { |
|
|
|
await exec(`${bin} -F ${probeChain} 2>/dev/null || true`); |
|
|
|
await exec(`${bin} -X ${probeChain} 2>/dev/null || true`); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Build the list of iptables/ip6tables commands needed to allow a client's |
|
|
|
* traffic. Pure function (no side effects) so it can be unit tested. |
|
|
|
* |
|
|
|
* When `useComments` is false the `-m comment` argument is omitted entirely, |
|
|
|
* which keeps rule creation working on hosts without the xt_comment kernel |
|
|
|
* module (see #2545). |
|
|
|
*/ |
|
|
|
function buildClientRuleCommands( |
|
|
|
client: FirewallClient, |
|
|
|
defaultAllowedIps: string[], |
|
|
|
enableIpv6: boolean, |
|
|
|
useComments: boolean |
|
|
|
): string[] { |
|
|
|
// Determine which IPs to use for firewall rules
|
|
|
|
// Priority: firewallIps > allowedIps > defaultAllowedIps
|
|
|
|
const effectiveIps = |
|
|
|
client.firewallIps && client.firewallIps.length > 0 |
|
|
|
? client.firewallIps |
|
|
|
: (client.allowedIps ?? defaultAllowedIps); |
|
|
|
|
|
|
|
const comment = useComments |
|
|
|
? sanitizeComment(client.id, client.name) |
|
|
|
: undefined; |
|
|
|
|
|
|
|
const commands: string[] = []; |
|
|
|
|
|
|
|
for (const ipEntry of effectiveIps) { |
|
|
|
const parsed = parseFirewallEntry(ipEntry); |
|
|
|
const baseIp = parsed.ip.split('/')[0] ?? parsed.ip; // Handle CIDR by checking base IP
|
|
|
|
const destIsIpv6 = isIPv6(baseIp); |
|
|
|
|
|
|
|
if (destIsIpv6) { |
|
|
|
if (enableIpv6) { |
|
|
|
const rules = generateRuleArgs(client.ipv6Address, parsed, comment); |
|
|
|
for (const rule of rules) { |
|
|
|
commands.push(`ip6tables ${rule}`); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
const rules = generateRuleArgs(client.ipv4Address, parsed, comment); |
|
|
|
for (const rule of rules) { |
|
|
|
commands.push(`iptables ${rule}`); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return commands; |
|
|
|
} |
|
|
|
|
|
|
|
export const firewall = { |
|
|
|
/** |
|
|
|
* Initialize the custom chain if it doesn't exist |
|
|
|
@ -198,39 +278,22 @@ export const firewall = { |
|
|
|
async applyClientRules( |
|
|
|
client: FirewallClient, |
|
|
|
defaultAllowedIps: string[], |
|
|
|
enableIpv6: boolean |
|
|
|
enableIpv6: boolean, |
|
|
|
useComments: boolean = true |
|
|
|
): Promise<void> { |
|
|
|
// Determine which IPs to use for firewall rules
|
|
|
|
// Priority: firewallIps > allowedIps > defaultAllowedIps
|
|
|
|
const effectiveIps = |
|
|
|
client.firewallIps && client.firewallIps.length > 0 |
|
|
|
? client.firewallIps |
|
|
|
: (client.allowedIps ?? defaultAllowedIps); |
|
|
|
const commands = buildClientRuleCommands( |
|
|
|
client, |
|
|
|
defaultAllowedIps, |
|
|
|
enableIpv6, |
|
|
|
useComments |
|
|
|
); |
|
|
|
|
|
|
|
FW_DEBUG( |
|
|
|
`Applying firewall rules for client ${client.name} (${client.id}): ${effectiveIps.join(', ')}` |
|
|
|
`Applying ${commands.length} firewall rule(s) for client ${client.name} (${client.id})` |
|
|
|
); |
|
|
|
|
|
|
|
const comment = sanitizeComment(client.id, client.name); |
|
|
|
|
|
|
|
for (const ipEntry of effectiveIps) { |
|
|
|
const parsed = parseFirewallEntry(ipEntry); |
|
|
|
const baseIp = parsed.ip.split('/')[0] ?? parsed.ip; // Handle CIDR by checking base IP
|
|
|
|
const destIsIpv6 = isIPv6(baseIp); |
|
|
|
|
|
|
|
if (destIsIpv6) { |
|
|
|
if (enableIpv6) { |
|
|
|
const rules = generateRuleArgs(client.ipv6Address, parsed, comment); |
|
|
|
for (const rule of rules) { |
|
|
|
await exec(`ip6tables ${rule}`); |
|
|
|
} |
|
|
|
} |
|
|
|
} else { |
|
|
|
const rules = generateRuleArgs(client.ipv4Address, parsed, comment); |
|
|
|
for (const rule of rules) { |
|
|
|
await exec(`iptables ${rule}`); |
|
|
|
} |
|
|
|
} |
|
|
|
for (const command of commands) { |
|
|
|
await exec(command); |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
@ -267,13 +330,18 @@ export const firewall = { |
|
|
|
// Flush existing rules
|
|
|
|
await this.flushChain(); |
|
|
|
|
|
|
|
// Detect once whether the comment module is usable. Without it, adding
|
|
|
|
// rules with `-m comment` fails and would abort the whole rebuild (#2545).
|
|
|
|
const useComments = await this.supportsComment(enableIpv6); |
|
|
|
|
|
|
|
// Apply rules for each enabled client
|
|
|
|
for (const client of clients) { |
|
|
|
if (!client.enabled) continue; |
|
|
|
await this.applyClientRules( |
|
|
|
client, |
|
|
|
userConfig.defaultAllowedIps, |
|
|
|
enableIpv6 |
|
|
|
enableIpv6, |
|
|
|
useComments |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
@ -349,11 +417,45 @@ export const firewall = { |
|
|
|
} |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* Check whether iptables (and ip6tables, when IPv6 is enabled) can insert |
|
|
|
* rules using the comment module. The result is cached. |
|
|
|
* |
|
|
|
* Some systems ship iptables without the xt_comment kernel module. Adding a |
|
|
|
* rule with `-m comment` then fails, which previously aborted the whole |
|
|
|
* firewall rebuild and made enabling the feature impossible (#2545). When |
|
|
|
* comments are unsupported we fall back to creating rules without them. |
|
|
|
* |
|
|
|
* @param enableIpv6 - If true, also require ip6tables to support comments. |
|
|
|
*/ |
|
|
|
async supportsComment(enableIpv6: boolean = true): Promise<boolean> { |
|
|
|
// Return cached result if we've already checked
|
|
|
|
if (commentSupported !== null) { |
|
|
|
return commentSupported; |
|
|
|
} |
|
|
|
|
|
|
|
const ipv4Supported = await probeComment('iptables'); |
|
|
|
const ipv6Supported = enableIpv6 ? await probeComment('ip6tables') : true; |
|
|
|
|
|
|
|
commentSupported = ipv4Supported && ipv6Supported; |
|
|
|
|
|
|
|
if (commentSupported) { |
|
|
|
FW_DEBUG('iptables comment module is available'); |
|
|
|
} else { |
|
|
|
FW_DEBUG( |
|
|
|
'iptables comment module (xt_comment) is not available, creating firewall rules without comments' |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
return commentSupported; |
|
|
|
}, |
|
|
|
|
|
|
|
/** |
|
|
|
* Clear the availability cache to force a re-check |
|
|
|
*/ |
|
|
|
clearAvailabilityCache(): void { |
|
|
|
iptablesAvailable = null; |
|
|
|
commentSupported = null; |
|
|
|
}, |
|
|
|
}; |
|
|
|
|
|
|
|
@ -361,4 +463,5 @@ export const firewallTestExports = { |
|
|
|
parseFirewallEntry, |
|
|
|
generateRuleArgs, |
|
|
|
sanitizeComment, |
|
|
|
buildClientRuleCommands, |
|
|
|
}; |
|
|
|
|