@takodotid/circuit 0.8.0 → 0.10.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/README.md +3 -2
- package/dist/chunks/index-ex1wcst7.js +109 -0
- package/dist/chunks/index-fq57hya4.js +20 -0
- package/dist/chunks/new-7n8qvtjd.js +168 -0
- package/dist/cli/index.js +4079 -0
- package/dist/index.js +48 -0
- package/dist/presets/index.js +257 -0
- package/dist/types/adapters/devices/catalog.d.ts +58 -0
- package/dist/types/adapters/devices/derive.d.ts +5 -0
- package/dist/types/adapters/devices/index.d.ts +3 -0
- package/dist/types/adapters/devices/lines.d.ts +56 -0
- package/dist/types/adapters/devices/raisecom-ros/index.d.ts +23 -0
- package/dist/types/adapters/devices/raisecom-ros/models.d.ts +14 -0
- package/dist/types/adapters/devices/routeros/export.d.ts +39 -0
- package/dist/types/adapters/devices/routeros/index.d.ts +17 -0
- package/dist/types/adapters/devices/routeros/models.d.ts +14 -0
- package/dist/types/adapters/devices/routeros/plan.d.ts +4 -0
- package/dist/types/adapters/devices/routeros/render.d.ts +96 -0
- package/dist/types/adapters/devices/types.d.ts +34 -0
- package/dist/types/adapters/devices/vrp/index.d.ts +24 -0
- package/dist/types/adapters/devices/vrp/models.d.ts +12 -0
- package/dist/types/adapters/registries/index.d.ts +3 -0
- package/dist/types/adapters/registries/peeringdb.d.ts +16 -0
- package/dist/types/core/addr.d.ts +24 -0
- package/dist/types/core/define.d.ts +50 -0
- package/dist/types/core/merge.d.ts +4 -0
- package/dist/types/core/secrets.d.ts +21 -0
- package/dist/types/core/time.d.ts +2 -0
- package/dist/types/core/validate.d.ts +2 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/presets/index.d.ts +99 -0
- package/dist/types/schema/common.d.ts +24 -0
- package/dist/types/schema/device.d.ts +80 -0
- package/dist/types/schema/firewall.d.ts +118 -0
- package/dist/types/schema/index.d.ts +7 -0
- package/dist/types/schema/interfaces.d.ts +181 -0
- package/dist/types/schema/policy.d.ts +88 -0
- package/dist/types/schema/routing.d.ts +115 -0
- package/dist/types/schema/system.d.ts +186 -0
- package/dist/types/transport/ssh.d.ts +22 -0
- package/package.json +19 -5
- package/src/adapters/devices/routeros/TRAPS.md +2 -0
- package/src/adapters/devices/routeros/render.ts +1 -0
- package/src/cli/index.ts +10 -2
- package/src/cli/new.ts +60 -12
- package/src/core/secrets.ts +7 -9
- package/src/presets/index.ts +27 -1
- package/src/schema/policy.ts +2 -0
- package/templates/project/AGENTS.md +10 -10
- package/templates/project/README.md +7 -7
- package/templates/project/tsconfig.json +1 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Address, IP, Prefix, Secret } from "./common";
|
|
2
|
+
/** A VLAN. The key it is declared under is its name. */
|
|
3
|
+
export type Vlan = {
|
|
4
|
+
/** The number carried on the wire, 1 to 4094. */
|
|
5
|
+
id: number;
|
|
6
|
+
/** What the VLAN is for. */
|
|
7
|
+
description?: string;
|
|
8
|
+
};
|
|
9
|
+
/** A storm-control threshold: packets per second, or percent of the link's speed. */
|
|
10
|
+
export type Threshold = {
|
|
11
|
+
pps: number;
|
|
12
|
+
} | {
|
|
13
|
+
percent: number;
|
|
14
|
+
};
|
|
15
|
+
/** A virtual router address shared with other devices on the same segment, RFC 5798. */
|
|
16
|
+
export type Vrrp = {
|
|
17
|
+
/** Virtual router number, 1 to 255. The same on every device sharing the address. */
|
|
18
|
+
id: number;
|
|
19
|
+
/** The shared address, without a length. */
|
|
20
|
+
address: IP;
|
|
21
|
+
/** Higher wins the master election, 1 to 254. 100 when absent. */
|
|
22
|
+
priority?: number;
|
|
23
|
+
/** Take mastership back from a lower priority device when this one returns. Off when absent. */
|
|
24
|
+
preempt?: boolean;
|
|
25
|
+
/** Seconds between advertisements. 1 when absent. */
|
|
26
|
+
interval?: number;
|
|
27
|
+
};
|
|
28
|
+
/** Layer 3 settings shared by every interface that can hold an address. */
|
|
29
|
+
export type Routed<F extends string> = {
|
|
30
|
+
/** Addresses with their length. Use `{ address, peer }` for a point-to-point address whose far end is outside its own prefix. */
|
|
31
|
+
addresses?: readonly (Address | {
|
|
32
|
+
address: Address;
|
|
33
|
+
peer: IP;
|
|
34
|
+
})[];
|
|
35
|
+
/** Send IPv6 router advertisements on this interface. Off when absent. */
|
|
36
|
+
ipv6_ra?: boolean;
|
|
37
|
+
/** Overrides the device's `hardware_offload` for traffic routed through this interface. Software forwarding is what makes the firewall apply. */
|
|
38
|
+
hardware_offload?: boolean;
|
|
39
|
+
/** Virtual router addresses held on this interface. */
|
|
40
|
+
vrrp?: readonly Vrrp[];
|
|
41
|
+
/** The VRF this interface routes in. The default table when absent. */
|
|
42
|
+
vrf?: F;
|
|
43
|
+
/** This interface is the network's presence at an internet exchange. Devices ignore it; `circuit peeringdb` publishes it. */
|
|
44
|
+
exchange?: Exchange;
|
|
45
|
+
};
|
|
46
|
+
/** Presence at an internet exchange, as a registry lists it. The addresses are the interface's own. */
|
|
47
|
+
export type Exchange = {
|
|
48
|
+
/** The exchange's name, for output. */
|
|
49
|
+
name: string;
|
|
50
|
+
/** Capacity toward the exchange, in Mbit/s. */
|
|
51
|
+
speed: number;
|
|
52
|
+
/** The exchange LAN's id at PeeringDB, its `ixlan_id`. */
|
|
53
|
+
peeringdb_ixlan?: number;
|
|
54
|
+
};
|
|
55
|
+
/** Spanning tree settings for one port. */
|
|
56
|
+
export type PortStp = {
|
|
57
|
+
/** The port faces a host, never a switch, so it forwards at once. */
|
|
58
|
+
edge?: boolean;
|
|
59
|
+
/** Path cost. Lower is preferred. Derived from speed when absent. */
|
|
60
|
+
cost?: number;
|
|
61
|
+
/** Port priority, 0 to 240 in steps of 16. Lower is preferred. */
|
|
62
|
+
priority?: number;
|
|
63
|
+
};
|
|
64
|
+
/** Layer 2 membership. A port or LAG with none of these is routed. */
|
|
65
|
+
export type Switched<V extends string> = {
|
|
66
|
+
/** Untagged member of one VLAN. */
|
|
67
|
+
access_vlan?: V;
|
|
68
|
+
/** Tagged member of these VLANs. */
|
|
69
|
+
trunk_vlans?: readonly V[];
|
|
70
|
+
/** Untagged frames on a trunk belong to this VLAN. Only with `trunk_vlans`. */
|
|
71
|
+
native_vlan?: V;
|
|
72
|
+
/** Overrides the device's `stp` for this port. `false` stops it sending or acting on BPDUs. */
|
|
73
|
+
stp?: boolean | PortStp;
|
|
74
|
+
/** Drop flooded traffic above these thresholds. */
|
|
75
|
+
storm_control?: {
|
|
76
|
+
broadcast?: Threshold;
|
|
77
|
+
multicast?: Threshold;
|
|
78
|
+
unknown_unicast?: Threshold;
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
/** The other end of a cable: another device and its port. Both ends must agree on how they are switched. */
|
|
82
|
+
export type Link = {
|
|
83
|
+
/** Name of the device at the other end. */
|
|
84
|
+
device: string;
|
|
85
|
+
/** The port on that device. */
|
|
86
|
+
port: string;
|
|
87
|
+
};
|
|
88
|
+
/** A physical port. Ports a device does not declare are shut down. */
|
|
89
|
+
export type Port<V extends string, A extends string, L extends string, F extends string> = Routed<F> & Switched<V> & {
|
|
90
|
+
/** What is plugged in. */
|
|
91
|
+
description?: string;
|
|
92
|
+
/** Largest IP packet, in bytes. 1500 when absent. */
|
|
93
|
+
mtu?: number;
|
|
94
|
+
/** Fixed speed, for an optic slower than the cage. Negotiated when absent. */
|
|
95
|
+
speed?: "100m" | "1g" | "10g" | "25g" | "40g" | "50g" | "100g";
|
|
96
|
+
/** Overrides the device's `lldp` for this port. */
|
|
97
|
+
lldp?: boolean;
|
|
98
|
+
/** Hardware ACL applied to traffic arriving on this port. */
|
|
99
|
+
acl?: A;
|
|
100
|
+
/** Member of this LAG. The LAG holds every other setting. */
|
|
101
|
+
lag?: L;
|
|
102
|
+
/** Another of our devices at the other end of the cable. */
|
|
103
|
+
link?: Link;
|
|
104
|
+
};
|
|
105
|
+
/** Settings every interface that is not a physical port shares. */
|
|
106
|
+
type Base<F extends string> = Routed<F> & {
|
|
107
|
+
/** What the interface is for. */
|
|
108
|
+
description?: string;
|
|
109
|
+
/** Largest IP packet, in bytes. */
|
|
110
|
+
mtu?: number;
|
|
111
|
+
};
|
|
112
|
+
/** A layer 3 interface on a VLAN. */
|
|
113
|
+
export type VlanInterface<V extends string, F extends string> = Base<F> & {
|
|
114
|
+
type: "vlan";
|
|
115
|
+
/** The VLAN it sits on. */
|
|
116
|
+
vlan: V;
|
|
117
|
+
};
|
|
118
|
+
/** An interface that is always up and belongs to no link, for addresses that must not depend on one. */
|
|
119
|
+
export type Loopback<F extends string> = Base<F> & {
|
|
120
|
+
type: "loopback";
|
|
121
|
+
};
|
|
122
|
+
/** A link aggregation. Member ports name it with `lag`. */
|
|
123
|
+
export type Lag<V extends string, F extends string> = Base<F> & Switched<V> & {
|
|
124
|
+
type: "lag";
|
|
125
|
+
/** LACP negotiates membership with the far end; static bundles without asking. LACP when absent. */
|
|
126
|
+
mode?: "lacp" | "static";
|
|
127
|
+
/** Number on platforms that name a LAG by number. Its position among the device's LAGs when absent. */
|
|
128
|
+
id?: number;
|
|
129
|
+
};
|
|
130
|
+
/** A GRE tunnel. */
|
|
131
|
+
export type Gre<F extends string> = Base<F> & {
|
|
132
|
+
type: "gre";
|
|
133
|
+
/** Our tunnel endpoint. */
|
|
134
|
+
local: IP;
|
|
135
|
+
/** The far tunnel endpoint. */
|
|
136
|
+
remote: IP;
|
|
137
|
+
};
|
|
138
|
+
/** A point-to-point VXLAN tunnel. */
|
|
139
|
+
export type Vxlan<F extends string> = Base<F> & {
|
|
140
|
+
type: "vxlan";
|
|
141
|
+
/** Segment identifier, the same on both ends. */
|
|
142
|
+
vni: number;
|
|
143
|
+
/** Our tunnel endpoint. */
|
|
144
|
+
local: IP;
|
|
145
|
+
/** The far tunnel endpoint. */
|
|
146
|
+
remote: IP;
|
|
147
|
+
/** UDP port. 4789 when absent. */
|
|
148
|
+
port?: number;
|
|
149
|
+
/** Fixed MAC address, so a rebuild keeps the one the far side learned. */
|
|
150
|
+
mac?: string;
|
|
151
|
+
};
|
|
152
|
+
/** One peer of a WireGuard interface. */
|
|
153
|
+
export type WireGuardPeer = {
|
|
154
|
+
/** Who or what the peer is. */
|
|
155
|
+
name: string;
|
|
156
|
+
/** The peer's public key. Public keys are not secrets. */
|
|
157
|
+
public_key: string;
|
|
158
|
+
/** Addresses the peer may send from and that are routed to it. */
|
|
159
|
+
allowed_addresses: readonly Prefix[];
|
|
160
|
+
/** `host:port`, for a peer this side dials. The peer dials in when absent. */
|
|
161
|
+
endpoint?: string;
|
|
162
|
+
/** Seconds between keepalives, for a peer behind NAT. None when absent. */
|
|
163
|
+
keepalive?: number;
|
|
164
|
+
/** What a generated client config routes into the tunnel. Affects no forwarding here. */
|
|
165
|
+
client_allowed_addresses?: readonly Prefix[];
|
|
166
|
+
};
|
|
167
|
+
/** A WireGuard interface. */
|
|
168
|
+
export type WireGuard<F extends string> = Base<F> & {
|
|
169
|
+
type: "wireguard";
|
|
170
|
+
/** UDP port peers connect to. */
|
|
171
|
+
listen_port: number;
|
|
172
|
+
/** The address clients connect to, for generated client configs. */
|
|
173
|
+
endpoint?: IP;
|
|
174
|
+
/** This interface's private key. Its public key is derived from it. */
|
|
175
|
+
private_key: Secret;
|
|
176
|
+
/** Who may connect. */
|
|
177
|
+
peers?: readonly WireGuardPeer[];
|
|
178
|
+
};
|
|
179
|
+
/** Any interface that is not a physical port. */
|
|
180
|
+
export type Interface<V extends string, F extends string> = VlanInterface<V, F> | Loopback<F> | Lag<V, F> | Gre<F> | Vxlan<F> | WireGuard<F>;
|
|
181
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Family, IP, Prefix } from "./common";
|
|
2
|
+
/** Registries a prefix set can be fetched from. */
|
|
3
|
+
export type Registry = "ripe-stat";
|
|
4
|
+
/** Prefixes listed here, or fetched from a registry by `refresh` and never typed by hand. */
|
|
5
|
+
export type PrefixSet = {
|
|
6
|
+
/** The prefixes, of one family. */
|
|
7
|
+
prefixes: readonly Prefix[];
|
|
8
|
+
} | {
|
|
9
|
+
source: {
|
|
10
|
+
/** Where the prefixes come from. */
|
|
11
|
+
registry: Registry;
|
|
12
|
+
/** What to ask for, for example `AS7713`. */
|
|
13
|
+
query: string;
|
|
14
|
+
/** Which family to keep. */
|
|
15
|
+
family: Family;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/** Conditions on a route. All must hold. An empty match holds for every route. */
|
|
19
|
+
export type Match<S extends string> = {
|
|
20
|
+
/** Only routes of this family. */
|
|
21
|
+
family?: Family;
|
|
22
|
+
/** Exactly this prefix. */
|
|
23
|
+
prefix?: Prefix;
|
|
24
|
+
/** Any prefix in this set. */
|
|
25
|
+
prefix_set?: S;
|
|
26
|
+
/** Prefix length within this range, inclusive. */
|
|
27
|
+
prefix_length?: {
|
|
28
|
+
min?: number;
|
|
29
|
+
max?: number;
|
|
30
|
+
};
|
|
31
|
+
/** AS path length within this range, inclusive. Every AS counts, prepends included. */
|
|
32
|
+
as_path_length?: {
|
|
33
|
+
min?: number;
|
|
34
|
+
max?: number;
|
|
35
|
+
};
|
|
36
|
+
/** The route's origin validation state. */
|
|
37
|
+
rpki?: "valid" | "invalid" | "not-found";
|
|
38
|
+
/** Carries this standard community, `asn:value`. */
|
|
39
|
+
community?: string;
|
|
40
|
+
/** Carries this large community, `asn:function:parameter`. */
|
|
41
|
+
large_community?: string;
|
|
42
|
+
};
|
|
43
|
+
/** Changes to a route. In community lists to remove, `*` stands for any value of a field, as in `64500:*:*`. */
|
|
44
|
+
export type Sets = {
|
|
45
|
+
/** Preference among routes to the same prefix within our AS. Higher wins. */
|
|
46
|
+
local_pref?: number;
|
|
47
|
+
/** Multi-exit discriminator offered to the neighbor. Lower wins. */
|
|
48
|
+
med?: number;
|
|
49
|
+
/** Prepend our own AS this many times, to make the path look longer. */
|
|
50
|
+
prepend?: number;
|
|
51
|
+
/** Replace the route's standard communities with these. */
|
|
52
|
+
communities?: readonly string[];
|
|
53
|
+
/** Add these standard communities. */
|
|
54
|
+
add_communities?: readonly string[];
|
|
55
|
+
/** Remove standard communities matching these. */
|
|
56
|
+
remove_communities?: readonly string[];
|
|
57
|
+
/** Replace the route's large communities with these. */
|
|
58
|
+
large_communities?: readonly string[];
|
|
59
|
+
/** Add these large communities. */
|
|
60
|
+
add_large_communities?: readonly string[];
|
|
61
|
+
/** Remove large communities matching these. */
|
|
62
|
+
remove_large_communities?: readonly string[];
|
|
63
|
+
/** Next hop for the route. */
|
|
64
|
+
next_hop?: IP;
|
|
65
|
+
/** Source address for traffic the device itself sends to the route. Applies to routes of the same family as the address. */
|
|
66
|
+
preferred_source?: IP;
|
|
67
|
+
/** Drop traffic to the route on this device, instead of forwarding it. */
|
|
68
|
+
blackhole?: true;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* One step of a route policy. Evaluated in this order: `call`, then `match`, then `set`, then `action`.
|
|
72
|
+
*
|
|
73
|
+
* A rule without `action` changes the route and moves on to the next rule.
|
|
74
|
+
*/
|
|
75
|
+
export type PolicyRule<P extends string, S extends string> = {
|
|
76
|
+
/** Shown beside the rule on the device. */
|
|
77
|
+
description?: string;
|
|
78
|
+
/** Evaluate another policy first. A route it accepts or rejects stops there. */
|
|
79
|
+
call?: P;
|
|
80
|
+
/** Which routes the rule applies to. Every route when absent. */
|
|
81
|
+
match?: Match<S>;
|
|
82
|
+
/** What to change on a matching route. */
|
|
83
|
+
set?: Sets;
|
|
84
|
+
/** Accept or reject a matching route, ending the policy for it. */
|
|
85
|
+
action?: "accept" | "reject";
|
|
86
|
+
};
|
|
87
|
+
/** Rules evaluated in order, first decision wins. A route no rule accepts is rejected. */
|
|
88
|
+
export type RoutePolicy<P extends string, S extends string> = readonly PolicyRule<P, S>[];
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Family, IP, Prefix, Secret } from "./common";
|
|
2
|
+
/** A route the device carries without learning it. */
|
|
3
|
+
export type StaticRoute<I extends string, F extends string> = {
|
|
4
|
+
/** Destination. */
|
|
5
|
+
prefix: Prefix;
|
|
6
|
+
/** Next-hop address. */
|
|
7
|
+
via?: IP;
|
|
8
|
+
/** Next-hop interface, for a link without a next-hop address. */
|
|
9
|
+
interface?: I;
|
|
10
|
+
/** Discard matching traffic. The usual way to originate an aggregate. */
|
|
11
|
+
blackhole?: true;
|
|
12
|
+
/** Preference against other routes to the same prefix. Lower wins. 1 when absent. */
|
|
13
|
+
distance?: number;
|
|
14
|
+
/** The VRF the route belongs to. The default table when absent. */
|
|
15
|
+
vrf?: F;
|
|
16
|
+
/** Why the route exists. */
|
|
17
|
+
description?: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* What a BGP neighbor is to us, with the roles RFC 9234 defines: `provider`, a transit we buy from; `customer`, one we sell transit to; `peer`, one we exchange our own routes with; `rs`, an exchange's route server; `rs-client`, a network using our route server.
|
|
21
|
+
*/
|
|
22
|
+
export type BgpRole = "provider" | "customer" | "peer" | "rs" | "rs-client";
|
|
23
|
+
/** Settings a BGP neighbor can state itself or take from its group. */
|
|
24
|
+
export type NeighborSettings<P extends string> = {
|
|
25
|
+
/** The neighbor's AS. Our own AS makes the session internal. */
|
|
26
|
+
remote_as?: number;
|
|
27
|
+
/** Who the neighbor is. */
|
|
28
|
+
description?: string;
|
|
29
|
+
/** Source address for the session. The outgoing interface's address when absent. */
|
|
30
|
+
local_address?: IP;
|
|
31
|
+
/** Policy for routes learned. Nothing is accepted when absent. */
|
|
32
|
+
import?: P;
|
|
33
|
+
/** Policy for routes announced. Nothing is announced when absent. */
|
|
34
|
+
export?: P;
|
|
35
|
+
/** What the neighbor is to us, such as `provider` for a transit. Both sides then reject a route leak, after RFC 9234. */
|
|
36
|
+
role?: BgpRole;
|
|
37
|
+
/** The neighbor is more than one hop away. */
|
|
38
|
+
multihop?: boolean;
|
|
39
|
+
/** TCP MD5 password shared with the neighbor. */
|
|
40
|
+
password?: Secret;
|
|
41
|
+
/** Close the session when the neighbor announces more prefixes than this. It stays closed until cleared by hand, on RouterOS with `/routing bgp session clear <session> flag=limit-exceeded`. */
|
|
42
|
+
max_prefixes?: number;
|
|
43
|
+
/** Seconds without a message before the session is declared down. */
|
|
44
|
+
hold_time?: number;
|
|
45
|
+
/** Seconds between keepalive messages. */
|
|
46
|
+
keepalive?: number;
|
|
47
|
+
/** Families exchanged. The family of the neighbor's address when absent. */
|
|
48
|
+
families?: readonly Family[];
|
|
49
|
+
/** Wait for the neighbor to connect instead of connecting to it. */
|
|
50
|
+
passive?: boolean;
|
|
51
|
+
/** Detect a dead link in under a second with BFD. */
|
|
52
|
+
bfd?: boolean;
|
|
53
|
+
};
|
|
54
|
+
/** One BGP session. Settings it does not state come from its group. */
|
|
55
|
+
export type Neighbor<P extends string, G extends string> = NeighborSettings<P> & {
|
|
56
|
+
/** The neighbor's address. */
|
|
57
|
+
address: IP;
|
|
58
|
+
/** Group whose settings this neighbor takes where it states none. */
|
|
59
|
+
group?: NoInfer<G>;
|
|
60
|
+
};
|
|
61
|
+
/** BGP on this device. */
|
|
62
|
+
export type Bgp<P extends string, G extends string> = {
|
|
63
|
+
/** Our AS number. */
|
|
64
|
+
asn: number;
|
|
65
|
+
/** Prefixes this device originates. Each needs a route, for example a static blackhole. Export policy still decides who receives them. */
|
|
66
|
+
networks?: readonly Prefix[];
|
|
67
|
+
/** Settings shared by several neighbors, keyed by group name. */
|
|
68
|
+
groups?: Record<G, NeighborSettings<P>>;
|
|
69
|
+
/** Sessions keyed by name. */
|
|
70
|
+
neighbors?: Record<string, Neighbor<P, G>>;
|
|
71
|
+
};
|
|
72
|
+
/** A route origin validation server, RFC 8210. */
|
|
73
|
+
export type RpkiServer = {
|
|
74
|
+
/** Where the validator listens. */
|
|
75
|
+
address: IP;
|
|
76
|
+
/** Its RTR port, often 323 or 8282. */
|
|
77
|
+
port: number;
|
|
78
|
+
/** Who runs it, or why it is here. */
|
|
79
|
+
description?: string;
|
|
80
|
+
};
|
|
81
|
+
/** OSPF on one interface. */
|
|
82
|
+
export type OspfInterface = {
|
|
83
|
+
/** Cost of sending through this interface. Lower is preferred. Derived from speed when absent. */
|
|
84
|
+
cost?: number;
|
|
85
|
+
/** Advertise the interface's network but form no adjacency on it. */
|
|
86
|
+
passive?: boolean;
|
|
87
|
+
/** How the link is treated. Broadcast when absent. */
|
|
88
|
+
network?: "broadcast" | "point-to-point";
|
|
89
|
+
/** Detect a dead neighbor in under a second with BFD. */
|
|
90
|
+
bfd?: boolean;
|
|
91
|
+
};
|
|
92
|
+
/** OSPF, version 2 for IPv4 and version 3 for IPv6. */
|
|
93
|
+
export type Ospf<I extends string> = {
|
|
94
|
+
/** Which versions run. IPv4 when absent. */
|
|
95
|
+
families?: readonly Family[];
|
|
96
|
+
/** Areas keyed by area ID, for example `0.0.0.0`, each with the interfaces in it. */
|
|
97
|
+
areas: Record<string, {
|
|
98
|
+
interfaces: Partial<Record<I, OspfInterface>>;
|
|
99
|
+
}>;
|
|
100
|
+
/** Routes from other sources announced into OSPF. */
|
|
101
|
+
redistribute?: readonly ("connected" | "static")[];
|
|
102
|
+
};
|
|
103
|
+
/** Routing on this device. */
|
|
104
|
+
export type Routing<P extends string, G extends string, I extends string, F extends string> = {
|
|
105
|
+
/** Identifies this router to its neighbors. */
|
|
106
|
+
router_id?: IP;
|
|
107
|
+
/** Routes the device carries without learning them. */
|
|
108
|
+
static?: readonly StaticRoute<I, F>[];
|
|
109
|
+
/** BGP sessions and what they exchange. */
|
|
110
|
+
bgp?: Bgp<P, G>;
|
|
111
|
+
/** OSPF areas and interfaces. */
|
|
112
|
+
ospf?: Ospf<I>;
|
|
113
|
+
/** Validators answering route origin validation. */
|
|
114
|
+
rpki?: readonly RpkiServer[];
|
|
115
|
+
};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { IP, Prefix, Secret, Severity } from "./common";
|
|
2
|
+
/** Device-wide settings. */
|
|
3
|
+
export type System = {
|
|
4
|
+
/** IANA zone, for example `Asia/Jakarta`. UTC when absent. */
|
|
5
|
+
timezone?: string;
|
|
6
|
+
/** Shown at login. */
|
|
7
|
+
banner?: string;
|
|
8
|
+
/** Resolvers the device uses. */
|
|
9
|
+
dns?: {
|
|
10
|
+
/** Resolvers, in order of preference. */
|
|
11
|
+
servers: readonly IP[];
|
|
12
|
+
/** Answer queries from others. Off when absent. */
|
|
13
|
+
serve?: boolean;
|
|
14
|
+
};
|
|
15
|
+
/** Time the device keeps. */
|
|
16
|
+
ntp?: {
|
|
17
|
+
/** Time sources, by address or name. */
|
|
18
|
+
servers: readonly string[];
|
|
19
|
+
/** Answer time queries from others. Off when absent. */
|
|
20
|
+
serve?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/** Where log messages go. */
|
|
23
|
+
logging?: {
|
|
24
|
+
/** Kept on the device's own storage, from `level` up. */
|
|
25
|
+
local?: {
|
|
26
|
+
/** The least severe message kept. */
|
|
27
|
+
level: Severity;
|
|
28
|
+
/** How many rotated files to keep. */
|
|
29
|
+
files?: number;
|
|
30
|
+
/** Lines in each file before it rotates. */
|
|
31
|
+
lines_per_file?: number;
|
|
32
|
+
};
|
|
33
|
+
/** Sent to syslog collectors, each from its own `level` up. */
|
|
34
|
+
remote?: readonly {
|
|
35
|
+
address: IP;
|
|
36
|
+
/** UDP port. 514 when absent. */
|
|
37
|
+
port?: number;
|
|
38
|
+
/** The least severe message sent. */
|
|
39
|
+
level: Severity;
|
|
40
|
+
}[];
|
|
41
|
+
};
|
|
42
|
+
/** Software release track the device updates from. */
|
|
43
|
+
release_channel?: string;
|
|
44
|
+
/** Read optic temperature, power and voltage. */
|
|
45
|
+
transceiver_monitoring?: boolean;
|
|
46
|
+
/** Host behaviour of the IP stack. */
|
|
47
|
+
ip?: {
|
|
48
|
+
/** Send ICMP redirects. Off when absent. */
|
|
49
|
+
icmp_redirects?: boolean;
|
|
50
|
+
/** Answer a TCP SYN flood with SYN cookies. Off when absent. */
|
|
51
|
+
syn_cookies?: boolean;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/** A login on the device. */
|
|
55
|
+
export type User = {
|
|
56
|
+
/** What the user may do. */
|
|
57
|
+
role: "admin" | "operator" | "read-only";
|
|
58
|
+
/** The login password. */
|
|
59
|
+
password: Secret;
|
|
60
|
+
/** Who the account is for. */
|
|
61
|
+
description?: string;
|
|
62
|
+
/** Public keys accepted instead of the password over SSH. */
|
|
63
|
+
ssh_keys?: readonly string[];
|
|
64
|
+
};
|
|
65
|
+
/** A way into the device. Absent means turned off. */
|
|
66
|
+
export type Service = {
|
|
67
|
+
/** Sources allowed. `management.allow` when absent. */
|
|
68
|
+
allow?: readonly Prefix[];
|
|
69
|
+
/** TCP or UDP port. The protocol's own when absent. */
|
|
70
|
+
port?: number;
|
|
71
|
+
};
|
|
72
|
+
/** One SNMPv3 user. */
|
|
73
|
+
export type SnmpUser = {
|
|
74
|
+
/** How messages are authenticated. */
|
|
75
|
+
auth: "sha1" | "sha256";
|
|
76
|
+
/** The authentication passphrase. */
|
|
77
|
+
auth_password: Secret;
|
|
78
|
+
/** How messages are encrypted. */
|
|
79
|
+
privacy: "aes128" | "des";
|
|
80
|
+
/** The encryption passphrase. */
|
|
81
|
+
privacy_password: Secret;
|
|
82
|
+
};
|
|
83
|
+
/** How the device is reached and managed. */
|
|
84
|
+
export type Management<I extends string, C extends string> = {
|
|
85
|
+
/** Sources allowed to every service that does not state its own. */
|
|
86
|
+
allow?: readonly Prefix[];
|
|
87
|
+
/** Secure shell. */
|
|
88
|
+
ssh?: Service & {
|
|
89
|
+
/** Also offer legacy ciphers, MACs and key exchanges. Off when absent. */
|
|
90
|
+
weak_crypto?: boolean;
|
|
91
|
+
/** Seconds a login may take. */
|
|
92
|
+
auth_timeout?: number;
|
|
93
|
+
/** Failed attempts allowed per connection. */
|
|
94
|
+
auth_retries?: number;
|
|
95
|
+
};
|
|
96
|
+
/** Unencrypted remote login. */
|
|
97
|
+
telnet?: Service;
|
|
98
|
+
/** Web interface, unencrypted. */
|
|
99
|
+
http?: Service;
|
|
100
|
+
/** Web interface over TLS. */
|
|
101
|
+
https?: Service & {
|
|
102
|
+
/** One of the device's `certificates`. */
|
|
103
|
+
certificate?: C;
|
|
104
|
+
};
|
|
105
|
+
/** The platform's own API, unencrypted. */
|
|
106
|
+
api?: Service;
|
|
107
|
+
/** The platform's own API over TLS. */
|
|
108
|
+
api_tls?: Service & {
|
|
109
|
+
/** One of the device's `certificates`. */
|
|
110
|
+
certificate?: C;
|
|
111
|
+
};
|
|
112
|
+
/** File transfer, unencrypted. */
|
|
113
|
+
ftp?: Service;
|
|
114
|
+
/** The platform's own management protocol. `interfaces` also allows it at layer 2 on those interfaces. */
|
|
115
|
+
native?: Service & {
|
|
116
|
+
interfaces?: readonly I[];
|
|
117
|
+
};
|
|
118
|
+
/** Polling by SNMP. */
|
|
119
|
+
snmp?: Service & {
|
|
120
|
+
/** Read-only community for version 2c. Version 2c is off when absent. */
|
|
121
|
+
community?: Secret;
|
|
122
|
+
/** Version 3 users, keyed by name. */
|
|
123
|
+
users?: Record<string, SnmpUser>;
|
|
124
|
+
/** Who to contact about the device. */
|
|
125
|
+
contact?: string;
|
|
126
|
+
/** Where the device is. */
|
|
127
|
+
location?: string;
|
|
128
|
+
};
|
|
129
|
+
/** Console login uses this password. The device's users when absent. */
|
|
130
|
+
console?: {
|
|
131
|
+
password: Secret;
|
|
132
|
+
};
|
|
133
|
+
/** Password for privileged mode, on platforms that have one. */
|
|
134
|
+
privilege_password?: Secret;
|
|
135
|
+
};
|
|
136
|
+
/** A DHCP server for one network. */
|
|
137
|
+
export type DhcpServer<I extends string> = {
|
|
138
|
+
/** Where it answers. */
|
|
139
|
+
interface: I;
|
|
140
|
+
/** The network it serves. */
|
|
141
|
+
network: Prefix;
|
|
142
|
+
/** Default gateway handed out. */
|
|
143
|
+
gateway: IP;
|
|
144
|
+
/** Resolvers handed out. */
|
|
145
|
+
dns?: readonly IP[];
|
|
146
|
+
/** First and last address handed out. */
|
|
147
|
+
pool: readonly [IP, IP];
|
|
148
|
+
/** Seconds a lease lasts. */
|
|
149
|
+
lease_time?: number;
|
|
150
|
+
/** Addresses fixed to a MAC. */
|
|
151
|
+
reservations?: readonly {
|
|
152
|
+
address: IP;
|
|
153
|
+
mac: string;
|
|
154
|
+
/** Whose address it is. */
|
|
155
|
+
description?: string;
|
|
156
|
+
}[];
|
|
157
|
+
};
|
|
158
|
+
/** Forward DHCP requests heard on an interface to servers elsewhere. */
|
|
159
|
+
export type DhcpRelay<I extends string> = {
|
|
160
|
+
/** Where requests are heard. */
|
|
161
|
+
interface: I;
|
|
162
|
+
/** Where they are forwarded. */
|
|
163
|
+
servers: readonly IP[];
|
|
164
|
+
};
|
|
165
|
+
/** A certificate the device presents or trusts. */
|
|
166
|
+
export type Certificate = {
|
|
167
|
+
/** The certificate, PEM. Public, so it lives in the repository. */
|
|
168
|
+
certificate: string;
|
|
169
|
+
/** Its private key: a PEM file through `secretFile`, or PEM base64-encoded as one line through `secret`. A certificate only trusted, such as a CA, has none. */
|
|
170
|
+
private_key?: Secret;
|
|
171
|
+
};
|
|
172
|
+
/** Export flow records or packet samples to collectors. */
|
|
173
|
+
export type FlowExport<I extends string> = {
|
|
174
|
+
/** The record format. */
|
|
175
|
+
protocol: "sflow" | "netflow-v9" | "ipfix";
|
|
176
|
+
/** Where records are sent. */
|
|
177
|
+
collectors: readonly {
|
|
178
|
+
address: IP;
|
|
179
|
+
/** UDP port. 6343 for sFlow and 2055 otherwise when absent. */
|
|
180
|
+
port?: number;
|
|
181
|
+
}[];
|
|
182
|
+
/** Sample one packet in this many. Every packet when absent, on a platform that can export every packet. */
|
|
183
|
+
sampling?: number;
|
|
184
|
+
/** Where traffic is observed. Every interface when absent. */
|
|
185
|
+
interfaces?: readonly I[];
|
|
186
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Client, type ClientChannel } from "ssh2";
|
|
2
|
+
export type Login = {
|
|
3
|
+
host: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
user: string;
|
|
6
|
+
password: string;
|
|
7
|
+
/** Host key algorithms to accept, for a device that negotiates one it then signs wrongly. */
|
|
8
|
+
hostKeys?: string[];
|
|
9
|
+
};
|
|
10
|
+
/** Open a session. */
|
|
11
|
+
export declare function connect(login: Login): Promise<Client>;
|
|
12
|
+
/** Run one command and return what it printed, stderr included. */
|
|
13
|
+
export declare function exec(client: Client, command: string, timeout?: number): Promise<string>;
|
|
14
|
+
/** An interactive shell, for platforms that take configuration only there. */
|
|
15
|
+
export declare function shell(client: Client, columns?: number, rows?: number): Promise<ClientChannel>;
|
|
16
|
+
/** Collect output until `done` holds, then wait `settle` for stragglers, or give up at `timeout`. */
|
|
17
|
+
export declare function readUntil(channel: ClientChannel, done: (output: string) => boolean, timeout: number, settle?: number): Promise<string>;
|
|
18
|
+
/** Send one line into an interactive shell and collect output until it goes quiet for `settle`. */
|
|
19
|
+
export declare function send(channel: ClientChannel, line: string, timeout?: number, settle?: number): Promise<string>;
|
|
20
|
+
export declare const sleep: (milliseconds: number) => Promise<unknown>;
|
|
21
|
+
/** Write a file to the device over SFTP. */
|
|
22
|
+
export declare function upload(client: Client, path: string, content: string | Buffer): Promise<void>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@takodotid/circuit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -39,19 +39,29 @@
|
|
|
39
39
|
],
|
|
40
40
|
"type": "module",
|
|
41
41
|
"exports": {
|
|
42
|
-
".":
|
|
43
|
-
|
|
42
|
+
".": {
|
|
43
|
+
"types": "./dist/types/index.d.ts",
|
|
44
|
+
"bun": "./src/index.ts",
|
|
45
|
+
"default": "./dist/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./presets": {
|
|
48
|
+
"types": "./dist/types/presets/index.d.ts",
|
|
49
|
+
"bun": "./src/presets/index.ts",
|
|
50
|
+
"default": "./dist/presets/index.js"
|
|
51
|
+
}
|
|
44
52
|
},
|
|
45
53
|
"bin": {
|
|
46
|
-
"circuit": "
|
|
54
|
+
"circuit": "dist/cli/index.js"
|
|
47
55
|
},
|
|
48
56
|
"files": [
|
|
49
57
|
"src",
|
|
58
|
+
"dist",
|
|
50
59
|
"templates",
|
|
51
60
|
"LICENSE",
|
|
52
61
|
"README.md"
|
|
53
62
|
],
|
|
54
63
|
"engines": {
|
|
64
|
+
"node": ">=20.12",
|
|
55
65
|
"bun": ">=1.3.5"
|
|
56
66
|
},
|
|
57
67
|
"publishConfig": {
|
|
@@ -60,22 +70,26 @@
|
|
|
60
70
|
"scripts": {
|
|
61
71
|
"typecheck": "bunx tsc --noEmit",
|
|
62
72
|
"test": "bun test",
|
|
73
|
+
"build": "bun scripts/build.ts",
|
|
63
74
|
"format": "bunx prettier --write .",
|
|
64
75
|
"format:check": "bunx prettier --check .",
|
|
65
76
|
"docs:dev": "bun scripts/schema-reference.ts && vitepress dev docs",
|
|
66
77
|
"docs:build": "bun scripts/schema-reference.ts && vitepress build docs",
|
|
67
78
|
"docs:preview": "vitepress preview docs",
|
|
68
79
|
"release": "bun scripts/release.ts",
|
|
69
|
-
"prepare": "husky || true"
|
|
80
|
+
"prepare": "husky || true",
|
|
81
|
+
"prepack": "bun run build"
|
|
70
82
|
},
|
|
71
83
|
"dependencies": {
|
|
72
84
|
"@types/ssh2": "^1.15.6",
|
|
85
|
+
"jiti": "^2.7.0",
|
|
73
86
|
"ssh2": "^1.17.0"
|
|
74
87
|
},
|
|
75
88
|
"devDependencies": {
|
|
76
89
|
"@commitlint/cli": "^19.5.0",
|
|
77
90
|
"@commitlint/config-conventional": "^19.5.0",
|
|
78
91
|
"@types/bun": "^1.4.2",
|
|
92
|
+
"@types/node": "^24",
|
|
79
93
|
"husky": "^9.1.6",
|
|
80
94
|
"prettier": "^3.9.9",
|
|
81
95
|
"typescript": "^7.0.2",
|
|
@@ -31,3 +31,5 @@ Behaviour that looks like something else. Each one has been hit on a real device
|
|
|
31
31
|
- `:find` on an array returns a number or `nil`. Globals persist between exec commands of one login.
|
|
32
32
|
- A fixed port speed is named by medium, `speed=10G-baseSR-LR` for an optic or `speed=1G-baseT-full` for copper, and only holds with `auto-negotiation=no`. With auto-negotiation back on, the export no longer prints the speed, so the plan resets only auto-negotiation. `speed=""` is refused as ambiguous and `unset speed` does not exist.
|
|
33
33
|
- `bgp-path-len` counts every AS in the path, prepends included, and takes the same strict comparisons as `dst-len`.
|
|
34
|
+
- `:parse` checks the command, not the text inside `rule=`. A routing filter rule is only checked when it is added.
|
|
35
|
+
- `/routing/filter/filter-wizard` adds the rule it builds; it is not a dry run. `set blackhole yes` was proved this way on 7.23, in an unused chain removed right after.
|
|
@@ -1006,6 +1006,7 @@ function actions(rule: AnyRule, family: Family): string[] {
|
|
|
1006
1006
|
// An address only means something to routes of its own family.
|
|
1007
1007
|
if (set.next_hop && familyOf(set.next_hop) === family) steps.push(`set gw ${set.next_hop}`);
|
|
1008
1008
|
if (set.preferred_source && familyOf(set.preferred_source) === family) steps.push(`set pref-src ${set.preferred_source}`);
|
|
1009
|
+
if (set.blackhole) steps.push("set blackhole yes");
|
|
1009
1010
|
if (rule.action) steps.push(rule.action);
|
|
1010
1011
|
return steps;
|
|
1011
1012
|
}
|