@takodotid/circuit 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.js +35 -15
- package/dist/presets/index.js +44 -1
- package/dist/types/adapters/devices/routeros/render.d.ts +5 -3
- package/dist/types/presets/index.d.ts +35 -0
- package/dist/types/schema/firewall.d.ts +14 -5
- package/package.json +1 -1
- package/src/adapters/devices/routeros/TRAPS.md +4 -0
- package/src/adapters/devices/routeros/render.ts +52 -21
- package/src/presets/index.ts +62 -0
- package/src/schema/firewall.ts +13 -5
package/dist/cli/index.js
CHANGED
|
@@ -1472,10 +1472,11 @@ function command(verb, properties) {
|
|
|
1472
1472
|
}
|
|
1473
1473
|
var yesNo = (on) => on ? "yes" : "no";
|
|
1474
1474
|
var familySuffix = (family) => family === "ipv4" ? "V4" : "V6";
|
|
1475
|
+
var eachPort = (ports) => [ports].flat().map((port) => typeof port === "number" ? String(port) : `${port.min}-${port.max}`);
|
|
1475
1476
|
function portList(ports) {
|
|
1476
1477
|
if (ports === undefined)
|
|
1477
1478
|
return;
|
|
1478
|
-
return
|
|
1479
|
+
return eachPort(ports).join(",");
|
|
1479
1480
|
}
|
|
1480
1481
|
function duration(seconds) {
|
|
1481
1482
|
const units = [
|
|
@@ -1940,12 +1941,26 @@ var HELPERS = ["ftp", "tftp", "irc", "h323", "sip", "pptp", "rtsp"];
|
|
|
1940
1941
|
var CHAINS = ["input", "forward", "output"];
|
|
1941
1942
|
function renderFirewall(out, ctx) {
|
|
1942
1943
|
for (const family of ["ipv4", "ipv6"]) {
|
|
1944
|
+
untracked(out, ctx, family);
|
|
1943
1945
|
filter(out, ctx, family);
|
|
1944
1946
|
nat(out, ctx, family);
|
|
1945
1947
|
}
|
|
1946
1948
|
helpers(out, ctx);
|
|
1947
1949
|
switchAcls(out, ctx);
|
|
1948
1950
|
}
|
|
1951
|
+
function untracked(out, ctx, family) {
|
|
1952
|
+
const menu = family === "ipv4" ? "/ip firewall raw" : "/ipv6 firewall raw";
|
|
1953
|
+
for (const prefix of ctx.device.firewall?.untracked ?? []) {
|
|
1954
|
+
if (familyOf(prefix) !== family)
|
|
1955
|
+
continue;
|
|
1956
|
+
for (const [direction, property] of [
|
|
1957
|
+
["to", "dst-address"],
|
|
1958
|
+
["from", "src-address"]
|
|
1959
|
+
]) {
|
|
1960
|
+
out.add(menu, command("add", { action: "notrack", chain: "prerouting", comment: `Untracked ${direction} ${prefix}`, [property]: prefix }));
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1949
1964
|
function families(ctx, match) {
|
|
1950
1965
|
const sets = ctx.device.firewall?.address_sets ?? {};
|
|
1951
1966
|
const address = match?.src ?? match?.dst;
|
|
@@ -2021,6 +2036,7 @@ function helpers(out, ctx) {
|
|
|
2021
2036
|
}
|
|
2022
2037
|
}
|
|
2023
2038
|
function switchAcls(out, ctx) {
|
|
2039
|
+
const each = (ports) => ports === undefined ? [undefined] : eachPort(ports);
|
|
2024
2040
|
for (const [port, settings] of Object.entries(ctx.ports)) {
|
|
2025
2041
|
if (!settings.acl)
|
|
2026
2042
|
continue;
|
|
@@ -2028,20 +2044,24 @@ function switchAcls(out, ctx) {
|
|
|
2028
2044
|
const match = rule.match ?? {};
|
|
2029
2045
|
const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address2) => address2 && familyOf(address2) === "ipv6");
|
|
2030
2046
|
const address = (prefix) => prefix && !ipv6 ? dottedMask(prefix) : prefix;
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2047
|
+
for (const srcPort of each(match.src_port)) {
|
|
2048
|
+
for (const dstPort of each(match.dst_port)) {
|
|
2049
|
+
out.add("/interface ethernet switch rule", command("add", {
|
|
2050
|
+
comment: rule.description,
|
|
2051
|
+
[ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
|
|
2052
|
+
"dst-port": dstPort,
|
|
2053
|
+
"mac-protocol": ipv6 ? "ipv6" : "ip",
|
|
2054
|
+
"new-dst-ports": rule.action === "drop" ? "" : undefined,
|
|
2055
|
+
ports: ctx.name(port),
|
|
2056
|
+
protocol: match.protocol,
|
|
2057
|
+
rate: rule.rate && bitrate(rule.rate),
|
|
2058
|
+
[ipv6 ? "src-address6" : "src-address"]: address(match.src),
|
|
2059
|
+
"src-port": srcPort,
|
|
2060
|
+
switch: "switch1",
|
|
2061
|
+
"vlan-id": match.vlan && ctx.vlanId(match.vlan)
|
|
2062
|
+
}));
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2045
2065
|
}
|
|
2046
2066
|
}
|
|
2047
2067
|
}
|
package/dist/presets/index.js
CHANGED
|
@@ -11,6 +11,47 @@ var antiSpoofing = (vlan, sources) => sources.map((src) => ({
|
|
|
11
11
|
match: { vlan, src },
|
|
12
12
|
action: "drop"
|
|
13
13
|
}));
|
|
14
|
+
var AMPLIFIERS = {
|
|
15
|
+
qotd: 17,
|
|
16
|
+
chargen: 19,
|
|
17
|
+
dns: 53,
|
|
18
|
+
portmap: 111,
|
|
19
|
+
ntp: 123,
|
|
20
|
+
netbios: 137,
|
|
21
|
+
snmp: 161,
|
|
22
|
+
cldap: 389,
|
|
23
|
+
slp: 427,
|
|
24
|
+
rip: 520,
|
|
25
|
+
"ms-sql": 1434,
|
|
26
|
+
ssdp: 1900,
|
|
27
|
+
ard: 3283,
|
|
28
|
+
"ws-discovery": 3702,
|
|
29
|
+
mdns: 5353,
|
|
30
|
+
coap: 5683,
|
|
31
|
+
ubiquiti: 10001,
|
|
32
|
+
memcached: 11211,
|
|
33
|
+
dvr: 37810
|
|
34
|
+
};
|
|
35
|
+
var ASKED = new Set([AMPLIFIERS.dns, AMPLIFIERS.ntp, AMPLIFIERS.snmp]);
|
|
36
|
+
function antiAmplification(destinations, options = {}) {
|
|
37
|
+
const below1024Except = (port) => [
|
|
38
|
+
{ min: 0, max: port - 1 },
|
|
39
|
+
{ min: port + 1, max: 1023 }
|
|
40
|
+
];
|
|
41
|
+
return destinations.flatMap((dst) => [
|
|
42
|
+
...(options.keep_fragments ?? []).filter((prefix) => contains(dst, prefix)).map((prefix) => ({
|
|
43
|
+
description: `UDP fragments to ${prefix} kept`,
|
|
44
|
+
match: { protocol: "udp", dst: prefix, src_port: 0 },
|
|
45
|
+
action: "accept"
|
|
46
|
+
})),
|
|
47
|
+
{ description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
|
|
48
|
+
...Object.entries(AMPLIFIERS).map(([name, port]) => ({
|
|
49
|
+
description: ASKED.has(port) ? `UDP to ${dst} from ${name}, to a port below 1024` : `UDP to ${dst} from ${name}`,
|
|
50
|
+
match: { protocol: "udp", dst, src_port: port, ...ASKED.has(port) ? { dst_port: below1024Except(port) } : {} },
|
|
51
|
+
action: "drop"
|
|
52
|
+
}))
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
14
55
|
var BAD_TCP_FLAGS = [
|
|
15
56
|
["FIN+SYN", ["fin", "syn"], []],
|
|
16
57
|
["SYN+RST", ["syn", "rst"], []],
|
|
@@ -337,6 +378,8 @@ export {
|
|
|
337
378
|
bgpSanity,
|
|
338
379
|
badTcpFlags,
|
|
339
380
|
antiSpoofing,
|
|
381
|
+
antiAmplification,
|
|
340
382
|
PRIVATE_RANGES,
|
|
341
|
-
MARTIANS
|
|
383
|
+
MARTIANS,
|
|
384
|
+
AMPLIFIERS
|
|
342
385
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Device, Family, Interface, Port } from "../../../schema";
|
|
1
|
+
import type { Device, Family, Interface, Port, Ports } from "../../../schema";
|
|
2
2
|
/** A property value as a command takes it. `undefined` and `false` leave the property out. */
|
|
3
3
|
export type Value = string | number | undefined | false;
|
|
4
4
|
/** Collects commands under their menu. */
|
|
@@ -21,8 +21,10 @@ export declare function command(verb: string, properties: Record<string, Value>)
|
|
|
21
21
|
export declare const yesNo: (on: boolean | undefined) => string;
|
|
22
22
|
/** `V4` or `V6`, the suffix of a chain rendered for one family. */
|
|
23
23
|
export declare const familySuffix: (family: Family) => string;
|
|
24
|
-
/**
|
|
25
|
-
export declare
|
|
24
|
+
/** Each port or range of ports as the platform writes it: `53`, `0-52`. */
|
|
25
|
+
export declare const eachPort: (ports: Ports) => string[];
|
|
26
|
+
/** One port, a range, or a list of them, as `53,0-52`. */
|
|
27
|
+
export declare function portList(ports: Ports | undefined): string | undefined;
|
|
26
28
|
/** Seconds as the platform writes a duration: `3600` becomes `1h`, `90` becomes `1m30s`. */
|
|
27
29
|
export declare function duration(seconds: number): string;
|
|
28
30
|
/** A rate as the switch chip takes it: `2G` becomes `2.0Gbps`. */
|
|
@@ -6,6 +6,41 @@ export declare const MARTIANS: readonly ["127.0.0.0/8", "169.254.0.0/16", "0.0.0
|
|
|
6
6
|
export declare const PRIVATE_RANGES: readonly ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"];
|
|
7
7
|
/** Hardware ACL rules dropping traffic arriving on a VLAN from sources that cannot be there: your own space, martians, private ranges. One rule per source. */
|
|
8
8
|
export declare const antiSpoofing: <V extends string>(vlan: V, sources: readonly Prefix[]) => AclRule<V>[];
|
|
9
|
+
/** UDP ports of services that answer a small request with a much larger reply, which attackers use to reflect floods at a victim. A reflected flood arrives from these ports. */
|
|
10
|
+
export declare const AMPLIFIERS: {
|
|
11
|
+
readonly qotd: 17;
|
|
12
|
+
readonly chargen: 19;
|
|
13
|
+
readonly dns: 53;
|
|
14
|
+
readonly portmap: 111;
|
|
15
|
+
readonly ntp: 123;
|
|
16
|
+
readonly netbios: 137;
|
|
17
|
+
readonly snmp: 161;
|
|
18
|
+
readonly cldap: 389;
|
|
19
|
+
readonly slp: 427;
|
|
20
|
+
readonly rip: 520;
|
|
21
|
+
readonly "ms-sql": 1434;
|
|
22
|
+
readonly ssdp: 1900;
|
|
23
|
+
readonly ard: 3283;
|
|
24
|
+
readonly "ws-discovery": 3702;
|
|
25
|
+
readonly mdns: 5353;
|
|
26
|
+
readonly coap: 5683;
|
|
27
|
+
readonly ubiquiti: 10001;
|
|
28
|
+
readonly memcached: 11211;
|
|
29
|
+
readonly dvr: 37810;
|
|
30
|
+
};
|
|
31
|
+
/** What `antiAmplification` leaves alone. */
|
|
32
|
+
export type AntiAmplificationOptions = {
|
|
33
|
+
/** Prefixes inside the destinations whose UDP fragments pass: for a host that receives UDP too large for one packet, such as SIP or some VPNs. Every fragment after the first is dropped when absent. */
|
|
34
|
+
keep_fragments?: readonly Prefix[];
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Hardware ACL rules dropping reflected UDP floods toward `destinations`, in the switch chip, without limiting anything else.
|
|
38
|
+
*
|
|
39
|
+
* - Replies from an amplifier no host asks across the internet are dropped.
|
|
40
|
+
* - Replies from DNS, NTP and SNMP are dropped only toward a port below 1024 that is not the service's own. A host that runs its own resolver, time server or monitoring keeps its answers.
|
|
41
|
+
* - The fragments after the first carry no port, and a switch chip reads them as port 0. They are dropped, but toward `keep_fragments`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function antiAmplification(destinations: readonly Prefix[], options?: AntiAmplificationOptions): AclRule<never>[];
|
|
9
44
|
/** Firewall rules dropping packets with a TCP flag combination no legitimate packet carries, for a forward chain. */
|
|
10
45
|
export declare const badTcpFlags: () => FilterRule<never, never>[];
|
|
11
46
|
/** The prefix lengths a BGP import accepts. */
|
|
@@ -3,6 +3,13 @@ import type { Family, IP, Prefix, Rate } from "./common";
|
|
|
3
3
|
export type Protocol = "tcp" | "udp" | "icmp" | "gre" | "esp" | "ah" | "ospf" | "vrrp";
|
|
4
4
|
/** The state connection tracking assigns a packet. */
|
|
5
5
|
export type ConnectionState = "new" | "established" | "related" | "untracked" | "invalid";
|
|
6
|
+
/** Ports from `min` to `max`, both included. */
|
|
7
|
+
export type PortRange = {
|
|
8
|
+
min: number;
|
|
9
|
+
max: number;
|
|
10
|
+
};
|
|
11
|
+
/** A port, a range of ports, or any of several. */
|
|
12
|
+
export type Ports = number | PortRange | readonly (number | PortRange)[];
|
|
6
13
|
/** A TCP header flag. */
|
|
7
14
|
export type TcpFlag = "fin" | "syn" | "rst" | "psh" | "ack" | "urg";
|
|
8
15
|
/** Conditions on a packet. All must hold. A rule with addresses of one family applies to that family only. */
|
|
@@ -20,9 +27,9 @@ export type FilterMatch<I extends string, AS extends string> = {
|
|
|
20
27
|
/** Destination is in this address set. */
|
|
21
28
|
dst_set?: AS;
|
|
22
29
|
/** Source port, or any of several. TCP and UDP only. */
|
|
23
|
-
src_port?:
|
|
30
|
+
src_port?: Ports;
|
|
24
31
|
/** Destination port, or any of several. TCP and UDP only. */
|
|
25
|
-
dst_port?:
|
|
32
|
+
dst_port?: Ports;
|
|
26
33
|
/** Arrived on this interface. */
|
|
27
34
|
in_interface?: I;
|
|
28
35
|
/** Leaves through this interface. */
|
|
@@ -89,6 +96,8 @@ export type Firewall<I extends string, AS extends string> = {
|
|
|
89
96
|
};
|
|
90
97
|
/** Helpers that run. None when absent. */
|
|
91
98
|
helpers?: readonly Helper[];
|
|
99
|
+
/** Traffic to and from these prefixes passes without connection tracking. A flood toward them cannot fill the connection table, and rules see its packets in the state `untracked`. Everything is tracked when absent. */
|
|
100
|
+
untracked?: readonly Prefix[];
|
|
92
101
|
};
|
|
93
102
|
/** A stateless rule evaluated in hardware where traffic arrives, before routing. */
|
|
94
103
|
export type AclRule<V extends string> = {
|
|
@@ -105,14 +114,14 @@ export type AclRule<V extends string> = {
|
|
|
105
114
|
/** Destination prefix. */
|
|
106
115
|
dst?: Prefix;
|
|
107
116
|
/** Source port, or any of several. */
|
|
108
|
-
src_port?:
|
|
117
|
+
src_port?: Ports;
|
|
109
118
|
/** Destination port, or any of several. */
|
|
110
|
-
dst_port?:
|
|
119
|
+
dst_port?: Ports;
|
|
111
120
|
/** Carried in this VLAN. */
|
|
112
121
|
vlan?: V;
|
|
113
122
|
};
|
|
114
123
|
/** What happens to a matching packet. */
|
|
115
124
|
action: "accept" | "drop";
|
|
116
|
-
/** Accepted traffic above this rate is dropped. */
|
|
125
|
+
/** Accepted traffic above this rate is dropped. On RouterOS it did not limit traffic the CPU routes; see its TRAPS.md. */
|
|
117
126
|
rate?: Rate;
|
|
118
127
|
};
|
package/package.json
CHANGED
|
@@ -14,6 +14,10 @@ Behaviour that looks like something else. Each one has been hit on a real device
|
|
|
14
14
|
- `tcp-md5-key` set from the CLI has been seen discarded without an error, and a mismatch logs nothing. Confirm with `/tool sniffer quick port=179`.
|
|
15
15
|
- Incoming GRE is subject to the input chain. Accept it before testing the tunnel.
|
|
16
16
|
- Switch-chip rules have no address lists and no connection state. A drop is an empty `new-dst-ports`.
|
|
17
|
+
- A switch-chip rule's `rate` did not limit traffic the CPU routes. On a CCR2216 on 7.23.5, under a 12 Gbps flood, a rule at `2G` and then at `100M` changed nothing that reached the CPU, while a drop with the same match left 3 Mbps. Drop what you can recognise instead of limiting it.
|
|
18
|
+
- The switch chip reads a UDP fragment after the first as port 0. `src-port=0` drops the rest of a fragmented flood whose first fragments another rule dropped by port; without it they reach the CPU, which holds them waiting for a first fragment that never comes.
|
|
19
|
+
- A switch-chip rule takes one port or a range, `1900-1901`; a list is refused. The render makes one rule for each port in a list.
|
|
20
|
+
- The connection table holds 1,048,576 entries on a CCR2216 with 16 GiB, and a full table refuses every new connection through the router, not only the target's. A SYN flood of 210,000 packets a second filled it in five seconds; a UDP flood from random sources fills it about six times faster, because an unanswered UDP entry lives 30 seconds. Traffic in `untracked` prefixes never enters it.
|
|
17
21
|
- With hardware offload on, routed traffic bypasses the forward chain entirely. `hardware_offload: false` on an interface is what makes the firewall apply to it, at the cost of CPU. The only proof of offload is the `H` flag on a route.
|
|
18
22
|
- `/ip cloud ddns-enabled` cannot be turned off on 7.23.5.
|
|
19
23
|
- `mac-server` and `mac-winbox` work at layer 2 and ignore the IP firewall; they are limited by interface list.
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
NeighborSettings,
|
|
14
14
|
PolicyRule,
|
|
15
15
|
Port,
|
|
16
|
+
Ports,
|
|
16
17
|
Service,
|
|
17
18
|
Severity,
|
|
18
19
|
} from "../../../schema";
|
|
@@ -72,11 +73,15 @@ export const yesNo = (on: boolean | undefined): string => (on ? "yes" : "no");
|
|
|
72
73
|
/** `V4` or `V6`, the suffix of a chain rendered for one family. */
|
|
73
74
|
export const familySuffix = (family: Family): string => (family === "ipv4" ? "V4" : "V6");
|
|
74
75
|
|
|
75
|
-
/**
|
|
76
|
-
export
|
|
76
|
+
/** Each port or range of ports as the platform writes it: `53`, `0-52`. */
|
|
77
|
+
export const eachPort = (ports: Ports): string[] =>
|
|
78
|
+
[ports].flat().map((port) => (typeof port === "number" ? String(port) : `${port.min}-${port.max}`));
|
|
79
|
+
|
|
80
|
+
/** One port, a range, or a list of them, as `53,0-52`. */
|
|
81
|
+
export function portList(ports: Ports | undefined): string | undefined {
|
|
77
82
|
if (ports === undefined) return undefined;
|
|
78
83
|
|
|
79
|
-
return
|
|
84
|
+
return eachPort(ports).join(",");
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
/** Seconds as the platform writes a duration: `3600` becomes `1h`, `90` becomes `1m30s`. */
|
|
@@ -734,6 +739,7 @@ const CHAINS = ["input", "forward", "output"] as const;
|
|
|
734
739
|
|
|
735
740
|
export function renderFirewall(out: Output, ctx: Context): void {
|
|
736
741
|
for (const family of ["ipv4", "ipv6"] as const) {
|
|
742
|
+
untracked(out, ctx, family);
|
|
737
743
|
filter(out, ctx, family);
|
|
738
744
|
nat(out, ctx, family);
|
|
739
745
|
}
|
|
@@ -742,6 +748,25 @@ export function renderFirewall(out: Output, ctx: Context): void {
|
|
|
742
748
|
switchAcls(out, ctx);
|
|
743
749
|
}
|
|
744
750
|
|
|
751
|
+
/** Untracked prefixes skip connection tracking in the raw table, both ways, before a connection is ever made. */
|
|
752
|
+
function untracked(out: Output, ctx: Context, family: Family): void {
|
|
753
|
+
const menu = family === "ipv4" ? "/ip firewall raw" : "/ipv6 firewall raw";
|
|
754
|
+
|
|
755
|
+
for (const prefix of ctx.device.firewall?.untracked ?? []) {
|
|
756
|
+
if (familyOf(prefix) !== family) continue;
|
|
757
|
+
|
|
758
|
+
for (const [direction, property] of [
|
|
759
|
+
["to", "dst-address"],
|
|
760
|
+
["from", "src-address"],
|
|
761
|
+
] as const) {
|
|
762
|
+
out.add(
|
|
763
|
+
menu,
|
|
764
|
+
command("add", { action: "notrack", chain: "prerouting", comment: `Untracked ${direction} ${prefix}`, [property]: prefix })
|
|
765
|
+
);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
745
770
|
/**
|
|
746
771
|
* The families a rule applies to. An address in the rule decides it; otherwise it applies to both.
|
|
747
772
|
*
|
|
@@ -836,9 +861,11 @@ function helpers(out: Output, ctx: Context): void {
|
|
|
836
861
|
}
|
|
837
862
|
|
|
838
863
|
/**
|
|
839
|
-
* ACLs run in the switch chip on the port traffic arrives on. The chip matches addresses with a dotted mask on IPv4, drops by forwarding to no port, and limits by rate.
|
|
864
|
+
* ACLs run in the switch chip on the port traffic arrives on. The chip matches addresses with a dotted mask on IPv4, drops by forwarding to no port, and limits by rate. A rule takes one port or range, so a list becomes one rule for each.
|
|
840
865
|
*/
|
|
841
866
|
function switchAcls(out: Output, ctx: Context): void {
|
|
867
|
+
const each = (ports: Ports | undefined) => (ports === undefined ? [undefined] : eachPort(ports));
|
|
868
|
+
|
|
842
869
|
for (const [port, settings] of Object.entries(ctx.ports)) {
|
|
843
870
|
if (!settings.acl) continue;
|
|
844
871
|
|
|
@@ -847,23 +874,27 @@ function switchAcls(out: Output, ctx: Context): void {
|
|
|
847
874
|
const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address) => address && familyOf(address) === "ipv6");
|
|
848
875
|
const address = (prefix: string | undefined) => (prefix && !ipv6 ? dottedMask(prefix) : prefix);
|
|
849
876
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
877
|
+
for (const srcPort of each(match.src_port)) {
|
|
878
|
+
for (const dstPort of each(match.dst_port)) {
|
|
879
|
+
out.add(
|
|
880
|
+
"/interface ethernet switch rule",
|
|
881
|
+
command("add", {
|
|
882
|
+
comment: rule.description,
|
|
883
|
+
[ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
|
|
884
|
+
"dst-port": dstPort,
|
|
885
|
+
"mac-protocol": ipv6 ? "ipv6" : "ip",
|
|
886
|
+
"new-dst-ports": rule.action === "drop" ? "" : undefined,
|
|
887
|
+
ports: ctx.name(port),
|
|
888
|
+
protocol: match.protocol,
|
|
889
|
+
rate: rule.rate && bitrate(rule.rate),
|
|
890
|
+
[ipv6 ? "src-address6" : "src-address"]: address(match.src),
|
|
891
|
+
"src-port": srcPort,
|
|
892
|
+
switch: "switch1",
|
|
893
|
+
"vlan-id": match.vlan && ctx.vlanId(match.vlan),
|
|
894
|
+
})
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
867
898
|
}
|
|
868
899
|
}
|
|
869
900
|
}
|
package/src/presets/index.ts
CHANGED
|
@@ -18,6 +18,68 @@ export const antiSpoofing = <V extends string>(vlan: V, sources: readonly Prefix
|
|
|
18
18
|
action: "drop",
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
|
+
/** UDP ports of services that answer a small request with a much larger reply, which attackers use to reflect floods at a victim. A reflected flood arrives from these ports. */
|
|
22
|
+
export const AMPLIFIERS = {
|
|
23
|
+
qotd: 17,
|
|
24
|
+
chargen: 19,
|
|
25
|
+
dns: 53,
|
|
26
|
+
portmap: 111,
|
|
27
|
+
ntp: 123,
|
|
28
|
+
netbios: 137,
|
|
29
|
+
snmp: 161,
|
|
30
|
+
cldap: 389,
|
|
31
|
+
slp: 427,
|
|
32
|
+
rip: 520,
|
|
33
|
+
"ms-sql": 1434,
|
|
34
|
+
ssdp: 1900,
|
|
35
|
+
ard: 3283,
|
|
36
|
+
"ws-discovery": 3702,
|
|
37
|
+
mdns: 5353,
|
|
38
|
+
coap: 5683,
|
|
39
|
+
ubiquiti: 10001,
|
|
40
|
+
memcached: 11211,
|
|
41
|
+
dvr: 37810,
|
|
42
|
+
} as const satisfies Record<string, number>;
|
|
43
|
+
|
|
44
|
+
/** Of the amplifiers, the services hosts do ask across the internet: resolvers, time servers, monitoring. A reply to a real request goes back to the asking port, 1024 or above, or to the service's own port when two servers talk. */
|
|
45
|
+
const ASKED: ReadonlySet<number> = new Set([AMPLIFIERS.dns, AMPLIFIERS.ntp, AMPLIFIERS.snmp]);
|
|
46
|
+
|
|
47
|
+
/** What `antiAmplification` leaves alone. */
|
|
48
|
+
export type AntiAmplificationOptions = {
|
|
49
|
+
/** Prefixes inside the destinations whose UDP fragments pass: for a host that receives UDP too large for one packet, such as SIP or some VPNs. Every fragment after the first is dropped when absent. */
|
|
50
|
+
keep_fragments?: readonly Prefix[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Hardware ACL rules dropping reflected UDP floods toward `destinations`, in the switch chip, without limiting anything else.
|
|
55
|
+
*
|
|
56
|
+
* - Replies from an amplifier no host asks across the internet are dropped.
|
|
57
|
+
* - Replies from DNS, NTP and SNMP are dropped only toward a port below 1024 that is not the service's own. A host that runs its own resolver, time server or monitoring keeps its answers.
|
|
58
|
+
* - The fragments after the first carry no port, and a switch chip reads them as port 0. They are dropped, but toward `keep_fragments`.
|
|
59
|
+
*/
|
|
60
|
+
export function antiAmplification(destinations: readonly Prefix[], options: AntiAmplificationOptions = {}): AclRule<never>[] {
|
|
61
|
+
const below1024Except = (port: number) => [
|
|
62
|
+
{ min: 0, max: port - 1 },
|
|
63
|
+
{ min: port + 1, max: 1023 },
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
return destinations.flatMap((dst) => [
|
|
67
|
+
...(options.keep_fragments ?? [])
|
|
68
|
+
.filter((prefix) => contains(dst, prefix))
|
|
69
|
+
.map((prefix): AclRule<never> => ({
|
|
70
|
+
description: `UDP fragments to ${prefix} kept`,
|
|
71
|
+
match: { protocol: "udp", dst: prefix, src_port: 0 },
|
|
72
|
+
action: "accept",
|
|
73
|
+
})),
|
|
74
|
+
{ description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
|
|
75
|
+
...Object.entries(AMPLIFIERS).map(([name, port]): AclRule<never> => ({
|
|
76
|
+
description: ASKED.has(port) ? `UDP to ${dst} from ${name}, to a port below 1024` : `UDP to ${dst} from ${name}`,
|
|
77
|
+
match: { protocol: "udp", dst, src_port: port, ...(ASKED.has(port) ? { dst_port: below1024Except(port) } : {}) },
|
|
78
|
+
action: "drop",
|
|
79
|
+
})),
|
|
80
|
+
]);
|
|
81
|
+
}
|
|
82
|
+
|
|
21
83
|
/** TCP flag combinations no legitimate packet carries: name, flags set, flags unset. */
|
|
22
84
|
const BAD_TCP_FLAGS = [
|
|
23
85
|
["FIN+SYN", ["fin", "syn"], []],
|
package/src/schema/firewall.ts
CHANGED
|
@@ -6,6 +6,12 @@ export type Protocol = "tcp" | "udp" | "icmp" | "gre" | "esp" | "ah" | "ospf" |
|
|
|
6
6
|
/** The state connection tracking assigns a packet. */
|
|
7
7
|
export type ConnectionState = "new" | "established" | "related" | "untracked" | "invalid";
|
|
8
8
|
|
|
9
|
+
/** Ports from `min` to `max`, both included. */
|
|
10
|
+
export type PortRange = { min: number; max: number };
|
|
11
|
+
|
|
12
|
+
/** A port, a range of ports, or any of several. */
|
|
13
|
+
export type Ports = number | PortRange | readonly (number | PortRange)[];
|
|
14
|
+
|
|
9
15
|
/** A TCP header flag. */
|
|
10
16
|
export type TcpFlag = "fin" | "syn" | "rst" | "psh" | "ack" | "urg";
|
|
11
17
|
|
|
@@ -24,9 +30,9 @@ export type FilterMatch<I extends string, AS extends string> = {
|
|
|
24
30
|
/** Destination is in this address set. */
|
|
25
31
|
dst_set?: AS;
|
|
26
32
|
/** Source port, or any of several. TCP and UDP only. */
|
|
27
|
-
src_port?:
|
|
33
|
+
src_port?: Ports;
|
|
28
34
|
/** Destination port, or any of several. TCP and UDP only. */
|
|
29
|
-
dst_port?:
|
|
35
|
+
dst_port?: Ports;
|
|
30
36
|
/** Arrived on this interface. */
|
|
31
37
|
in_interface?: I;
|
|
32
38
|
/** Leaves through this interface. */
|
|
@@ -98,6 +104,8 @@ export type Firewall<I extends string, AS extends string> = {
|
|
|
98
104
|
};
|
|
99
105
|
/** Helpers that run. None when absent. */
|
|
100
106
|
helpers?: readonly Helper[];
|
|
107
|
+
/** Traffic to and from these prefixes passes without connection tracking. A flood toward them cannot fill the connection table, and rules see its packets in the state `untracked`. Everything is tracked when absent. */
|
|
108
|
+
untracked?: readonly Prefix[];
|
|
101
109
|
};
|
|
102
110
|
|
|
103
111
|
/** A stateless rule evaluated in hardware where traffic arrives, before routing. */
|
|
@@ -115,14 +123,14 @@ export type AclRule<V extends string> = {
|
|
|
115
123
|
/** Destination prefix. */
|
|
116
124
|
dst?: Prefix;
|
|
117
125
|
/** Source port, or any of several. */
|
|
118
|
-
src_port?:
|
|
126
|
+
src_port?: Ports;
|
|
119
127
|
/** Destination port, or any of several. */
|
|
120
|
-
dst_port?:
|
|
128
|
+
dst_port?: Ports;
|
|
121
129
|
/** Carried in this VLAN. */
|
|
122
130
|
vlan?: V;
|
|
123
131
|
};
|
|
124
132
|
/** What happens to a matching packet. */
|
|
125
133
|
action: "accept" | "drop";
|
|
126
|
-
/** Accepted traffic above this rate is dropped. */
|
|
134
|
+
/** Accepted traffic above this rate is dropped. On RouterOS it did not limit traffic the CPU routes; see its TRAPS.md. */
|
|
127
135
|
rate?: Rate;
|
|
128
136
|
};
|