@takodotid/circuit 0.34.3 → 0.35.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.
Files changed (240) hide show
  1. package/README.md +13 -14
  2. package/dist/chunks/index-20kcjxjg.js +174 -0
  3. package/dist/chunks/index-2r4bbhac.js +977 -0
  4. package/dist/chunks/index-n7w3tg5y.js +149 -0
  5. package/dist/chunks/new-w3zys5y4.js +172 -0
  6. package/dist/chunks/server-gzsf5me5.js +259 -0
  7. package/dist/cli/index.js +11345 -9024
  8. package/dist/helpers/index.js +594 -0
  9. package/dist/index.js +9 -28
  10. package/dist/types/core/{addresses.d.ts → address-space.d.ts} +23 -7
  11. package/dist/types/core/address.d.ts +44 -0
  12. package/dist/types/core/cabling.d.ts +41 -0
  13. package/dist/types/core/cross-checks.d.ts +28 -0
  14. package/dist/types/core/define.d.ts +25 -3
  15. package/dist/types/core/project-files.d.ts +12 -0
  16. package/dist/types/core/schema-docs.d.ts +13 -4
  17. package/dist/types/core/secrets.d.ts +62 -21
  18. package/dist/types/core/{graph.d.ts → topology.d.ts} +32 -12
  19. package/dist/types/core/utils.d.ts +8 -0
  20. package/dist/types/core/validator.d.ts +19 -0
  21. package/dist/types/helpers/bgp.d.ts +97 -0
  22. package/dist/types/helpers/checks.d.ts +32 -0
  23. package/dist/types/helpers/exchange.d.ts +49 -0
  24. package/dist/types/helpers/filters.d.ts +51 -0
  25. package/dist/types/helpers/index.d.ts +10 -0
  26. package/dist/types/helpers/route-server.d.ts +52 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/integrations/librenms.d.ts +60 -0
  29. package/dist/types/integrations/netbox.d.ts +89 -0
  30. package/dist/types/integrations/peeringdb.d.ts +24 -0
  31. package/dist/types/integrations/prometheus.d.ts +24 -0
  32. package/dist/types/integrations/registries.d.ts +8 -0
  33. package/dist/types/platforms/bgp-wait.d.ts +18 -0
  34. package/dist/types/platforms/catalog.d.ts +109 -0
  35. package/dist/types/platforms/derive.d.ts +17 -0
  36. package/dist/types/platforms/fortios/firewall.d.ts +45 -0
  37. package/dist/types/platforms/fortios/index.d.ts +134 -0
  38. package/dist/types/platforms/fortios/names.d.ts +52 -0
  39. package/dist/types/platforms/fortios/plan.d.ts +104 -0
  40. package/dist/types/platforms/fortios/render.d.ts +75 -0
  41. package/dist/types/platforms/fortios/routing.d.ts +56 -0
  42. package/dist/types/platforms/fortios/tree.d.ts +37 -0
  43. package/dist/types/platforms/index.d.ts +13 -0
  44. package/dist/types/platforms/junos/bgp.d.ts +44 -0
  45. package/dist/types/platforms/junos/index.d.ts +114 -0
  46. package/dist/types/platforms/junos/render.d.ts +154 -0
  47. package/dist/types/platforms/line-diff.d.ts +67 -0
  48. package/dist/types/platforms/platform.d.ts +120 -0
  49. package/dist/types/platforms/raisecom-ros/index.d.ts +71 -0
  50. package/dist/types/platforms/raisecom-ros/render.d.ts +41 -0
  51. package/dist/types/platforms/routeros/export.d.ts +59 -0
  52. package/dist/types/platforms/routeros/index.d.ts +96 -0
  53. package/dist/types/{adapters/devices → platforms}/routeros/models.d.ts +4 -0
  54. package/dist/types/platforms/routeros/plan.d.ts +54 -0
  55. package/dist/types/platforms/routeros/render.d.ts +240 -0
  56. package/dist/types/platforms/vrp/index.d.ts +83 -0
  57. package/dist/types/platforms/vrp/render.d.ts +96 -0
  58. package/dist/types/schema/common.d.ts +12 -6
  59. package/dist/types/schema/index.d.ts +1 -0
  60. package/dist/types/schema/interfaces.d.ts +14 -6
  61. package/dist/types/schema/policy.d.ts +18 -10
  62. package/dist/types/schema/routing.d.ts +14 -1
  63. package/dist/types/schema/system.d.ts +16 -6
  64. package/dist/types/schema/time-zones.d.ts +2 -0
  65. package/dist/types/transport/console.d.ts +79 -23
  66. package/dist/types/transport/path-check.d.ts +32 -0
  67. package/dist/types/transport/ssh.d.ts +17 -15
  68. package/package.json +17 -11
  69. package/src/cli/graph-format.ts +91 -0
  70. package/src/cli/index.ts +1074 -484
  71. package/src/cli/new.ts +208 -175
  72. package/src/cli/rename.ts +97 -91
  73. package/src/core/address-space.ts +320 -0
  74. package/src/core/address.ts +202 -0
  75. package/src/core/cabling.ts +154 -0
  76. package/src/core/cross-checks.ts +364 -0
  77. package/src/core/define.ts +26 -3
  78. package/src/core/project-files.ts +35 -0
  79. package/src/core/schema-docs.ts +138 -130
  80. package/src/core/secrets.ts +170 -76
  81. package/src/core/topology.ts +656 -0
  82. package/src/core/utils.ts +37 -0
  83. package/src/core/validator.ts +407 -0
  84. package/src/helpers/bgp.ts +301 -0
  85. package/src/helpers/checks.ts +178 -0
  86. package/src/helpers/exchange.ts +117 -0
  87. package/src/helpers/filters.ts +107 -0
  88. package/src/helpers/index.ts +12 -0
  89. package/src/helpers/route-server.ts +212 -0
  90. package/src/index.ts +6 -3
  91. package/src/integrations/librenms.ts +291 -0
  92. package/src/integrations/netbox.ts +620 -0
  93. package/src/integrations/peeringdb.ts +173 -0
  94. package/src/integrations/prometheus.ts +90 -0
  95. package/src/integrations/registries.ts +25 -0
  96. package/src/platforms/bgp-wait.ts +41 -0
  97. package/src/platforms/catalog.ts +69 -0
  98. package/src/platforms/derive.ts +117 -0
  99. package/src/{adapters/devices → platforms}/fortios/TRAPS.md +6 -0
  100. package/src/platforms/fortios/firewall.ts +377 -0
  101. package/src/platforms/fortios/index.ts +1060 -0
  102. package/src/platforms/fortios/names.ts +130 -0
  103. package/src/platforms/fortios/plan.ts +929 -0
  104. package/src/platforms/fortios/render.ts +779 -0
  105. package/src/platforms/fortios/routing.ts +476 -0
  106. package/src/platforms/fortios/tree.ts +129 -0
  107. package/src/platforms/index.ts +36 -0
  108. package/src/{adapters/devices → platforms}/junos/TRAPS.md +8 -0
  109. package/src/platforms/junos/bgp.ts +293 -0
  110. package/src/platforms/junos/index.ts +1006 -0
  111. package/src/platforms/junos/render.ts +967 -0
  112. package/src/platforms/line-diff.ts +244 -0
  113. package/src/platforms/platform.ts +122 -0
  114. package/src/platforms/raisecom-ros/TRAPS.md +40 -0
  115. package/src/platforms/raisecom-ros/index.ts +731 -0
  116. package/src/platforms/raisecom-ros/render.ts +328 -0
  117. package/src/{adapters/devices → platforms}/routeros/TRAPS.md +16 -2
  118. package/src/platforms/routeros/export.ts +342 -0
  119. package/src/platforms/routeros/index.ts +636 -0
  120. package/src/{adapters/devices → platforms}/routeros/models.ts +2 -0
  121. package/src/platforms/routeros/plan.ts +444 -0
  122. package/src/platforms/routeros/render.ts +1483 -0
  123. package/src/platforms/vrp/TRAPS.md +45 -0
  124. package/src/platforms/vrp/index.ts +652 -0
  125. package/src/platforms/vrp/render.ts +615 -0
  126. package/src/schema/common.ts +15 -6
  127. package/src/schema/index.ts +1 -0
  128. package/src/schema/interfaces.ts +15 -5
  129. package/src/schema/policy.ts +22 -10
  130. package/src/schema/routing.ts +16 -1
  131. package/src/schema/system.ts +16 -6
  132. package/src/schema/time-zones.ts +482 -0
  133. package/src/studio/data.ts +172 -168
  134. package/src/studio/git.ts +95 -90
  135. package/src/studio/public/app.js +161 -150
  136. package/src/studio/public/compare.js +219 -212
  137. package/src/studio/public/diff.js +77 -75
  138. package/src/studio/public/dom.js +118 -108
  139. package/src/studio/public/evaluate.js +584 -545
  140. package/src/studio/public/export.js +120 -118
  141. package/src/studio/public/graph.js +1343 -1266
  142. package/src/studio/public/net.js +115 -109
  143. package/src/studio/public/tables.js +1135 -1083
  144. package/src/studio/public/tools.js +561 -529
  145. package/src/studio/server.ts +202 -196
  146. package/src/transport/console.ts +363 -271
  147. package/src/transport/path-check.ts +91 -0
  148. package/src/transport/ssh.ts +98 -94
  149. package/templates/colocation/checks.ts +11 -6
  150. package/templates/colocation/edge-01.ts +9 -9
  151. package/templates/colocation/routing.ts +2 -2
  152. package/templates/colocation/tenancy.ts +3 -3
  153. package/templates/colocation/tor-01.ts +3 -3
  154. package/templates/edge-router/checks.ts +4 -4
  155. package/templates/edge-router/edge-01.ts +5 -5
  156. package/templates/edge-router/routing.ts +2 -2
  157. package/templates/project/AGENTS.md +5 -1
  158. package/templates/single-site/checks.ts +2 -2
  159. package/dist/chunks/index-47debt16.js +0 -956
  160. package/dist/chunks/index-v8zgr7wm.js +0 -128
  161. package/dist/chunks/new-ny89s7y7.js +0 -166
  162. package/dist/chunks/server-tjbb3rqc.js +0 -251
  163. package/dist/presets/index.js +0 -394
  164. package/dist/types/adapters/devices/catalog.d.ts +0 -86
  165. package/dist/types/adapters/devices/derive.d.ts +0 -7
  166. package/dist/types/adapters/devices/fortios/firewall.d.ts +0 -23
  167. package/dist/types/adapters/devices/fortios/index.d.ts +0 -58
  168. package/dist/types/adapters/devices/fortios/names.d.ts +0 -48
  169. package/dist/types/adapters/devices/fortios/plan.d.ts +0 -28
  170. package/dist/types/adapters/devices/fortios/render.d.ts +0 -38
  171. package/dist/types/adapters/devices/fortios/routing.d.ts +0 -32
  172. package/dist/types/adapters/devices/fortios/tree.d.ts +0 -33
  173. package/dist/types/adapters/devices/index.d.ts +0 -3
  174. package/dist/types/adapters/devices/junos/bgp.d.ts +0 -11
  175. package/dist/types/adapters/devices/junos/index.d.ts +0 -42
  176. package/dist/types/adapters/devices/junos/render.d.ts +0 -52
  177. package/dist/types/adapters/devices/lines.d.ts +0 -56
  178. package/dist/types/adapters/devices/raisecom-ros/index.d.ts +0 -28
  179. package/dist/types/adapters/devices/routeros/export.d.ts +0 -39
  180. package/dist/types/adapters/devices/routeros/index.d.ts +0 -30
  181. package/dist/types/adapters/devices/routeros/plan.d.ts +0 -4
  182. package/dist/types/adapters/devices/routeros/render.d.ts +0 -100
  183. package/dist/types/adapters/devices/types.d.ts +0 -40
  184. package/dist/types/adapters/devices/vrp/index.d.ts +0 -27
  185. package/dist/types/adapters/registries/index.d.ts +0 -3
  186. package/dist/types/adapters/registries/librenms.d.ts +0 -15
  187. package/dist/types/adapters/registries/peeringdb.d.ts +0 -16
  188. package/dist/types/adapters/registries/prometheus.d.ts +0 -16
  189. package/dist/types/core/across.d.ts +0 -4
  190. package/dist/types/core/addr.d.ts +0 -34
  191. package/dist/types/core/files.d.ts +0 -6
  192. package/dist/types/core/merge.d.ts +0 -4
  193. package/dist/types/core/time.d.ts +0 -2
  194. package/dist/types/core/validate.d.ts +0 -2
  195. package/dist/types/presets/index.d.ts +0 -183
  196. package/src/adapters/devices/catalog.ts +0 -41
  197. package/src/adapters/devices/derive.ts +0 -59
  198. package/src/adapters/devices/fortios/firewall.ts +0 -362
  199. package/src/adapters/devices/fortios/index.ts +0 -923
  200. package/src/adapters/devices/fortios/names.ts +0 -115
  201. package/src/adapters/devices/fortios/plan.ts +0 -888
  202. package/src/adapters/devices/fortios/render.ts +0 -768
  203. package/src/adapters/devices/fortios/routing.ts +0 -444
  204. package/src/adapters/devices/fortios/tree.ts +0 -121
  205. package/src/adapters/devices/index.ts +0 -9
  206. package/src/adapters/devices/junos/bgp.ts +0 -269
  207. package/src/adapters/devices/junos/index.ts +0 -862
  208. package/src/adapters/devices/junos/render.ts +0 -872
  209. package/src/adapters/devices/lines.ts +0 -212
  210. package/src/adapters/devices/raisecom-ros/TRAPS.md +0 -26
  211. package/src/adapters/devices/raisecom-ros/index.ts +0 -700
  212. package/src/adapters/devices/routeros/export.ts +0 -321
  213. package/src/adapters/devices/routeros/index.ts +0 -346
  214. package/src/adapters/devices/routeros/plan.ts +0 -398
  215. package/src/adapters/devices/routeros/render.ts +0 -1490
  216. package/src/adapters/devices/types.ts +0 -38
  217. package/src/adapters/devices/vrp/TRAPS.md +0 -26
  218. package/src/adapters/devices/vrp/index.ts +0 -865
  219. package/src/adapters/registries/index.ts +0 -19
  220. package/src/adapters/registries/librenms.ts +0 -92
  221. package/src/adapters/registries/peeringdb.ts +0 -169
  222. package/src/adapters/registries/prometheus.ts +0 -71
  223. package/src/cli/graph.ts +0 -89
  224. package/src/core/across.ts +0 -350
  225. package/src/core/addr.ts +0 -174
  226. package/src/core/addresses.ts +0 -316
  227. package/src/core/files.ts +0 -32
  228. package/src/core/graph.ts +0 -640
  229. package/src/core/merge.ts +0 -19
  230. package/src/core/time.ts +0 -15
  231. package/src/core/validate.ts +0 -348
  232. package/src/presets/index.ts +0 -579
  233. /package/dist/types/{adapters/devices → platforms}/fortios/models.d.ts +0 -0
  234. /package/dist/types/{adapters/devices → platforms}/junos/models.d.ts +0 -0
  235. /package/dist/types/{adapters/devices → platforms}/raisecom-ros/models.d.ts +0 -0
  236. /package/dist/types/{adapters/devices → platforms}/vrp/models.d.ts +0 -0
  237. /package/src/{adapters/devices → platforms}/fortios/models.ts +0 -0
  238. /package/src/{adapters/devices → platforms}/junos/models.ts +0 -0
  239. /package/src/{adapters/devices → platforms}/raisecom-ros/models.ts +0 -0
  240. /package/src/{adapters/devices → platforms}/vrp/models.ts +0 -0
