@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 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
- out.add("/interface ethernet switch rule", command("add", {
2032
- comment: rule.description,
2033
- [ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
2034
- "dst-port": portList(match.dst_port),
2035
- "mac-protocol": ipv6 ? "ipv6" : "ip",
2036
- "new-dst-ports": rule.action === "drop" ? "" : undefined,
2037
- ports: ctx.name(port),
2038
- protocol: match.protocol,
2039
- rate: rule.rate && bitrate(rule.rate),
2040
- [ipv6 ? "src-address6" : "src-address"]: address(match.src),
2041
- "src-port": portList(match.src_port),
2042
- switch: "switch1",
2043
- "vlan-id": match.vlan && ctx.vlanId(match.vlan)
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
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takodotid/circuit",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Declarative, vendor-neutral network configuration: describe each device in TypeScript, and Circuit makes the device match. Alpha.",
5
5
  "keywords": [
6
6
  "network",
@@ -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
- out.add(
851
- "/interface ethernet switch rule",
852
- command("add", {
853
- comment: rule.description,
854
- [ipv6 ? "dst-address6" : "dst-address"]: address(match.dst),
855
- "dst-port": portList(match.dst_port),
856
- "mac-protocol": ipv6 ? "ipv6" : "ip",
857
- "new-dst-ports": rule.action === "drop" ? "" : undefined,
858
- ports: ctx.name(port),
859
- protocol: match.protocol,
860
- rate: rule.rate && bitrate(rule.rate),
861
- [ipv6 ? "src-address6" : "src-address"]: address(match.src),
862
- "src-port": portList(match.src_port),
863
- switch: "switch1",
864
- "vlan-id": match.vlan && ctx.vlanId(match.vlan),
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
  }
@@ -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"], []],
@@ -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
  };