@takodotid/circuit 0.6.0 → 0.7.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 +14 -174
- package/package.json +2 -1
- package/src/cli/index.ts +73 -12
- package/src/cli/new.ts +153 -0
- package/src/core/define.ts +2 -4
- package/src/core/secrets.ts +62 -15
- package/src/presets/index.ts +135 -8
- package/src/schema/common.ts +3 -3
- package/templates/colocation/checks.ts +38 -0
- package/templates/colocation/circuit.config.ts +9 -0
- package/templates/colocation/edge-01.ts +131 -0
- package/templates/colocation/routing.ts +13 -0
- package/templates/colocation/tenancy.ts +97 -0
- package/templates/colocation/tenants/acme.ts +19 -0
- package/templates/colocation/tenants/index.ts +5 -0
- package/templates/colocation/tor-01.ts +33 -0
- package/templates/edge-router/checks.ts +20 -0
- package/templates/edge-router/circuit.config.ts +8 -0
- package/templates/edge-router/edge-01.ts +108 -0
- package/templates/edge-router/routing.ts +13 -0
- package/templates/project/AGENTS.md +44 -0
- package/templates/project/CLAUDE.md +1 -0
- package/templates/project/README.md +15 -0
- package/templates/project/gitignore +5 -0
- package/templates/project/tsconfig.json +13 -0
- package/templates/single-site/checks.ts +26 -0
- package/templates/single-site/circuit.config.ts +8 -0
- package/templates/single-site/router.ts +89 -0
- package/templates/single-site/site.ts +18 -0
- package/templates/single-site/switch.ts +34 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The top-of-rack switch: the tenants' servers, and one uplink to the router.
|
|
2
|
+
|
|
3
|
+
import { defineDevice, merge, secret } from "@takodotid/circuit";
|
|
4
|
+
import { internetVlanNames, internetVlans, portsOn, privateVlans } from "./tenancy";
|
|
5
|
+
import { TENANTS } from "./tenants";
|
|
6
|
+
|
|
7
|
+
export default defineDevice({
|
|
8
|
+
name: "tor-01",
|
|
9
|
+
platform: "vrp",
|
|
10
|
+
model: "CE6855-48S6Q-HI",
|
|
11
|
+
connection: { host: "10.0.99.2", user: "operator" },
|
|
12
|
+
users: { operator: { role: "admin", password: secret("TOR_01_PASSWORD") } },
|
|
13
|
+
|
|
14
|
+
system: { timezone: "Asia/Jakarta", ntp: { servers: ["10.0.99.1"] } },
|
|
15
|
+
management: { allow: ["10.0.0.0/24"], ssh: {} },
|
|
16
|
+
|
|
17
|
+
lldp: true,
|
|
18
|
+
stp: {},
|
|
19
|
+
|
|
20
|
+
vlans: merge({ mgmt: { id: 99, description: "Management" } }, internetVlans(TENANTS), privateVlans(TENANTS, "tor-01")),
|
|
21
|
+
|
|
22
|
+
// Every tenant's ports, from its file in tenants/, beside the switch's own.
|
|
23
|
+
ports: merge(portsOn(TENANTS, "tor-01"), {
|
|
24
|
+
"40g-1": {
|
|
25
|
+
description: "edge-01",
|
|
26
|
+
trunk_vlans: ["mgmt", ...internetVlanNames(TENANTS)],
|
|
27
|
+
link: { device: "edge-01", port: "100g-1" },
|
|
28
|
+
},
|
|
29
|
+
}),
|
|
30
|
+
|
|
31
|
+
interfaces: { mgmt: { type: "vlan", vlan: "mgmt", addresses: ["10.0.99.2/24"] } },
|
|
32
|
+
routing: { static: [{ prefix: "0.0.0.0/0", via: "10.0.99.1" }] },
|
|
33
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Rules of your own. `circuit validate` runs them beside Circuit's own, and an error stops `diff` and `apply`.
|
|
2
|
+
|
|
3
|
+
import type { Check } from "@takodotid/circuit";
|
|
4
|
+
import { exportsEndInReject, neighborPolicies, trustBoundary } from "@takodotid/circuit/presets";
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
// Nothing leaves by accident: every export ends by rejecting what it did not accept.
|
|
8
|
+
exportsEndInReject,
|
|
9
|
+
|
|
10
|
+
// Traffic from the transit and the exchange reaches the rest of the network only through the router.
|
|
11
|
+
trustBoundary({ untrusted: ["transit", "ix"], routers: ["edge-01"] }),
|
|
12
|
+
|
|
13
|
+
// Every BGP session filters what it takes in.
|
|
14
|
+
(devices) =>
|
|
15
|
+
devices.flatMap((device) =>
|
|
16
|
+
neighborPolicies(device)
|
|
17
|
+
.filter((neighbor) => !neighbor.import)
|
|
18
|
+
.map((neighbor) => ({ level: "error" as const, device: device.name, message: `${neighbor.name} has no import policy` }))
|
|
19
|
+
),
|
|
20
|
+
] satisfies Check[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The network: every device Circuit manages, your own checks, and what is published about the network.
|
|
2
|
+
|
|
3
|
+
import { defineNetwork } from "@takodotid/circuit";
|
|
4
|
+
import checks from "./checks";
|
|
5
|
+
import edge from "./edge-01";
|
|
6
|
+
import { ASN, communities } from "./routing";
|
|
7
|
+
|
|
8
|
+
export default defineNetwork({ devices: [edge], checks, asn: ASN, communities: communities.catalogue });
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// The edge router: BGP with an IP transit and an internet exchange, filtered on the way in and on the way out.
|
|
2
|
+
|
|
3
|
+
import type { Prefix } from "@takodotid/circuit";
|
|
4
|
+
import { defineDevice, secret } from "@takodotid/circuit";
|
|
5
|
+
import { antiSpoofing, badTcpFlags, bgpSanity, MARTIANS, PRIVATE_RANGES } from "@takodotid/circuit/presets";
|
|
6
|
+
import { ASN, communities } from "./routing";
|
|
7
|
+
|
|
8
|
+
/** The address space this router announces. */
|
|
9
|
+
const OURS = ["198.51.100.0/24", "2001:db8:100::/48"] as const satisfies readonly Prefix[];
|
|
10
|
+
|
|
11
|
+
/** Where the router is managed from. */
|
|
12
|
+
const MANAGEMENT = "10.0.0.0/24" satisfies Prefix;
|
|
13
|
+
|
|
14
|
+
/** Sources that never arrive from the internet: our own space, and addresses that are not on the internet at all. */
|
|
15
|
+
const SPOOFED = [OURS[0], ...MARTIANS, ...PRIVATE_RANGES];
|
|
16
|
+
|
|
17
|
+
export default defineDevice({
|
|
18
|
+
name: "edge-01",
|
|
19
|
+
platform: "routeros",
|
|
20
|
+
model: "CCR2216-1G-12XS-2XQ",
|
|
21
|
+
connection: { host: "10.0.0.1", user: "operator" },
|
|
22
|
+
users: { operator: { role: "admin", password: secret("EDGE_01_PASSWORD") } },
|
|
23
|
+
|
|
24
|
+
system: { timezone: "Asia/Jakarta", ntp: { servers: ["pool.ntp.org"] } },
|
|
25
|
+
management: { allow: [MANAGEMENT], ssh: {} },
|
|
26
|
+
|
|
27
|
+
vlans: {
|
|
28
|
+
transit: { id: 300, description: "IP transit from Example Transit, AS64501" },
|
|
29
|
+
ix: { id: 200, description: "Peering LAN of Example IX" },
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
ports: {
|
|
33
|
+
"1g-1": { description: "Management", addresses: ["10.0.0.1/24"] },
|
|
34
|
+
"25g-1": { description: "Example Transit", access_vlan: "transit", acl: "edge" },
|
|
35
|
+
"25g-2": { description: "Example IX", access_vlan: "ix", acl: "edge" },
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
interfaces: {
|
|
39
|
+
transit: { type: "vlan", vlan: "transit", addresses: ["203.0.113.2/30"] },
|
|
40
|
+
ix: { type: "vlan", vlan: "ix", addresses: ["192.0.2.10/24", "2001:db8:ffff::10/64"] },
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
routing: {
|
|
44
|
+
router_id: "203.0.113.2",
|
|
45
|
+
// A route for each block we announce, so it is in the table even when nothing inside it is up.
|
|
46
|
+
static: OURS.map((prefix) => ({ prefix, blackhole: true, description: "Originate" })),
|
|
47
|
+
bgp: {
|
|
48
|
+
asn: ASN,
|
|
49
|
+
networks: [...OURS],
|
|
50
|
+
neighbors: {
|
|
51
|
+
"example-transit": {
|
|
52
|
+
address: "203.0.113.1",
|
|
53
|
+
remote_as: 64501,
|
|
54
|
+
local_role: "customer",
|
|
55
|
+
import: "TRANSIT-IN",
|
|
56
|
+
export: "OUT",
|
|
57
|
+
max_prefixes: 1_200_000,
|
|
58
|
+
},
|
|
59
|
+
"example-ix-rs-v4": {
|
|
60
|
+
address: "192.0.2.1",
|
|
61
|
+
remote_as: 64502,
|
|
62
|
+
local_role: "rs-client",
|
|
63
|
+
import: "IX-IN",
|
|
64
|
+
export: "OUT",
|
|
65
|
+
max_prefixes: 100_000,
|
|
66
|
+
},
|
|
67
|
+
"example-ix-rs-v6": {
|
|
68
|
+
address: "2001:db8:ffff::1",
|
|
69
|
+
remote_as: 64502,
|
|
70
|
+
local_role: "rs-client",
|
|
71
|
+
import: "IX-IN",
|
|
72
|
+
export: "OUT",
|
|
73
|
+
max_prefixes: 50_000,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
policies: {
|
|
80
|
+
SANITY: bgpSanity(),
|
|
81
|
+
// A route from the exchange is preferred over the same route from the transit: higher local preference wins.
|
|
82
|
+
"TRANSIT-IN": [{ call: "SANITY" }, communities.tag("transit", 64501), { set: { local_pref: 100 } }, { action: "accept" }],
|
|
83
|
+
"IX-IN": [{ call: "SANITY" }, communities.tag("exchange", 64502), { set: { local_pref: 200 } }, { action: "accept" }],
|
|
84
|
+
OUT: [
|
|
85
|
+
...OURS.map((prefix) => ({ description: `Announce ${prefix}`, match: { prefix }, action: "accept" as const })),
|
|
86
|
+
{ description: "Nothing else leaves", action: "reject" },
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
firewall: {
|
|
91
|
+
address_sets: { "bgp-neighbors": ["203.0.113.1", "192.0.2.1", "2001:db8:ffff::1"] },
|
|
92
|
+
filter: {
|
|
93
|
+
input: {
|
|
94
|
+
default: "drop",
|
|
95
|
+
rules: [
|
|
96
|
+
{ description: "Replies", match: { state: ["established", "related"] }, action: "accept" },
|
|
97
|
+
{ description: "Ping", match: { protocol: "icmp" }, action: "accept" },
|
|
98
|
+
{ description: "Management", match: { src: MANAGEMENT }, action: "accept" },
|
|
99
|
+
{ description: "BGP", match: { protocol: "tcp", dst_port: 179, src_set: "bgp-neighbors" }, action: "accept" },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
forward: { rules: [...badTcpFlags()] },
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// In the switch chip, before a packet reaches the router's CPU.
|
|
107
|
+
acls: { edge: [...antiSpoofing("transit", SPOOFED), ...antiSpoofing("ix", SPOOFED)] },
|
|
108
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// What your network is on the internet: its AS number and its BGP communities.
|
|
2
|
+
|
|
3
|
+
import { communityScheme } from "@takodotid/circuit/presets";
|
|
4
|
+
|
|
5
|
+
/** Your AS number. */
|
|
6
|
+
export const ASN = 64500;
|
|
7
|
+
|
|
8
|
+
/** What your BGP communities mean. The router tags each route it learns with them, and `circuit communities` publishes them. */
|
|
9
|
+
export const communities = communityScheme({
|
|
10
|
+
asn: ASN,
|
|
11
|
+
learned_from: { function: 1, classes: { transit: 1, exchange: 2 } },
|
|
12
|
+
learned_from_as: 3,
|
|
13
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This repository describes a computer network with [Circuit](https://circuit.tako.id). Each network device, such as a router or a switch, has a TypeScript file that says how it should be configured. Circuit compares that file with what the device runs, and changes the device to match.
|
|
4
|
+
|
|
5
|
+
You are helping the owner of this network. Changes you make here can take a network down, so work carefully and let the owner decide.
|
|
6
|
+
|
|
7
|
+
## How Circuit works
|
|
8
|
+
|
|
9
|
+
1. `circuit.config.ts` lists every device and the checks to run.
|
|
10
|
+
2. Each device file calls `defineDevice`. What the file says is everything the device runs: a port, a user or a service that the file does not mention is removed or turned off when the device is applied.
|
|
11
|
+
3. A field is only written when it has a value. There is no `enabled: false`; leave a field out to turn it off.
|
|
12
|
+
4. Secrets are never written in a file. A file says `secret("NAME")`, and the value comes from `.env` or the environment. Never put a password, a key or a token in any other file, and never print one.
|
|
13
|
+
5. `.circuit/` is written by Circuit. Never edit it. `.circuit/state/` holds what each device ran when Circuit last read it.
|
|
14
|
+
|
|
15
|
+
What each field means is written as a comment on the field, in `node_modules/@takodotid/circuit/src/schema/`. Read it before using a field you have not used before. The documentation is at https://circuit.tako.id.
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
| Command | What it does | Changes a device |
|
|
20
|
+
| -------------------------------------- | -------------------------------------------------------------------- | ---------------- |
|
|
21
|
+
| `bun circuit validate` | Checks every file and every rule in the checks | no |
|
|
22
|
+
| `bun circuit diff [device]` | Shows what would change, compared with the last snapshot | no |
|
|
23
|
+
| `bun circuit build <device>` | Prints the full configuration the device should run | no |
|
|
24
|
+
| `bun circuit snapshot [device]` | Reads what each device runs now into `.circuit/state/` | no, only reads |
|
|
25
|
+
| `bun circuit apply <device>` | Reads the device, then shows what would change on it | no |
|
|
26
|
+
| `bun circuit apply <device> --confirm` | Sends the change to the device | **yes** |
|
|
27
|
+
| `bun circuit secrets` | Lists every secret the config uses, and whether each one can be read | no |
|
|
28
|
+
|
|
29
|
+
## Making a change
|
|
30
|
+
|
|
31
|
+
1. Edit the device's file.
|
|
32
|
+
2. Run `bun circuit validate`. Fix every error.
|
|
33
|
+
3. Run `bun circuit diff <device>` and read the result.
|
|
34
|
+
4. Show the owner what will change and why, in plain words.
|
|
35
|
+
5. Run `bun circuit apply <device>` to see the plan against the live device.
|
|
36
|
+
6. **Stop.** Only run `apply <device> --confirm` after the owner says yes to that exact plan. Never confirm on your own, even if you were told to finish the task.
|
|
37
|
+
7. After the apply, commit the device file together with the updated `.circuit/state/`.
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
- Never run `apply --confirm`, `refresh --confirm` or `peeringdb --confirm` without the owner's explicit approval for that run.
|
|
42
|
+
- If a plan removes something you did not expect, stop and ask. It usually means the file is missing something the device needs.
|
|
43
|
+
- Be careful with anything that carries your own access to the device: the management address, the management VLAN, the uplink, `management.allow`, and the login user. A mistake there locks everyone out.
|
|
44
|
+
- When you are not sure what a field does, or what the owner wants, ask instead of guessing.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Network
|
|
2
|
+
|
|
3
|
+
This repository describes the network with [Circuit](https://circuit.tako.id). Each device has a file; Circuit makes the device match it.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun install # once
|
|
7
|
+
bun circuit secrets # check every secret can be read
|
|
8
|
+
bun circuit validate # check the files
|
|
9
|
+
bun circuit snapshot # read what each device runs now
|
|
10
|
+
bun circuit diff # see what would change
|
|
11
|
+
bun circuit apply <device> # see the plan against the device
|
|
12
|
+
bun circuit apply <device> --confirm
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Secrets go in `.env`, which is never committed. `.circuit/` is written by Circuit; commit it, but never edit it.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Rules of your own. `circuit validate` runs them beside Circuit's own, and an error stops `diff` and `apply`.
|
|
2
|
+
|
|
3
|
+
import type { Check } from "@takodotid/circuit";
|
|
4
|
+
import { vlansOf } from "@takodotid/circuit/presets";
|
|
5
|
+
|
|
6
|
+
export default [
|
|
7
|
+
// A port without a description is hard to trace later. A warning, so it does not block anything.
|
|
8
|
+
(devices) =>
|
|
9
|
+
devices.flatMap((device) =>
|
|
10
|
+
Object.entries(device.ports ?? {})
|
|
11
|
+
.filter(([, port]) => port && !port.description)
|
|
12
|
+
.map(([name]) => ({ level: "warning" as const, device: device.name, message: `port ${name} has no description` }))
|
|
13
|
+
),
|
|
14
|
+
|
|
15
|
+
// Someone plugging into a wall socket must not land on the management network. Only a trunk between devices carries it.
|
|
16
|
+
(devices) =>
|
|
17
|
+
devices.flatMap((device) =>
|
|
18
|
+
Object.entries(device.ports ?? {})
|
|
19
|
+
.filter(([, port]) => port?.access_vlan && vlansOf(port).includes("mgmt"))
|
|
20
|
+
.map(([name]) => ({
|
|
21
|
+
level: "error" as const,
|
|
22
|
+
device: device.name,
|
|
23
|
+
message: `port ${name} gives management to whatever plugs in`,
|
|
24
|
+
}))
|
|
25
|
+
),
|
|
26
|
+
] satisfies Check[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The network: every device Circuit manages, and your own checks.
|
|
2
|
+
|
|
3
|
+
import { defineNetwork } from "@takodotid/circuit";
|
|
4
|
+
import checks from "./checks";
|
|
5
|
+
import router from "./router";
|
|
6
|
+
import sw from "./switch";
|
|
7
|
+
|
|
8
|
+
export default defineNetwork({ devices: [router, sw], checks });
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The router: the internet connection, a gateway and DHCP for each VLAN, and the firewall.
|
|
2
|
+
|
|
3
|
+
import { defineDevice, secret } from "@takodotid/circuit";
|
|
4
|
+
import { MANAGEMENT, ROUTER, TIMEZONE, vlans } from "./site";
|
|
5
|
+
|
|
6
|
+
export default defineDevice({
|
|
7
|
+
name: "router",
|
|
8
|
+
platform: "routeros",
|
|
9
|
+
model: "CCR2216-1G-12XS-2XQ",
|
|
10
|
+
connection: { host: ROUTER, user: "operator" },
|
|
11
|
+
users: { operator: { role: "admin", password: secret("ROUTER_PASSWORD") } },
|
|
12
|
+
|
|
13
|
+
system: {
|
|
14
|
+
timezone: TIMEZONE,
|
|
15
|
+
dns: { servers: ["1.1.1.1", "1.0.0.1"], serve: true },
|
|
16
|
+
ntp: { servers: ["pool.ntp.org"], serve: true },
|
|
17
|
+
},
|
|
18
|
+
management: { allow: [MANAGEMENT], ssh: {} },
|
|
19
|
+
|
|
20
|
+
vlans,
|
|
21
|
+
|
|
22
|
+
ports: {
|
|
23
|
+
"1g-1": { description: "Internet provider", addresses: ["203.0.113.2/30"] },
|
|
24
|
+
"25g-1": {
|
|
25
|
+
description: "Switch",
|
|
26
|
+
speed: "10g",
|
|
27
|
+
trunk_vlans: ["mgmt", "home", "guests"],
|
|
28
|
+
link: { device: "switch", port: "10g-1" },
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
// The gateway of each VLAN.
|
|
33
|
+
interfaces: {
|
|
34
|
+
mgmt: { type: "vlan", vlan: "mgmt", addresses: [`${ROUTER}/24`] },
|
|
35
|
+
home: { type: "vlan", vlan: "home", addresses: ["192.168.20.1/24"] },
|
|
36
|
+
guests: { type: "vlan", vlan: "guests", addresses: ["192.168.30.1/24"] },
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
routing: { static: [{ prefix: "0.0.0.0/0", via: "203.0.113.1", description: "Internet provider" }] },
|
|
40
|
+
|
|
41
|
+
dhcp: {
|
|
42
|
+
home: {
|
|
43
|
+
interface: "home",
|
|
44
|
+
network: "192.168.20.0/24",
|
|
45
|
+
gateway: "192.168.20.1",
|
|
46
|
+
dns: ["192.168.20.1"],
|
|
47
|
+
pool: ["192.168.20.100", "192.168.20.200"],
|
|
48
|
+
},
|
|
49
|
+
guests: {
|
|
50
|
+
interface: "guests",
|
|
51
|
+
network: "192.168.30.0/24",
|
|
52
|
+
gateway: "192.168.30.1",
|
|
53
|
+
dns: ["192.168.30.1"],
|
|
54
|
+
pool: ["192.168.30.100", "192.168.30.200"],
|
|
55
|
+
lease_time: 3600,
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
firewall: {
|
|
60
|
+
filter: {
|
|
61
|
+
// Traffic to the router itself.
|
|
62
|
+
input: {
|
|
63
|
+
default: "drop",
|
|
64
|
+
rules: [
|
|
65
|
+
{ description: "Replies", match: { state: ["established", "related"] }, action: "accept" },
|
|
66
|
+
{ description: "Ping", match: { protocol: "icmp" }, action: "accept" },
|
|
67
|
+
{ description: "Management", match: { src: MANAGEMENT }, action: "accept" },
|
|
68
|
+
{ description: "DNS from home", match: { in_interface: "home", protocol: "udp", dst_port: 53 }, action: "accept" },
|
|
69
|
+
{ description: "DNS from guests", match: { in_interface: "guests", protocol: "udp", dst_port: 53 }, action: "accept" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
// Traffic passing through the router.
|
|
73
|
+
forward: {
|
|
74
|
+
rules: [
|
|
75
|
+
{ description: "Replies", match: { state: ["established", "related"] }, action: "accept", offload: true },
|
|
76
|
+
{
|
|
77
|
+
description: "Guests stay off the home network",
|
|
78
|
+
match: { in_interface: "guests", out_interface: "home" },
|
|
79
|
+
action: "drop",
|
|
80
|
+
},
|
|
81
|
+
{ description: "Guests stay off management", match: { in_interface: "guests", out_interface: "mgmt" }, action: "drop" },
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
nat: {
|
|
86
|
+
source: [{ description: "Share the public address", match: { out_interface: "1g-1" }, action: "masquerade" }],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Values both devices share. Change them to your own.
|
|
2
|
+
|
|
3
|
+
import type { Prefix, Vlan } from "@takodotid/circuit";
|
|
4
|
+
|
|
5
|
+
/** The management network. Only addresses in it can log in to the devices. */
|
|
6
|
+
export const MANAGEMENT = "192.168.10.0/24" satisfies Prefix;
|
|
7
|
+
|
|
8
|
+
/** The router's address on the management network, the switch's gateway. */
|
|
9
|
+
export const ROUTER = "192.168.10.1";
|
|
10
|
+
|
|
11
|
+
export const TIMEZONE = "Asia/Jakarta";
|
|
12
|
+
|
|
13
|
+
/** Every VLAN on the site. Each device lists the ones it carries. */
|
|
14
|
+
export const vlans = {
|
|
15
|
+
mgmt: { id: 10, description: "Management" },
|
|
16
|
+
home: { id: 20, description: "Home" },
|
|
17
|
+
guests: { id: 30, description: "Guests" },
|
|
18
|
+
} as const satisfies Record<string, Vlan>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The switch: a port for each device on the site, on the VLAN it belongs to.
|
|
2
|
+
|
|
3
|
+
import { defineDevice, secret } from "@takodotid/circuit";
|
|
4
|
+
import { MANAGEMENT, ROUTER, TIMEZONE, vlans } from "./site";
|
|
5
|
+
|
|
6
|
+
export default defineDevice({
|
|
7
|
+
name: "switch",
|
|
8
|
+
platform: "vrp",
|
|
9
|
+
model: "CE6855-48S6Q-HI",
|
|
10
|
+
connection: { host: "192.168.10.2", user: "operator" },
|
|
11
|
+
users: { operator: { role: "admin", password: secret("SWITCH_PASSWORD") } },
|
|
12
|
+
|
|
13
|
+
system: { timezone: TIMEZONE, ntp: { servers: [ROUTER] } },
|
|
14
|
+
management: { allow: [MANAGEMENT], ssh: {} },
|
|
15
|
+
|
|
16
|
+
lldp: true,
|
|
17
|
+
stp: {},
|
|
18
|
+
|
|
19
|
+
vlans,
|
|
20
|
+
|
|
21
|
+
ports: {
|
|
22
|
+
"10g-1": {
|
|
23
|
+
description: "Router",
|
|
24
|
+
trunk_vlans: ["mgmt", "home", "guests"],
|
|
25
|
+
link: { device: "router", port: "25g-1" },
|
|
26
|
+
},
|
|
27
|
+
"10g-2": { description: "Desk", access_vlan: "home", stp: { edge: true } },
|
|
28
|
+
"10g-3": { description: "Living room", access_vlan: "home", stp: { edge: true } },
|
|
29
|
+
"10g-4": { description: "Guest access point", access_vlan: "guests", stp: { edge: true } },
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
interfaces: { mgmt: { type: "vlan", vlan: "mgmt", addresses: ["192.168.10.2/24"] } },
|
|
33
|
+
routing: { static: [{ prefix: "0.0.0.0/0", via: ROUTER }] },
|
|
34
|
+
});
|