@@ -0,0 +1,779 @@
1
+ // The neutral model as FortiOS configuration, in the form `show` prints, so the plan can compare the two section by section.
2
+
3
+ import { Address } from "../../core/address";
4
+ import { Secrets } from "../../core/secrets";
5
+ import type { Device, Helper, Severity } from "../../schema";
6
+ import { Catalog } from "../catalog";
7
+ import { FortiOsFirewall } from "./firewall";
8
+ import { FortiOsNames } from "./names";
9
+ import { FortiOsRouting } from "./routing";
10
+ import { FortiOsBlock, FortiOsTree } from "./tree";
11
+
12
+ export type Section = (typeof FortiOsRender.SECTIONS)[number];
13
+
14
+ type RoutedSettings = {
15
+ addresses?: readonly (string | { address: string; peer: string })[];
16
+ vrrp?: readonly { id: number; address: string; priority?: number; preempt?: boolean; interval?: number }[];
17
+ lldp?: boolean;
18
+ vrf?: string;
19
+ ipv6_ra?: boolean;
20
+ };
21
+
22
+ export class FortiOsRender {
23
+ /** Where the pre-login banner's text is kept. */
24
+ static readonly BANNER = 'system replacemsg admin "pre_admin-disclaimer-text"';
25
+
26
+ /** Every section Circuit manages, in the order a change goes in: what others name comes first. */
27
+ static readonly SECTIONS = [
28
+ "vpn certificate local",
29
+ "vpn certificate ca",
30
+ "system global",
31
+ FortiOsRender.BANNER,
32
+ "system settings",
33
+ "system accprofile",
34
+ "system admin",
35
+ "system interface",
36
+ "system gre-tunnel",
37
+ "system vxlan",
38
+ "system dns",
39
+ "system dns-server",
40
+ "system ntp",
41
+ "system snmp sysinfo",
42
+ "system snmp community",
43
+ "system snmp user",
44
+ "log memory setting",
45
+ "log memory filter",
46
+ "log disk setting",
47
+ ...["", "2", "3", "4"].flatMap((number) => [`log syslogd${number} setting`, `log syslogd${number} filter`]),
48
+ "system sflow",
49
+ "system netflow",
50
+ "system dhcp server",
51
+ "router static",
52
+ "router static6",
53
+ "router prefix-list",
54
+ "router prefix-list6",
55
+ "router community-list",
56
+ "router route-map",
57
+ "router ospf",
58
+ "router ospf6",
59
+ "router bgp",
60
+ "system session-helper",
61
+ "firewall address",
62
+ "firewall address6",
63
+ "firewall addrgrp",
64
+ "firewall addrgrp6",
65
+ "firewall service custom",
66
+ "firewall ippool",
67
+ "firewall vip",
68
+ "firewall policy",
69
+ "firewall policy6",
70
+ "firewall local-in-policy",
71
+ "firewall local-in-policy6",
72
+ "firewall central-snat-map",
73
+ "firewall acl",
74
+ "firewall acl6",
75
+ ] as const;
76
+
77
+ /** Sections `show` prints without what matters, which `show full-configuration` prints: a certificate's text. */
78
+ static readonly FULL: readonly string[] = ["vpn certificate local", "vpn certificate ca"];
79
+
80
+ /** The FortiOS zone number of each IANA zone it has one for. A number names a list of places in one zone; `00` is the device's default. */
81
+ private static readonly ZONES: Record<string, readonly string[]> = {
82
+ "00": ["Pacific/Kwajalein"],
83
+ "01": ["Pacific/Midway", "Pacific/Pago_Pago"],
84
+ "02": ["Pacific/Honolulu"],
85
+ "03": ["America/Anchorage"],
86
+ "04": ["America/Los_Angeles", "America/Vancouver", "America/Tijuana"],
87
+ "05": ["America/Phoenix"],
88
+ "06": ["America/Denver", "America/Edmonton"],
89
+ "07": ["America/Guatemala", "America/Costa_Rica", "America/El_Salvador", "America/Tegucigalpa", "America/Managua"],
90
+ "08": ["America/Chicago", "America/Winnipeg"],
91
+ "09": ["America/Mexico_City"],
92
+ "10": ["America/Regina"],
93
+ "11": ["America/Bogota", "America/Lima", "America/Guayaquil"],
94
+ "12": ["America/New_York", "America/Toronto"],
95
+ "13": ["America/Indiana/Indianapolis"],
96
+ "14": ["America/Halifax"],
97
+ "15": ["America/La_Paz"],
98
+ "16": ["America/Santiago"],
99
+ "17": ["America/St_Johns"],
100
+ "18": ["America/Sao_Paulo"],
101
+ "19": ["America/Argentina/Buenos_Aires"],
102
+ "20": ["America/Nuuk", "America/Godthab"],
103
+ "21": ["Atlantic/South_Georgia"],
104
+ "22": ["Atlantic/Azores"],
105
+ "23": ["Atlantic/Cape_Verde"],
106
+ "24": ["Africa/Monrovia"],
107
+ "25": ["Europe/London", "Europe/Dublin", "Europe/Lisbon", "Atlantic/Canary"],
108
+ "26": ["Europe/Amsterdam", "Europe/Berlin", "Europe/Zurich", "Europe/Rome", "Europe/Stockholm", "Europe/Vienna"],
109
+ "27": ["Europe/Belgrade", "Europe/Bratislava", "Europe/Budapest", "Europe/Ljubljana", "Europe/Prague"],
110
+ "28": ["Europe/Brussels", "Europe/Copenhagen", "Europe/Madrid", "Europe/Paris"],
111
+ "29": ["Europe/Sarajevo", "Europe/Skopje", "Europe/Warsaw", "Europe/Zagreb"],
112
+ "30": ["Africa/Lagos"],
113
+ "31": ["Europe/Athens", "Europe/Sofia", "Europe/Vilnius"],
114
+ "32": ["Europe/Bucharest"],
115
+ "33": ["Africa/Cairo"],
116
+ "34": ["Africa/Harare", "Africa/Johannesburg"],
117
+ "35": ["Europe/Helsinki", "Europe/Riga", "Europe/Tallinn"],
118
+ "36": ["Asia/Jerusalem"],
119
+ "37": ["Asia/Baghdad"],
120
+ "38": ["Asia/Kuwait", "Asia/Riyadh"],
121
+ "39": ["Europe/Volgograd"],
122
+ "40": ["Africa/Nairobi"],
123
+ "41": ["Asia/Tehran"],
124
+ "42": ["Asia/Dubai", "Asia/Muscat"],
125
+ "43": ["Asia/Baku"],
126
+ "44": ["Asia/Kabul"],
127
+ "45": ["Asia/Yekaterinburg"],
128
+ "46": ["Asia/Karachi", "Asia/Tashkent"],
129
+ "47": ["Asia/Kolkata", "Asia/Calcutta"],
130
+ "48": ["Asia/Kathmandu"],
131
+ "49": ["Asia/Almaty", "Asia/Novosibirsk"],
132
+ "50": ["Asia/Dhaka"],
133
+ "51": ["Asia/Colombo"],
134
+ "52": ["Asia/Yangon", "Asia/Rangoon"],
135
+ "53": ["Asia/Jakarta", "Asia/Bangkok", "Asia/Ho_Chi_Minh"],
136
+ "54": ["Asia/Krasnoyarsk"],
137
+ "55": ["Asia/Shanghai", "Asia/Chongqing", "Asia/Hong_Kong", "Asia/Urumqi", "Asia/Irkutsk"],
138
+ "56": ["Asia/Ulaanbaatar"],
139
+ "57": ["Asia/Kuala_Lumpur", "Asia/Singapore"],
140
+ "58": ["Australia/Perth"],
141
+ "59": ["Asia/Taipei"],
142
+ "60": ["Asia/Tokyo", "Asia/Seoul"],
143
+ "61": ["Asia/Yakutsk"],
144
+ "62": ["Australia/Adelaide"],
145
+ "63": ["Australia/Darwin"],
146
+ "64": ["Australia/Brisbane"],
147
+ "65": ["Australia/Sydney", "Australia/Melbourne", "Australia/Canberra"],
148
+ "66": ["Pacific/Guam", "Pacific/Port_Moresby"],
149
+ "67": ["Australia/Hobart"],
150
+ "68": ["Asia/Vladivostok"],
151
+ "69": ["Asia/Magadan"],
152
+ "70": ["Pacific/Guadalcanal", "Pacific/Noumea"],
153
+ "71": ["Pacific/Auckland"],
154
+ "72": ["Pacific/Fiji", "Asia/Kamchatka", "Pacific/Majuro"],
155
+ "73": ["Pacific/Tongatapu"],
156
+ "74": ["America/Caracas"],
157
+ "75": ["America/Montevideo"],
158
+ "76": ["Pacific/Kiritimati"],
159
+ "77": ["America/Guyana"],
160
+ "78": ["Africa/Windhoek"],
161
+ "79": ["Africa/Casablanca"],
162
+ "80": ["UTC", "Etc/UTC", "Etc/GMT", "GMT"],
163
+ "81": ["America/Mazatlan", "America/Chihuahua"],
164
+ "82": ["Pacific/Chatham"],
165
+ "83": ["Europe/Moscow"],
166
+ "84": ["Europe/Minsk"],
167
+ "85": ["Europe/Istanbul"],
168
+ "86": ["Pacific/Apia"],
169
+ "87": ["America/Asuncion"],
170
+ };
171
+
172
+ /** What each role may do: the factory profile for an admin, and a profile Circuit adds for the others. */
173
+ static readonly PROFILE = { admin: "super_admin", operator: "circuit-operator", "read-only": "circuit-read-only" } as const;
174
+
175
+ /** The permission groups of an access profile. */
176
+ private static readonly PERMISSION_GROUPS = [
177
+ "secfabgrp",
178
+ "ftviewgrp",
179
+ "authgrp",
180
+ "sysgrp",
181
+ "netgrp",
182
+ "loggrp",
183
+ "fwgrp",
184
+ "vpngrp",
185
+ "utmgrp",
186
+ "wanoptgrp",
187
+ "wifi",
188
+ ];
189
+
190
+ /** The most public keys a user holds. */
191
+ static readonly SSH_KEYS = 3;
192
+
193
+ /** The most resolvers the device and a DHCP server hold, per family. */
194
+ static readonly RESOLVERS = { device: 2, dhcp: 4 } as const;
195
+
196
+ /** The most syslog collectors the device sends to. */
197
+ static readonly COLLECTORS = 4;
198
+
199
+ /** The most VRFs the device holds besides its default table. */
200
+ static readonly VRFS = 31;
201
+
202
+ /** A severity as the platform names it. */
203
+ static readonly LEVEL: Record<Severity, string> = {
204
+ emergency: "emergency",
205
+ alert: "alert",
206
+ critical: "critical",
207
+ error: "error",
208
+ warning: "warning",
209
+ notice: "notification",
210
+ info: "information",
211
+ debug: "debug",
212
+ };
213
+
214
+ /** The helpers the factory runs, by the name a config gives them: protocol and port of each. */
215
+ static readonly HELPERS: Partial<Record<Helper, readonly [string, number, number][]>> = {
216
+ ftp: [["ftp", 6, 21]],
217
+ tftp: [["tftp", 17, 69]],
218
+ sip: [["sip", 17, 5060]],
219
+ h323: [
220
+ ["h323", 6, 1720],
221
+ ["ras", 17, 1719],
222
+ ],
223
+ pptp: [["pptp", 6, 1723]],
224
+ rtsp: [
225
+ ["rtsp", 6, 554],
226
+ ["rtsp", 6, 7070],
227
+ ["rtsp", 6, 8554],
228
+ ],
229
+ };
230
+
231
+ /** Speed by the platform's spelling, for each class a port's cage takes. */
232
+ static readonly SPEEDS: Record<string, Partial<Record<string, string>>> = {
233
+ "40g": { "40g": "40000full" },
234
+ "10g": { "10g": "10000full", "1g": "1000full" },
235
+ "1g": { "1g": "1000full", "100m": "100full" },
236
+ };
237
+
238
+ /** The management address of a device in transparent mode, which is reached only through its console: link-local, on no network it can reach. */
239
+ static readonly TRANSPARENT_MANAGEIP = "169.254.0.1/255.255.255.255";
240
+
241
+ /** The zone number for an IANA zone. UTC when the config names none. */
242
+ static zoneOf(timezone = "UTC"): string | undefined {
243
+ return Object.entries(FortiOsRender.ZONES).find(([, places]) => places.includes(timezone))?.[0];
244
+ }
245
+
246
+ /** The services a port or an interface answers on its addresses: ping always, and every management service the config turns on. */
247
+ private static allowAccess(device: Device): string {
248
+ const management = device.management ?? {};
249
+ const services = ["ping"];
250
+
251
+ if (management.https) services.push("https");
252
+ if (management.ssh) services.push("ssh");
253
+ if (management.snmp) services.push("snmp");
254
+ if (management.http) services.push("http");
255
+ if (management.telnet) services.push("telnet");
256
+ if (management.native) services.push("fgfm");
257
+
258
+ return services.join(" ");
259
+ }
260
+
261
+ /** The whole configuration the device should run, in the form `show` prints. */
262
+ static of(device: Device): string {
263
+ const root = new FortiOsBlock();
264
+ for (const section of FortiOsRender.SECTIONS) root.block(section);
265
+
266
+ const section = (name: Section) => root.block(name);
267
+
268
+ FortiOsRender.certificates(section("vpn certificate local"), section("vpn certificate ca"), device);
269
+ FortiOsRender.global(section("system global"), device);
270
+ if (device.system?.banner) section(FortiOsRender.BANNER).set("buffer", FortiOsTree.quote(device.system.banner));
271
+ // A bridge needs transparent mode, and transparent mode a management address: the config's, or one on no network the device can reach, which keeps it reached only through its console.
272
+ if (FortiOsNames.isTransparent(device)) {
273
+ const management = FortiOsNames.transparentManagement(device);
274
+ const manageip = management
275
+ ? `${Address.host(management.address)}/${Address.netmask(Address.split(management.address).len)}`
276
+ : FortiOsRender.TRANSPARENT_MANAGEIP;
277
+ section("system settings").set("opmode", "transparent").set("manageip", manageip);
278
+ }
279
+ if (FortiOsFirewall.natRules(device).length) section("system settings").set("central-nat", "enable");
280
+ // SIP is otherwise rewritten by its proxy whatever the helpers say; this way only the `sip` helper does.
281
+ section("system settings").set("default-voip-alg-mode", "kernel-helper-based");
282
+ FortiOsRender.bfd(section("system settings"), device);
283
+ FortiOsRender.users(section("system accprofile"), section("system admin"), device);
284
+ FortiOsRender.interfaces(root, device);
285
+ FortiOsRender.dns(section("system dns"), section("system dns-server"), device);
286
+ FortiOsRender.ntp(section("system ntp"), device);
287
+ FortiOsRender.snmp(root, device);
288
+ FortiOsRender.logging(root, device);
289
+ FortiOsRender.flowExport(root, device);
290
+ FortiOsRender.dhcpServers(section("system dhcp server"), device);
291
+ FortiOsRender.staticRoutes(section("router static"), section("router static6"), device);
292
+ FortiOsRouting.render(root, device);
293
+ FortiOsRender.helpers(section("system session-helper"), device);
294
+ FortiOsFirewall.render(root, device);
295
+
296
+ return FortiOsTree.print(root);
297
+ }
298
+
299
+ /** A certificate with its key is the device's own; one without is only trusted. */
300
+ private static certificates(local: FortiOsBlock, trusted: FortiOsBlock, device: Device): void {
301
+ for (const [name, certificate] of Object.entries(device.certificates ?? {})) {
302
+ const text = FortiOsTree.quote(certificate.certificate.trim());
303
+
304
+ if (certificate.private_key)
305
+ local
306
+ .entry(name)
307
+ .set("private-key", FortiOsTree.quote(Secrets.marker(certificate.private_key)))
308
+ .set("certificate", text);
309
+ else trusted.entry(name).set("ca", text);
310
+ }
311
+ }
312
+
313
+ private static global(block: FortiOsBlock, device: Device): void {
314
+ const management = device.management ?? {};
315
+
316
+ block.set("hostname", FortiOsTree.quote(device.name));
317
+
318
+ const zone = FortiOsRender.zoneOf(device.system?.timezone);
319
+ if (zone && zone !== "00") block.set("timezone", zone);
320
+
321
+ if (management.ssh?.port) block.set("admin-ssh-port", management.ssh.port);
322
+ if (management.ssh?.auth_timeout) block.set("admin-ssh-grace-time", management.ssh.auth_timeout);
323
+ if (management.ssh?.weak_crypto) block.set("strong-crypto", "disable");
324
+ if (management.https?.port) block.set("admin-sport", management.https.port);
325
+ if (management.https?.certificate) block.set("admin-server-cert", FortiOsTree.quote(management.https.certificate));
326
+ if (management.http?.port) block.set("admin-port", management.http.port);
327
+ if (management.telnet?.port) block.set("admin-telnet-port", management.telnet.port);
328
+ // The web interface over HTTP otherwise only redirects to HTTPS.
329
+ if (management.http) block.set("admin-https-redirect", "disable");
330
+ if (device.system?.banner) block.set("pre-login-banner", "enable");
331
+
332
+ if (device.lldp === true) block.set("lldp-transmission", "enable").set("lldp-reception", "enable");
333
+ }
334
+
335
+ /** Each user, with the profile its role takes. */
336
+ private static users(profiles: FortiOsBlock, admins: FortiOsBlock, device: Device): void {
337
+ const roles = new Set(Object.values(device.users ?? {}).map((user) => user.role));
338
+
339
+ // An operator changes everything but the system's own settings, which hold the users; read-only changes nothing.
340
+ for (const role of ["operator", "read-only"] as const) {
341
+ if (!roles.has(role)) continue;
342
+
343
+ const profile = profiles.entry(FortiOsRender.PROFILE[role]);
344
+ for (const group of FortiOsRender.PERMISSION_GROUPS)
345
+ profile.set(group, role === "operator" && group !== "sysgrp" ? "read-write" : "read");
346
+ }
347
+
348
+ for (const [name, user] of Object.entries(device.users ?? {})) {
349
+ const admin = admins.entry(name);
350
+
351
+ admin.set("accprofile", FortiOsTree.quote(FortiOsRender.PROFILE[user.role]));
352
+ if (user.description) admin.set("comments", FortiOsTree.quote(user.description));
353
+ admin.set("vdom", FortiOsTree.quote("root"));
354
+ (user.ssh_keys ?? []).forEach((key, index) => admin.set(`ssh-public-key${index + 1}`, FortiOsTree.quote(key)));
355
+ admin.set("password", FortiOsTree.quote(Secrets.marker(user.password)));
356
+ }
357
+ }
358
+
359
+ /** Ports, then the LAGs they join, VLAN interfaces on them, loopbacks, and the tunnels with their own tables. */
360
+ private static interfaces(root: FortiOsBlock, device: Device): void {
361
+ const block = root.block("system interface");
362
+ const access = FortiOsRender.allowAccess(device);
363
+
364
+ for (const port of Catalog.ports(device.platform, device.model)) {
365
+ const settings = device.ports?.[port as keyof typeof device.ports];
366
+ const entry = block.entry(Catalog.spell(device.platform, device.model, port));
367
+
368
+ entry.set("vdom", FortiOsTree.quote("root"));
369
+ entry.set("type", "physical");
370
+
371
+ if (!settings) {
372
+ FortiOsRender.redirects(entry, device);
373
+ entry.set("status", "down");
374
+ continue;
375
+ }
376
+
377
+ if (settings.description) entry.set("description", FortiOsTree.quote(settings.description));
378
+
379
+ const speed = settings.speed && FortiOsRender.SPEEDS[port.split("-")[0]!]?.[settings.speed];
380
+ if (speed) entry.set("speed", speed);
381
+
382
+ // A port in a LAG has no redirects of its own: `command parse error`.
383
+ if (settings.lag) continue;
384
+
385
+ FortiOsRender.redirects(entry, device);
386
+ if (settings.mtu) entry.set("mtu-override", "enable").set("mtu", settings.mtu);
387
+ FortiOsRender.routed(entry, device, port, settings, access);
388
+ }
389
+
390
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
391
+ if (iface.type !== "lag") continue;
392
+
393
+ const members = Object.entries(device.ports ?? {})
394
+ .filter(([, port]) => port?.lag === name)
395
+ .map(([port]) => Catalog.spell(device.platform, device.model, port));
396
+
397
+ const entry = block.entry(name);
398
+ entry.set("vdom", FortiOsTree.quote("root"));
399
+ entry.set("type", "aggregate");
400
+ if (members.length) entry.set("member", FortiOsTree.quoteAll(members));
401
+ if (iface.mode === "static") entry.set("lacp-mode", "static");
402
+ if (iface.description) entry.set("description", FortiOsTree.quote(iface.description));
403
+ if (iface.mtu) entry.set("mtu-override", "enable").set("mtu", iface.mtu);
404
+ FortiOsRender.redirects(entry, device);
405
+ FortiOsRender.routed(entry, device, name, iface, access);
406
+ }
407
+
408
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
409
+ if (iface.type !== "vlan") continue;
410
+
411
+ const parent = FortiOsRender.vlanParent(device, iface.vlan);
412
+ const entry = block.entry(name);
413
+ entry.set("vdom", FortiOsTree.quote("root"));
414
+ if (parent) entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, parent)));
415
+ entry.set("vlanid", device.vlans?.[iface.vlan]?.id ?? 0);
416
+ const domain = FortiOsNames.forwardDomainOf(device, name);
417
+ if (domain) entry.set("forward-domain", domain);
418
+ if (iface.description) entry.set("description", FortiOsTree.quote(iface.description));
419
+ if (iface.mtu) entry.set("mtu-override", "enable").set("mtu", iface.mtu);
420
+ FortiOsRender.redirects(entry, device);
421
+ FortiOsRender.routed(entry, device, name, iface, access);
422
+ }
423
+
424
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
425
+ if (iface.type !== "loopback") continue;
426
+
427
+ const entry = block.entry(name);
428
+ entry.set("vdom", FortiOsTree.quote("root"));
429
+ entry.set("type", "loopback");
430
+ if (iface.description) entry.set("description", FortiOsTree.quote(iface.description));
431
+ FortiOsRender.redirects(entry, device);
432
+ FortiOsRender.routed(entry, device, name, iface, access);
433
+ }
434
+
435
+ // A tunnel's interface is created by its own table, which names the interface it leaves through.
436
+ for (const [name, iface] of Object.entries(device.interfaces ?? {})) {
437
+ if (iface.type !== "gre" && iface.type !== "vxlan") continue;
438
+
439
+ const outgoing = FortiOsNames.holderOf(device, iface.local);
440
+ const via = outgoing ? FortiOsTree.quote(FortiOsNames.nameOf(device, outgoing)) : '""';
441
+
442
+ // Between IPv6 endpoints the family is stated first: the addresses of the other family are not settings until it is.
443
+ const ipv6 = Address.family(iface.remote) === "ipv6";
444
+
445
+ if (iface.type === "gre") {
446
+ const tunnel = root.block("system gre-tunnel").entry(name).set("interface", via);
447
+ if (ipv6) tunnel.set("ip-version", 6).set("remote-gw6", iface.remote).set("local-gw6", iface.local);
448
+ else tunnel.set("remote-gw", iface.remote).set("local-gw", iface.local);
449
+ } else {
450
+ const tunnel = root.block("system vxlan").entry(name).set("interface", via).set("vni", iface.vni);
451
+ if (ipv6) tunnel.set("ip-version", "ipv6-unicast").set("remote-ip6", FortiOsTree.quote(iface.remote));
452
+ else tunnel.set("remote-ip", FortiOsTree.quote(iface.remote));
453
+ if (iface.port !== undefined && iface.port !== 4789) tunnel.set("dstport", iface.port);
454
+ }
455
+
456
+ const entry = block.entry(name);
457
+ entry.set("vdom", FortiOsTree.quote("root"));
458
+ entry.set("type", iface.type === "gre" ? "tunnel" : "vxlan");
459
+ entry.set("interface", via);
460
+ if (iface.type === "vxlan" && iface.mac) entry.set("macaddr", iface.mac.toLowerCase());
461
+ if (iface.description) entry.set("description", FortiOsTree.quote(iface.description));
462
+ if (iface.mtu) entry.set("mtu-override", "enable").set("mtu", iface.mtu);
463
+ FortiOsRender.redirects(entry, device);
464
+ FortiOsRender.routed(entry, device, name, iface, access);
465
+ }
466
+ }
467
+
468
+ /** The platform sends ICMP redirects unless an interface says otherwise; the config sends them only when asked. */
469
+ private static redirects(entry: FortiOsBlock, device: Device): void {
470
+ if (!device.system?.ip?.icmp_redirects) entry.set("icmp-send-redirect", "disable");
471
+ }
472
+
473
+ /** The one port or LAG that carries a VLAN tagged, which its VLAN interface sits on. `unsupported()` refuses any other case. */
474
+ static vlanParent(device: Device, vlan: string): string | undefined {
475
+ const carriers = [
476
+ ...Object.entries(device.ports ?? {}).filter(([, port]) => !port?.lag && port?.trunk_vlans?.includes(vlan)),
477
+ ...Object.entries(device.interfaces ?? {}).filter(([, iface]) => iface.type === "lag" && iface.trunk_vlans?.includes(vlan)),
478
+ ];
479
+
480
+ return carriers.length === 1 ? carriers[0]![0] : undefined;
481
+ }
482
+
483
+ /** Addresses and their routing table, the services answered on them, VRRP, DHCP relay, router advertisements, LLDP and flow sampling of a port or an interface. */
484
+ private static routed(entry: FortiOsBlock, device: Device, name: string, settings: RoutedSettings, access: string): void {
485
+ // In transparent mode no interface holds an address: the management address is the device's, and answers on its interface alone.
486
+ if (FortiOsNames.isTransparent(device)) {
487
+ if (FortiOsNames.transparentManagement(device)?.name === name) entry.set("allowaccess", access);
488
+ return;
489
+ }
490
+
491
+ // Services answer only where the config says, when it says; ping everywhere.
492
+ const answering = device.management?.interfaces;
493
+ if (answering && !answering.includes(name)) access = "ping";
494
+
495
+ const addresses = FortiOsNames.addressesOf(settings);
496
+ const ipv4 = addresses.filter((address) => Address.family(Address.host(address)) === "ipv4");
497
+ const ipv6 = addresses.filter((address) => Address.family(Address.host(address)) === "ipv6");
498
+
499
+ const vrf = FortiOsNames.vrfNumber(device, settings.vrf);
500
+ if (vrf) entry.set("vrf", vrf);
501
+
502
+ // A tunnel holds an IPv4 address as a host and its peer with the link's mask.
503
+ const peer = settings.addresses?.find((item): item is { address: string; peer: string } => typeof item !== "string");
504
+
505
+ if (peer) {
506
+ entry.set("ip", `${Address.host(peer.address)} 255.255.255.255`);
507
+ entry.set("remote-ip", `${peer.peer} ${Address.netmask(Address.split(peer.address).len)}`);
508
+ entry.set("allowaccess", access);
509
+ } else if (ipv4.length) {
510
+ entry.set("ip", FortiOsNames.subnet(ipv4[0]!));
511
+ entry.set("allowaccess", access);
512
+ }
513
+
514
+ if (!peer && ipv4.length > 1) {
515
+ entry.set("secondary-IP", "enable");
516
+ const secondaries = entry.block("secondaryip");
517
+ ipv4.slice(1).forEach((address, index) =>
518
+ secondaries
519
+ .entry(index + 1)
520
+ .set("ip", FortiOsNames.subnet(address))
521
+ .set("allowaccess", access)
522
+ );
523
+ }
524
+
525
+ const relays = Object.values(device.dhcp_relay ?? {}).filter((relay) => relay.interface === name);
526
+ const relayed = relays.flatMap((relay) => relay.servers);
527
+ const relayed4 = relayed.filter((server) => Address.family(server) === "ipv4");
528
+ const relayed6 = relayed.filter((server) => Address.family(server) === "ipv6");
529
+
530
+ if (ipv6.length || relayed6.length) {
531
+ const block = entry.block("ipv6");
532
+
533
+ if (ipv6.length) {
534
+ block.set("ip6-address", ipv6[0]!);
535
+ block.set("ip6-allowaccess", access);
536
+ }
537
+
538
+ if (settings.ipv6_ra) {
539
+ block.set("ip6-send-adv", "enable");
540
+ const prefixes = block.block("ip6-prefix-list");
541
+ for (const address of ipv6) prefixes.entry(FortiOsNames.network(address), true);
542
+ }
543
+
544
+ if (relayed6.length) block.set("dhcp6-relay-service", "enable").set("dhcp6-relay-ip", relayed6.join(" "));
545
+
546
+ if (ipv6.length > 1) {
547
+ const extra = block.block("ip6-extra-addr");
548
+ for (const address of ipv6.slice(1)) extra.entry(address, true);
549
+ }
550
+ }
551
+
552
+ if (settings.vrrp?.length) {
553
+ const groups = entry.block("vrrp");
554
+
555
+ for (const group of settings.vrrp) {
556
+ const vrrp = groups.entry(group.id).set("vrip", group.address);
557
+ if (group.priority !== undefined && group.priority !== 100) vrrp.set("priority", group.priority);
558
+ if (group.interval !== undefined && group.interval !== 1) vrrp.set("adv-interval", group.interval);
559
+ // The platform preempts unless told not to; the config preempts only when asked.
560
+ if (!group.preempt) vrrp.set("preempt", "disable");
561
+ }
562
+ }
563
+
564
+ if (relayed4.length) {
565
+ entry.set("dhcp-relay-service", "enable");
566
+ entry.set("dhcp-relay-ip", FortiOsTree.quoteAll(relayed4));
567
+ }
568
+
569
+ // LLDP follows the device unless a port says otherwise, or the device lists where it runs.
570
+ const listed = typeof device.lldp === "object" && device.lldp.interfaces.includes(name as never);
571
+ if (settings.lldp === false) entry.set("lldp-transmission", "disable").set("lldp-reception", "disable");
572
+ else if (listed || (settings.lldp === true && device.lldp !== true))
573
+ entry.set("lldp-transmission", "enable").set("lldp-reception", "enable");
574
+
575
+ // A tunnel is not sampled: its traffic is, on the interface it leaves through.
576
+ const flow = device.flow_export;
577
+ const tunnel = ["gre", "vxlan"].includes(device.interfaces?.[name]?.type ?? "");
578
+ const sampled = flow && !tunnel && (!flow.interfaces || flow.interfaces.includes(name as never));
579
+
580
+ if (sampled && flow.protocol === "sflow") {
581
+ entry.set("sflow-sampler", "enable");
582
+ if (flow.sampling !== undefined && flow.sampling !== 2000) entry.set("sample-rate", flow.sampling);
583
+ }
584
+
585
+ if (sampled && flow.protocol === "netflow-v9") entry.set("netflow-sampler", "both");
586
+ }
587
+
588
+ /** The device's resolvers, and the interfaces it answers DNS queries on when it serves them. */
589
+ private static dns(block: FortiOsBlock, servers: FortiOsBlock, device: Device): void {
590
+ const resolvers = device.system?.dns?.servers ?? [];
591
+ const ipv4 = resolvers.filter((server) => Address.family(server) === "ipv4");
592
+ const ipv6 = resolvers.filter((server) => Address.family(server) === "ipv6");
593
+
594
+ if (ipv4[0]) block.set("primary", ipv4[0]);
595
+ if (ipv4[1]) block.set("secondary", ipv4[1]);
596
+ if (ipv6[0]) block.set("ip6-primary", ipv6[0]);
597
+ if (ipv6[1]) block.set("ip6-secondary", ipv6[1]);
598
+
599
+ // Recursive, the default mode, answers from what the device has resolved itself.
600
+ if (device.system?.dns?.serve) for (const name of FortiOsNames.addressed(device)) servers.entry(FortiOsNames.nameOf(device, name));
601
+ }
602
+
603
+ /** The servers the device keeps time from, and the interfaces it answers time queries on when it serves them. */
604
+ private static ntp(block: FortiOsBlock, device: Device): void {
605
+ const ntp = device.system?.ntp;
606
+ const servers = ntp?.servers ?? [];
607
+
608
+ if (servers.length) {
609
+ block.set("ntpsync", "enable");
610
+ block.set("type", "custom");
611
+
612
+ const list = block.block("ntpserver");
613
+ servers.forEach((server, index) => list.entry(index + 1).set("server", FortiOsTree.quote(server)));
614
+ }
615
+
616
+ if (ntp?.serve) {
617
+ block.set("server-mode", "enable");
618
+ block.set("interface", FortiOsTree.quoteAll(FortiOsNames.addressed(device).map((name) => FortiOsNames.nameOf(device, name))));
619
+ }
620
+ }
621
+
622
+ /** SNMP version 2c with one community, guarded by its hosts, and version 3 users. */
623
+ private static snmp(root: FortiOsBlock, device: Device): void {
624
+ const snmp = device.management?.snmp;
625
+ if (!snmp) return;
626
+
627
+ const sysinfo = root.block("system snmp sysinfo");
628
+ sysinfo.set("status", "enable");
629
+ if (snmp.contact) sysinfo.set("contact-info", FortiOsTree.quote(snmp.contact));
630
+ if (snmp.location) sysinfo.set("location", FortiOsTree.quote(snmp.location));
631
+
632
+ if (snmp.community) {
633
+ const community = root.block("system snmp community").entry(1);
634
+ community.set("name", FortiOsTree.quote(Secrets.marker(snmp.community)));
635
+
636
+ const allow = snmp.allow ?? device.management?.allow ?? [];
637
+ const ipv4 = allow.filter((prefix) => Address.family(Address.host(prefix)) === "ipv4");
638
+ const ipv6 = allow.filter((prefix) => Address.family(Address.host(prefix)) === "ipv6");
639
+ ipv4.forEach((prefix, index) =>
640
+ community
641
+ .block("hosts")
642
+ .entry(index + 1)
643
+ .set("ip", FortiOsNames.subnet(FortiOsNames.network(prefix)))
644
+ );
645
+ ipv6.forEach((prefix, index) =>
646
+ community
647
+ .block("hosts6")
648
+ .entry(index + 1)
649
+ .set("ipv6", FortiOsNames.network(prefix))
650
+ );
651
+
652
+ // Version 2c only, and no traps: the config has nowhere to send them.
653
+ community.set("query-v1-status", "disable");
654
+ if (snmp.port) community.set("query-v2c-port", snmp.port);
655
+ community.set("trap-v1-status", "disable");
656
+ community.set("trap-v2c-status", "disable");
657
+ }
658
+
659
+ for (const [name, user] of Object.entries(snmp.users ?? {})) {
660
+ const entry = root.block("system snmp user").entry(name);
661
+ entry.set("security-level", "auth-priv");
662
+ if (snmp.port) entry.set("query-port", snmp.port);
663
+ // SHA1 and AES128 are the platform's defaults and are not printed.
664
+ entry.set("auth-pwd", FortiOsTree.quote(Secrets.marker(user.auth_password)));
665
+ if (user.privacy === "des") entry.set("priv-proto", "des");
666
+ entry.set("priv-pwd", FortiOsTree.quote(Secrets.marker(user.privacy_password)));
667
+ }
668
+ }
669
+
670
+ /** Logs kept in memory from a level up, none on disk, and up to four syslog collectors, each with the least severe message it takes. */
671
+ private static logging(root: FortiOsBlock, device: Device): void {
672
+ const logging = device.system?.logging;
673
+
674
+ // The device prints whether it keeps logs in memory either way, and keeps them unless told not to.
675
+ root.block("log memory setting").set("status", logging?.local ? "enable" : "disable");
676
+ if (logging?.local && logging.local.level !== "info")
677
+ root.block("log memory filter").set("severity", FortiOsRender.LEVEL[logging.local.level]);
678
+
679
+ // The disk keeps logs unless told not to; the config keeps them only in memory.
680
+ root.block("log disk setting").set("status", "disable");
681
+
682
+ (logging?.remote ?? []).slice(0, FortiOsRender.COLLECTORS).forEach((collector, index) => {
683
+ const number = index === 0 ? "" : String(index + 1);
684
+ const setting = root.block(`log syslogd${number} setting`);
685
+ setting.set("status", "enable");
686
+ setting.set("server", FortiOsTree.quote(collector.address));
687
+ if (collector.port && collector.port !== 514) setting.set("port", collector.port);
688
+
689
+ if (collector.level !== "info") root.block(`log syslogd${number} filter`).set("severity", FortiOsRender.LEVEL[collector.level]);
690
+ });
691
+ }
692
+
693
+ /** One collector, and sampling on each interface it observes. */
694
+ private static flowExport(root: FortiOsBlock, device: Device): void {
695
+ const flow = device.flow_export;
696
+ const collector = flow?.collectors[0];
697
+ if (!flow || !collector) return;
698
+
699
+ if (flow.protocol === "sflow") {
700
+ const block = root.block("system sflow").set("collector-ip", collector.address);
701
+ if (collector.port && collector.port !== 6343) block.set("collector-port", collector.port);
702
+ }
703
+
704
+ if (flow.protocol === "netflow-v9") {
705
+ const block = root.block("system netflow").set("collector-ip", collector.address);
706
+ if (collector.port && collector.port !== 2055) block.set("collector-port", collector.port);
707
+ }
708
+ }
709
+
710
+ private static dhcpServers(block: FortiOsBlock, device: Device): void {
711
+ Object.values(device.dhcp ?? {}).forEach((server, index) => {
712
+ const entry = block.entry(index + 1);
713
+ if (server.lease_time !== undefined && server.lease_time !== 604800) entry.set("lease-time", server.lease_time);
714
+ entry.set("default-gateway", server.gateway);
715
+ entry.set("netmask", Address.netmask(Address.split(server.network).len));
716
+ entry.set("interface", FortiOsTree.quote(FortiOsNames.nameOf(device, server.interface)));
717
+ entry.set("dns-service", "specify");
718
+ entry.block("ip-range").entry(1).set("start-ip", server.pool[0]).set("end-ip", server.pool[1]);
719
+
720
+ (server.reservations ?? []).forEach((reservation, number) => {
721
+ const reserved = entry.block("reserved-address").entry(number + 1);
722
+ reserved.set("ip", reservation.address).set("mac", reservation.mac.toLowerCase());
723
+ if (reservation.description) reserved.set("description", FortiOsTree.quote(reservation.description));
724
+ });
725
+
726
+ (server.dns ?? []).forEach((resolver, number) => entry.set(`dns-server${number + 1}`, resolver));
727
+ });
728
+ }
729
+
730
+ /** Each route by its position. A route goes in the routing table of the interface it leaves through. A default route is the platform's default destination and is not printed. */
731
+ private static staticRoutes(ipv4: FortiOsBlock, ipv6: FortiOsBlock, device: Device): void {
732
+ const counters = { ipv4: 0, ipv6: 0 };
733
+
734
+ for (const route of device.routing?.static ?? []) {
735
+ const family = Address.family(Address.split(route.prefix).addr);
736
+ const entry = (family === "ipv4" ? ipv4 : ipv6).entry(++counters[family]);
737
+ const prefix = FortiOsNames.network(route.prefix);
738
+
739
+ if (family === "ipv6" && prefix !== "::/0") entry.set("dst", prefix);
740
+ if (family === "ipv4" && prefix !== "0.0.0.0/0") entry.set("dst", FortiOsNames.subnet(prefix));
741
+
742
+ if (route.via) entry.set("gateway", route.via);
743
+
744
+ // In transparent mode a route names no interface: it carries only the management address's own traffic.
745
+ const outgoing = route.interface ?? (route.via ? FortiOsNames.interfaceFor(device, route.via, route.vrf) : undefined);
746
+ if (outgoing && !route.blackhole && !FortiOsNames.isTransparent(device))
747
+ entry.set("device", FortiOsTree.quote(FortiOsNames.nameOf(device, outgoing)));
748
+
749
+ if (route.distance !== undefined && route.distance !== 10) entry.set("distance", route.distance);
750
+ if (route.description) entry.set("comment", FortiOsTree.quote(route.description));
751
+ if (route.blackhole) entry.set("blackhole", "enable");
752
+ if (route.bfd) entry.set("bfd", "enable");
753
+ }
754
+ }
755
+
756
+ /** BFD runs once a route or a session uses it, or the config sets its timers, which the device shows only while it runs. */
757
+ private static bfd(settings: FortiOsBlock, device: Device): void {
758
+ const timers = device.routing?.bfd;
759
+ if (!FortiOsRouting.usesBfd(device) && !timers) return;
760
+
761
+ settings.set("bfd", "enable");
762
+ if (!timers) return;
763
+
764
+ settings
765
+ .set("bfd-desired-min-tx", timers.interval)
766
+ .set("bfd-required-min-rx", timers.interval)
767
+ .set("bfd-detect-mult", timers.multiplier);
768
+ }
769
+
770
+ /** The helpers the config lists, each on the protocols and ports the factory gives it. None run otherwise. */
771
+ private static helpers(block: FortiOsBlock, device: Device): void {
772
+ let number = 0;
773
+
774
+ for (const helper of device.firewall?.helpers ?? []) {
775
+ for (const [name, protocol, port] of FortiOsRender.HELPERS[helper] ?? [])
776
+ block.entry(++number).set("name", name).set("protocol", protocol).set("port", port);
777
+ }
778
+ }
779
+ }