@takodotid/circuit 0.16.0 → 0.17.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 +19 -14
- package/dist/presets/index.js +37 -1
- package/dist/types/presets/index.d.ts +26 -0
- package/dist/types/schema/firewall.d.ts +1 -1
- package/package.json +1 -1
- package/src/adapters/devices/routeros/TRAPS.md +3 -0
- package/src/adapters/devices/routeros/render.ts +24 -18
- package/src/presets/index.ts +41 -0
- package/src/schema/firewall.ts +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -2021,6 +2021,7 @@ function helpers(out, ctx) {
|
|
|
2021
2021
|
}
|
|
2022
2022
|
}
|
|
2023
2023
|
function switchAcls(out, ctx) {
|
|
2024
|
+
const each = (ports) => ports === undefined ? [undefined] : [ports].flat();
|
|
2024
2025
|
for (const [port, settings] of Object.entries(ctx.ports)) {
|
|
2025
2026
|
if (!settings.acl)
|
|
2026
2027
|
continue;
|
|
@@ -2028,20 +2029,24 @@ function switchAcls(out, ctx) {
|
|
|
2028
2029
|
const match = rule.match ?? {};
|
|
2029
2030
|
const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address2) => address2 && familyOf(address2) === "ipv6");
|
|
2030
2031
|
const address = (prefix) => prefix && !ipv6 ? dottedMask(prefix) : prefix;
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2032
|
+
for (const srcPort of each(match.src_port)) {
|
|
2033
|
+
for (const dstPort of each(match.dst_port)) {
|
|
2034
|
+
out.add("/interface ethernet switch rule", command("add", {
|
|
2035
|
+
comment: rule.description,
|
|
2036
|
+
[ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
|
|
2037
|
+
"dst-port": dstPort,
|
|
2038
|
+
"mac-protocol": ipv6 ? "ipv6" : "ip",
|
|
2039
|
+
"new-dst-ports": rule.action === "drop" ? "" : undefined,
|
|
2040
|
+
ports: ctx.name(port),
|
|
2041
|
+
protocol: match.protocol,
|
|
2042
|
+
rate: rule.rate && bitrate(rule.rate),
|
|
2043
|
+
[ipv6 ? "src-address6" : "src-address"]: address(match.src),
|
|
2044
|
+
"src-port": srcPort,
|
|
2045
|
+
switch: "switch1",
|
|
2046
|
+
"vlan-id": match.vlan && ctx.vlanId(match.vlan)
|
|
2047
|
+
}));
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2045
2050
|
}
|
|
2046
2051
|
}
|
|
2047
2052
|
}
|
package/dist/presets/index.js
CHANGED
|
@@ -11,6 +11,40 @@ 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 antiAmplification = (destinations, trusted = []) => destinations.flatMap((dst) => [
|
|
36
|
+
...trusted.map((src) => ({
|
|
37
|
+
description: `UDP to ${dst} from ${src}`,
|
|
38
|
+
match: { protocol: "udp", src, dst },
|
|
39
|
+
action: "accept"
|
|
40
|
+
})),
|
|
41
|
+
{ description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
|
|
42
|
+
...Object.entries(AMPLIFIERS).map(([name, port]) => ({
|
|
43
|
+
description: `UDP to ${dst} from ${name}`,
|
|
44
|
+
match: { protocol: "udp", dst, src_port: port },
|
|
45
|
+
action: "drop"
|
|
46
|
+
}))
|
|
47
|
+
]);
|
|
14
48
|
var BAD_TCP_FLAGS = [
|
|
15
49
|
["FIN+SYN", ["fin", "syn"], []],
|
|
16
50
|
["SYN+RST", ["syn", "rst"], []],
|
|
@@ -337,6 +371,8 @@ export {
|
|
|
337
371
|
bgpSanity,
|
|
338
372
|
badTcpFlags,
|
|
339
373
|
antiSpoofing,
|
|
374
|
+
antiAmplification,
|
|
340
375
|
PRIVATE_RANGES,
|
|
341
|
-
MARTIANS
|
|
376
|
+
MARTIANS,
|
|
377
|
+
AMPLIFIERS
|
|
342
378
|
};
|
|
@@ -6,6 +6,32 @@ 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
|
+
/**
|
|
32
|
+
* Hardware ACL rules dropping reflected UDP floods toward `destinations`: replies from the ports in `AMPLIFIERS`, and the fragments after the first, which carry no port and which a switch chip reads as port 0. UDP from `trusted` sources, the resolvers and time servers your own hosts ask, is accepted before anything is dropped. Traffic that matches is dropped whole and everything else passes untouched, so nothing legitimate is slowed.
|
|
33
|
+
*/
|
|
34
|
+
export declare const antiAmplification: (destinations: readonly Prefix[], trusted?: readonly Prefix[]) => AclRule<never>[];
|
|
9
35
|
/** Firewall rules dropping packets with a TCP flag combination no legitimate packet carries, for a forward chain. */
|
|
10
36
|
export declare const badTcpFlags: () => FilterRule<never, never>[];
|
|
11
37
|
/** The prefix lengths a BGP import accepts. */
|
|
@@ -113,6 +113,6 @@ export type AclRule<V extends string> = {
|
|
|
113
113
|
};
|
|
114
114
|
/** What happens to a matching packet. */
|
|
115
115
|
action: "accept" | "drop";
|
|
116
|
-
/** Accepted traffic above this rate is dropped. */
|
|
116
|
+
/** Accepted traffic above this rate is dropped. On RouterOS it did not limit traffic the CPU routes; see its TRAPS.md. */
|
|
117
117
|
rate?: Rate;
|
|
118
118
|
};
|
package/package.json
CHANGED
|
@@ -14,6 +14,9 @@ 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.
|
|
17
20
|
- 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
21
|
- `/ip cloud ddns-enabled` cannot be turned off on 7.23.5.
|
|
19
22
|
- `mac-server` and `mac-winbox` work at layer 2 and ignore the IP firewall; they are limited by interface list.
|
|
@@ -836,9 +836,11 @@ function helpers(out: Output, ctx: Context): void {
|
|
|
836
836
|
}
|
|
837
837
|
|
|
838
838
|
/**
|
|
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.
|
|
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. A rule takes one port, so a list of ports becomes one rule for each.
|
|
840
840
|
*/
|
|
841
841
|
function switchAcls(out: Output, ctx: Context): void {
|
|
842
|
+
const each = (ports: number | readonly number[] | undefined) => (ports === undefined ? [undefined] : [ports].flat());
|
|
843
|
+
|
|
842
844
|
for (const [port, settings] of Object.entries(ctx.ports)) {
|
|
843
845
|
if (!settings.acl) continue;
|
|
844
846
|
|
|
@@ -847,23 +849,27 @@ function switchAcls(out: Output, ctx: Context): void {
|
|
|
847
849
|
const ipv6 = match.family === "ipv6" || [match.src, match.dst].some((address) => address && familyOf(address) === "ipv6");
|
|
848
850
|
const address = (prefix: string | undefined) => (prefix && !ipv6 ? dottedMask(prefix) : prefix);
|
|
849
851
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
852
|
+
for (const srcPort of each(match.src_port)) {
|
|
853
|
+
for (const dstPort of each(match.dst_port)) {
|
|
854
|
+
out.add(
|
|
855
|
+
"/interface ethernet switch rule",
|
|
856
|
+
command("add", {
|
|
857
|
+
comment: rule.description,
|
|
858
|
+
[ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
|
|
859
|
+
"dst-port": dstPort,
|
|
860
|
+
"mac-protocol": ipv6 ? "ipv6" : "ip",
|
|
861
|
+
"new-dst-ports": rule.action === "drop" ? "" : undefined,
|
|
862
|
+
ports: ctx.name(port),
|
|
863
|
+
protocol: match.protocol,
|
|
864
|
+
rate: rule.rate && bitrate(rule.rate),
|
|
865
|
+
[ipv6 ? "src-address6" : "src-address"]: address(match.src),
|
|
866
|
+
"src-port": srcPort,
|
|
867
|
+
switch: "switch1",
|
|
868
|
+
"vlan-id": match.vlan && ctx.vlanId(match.vlan),
|
|
869
|
+
})
|
|
870
|
+
);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
867
873
|
}
|
|
868
874
|
}
|
|
869
875
|
}
|
package/src/presets/index.ts
CHANGED
|
@@ -18,6 +18,47 @@ 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
|
+
/**
|
|
45
|
+
* Hardware ACL rules dropping reflected UDP floods toward `destinations`: replies from the ports in `AMPLIFIERS`, and the fragments after the first, which carry no port and which a switch chip reads as port 0. UDP from `trusted` sources, the resolvers and time servers your own hosts ask, is accepted before anything is dropped. Traffic that matches is dropped whole and everything else passes untouched, so nothing legitimate is slowed.
|
|
46
|
+
*/
|
|
47
|
+
export const antiAmplification = (destinations: readonly Prefix[], trusted: readonly Prefix[] = []): AclRule<never>[] =>
|
|
48
|
+
destinations.flatMap((dst) => [
|
|
49
|
+
...trusted.map((src): AclRule<never> => ({
|
|
50
|
+
description: `UDP to ${dst} from ${src}`,
|
|
51
|
+
match: { protocol: "udp", src, dst },
|
|
52
|
+
action: "accept",
|
|
53
|
+
})),
|
|
54
|
+
{ description: `UDP fragments to ${dst}`, match: { protocol: "udp", dst, src_port: 0 }, action: "drop" },
|
|
55
|
+
...Object.entries(AMPLIFIERS).map(([name, port]): AclRule<never> => ({
|
|
56
|
+
description: `UDP to ${dst} from ${name}`,
|
|
57
|
+
match: { protocol: "udp", dst, src_port: port },
|
|
58
|
+
action: "drop",
|
|
59
|
+
})),
|
|
60
|
+
]);
|
|
61
|
+
|
|
21
62
|
/** TCP flag combinations no legitimate packet carries: name, flags set, flags unset. */
|
|
22
63
|
const BAD_TCP_FLAGS = [
|
|
23
64
|
["FIN+SYN", ["fin", "syn"], []],
|
package/src/schema/firewall.ts
CHANGED
|
@@ -123,6 +123,6 @@ export type AclRule<V extends string> = {
|
|
|
123
123
|
};
|
|
124
124
|
/** What happens to a matching packet. */
|
|
125
125
|
action: "accept" | "drop";
|
|
126
|
-
/** Accepted traffic above this rate is dropped. */
|
|
126
|
+
/** Accepted traffic above this rate is dropped. On RouterOS it did not limit traffic the CPU routes; see its TRAPS.md. */
|
|
127
127
|
rate?: Rate;
|
|
128
128
|
};
|