Compare commits

...

210 Commits

Author SHA1 Message Date
06d85ca961 Make log level configurable, remove custom log message handler 2024-11-09 17:11:10 +01:00
032bdb9e43 Have hosts update garage cluster layout to remove other nodes if necessary 2024-11-08 22:57:31 +01:00
53a06af9ba Use the term 'role' or 'node' rather than 'peer' in the context of garage 2024-11-08 17:46:44 +01:00
734406d4bb Remove legacy tests, document new ones 2024-11-05 23:47:44 +01:00
04978fa9db Block for bucket list to sync across garage instances during init 2024-11-05 21:25:04 +01:00
8e264cf028 Rework how Network background jobs are managed a bit 2024-11-05 11:59:45 +01:00
efdab29ae6 Update isle link in README 2024-11-05 11:23:28 +01:00
932c8e2244 Refactor arch package building a bit, to make it easier to test 2024-11-03 15:23:43 +01:00
6ac473edcb Fix garage health check to wait for full replication factor of nodes 2024-10-31 13:13:17 +01:00
2cdec586b2 Implement removal of nodes from the garage layout 2024-10-31 13:04:19 +01:00
8dab458291 Use Children's Reload method as part of SetConfig 2024-10-31 10:16:06 +01:00
7274815cfd Manage child processes individually, not via Pmux. Also route child logs through mlog 2024-10-29 17:49:24 +01:00
b7c097ef63 Refactor children process reloading, add garage reloading 2024-10-27 14:44:53 +01:00
433328524d Re-organize some reload logic 2024-10-24 22:14:13 +02:00
88ffa97c0f Small changes to how process reloading works 2024-10-24 21:19:58 +02:00
5c41cedea3 Implement SetConfig, but in a stupid way 2024-10-24 19:52:08 +02:00
63cefd403e Use a test logger 2024-10-23 20:38:39 +02:00
bbae88ab4b Add GetConfig method to Network 2024-10-23 20:18:11 +02:00
9e9e98584f Remove bootstrap argument from default.nix 2024-10-15 21:45:29 +02:00
f639d460cf Implement basic test for network.Join 2024-10-14 12:12:43 +02:00
cb6c11acef Test network.Load 2024-10-07 22:41:46 +02:00
7f3cbf628f Cleanup idle HTTP connections to make shutdown smoother 2024-10-07 22:17:06 +02:00
f146b77187 Generalize create network code in network package integration tests 2024-10-06 19:38:35 +02:00
010c53e5c7 Some general cleanup 2024-10-06 17:15:40 +02:00
71bc182ab4 Implement initial go test for network.Create 2024-10-05 23:03:26 +02:00
168b65ea1d Add ./go/tests.sh 2024-10-05 21:12:29 +02:00
48611df2cb Update documentation 2024-10-03 18:59:48 +02:00
61c5338b84 Update go modules vendorHash to be correct 2024-09-30 16:35:51 +02:00
31af39ce4c Embed default daemon.yml directly in entrypoint 2024-09-24 11:27:54 +02:00
64fdba0a48 Implement network(s) list sub-command 2024-09-24 11:03:18 +02:00
de7aac1f25 Refactor command-line parsing, pass --network to most commands 2024-09-23 20:50:45 +02:00
16aca610b4 Add multi-network support to daemon
It's still not possible to pick a network from the command-line, so this
is a bit broken, but the daemon component should handle it correctly at
least.
2024-09-23 19:04:14 +02:00
6c036d1183 Check that two different networks aren't trying to use the same nebula port 2024-09-12 08:59:23 +02:00
df4eae8a5c Support configuring more than one network 2024-09-10 22:51:33 +02:00
c022c97b19 include a migration for multi-network state directories 2024-09-10 21:02:07 +02:00
6d99fb5368 Remove randStr private utility function 2024-09-09 21:38:10 +02:00
8c3e6a2845 Separate Daemon and Network logic into separate packages
In a world where the daemon can manage more than one network, the Daemon
is really responsible only for knowing which networks are currently
joined, creating/joining/leaving networks, and routing incoming RPC
requests to the correct network handler as needed.

The new network package, with its Network interface, inherits most of
the logic that Daemon used to have, leaving Daemon only the parts needed
for the functionality just described. There's a lot of cleanup done here
in order to really nail down the separation of concerns between the two,
especially around directory creation.
2024-09-09 16:34:00 +02:00
86b2ba7bfa Factor daemon.Children into its own package 2024-09-07 15:46:59 +02:00
a840d0e701 Move common daemon types and values into daecommon 2024-09-07 15:11:04 +02:00
ef86c1bbd1 Make Daemon into a concrete type which implements RPC directly 2024-09-07 14:05:07 +02:00
fed79c6ec7 Update documentation on jsonrpc2.NewDispatchHandler 2024-09-05 19:36:21 +02:00
8d3b17e1cb Remove extraneous empty struct returns from RPC interface 2024-09-05 17:28:10 +02:00
038a28bb02 Remove remaining extraneous 'Result' RPC-related struct types 2024-09-04 22:46:38 +02:00
06a345ecd1 Embed context directly into subCmdCtx 2024-09-04 22:35:29 +02:00
6c185f6263 Allow variadic number of parameters on RPC calls 2024-09-04 22:25:38 +02:00
53ad8a91b4 Generate RPC client wrapper 2024-09-04 21:24:45 +02:00
5138ed7c6a Attempt to delete socket file before listening on the path 2024-09-04 19:44:58 +02:00
4f6a89ced0 Roadmap has been moved to micropelago.net 2024-09-01 12:20:37 +02:00
39e12f6ebd disallow -h and --help as flags in sub-commands 2024-07-22 16:37:22 +02:00
d31be8455b Pluralize 'host(s)' subcommands 2024-07-22 15:52:51 +02:00
ca62a37692 Fix rendering of text flag defaults 2024-07-22 10:42:25 +02:00
af69f1cfba Fix panic when starting up daemon with existing bootstrap 2024-07-21 17:20:48 +02:00
1ea16d80e4 Require host in garage for nebula create-cert command 2024-07-21 17:12:35 +02:00
ee30199c4c Automatically choose IP for new hosts 2024-07-21 17:10:28 +02:00
1411370b0e Write new host to garage as part of CreateHost 2024-07-20 12:36:21 +02:00
c94f8e3475 Move creation of children into daemon initialize method 2024-07-20 11:14:59 +02:00
7aa11ebe29 Only restart sub-processes which need restarting on bootstrap changes 2024-07-20 10:42:26 +02:00
bc9a2b62ef Upgrade pmux to latest 2024-07-19 17:06:12 +02:00
e657061482 Set permission bits on unix socket, so it's group read/writable 2024-07-16 17:30:36 +02:00
3980dc6083 Write files to runtime dir with write permissions, so they can be overwritten by the restartLoop 2024-07-14 15:51:04 +02:00
691727fe99 Fix arch package cross-compiling 2024-07-14 15:26:34 +02:00
5de93e3711 Add back the ability to specify IP for nebula create-cert 2024-07-14 14:43:17 +02:00
0f42d9367c Fix nebula create-cert error message 2024-07-14 14:28:01 +02:00
6fff1dfaeb Default HTTP socket to the tmp directory 2024-07-14 14:20:45 +02:00
8dd6768786 Fixes for releasing 2024-07-14 13:56:43 +02:00
67d17efde0 Allow including CA signing key with JoiningBootstrap, and update docs 2024-07-14 13:33:29 +02:00
d2710db8f1 State CA signing key in secrets store, eliminate admin bundle 2024-07-14 13:11:18 +02:00
9d5c8ea4db Use secrets store for global garage bucket creds 2024-07-14 12:19:39 +02:00
86abdb6ae1 Propagate garage RPC secret with created host bootstrap 2024-07-14 11:58:39 +02:00
56f796e3fb Implement basic secrets architecture, use it for garage RPC secret 2024-07-13 17:42:41 +02:00
b5059be7fa Move create-bootstrap logic into daemon, rename to hosts create 2024-07-13 16:31:52 +02:00
cb8fef38c4 Move create-nebula-cert into nebula create-cert, move most logic into daemon 2024-07-13 16:08:13 +02:00
cc121f0752 Move RemoveHost into daemon 2024-07-12 17:05:39 +02:00
778db848c6 Fix daemon EnvVar dirs not being created on startup 2024-07-12 16:35:32 +02:00
c5e919dc86 Remove runtime dir locking code 2024-07-12 16:13:44 +02:00
7ca8ff3467 Fetch nebula CAPublicCredentials from daemon 2024-07-12 16:11:42 +02:00
30c8ca332a Fetch GarageClientParams from daemon 2024-07-12 16:03:37 +02:00
736b23429c Do proper type-based validation or hostnames and ipnets 2024-07-12 15:31:43 +02:00
1ee396c976 Use RPC for create-bootstrap 2024-07-09 15:14:29 +02:00
279c79a9f1 Replace admin create-network with network create over RPC 2024-07-09 11:43:17 +02:00
f9d033b89f Implement Daemon.CreateNetwork, but it's not yet used or tested 2024-07-07 20:01:10 +02:00
ce5df164e1 Update documentation 2024-07-07 13:49:11 +02:00
7d8b274445 Implement JoinNetwork RPC method, and accompanying sub-command 2024-07-07 12:44:49 +02:00
81368821b7 Refactor Daemon into Children and DaemonRestarter into Daemon 2024-07-06 15:36:48 +02:00
179059fd3d Rename ISLE_SOCKET_PATH and refactor how it's loaded a bit 2024-07-06 14:26:06 +02:00
05e91cd657 Move daemon restarting logic into daemon package 2024-06-24 18:55:36 +02:00
c808fa81b9 Move some environment variables into daemon package 2024-06-24 14:45:57 +02:00
c3609252a5 Implement RPC socket and use it to list hosts 2024-06-23 14:37:10 +02:00
47e53dffb7 Switch to using latest mediocre-go-lib 2024-06-22 17:49:56 +02:00
4664ec4a70 Implement jsonrpc2 package, intended for use over the daemon socket 2024-06-22 17:37:15 +02:00
4e5d3b28ab Move some Bootstrap methods onto Daemon 2024-06-17 22:15:28 +02:00
a8893e4fc6 Move daemon sub-process logic into daemon package 2024-06-17 21:06:57 +02:00
aa1a8ea806 Implement archlinux pkg build as part of release 2024-06-17 17:20:34 +02:00
8a1c8d2ed6 Use XDG_STATE_HOME rather than XDG_DATA_HOME for storing bootstrap 2024-06-17 17:20:26 +02:00
c645a8c767 Refactor how signing/encryption keys are typed and (un)marshaled 2024-06-15 23:02:24 +02:00
65fa208a34 Move garage admin API calls into garage package 2024-06-12 10:55:55 +02:00
842c169169 Separate garage server logic into its own package 2024-06-12 10:18:33 +02:00
dee4af012e Fix tests.sh verbose output redirection 2024-06-11 16:57:31 +02:00
68f417b5ba Upgrade garage to v1.0.0
This required switching all garage admin API calls to the new v1
versions, and redoing how the global bucket key is created so it is
created via the "create key" API call.
2024-06-11 16:57:31 +02:00
2768be00d8 Refactor how host data is signed, now it's simpler and probably more secure 2024-06-10 22:33:26 +02:00
f13a08abfb Use JSON instead of YAML for files which aren't intended for human editing 2024-06-10 18:56:36 +02:00
b36a38446e Upgrade nixpkgs to 24.05 (primarily for a more up-to-date golang) 2024-06-10 16:01:27 +02:00
b97ff9b99b Small qol fixes to release.sh 2024-03-11 23:22:42 +01:00
Brian Picciano
249c46c586 Update docs for obtaining a binary 2023-12-23 17:07:32 +01:00
Brian Picciano
3d02be1be0 Year-end roadmap review 2023-12-22 17:30:24 +01:00
Brian Picciano
a7429bd176 Update testing notest 2023-11-07 22:42:14 +01:00
Brian Picciano
745f7786e8 Update roadmap, gateway doc is no longer necessary 2023-09-06 22:25:02 +02:00
Brian Picciano
5ee80b1b7d Add tests for DNS 2023-09-06 21:55:05 +02:00
Brian Picciano
96a3ecfe14 Perform garage tests on each of the running nodes 2023-09-06 21:06:03 +02:00
Brian Picciano
8dcc436aaa Refactor tests some more, use shared across all of them 2023-09-05 23:14:40 +02:00
Brian Picciano
ceab16d05f move admin tests into their own subdir 2023-09-04 21:40:11 +02:00
Brian Picciano
3c3bd8649a Fix minio-client creating config directory in user's home 2023-09-04 21:38:28 +02:00
Brian Picciano
98e5f4c98c Add create-bootstrap test case 2023-09-04 20:56:48 +02:00
Brian Picciano
0a482607d5 Move temp directory creation into test/utils 2023-09-04 19:56:15 +02:00
Brian Picciano
73db21f841 add more checks to 01-create-network test 2023-09-03 18:08:14 +02:00
Brian Picciano
56f38ad451 Refactor how tests are organized 2023-09-01 20:24:42 +02:00
Brian Picciano
a77617ae96 Add test for network creation 2023-09-01 17:20:03 +02:00
Brian Picciano
ae70278a9f Add --keep-tmp flag to test script 2023-09-01 17:19:48 +02:00
Brian Picciano
0b486d5d27 Allow setting tun name in daemon config 2023-09-01 16:45:21 +02:00
Brian Picciano
d2d25d3621 Set XDG variables on a per-test basis 2023-09-01 16:18:23 +02:00
Brian Picciano
bc798acffa Have tests each create a separate tmpdir, and cd into it 2023-08-31 22:07:36 +02:00
Brian Picciano
48675ee095 Fix output of error logs in verbose testing 2023-08-31 21:46:56 +02:00
Brian Picciano
aa0d489e88 Add verbose flag to test shell script 2023-08-30 20:08:40 +02:00
Brian Picciano
2876b56afb Fix how revision is embedded so that AppImage isn't always recompiled 2023-08-30 18:24:09 +02:00
Brian Picciano
e66f67da4a Super basic testing framework 2023-08-30 18:16:19 +02:00
Brian Picciano
3d6ed8604a Add ability to sign nebula public keys, and show nebula network info
The new commands are:

- `isle admin create-nebula-cert`
- `isle nebula show`

Between these two commands it's possible, with some effort, to get a
nebula mobile client hooked up to an isle server.
2023-08-27 16:09:03 +02:00
Brian Picciano
661e2b28cb Move go code into 'go' tld, to make organization clearer 2023-08-25 15:19:31 +02:00
Brian Picciano
b5e8ad274e Add AGPLv3 license 2023-08-25 14:16:52 +02:00
Brian Picciano
633c7147b1 Fix create host docs 2023-08-13 16:37:37 +02:00
Brian Picciano
6480f6c843 Fix global shared bucket name to conform to S3 naming standards 2023-08-13 16:14:59 +02:00
Brian Picciano
fafd711b1b Fix doc generation, remove cryptic references from plantuml 2023-08-13 15:51:06 +02:00
Brian Picciano
9fa32749b9 Remove old cryptic references completely, fuck it 2023-08-13 15:43:33 +02:00
Brian Picciano
b7fb1d9c0a Complete in-code changes required by rename 2023-08-07 22:12:51 +02:00
Brian Picciano
3d7651208f Perform all in-code renames which don't affect actual functionality 2023-08-05 23:53:17 +02:00
Brian Picciano
2b9601f031 Renaming in AppDir and documentation 2023-08-05 16:56:34 +02:00
Brian Picciano
4e3847ea84 cryptic-io org has been renamed to micropelago 2023-07-06 17:51:38 +02:00
Brian Picciano
257b961459 Gateway doc 2023-04-24 21:31:59 +02:00
Brian Picciano
a1b3ff71b3 Use entrypoint directly as AppRun
This removes the intermediate bash script which was running, which
_potentially_ fixes #2.

Since that bash script is no longer setting PATH, the daemon must
manually create the binary path for each sub-process anyway.
2023-04-23 16:30:47 +02:00
Brian Picciano
57f63750f3 Fix appimagetool build 2023-04-12 01:58:52 +02:00
Brian Picciano
1180540ce3 Make sure we can use nix cache for non-cross-compiling 2023-03-25 17:05:29 +01:00
Brian Picciano
3a3bd56295 Implement release script 2023-03-25 15:58:20 +01:00
Brian Picciano
e9190e4dbb Allow injecting bootstrap again, plus some additions to version 2023-02-15 15:09:31 +01:00
Brian Picciano
94c6ad8774 Fix up some TODOs 2023-02-15 14:58:47 +01:00
Brian Picciano
8e800951a6 Remove sources from flake, everything is defined in default.nix now
This includes re-adding the garage build ourselves, rather than using
their flake.
2023-02-15 13:56:22 +01:00
Brian Picciano
b7d49bff5b Allow building from either flake or nix-build 2023-02-15 12:04:56 +01:00
Brian Picciano
1354c96ba9 Accidentally left flake building garage rather than full appimage 2023-01-29 19:08:16 +01:00
Brian Picciano
a8856fba99 Update docs a bit in light of the new architectures 2023-01-29 19:01:58 +01:00
Brian Picciano
1379291c1e Got flake set up for cross-compilation, but it still doesn't work 2023-01-29 18:57:55 +01:00
Brian Picciano
05f9064d10 Update nixpkgs, add appimagetool for other archs 2023-01-29 15:31:11 +01:00
Brian Picciano
5061fb5670 Update appimagetool-ing to something which can theoretically be used on other architectures 2023-01-28 22:57:06 +01:00
Brian Picciano
17fb9bbd77 Add a flake.nix
I spent some time trying to get compilation on non-x86_64 systems
possibly working, but we're currently limited by AppImage, which doesn't
want to work properly.
2023-01-28 20:43:09 +01:00
Brian Picciano
1dc22701cd Write rpc_port file to garage meta dir
This file is then used in later startups when determining the rpc port,
thus preventing the user from changing the port by accident.
2023-01-17 20:31:22 +01:00
Brian Picciano
ca003eaf85 Improve version sub-cmd output 2022-11-22 12:57:27 +01:00
Brian Picciano
e96fccae1b Upgrade to garage v0.8.0, plus a commit to fix panics
We were encountering panics (see
https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/414). The new garage
commit is simply 0.8.0 plus the fix for that issue.
2022-11-22 12:51:24 +01:00
Brian Picciano
0a6516b44e Add release target in nix 2022-11-16 17:45:13 +01:00
Brian Picciano
03ab15902c Update docs for storage allocs in daemon.yml 2022-11-16 17:30:55 +01:00
Brian Picciano
53194614df Switch to using camelCase for logs 2022-11-16 17:27:42 +01:00
Brian Picciano
2181da14a1 Added some debug logging, ultimately not needed 2022-11-16 17:25:55 +01:00
Brian Picciano
b498ee271d Don't divide layout capacity by 100
See discussion in
https://git.deuxfleurs.fr/Deuxfleurs/garage/issues/414. Apparently the
capacity is only relative to that of other instances in the layout, it
has no absolute value of its own. We can say it corresponds to GB for
ease-of-use, but garage doesn't see it that way.
2022-11-16 15:51:13 +01:00
Brian Picciano
877e519821 Apply garage layout prior to putting bootstrap 2022-11-15 20:11:47 +01:00
Brian Picciano
b1fa3be970 Fix garage mc not getting default secret key set correctly 2022-11-13 20:14:16 +01:00
Brian Picciano
739e88d6c9 Add secret ability to specify zone 2022-11-13 20:09:03 +01:00
Brian Picciano
da100c6170 Fix garage Wait method not sleeping properly between tries 2022-11-13 16:49:23 +01:00
Brian Picciano
629a8ec9b2 Improve logging, introduce log levels
I switched to using mlog for logging, as opposed to writing directly to
Stderr. This gives us control over log levels, as well as coordination
so that we don't have multiple go-routines writing to stderr at the same
time.
2022-11-13 16:45:42 +01:00
Brian Picciano
90a30bef5e Fix startup sequence for daemon
Putting bootstrap host data into garage, and applying garage layout
diff, no longer happen simultaneously in the background. This was
causing some weird non-determinism in the startup which wasn't really
breaking anything, but made the logs harder to debug.

This also potentially fixes `waitForGarageAndNebula`, which was
neglecting to wait for nebula if there were allocations defined.
2022-11-13 14:55:25 +01:00
Brian Picciano
838c548706 Fix random bugs related to using garage cli tools 2022-11-08 14:54:31 +01:00
Brian Picciano
c4b7abbcc4 Add documentation related to firewalls 2022-11-05 17:16:25 +01:00
Brian Picciano
97d4aacc15 Make output of hosts list more useful 2022-11-05 16:55:17 +01:00
Brian Picciano
0d7d69679f Rename to make-bootstrap to create-bootstrap 2022-11-05 16:41:14 +01:00
Brian Picciano
4bc0750797 Adjust .gitignore after change to yml 2022-11-05 16:32:26 +01:00
Brian Picciano
d916d1a630 Fix dnsmasq config 2022-11-05 16:25:24 +01:00
Brian Picciano
a153911948 Forgot to write bootstrap to data dir during reload 2022-11-05 16:21:49 +01:00
Brian Picciano
0b094f057e Fix how host object is initialized during daemon startup 2022-11-05 15:50:04 +01:00
Brian Picciano
ffd276bd3e Refactor how nebula certs are signed and propagated
I had previously made the mistake of thinking that the Curve25519 key
which is generated for each host to use in nebula communication could
also be used for signing. This is not the case, Ed25519 is used for
signing and is different thant Curve25519.

Rather than figuring out how to convert the Curve25519 key into an
Ed25519 key, which there is no apparent support for in the standard
library, I opted to instead ship a separate key just for signing with
each host. Doing this required a bit of refactoring in order to keep all
the different keys straight and ensure all data which needs a signature
still has it.
2022-11-05 15:23:29 +01:00
Brian Picciano
e9ac1336ba Small fixes to documentation and various small bugs 2022-11-05 13:57:21 +01:00
Brian Picciano
c0ebca193d Add Name field to admin.CreationParams 2022-11-05 13:15:42 +01:00
Brian Picciano
bd5a5552bc Add Glossary, remove "participant" as a term 2022-11-05 12:03:51 +01:00
Brian Picciano
46685113e0 "cryptic-net network" -> "cryptic network" throughout docs 2022-11-05 11:50:11 +01:00
Brian Picciano
5c8c24e73e Add documentation for admin create-network 2022-11-03 15:38:31 +01:00
Brian Picciano
b935457439 Various fixes to bugs in admin create-network 2022-11-03 14:54:46 +01:00
Brian Picciano
6ba8b948c2 Update firewall information in "Contributing Storage" doc 2022-11-03 14:30:54 +01:00
Brian Picciano
be25907444 Remove reference to cryptic.io in daemon.yml 2022-11-03 14:25:50 +01:00
Brian Picciano
3ac86e07cf Use yaml instead of tgz for bootstrap file 2022-11-02 14:34:40 +01:00
Brian Picciano
7d95825f97 Use yaml to encode admin file, not tgz 2022-11-02 14:02:21 +01:00
Brian Picciano
745fe31324 Updates to existing documentation 2022-10-30 02:22:03 +02:00
Brian Picciano
287313e00a Update daemon-process-tree 2022-10-30 00:38:26 +02:00
Brian Picciano
7dceb659ef Store full nebula cert for each host in garage, rather than just the IP
This allows each host to verify the cert against the CA cert. We also
now have each host sign the yaml file that it posts to garage, to ensure
that a host can't arbitrarily overwrite another host's file.
2022-10-29 21:11:40 +02:00
Brian Picciano
711d568036 Use a real private key for garage instances 2022-10-29 00:09:18 +02:00
Brian Picciano
b26f4bdd6a Move proc locking into entrypoint
This completely cleans up all logic that used to be in crypticnet.
2022-10-27 00:45:40 +02:00
Brian Picciano
28159608c8 Factor out crypticnet.Env completely 2022-10-27 00:37:03 +02:00
Brian Picciano
b23a4cafa6 Remove Bootstrap from Env 2022-10-27 00:25:58 +02:00
Brian Picciano
08f47bd514 Move daemon.yml types and functionality out of entrypoint and Env 2022-10-26 23:21:31 +02:00
Brian Picciano
03618ba72c Reimplement dnsmasq-entrypoint in go
This allowed for deleting all script utilities and environment variable
logic.
2022-10-26 22:18:16 +02:00
Brian Picciano
2200d85992 Make populating garage ports optional 2022-10-26 21:47:39 +02:00
Brian Picciano
6ef21ff186 Don't set bootstrap host entry during admin create-network 2022-10-26 21:30:30 +02:00
Brian Picciano
be2250fddd Small fixes to get admin create-network working 2022-10-25 21:15:09 +02:00
Brian Picciano
9288d8cf48 Fix and improve version string 2022-10-20 22:30:30 +02:00
Brian Picciano
5e399209b2 Rename go-workspace to just entrypoint, clean out unused wait-for tools 2022-10-20 22:06:22 +02:00
Brian Picciano
47e45e0071 Factor out nebula-entrypoint
As part of this all "wait" constraints have been migrated to pure-go
implementations, taking advantage of pmux's `StartAfterFunc` argument.

nebula-entrypoint was the final main process besides the entrypoint
itself, allowing us to get rid of cryptic-net-main.
2022-10-20 21:59:46 +02:00
Brian Picciano
8ba88b4dfc Use migrated and upgraded pmux 2022-10-20 20:57:26 +02:00
Brian Picciano
8d92b9fe2b Factor out update-global-bucket 2022-10-19 16:53:38 +02:00
Brian Picciano
0d53d0c6d6 move garage/default.nix to nix/garage.nix 2022-10-19 16:25:11 +02:00
Brian Picciano
936ca8d48f Factor out garage-apply-layout-diff
The new code runs the equivalent functionality within the daemon go
code. It was required to make Env be immutable in order to prevent race
conditions (this really should have been done from the beginning
anyway).
2022-10-19 16:20:26 +02:00
Brian Picciano
41e0b56617 Implement admin create-network command
This required a lot of re-implementation of how garage gets interacted
with, including updating cluster layout using the admin API and
initialization of the global bucket key.
2022-10-19 15:41:18 +02:00
Brian Picciano
7a25e1b6e6 Initial implementation of garage.AdminClient 2022-10-16 22:17:24 +02:00
Brian Picciano
eba9b23e61 Introduce admin.CreationParams 2022-10-16 22:07:03 +02:00
Brian Picciano
f720d7accd Enable the garage admin interface 2022-10-16 21:22:58 +02:00
Brian Picciano
51e21c3e46 Get rid of garage web port
It's not clear how we would be using it at this point, and garage 0.8.0
allows us to leave it off, so might as well do so.
2022-10-16 21:12:33 +02:00
Brian Picciano
5e08061cd6 Factor out garage-entrypoint
The daemon entrypoint now starts the garage child processes directly,
without the extra step of indirection
2022-10-16 20:48:33 +02:00
Brian Picciano
18422a1084 Re-arrange sub-commands in entrypoint somewhat 2022-10-16 20:44:24 +02:00
180 changed files with 11209 additions and 4624 deletions

4
.gitignore vendored
View File

@ -1,4 +1,4 @@
*-bin
*-admin.tgz*
*-bootstrap.tgz
*admin.yml*
*bootstrap.yml*
result

View File

@ -1,4 +0,0 @@
#!/bin/sh
export PATH=$APPDIR/bin
exec cryptic-net-main entrypoint "$@"

View File

@ -1,6 +1,6 @@
[Desktop Entry]
Name=Cryptic Net
Name[en]=Cryptic Net
Name=Isle
Name[en]=Isle
Exec=AppRun
Icon=cryptic-logo

View File

@ -1,9 +0,0 @@
ip="$1"
shift;
echo "waiting for $ip to become available..."
while true; do ping -c1 -W1 "$ip" &> /dev/null && break; done
exec "$@"

View File

@ -1,76 +0,0 @@
#
# This file defines all configuration directives which can be modified for
# the cryptic-net daemon at runtime. All values specified here are the
# default values.
#
################################################################################
# A DNS service runs as part of every cryptic-net process.
dns:
# list of IPs that the DNS service will use to resolve non-cryptic.io
# hostnames.
resolvers:
- 1.1.1.1
- 8.8.8.8
# A VPN service runs as part of every cryptic-net process.
vpn:
# Enable this field if the vpn will be made to be publicly accessible at a
# particular IP or hostname. At least one host must have a publicly accessible
# VPN process at any given moment.
#public_addr: "host:port"
# Firewall directives, as described here:
# https://github.com/slackhq/nebula/blob/v1.6.1/examples/config.yml#L260
firewall:
conntrack:
tcp_timeout: 12m
udp_timeout: 3m
default_timeout: 10m
max_connections: 100000
outbound:
# Allow all outbound traffic from this node.
- port: any
proto: any
host: any
inbound:
# If any storage allocations are declared below, the ports used will be
# allowed here automatically.
# Allow ICMP between hosts.
- port: any
proto: icmp
host: any
# That's it.
storage:
# Allocations defined here are used to store data in the distributed storage
# network. If no allocations are defined then no data is replicated to this
# node.
#
# The data directory of each allocation should be on a different drive, while
# the meta directories can be anywhere (ideally on an SSD).
#
# Capacity declares how many gigabytes can be stored in each allocation, and
# is required. It must be a multiple of 100.
#
# The various ports are all required and must all be unique within and across
# allocations.
allocations:
#- data_path: /foo/bar/data
# meta_path: /foo/bar/meta
# capacity: 1200
# api_port: 3900
# rpc_port: 3901
# web_port: 3902

661
LICENSE.txt Normal file
View File

@ -0,0 +1,661 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.

123
README.md
View File

@ -4,110 +4,43 @@ rely on it for anything._**
-----
# cryptic-net
# Isle
The cryptic-net project provides the foundation for an **autonomous community
cloud infrastructure**.
Welcome to Isle's technical documentation. You can find a less technical
entrypoint to Isle on [the Micropelago website][isle].
This project targets communities of individuals, where certain members of the
community would like to host services and applications from servers running in
their homes or offices. These servers can range from simple Raspberry Pis to
full-sized home PCs.
Isle runs on a host as a server daemon, and connects to other isle instances to
form a peer-to-peer network. Isle networks are completely self-hosted; no
third-parties are required for a network to function.
The core components of cryptic-net, currently, are:
Members of a network are able to build upon the capabilities provided by Isle to
host services for themselves and others. These capabilities include:
* A VPN which enables direct peer-to-peer communication. Even if most hosts in
the network are on a private LAN (e.g. their home WiFi network) or have a
dynamic IP, they can still communicate directly with each other.
* A VPN which enables direct peer-to-peer communication between network members.
Even if most hosts in the network are on a private LAN (e.g. their home WiFi
network) or have a dynamic IP, they can still communicate directly with each
other.
* An S3-compatible network filesystem. Each participant can provide as much
storage as they care to, if any. Stored data is sharded and replicated across
all hosts that choose to provide storage.
* An S3-compatible network filesystem. Each member can provide as much storage
as they care to, if any. Stored data is sharded and replicated across all
hosts that choose to provide storage.
These components are wrapped into a single binary, with all setup being
automated. cryptic-net takes "just works" very seriously.
* A DNS server which provides automatic host discovery within the network.
Participants are able to build upon these foundations to host services for
themselves and others. They can be assured that their communications are private
and their storage is reliable, all with zero administrative overhead and zero
third parties involved.
Every isle daemon is able to create or join multiple independent networks. In
this case the networks remain siloed from each other, such that members of one
network are unable to access resources or communicate with members of the other.
[nebula]: https://github.com/slackhq/nebula
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/
[isle]: https://micropelago.net/isle/
## Documentation
## Getting Started
_NOTE: There is currently only a single live cryptic-net which can be joined,
though generalizing the bootstrap process so others can create their own network
is [planned][roadmap]. If you do not know the admins of this cryptic-net then
unfortunately there's not much you can do right now._
The following pages will guide you through setup of Isle, joining an existing
network, and all other functionality available via the command-line.
cryptic-net users fall into different roles, depending on their level of
involvement and expertise within their particular network. The documentation for
cryptic-net is broken down by these categories, so that the reader can easily
know which documents they need to care about.
* [Installation](./docs/install.md)
* [Command-line Usage](./docs/command-line.md)
* [Join a Network](./docs/user/join-a-network.md)
### User Docs
Users are participants who use cryptic-net resources, but do not provide any
network or storage resources themselves. Users may be accessing the network from
a laptop, and so are not expected to be online at any particular moment.
Documentation for users:
* [Getting Started](docs/user/getting-started.md)
* [Creating a daemon.yml File](docs/user/creating-a-daemonyml-file.md)
* [Using DNS](docs/user/using-dns.md) (advanced)
* Restic example (TODO)
### Operator Docs
Operators are participants who own a dedicated host which they can expect to be
always-online (to the extent that's possible in a residential environment).
Operator hosts will need at least one of the following to be useful:
* A static public IP, or a dynamic public IP with [dDNS][ddns] set up.
* At least 100GB of unused storage which can be reserved for the network.
Operators are expected to be familiar with server administration, and to not be
afraid of a terminal.
Documentation for operators:
* [Contributing Storage](docs/operator/contributing-storage.md)
* [Contributing a Lighthouse](docs/operator/contributing-a-lighthouse.md)
* [Managing garage](docs/operator/managing-garage.md)
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
### Admin Docs
Admins are participants who control membership within the network. They are
likely operators as well.
Documentation for admins:
* [Adding a Host to the Network](docs/admin/adding-a-host-to-the-network.md)
* Removing a Host From the Network (TODO)
### Dev Docs
Dev may or may not be participants in any particular cryptic-net. They instead
are those who work on the actual code for cryptic-net.
Documentation for devs:
* [Design Principles](docs/dev/design-principles.md)
* [`cryptic-net daemon` process tree](docs/dev/daemon-process-tree.svg): Diagram
describing the [pmux](https://github.com/cryptic-io/pmux) process tree created
by `cryptic-net daemon` at runtime.
* [Rebuilding Documentation](docs/dev/rebuilding-documentation.md)
## Misc
Besides documentation, there are a few other pages which might be useful:
* [Roadmap][roadmap]
[roadmap]: docs/roadmap.md
Those who want to dive in and contribute to the Isle codebase should check out
the [Developer Documentation](./docs/dev/index.md).

View File

@ -1,116 +1,172 @@
{
buildSystem ? builtins.currentSystem,
hostSystem ? buildSystem,
pkgsNix ? (import ./nix/pkgs.nix),
pkgs ? (import ./nix/pkgs.nix).stable,
bootstrap ? null,
revision ? "dev",
releaseName ? "dev",
}: let
}: rec {
rootedBootstrap = pkgs.stdenv.mkDerivation {
name = "cryptic-net-rooted-bootstrap";
src = bootstrap;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
mkdir -p "$out"/share
cp "$src" "$out"/share/bootstrap.tgz
'';
pkgs = pkgsNix.default {
inherit buildSystem hostSystem;
};
version = pkgs.stdenv.mkDerivation {
name = "cryptic-net-version";
pkgsNative = pkgsNix.default {
inherit buildSystem;
hostSystem = buildSystem;
};
buildInputs = [ pkgs.git pkgs.go ];
src = ./.;
inherit bootstrap;
garageNix = (import ./nix/garage.nix);
in rec {
version = pkgs.stdenv.mkDerivation {
name = "isle-version";
inherit buildSystem hostSystem revision releaseName;
nativeBuildInputs = [ pkgsNative.git ];
goVersion = pkgs.go.version;
garageVersion = garageNix.version;
nixpkgsVersion = pkgsNix.version;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
versionFile=version
if [ "$bootstrap" != "" ]; then
hostName=$(tar -xzf "$bootstrap" --to-stdout ./hostname)
echo "Built for host: $hostName" >> "$versionFile"
fi
echo "Build date: $(date)" >> "$versionFile"
echo "Git status: $(cd "$src" && git describe --always --long --dirty=' (dirty)')" >> "$versionFile"
echo "Go version: $(go version)" >> "$versionFile"
echo "Build host info: $(uname -srvm)" >> "$versionFile"
echo "Release: $releaseName" >> "$versionFile"
echo "Platform: $hostSystem" >> "$versionFile"
echo "Git Revision: $revision" >> "$versionFile"
echo "Go Version: $goVersion" >> "$versionFile"
echo "Garage Version: $garageVersion" >> "$versionFile"
echo "NixPkgs Version: $nixpkgsVersion" >> "$versionFile"
echo "Build Platform: $buildSystem" >> "$versionFile"
mkdir -p "$out"/share
cp "$versionFile" "$out"/share
'';
};
goWorkspace = pkgs.callPackage ./go-workspace {};
goBinaries = pkgs.buildGoModule {
pname = "isle-go-binaries";
version = "unstable";
dnsmasq = (pkgs.callPackage ./dnsmasq {
glibcStatic = pkgs.glibc.static;
}).env;
# If this seems pointless, that's because it is! buildGoModule doesn't like
# it if the src derivation's name ends in "-go". So this mkDerivation here
# only serves to give buildGoModule a src derivation with a name it likes.
src = pkgs.stdenv.mkDerivation {
name = "isle-go-src";
src = ./go;
builder = builtins.toFile "builder.sh" ''
source $stdenv/setup
cp -r "$src" "$out"
'';
};
garage = (pkgs.callPackage ./garage {}).env;
vendorHash = "sha256-xAm2DqgXpZEErjASSZQoEH9GPwFbBh4h2cY4FWzPVZM=";
waitFor = pkgs.callPackage ./nix/wait-for.nix {};
subPackages = [
"./cmd/entrypoint"
];
};
appDir = pkgs.buildEnv {
name = "cryptic-net-AppDir";
dnsmasq = (pkgs.callPackage ./nix/dnsmasq.nix {
stdenv = pkgs.pkgsStatic.stdenv;
});
nebula = pkgs.callPackage ./nix/nebula.nix {};
garage = let
hostPlatform = pkgs.stdenv.hostPlatform.parsed;
in pkgs.callPackage garageNix.package {
inherit buildSystem;
hostSystem = "${hostPlatform.cpu.name}-unknown-${hostPlatform.kernel.name}-musl";
pkgsSrc = pkgsNix.src;
};
appDirBase = pkgs.buildEnv {
name = "isle-AppDir-base";
paths = [
pkgs.pkgsStatic.bash
pkgs.pkgsStatic.coreutils
pkgs.pkgsStatic.unixtools.ping
pkgs.pkgsStatic.netcat # required by waitFor
pkgs.pkgsStatic.gnutar
pkgs.pkgsStatic.gzip
# custom packages from ./pkgs.nix
pkgs.yq-go
pkgs.nebula
./AppDir
version
dnsmasq
nebula
garage
waitFor
goWorkspace.crypticNetMain
] ++ (if bootstrap != null then [ rootedBootstrap ] else []);
pkgs.minio-client
];
};
appimagetool = pkgs.callPackage ./nix/appimagetool.nix {};
appDir = pkgs.stdenv.mkDerivation {
name = "isle-AppDir";
appImage = pkgs.stdenv.mkDerivation {
name = "cryptic-net-AppImage";
src = appDir;
buildInputs = [ appimagetool ];
ARCH = "x86_64";
src = appDirBase;
inherit goBinaries;
builder = builtins.toFile "build.sh" ''
source $stdenv/setup
cp -rL "$src" cryptic-net
chmod +w cryptic-net -R
mkdir $out
appimagetool cryptic-net "$out/cryptic-net"
cp -rL "$src" "$out"
chmod +w "$out" -R
cd "$out"
cp $goBinaries/bin/entrypoint ./AppRun
'';
};
service = pkgs.writeText "cryptic-service" ''
[Unit]
Description=cryptic nebula
Requires=network.target
After=network.target
devShell = pkgs.mkShell {
buildInputs = [
pkgs.go
pkgs.golangci-lint
pkgs.gopls
(pkgs.callPackage ./nix/gowrap.nix {})
];
shellHook = ''
true # placeholder
'';
};
[Service]
Restart=always
RestartSec=1s
User=root
ExecStart=${appImage}/cryptic-net
testShell = pkgs.mkShell {
APPDIR = appDirBase;
buildInputs = [
pkgs.go
];
};
[Install]
WantedBy=multi-user.target
'';
build = rec {
appImage = pkgs.stdenv.mkDerivation {
name = "isle-AppImage";
src = appDir;
nativeBuildInputs = [
(pkgsNative.callPackage ./nix/appimagetool.nix {})
];
ARCH = pkgs.stdenv.hostPlatform.parsed.cpu.name;
builder = builtins.toFile "build.sh" ''
source $stdenv/setup
cp -rL "$src" isle.AppDir
chmod +w isle.AppDir -R
export VERSION=debug
# https://github.com/probonopd/go-appimage/issues/155
unset SOURCE_DATE_EPOCH
appimagetool ./isle.AppDir
mkdir -p "$out"/bin
mv Isle-* "$out"/bin/isle
'';
};
archPkg = ((import ./dist/linux/arch) {
inherit hostSystem releaseName appImage;
pkgs = pkgsNative;
});
};
}

86
dist/linux/arch/default.nix vendored Normal file
View File

@ -0,0 +1,86 @@
{
pkgs,
hostSystem,
releaseName,
appImage,
}: let
cpuArch = (pkgs.lib.systems.parse.mkSystemFromString hostSystem).cpu.name;
pkgbuild = pkgs.writeText "isle-arch-PKGBUILD-${releaseName}-${cpuArch}" ''
pkgname=isle
pkgver=${builtins.replaceStrings ["-"] ["_"] releaseName}
pkgrel=0
pkgdesc="The foundation for an autonomous community cloud infrastructure."
arch=('${cpuArch}')
url="https://code.betamike.com/micropelago/isle"
license=('AGPL-3.0-or-later')
depends=(
'fuse2'
)
# The appImage is deliberately kept separate from the src.tar.zst. For some
# reason including the appImage within the archive results in a large part
# of the binary being stripped away and some weird skeleton appImage comes
# out the other end.
source=('isle' 'src.tar.zst')
md5sums=('SKIP' 'SKIP')
noextract=('isle')
package() {
cp -r etc "$pkgdir"/etc
cp -r usr "$pkgdir"/usr
mkdir -p "$pkgdir"/usr/bin/
cp isle "$pkgdir"/usr/bin/
}
'';
in
pkgs.stdenv.mkDerivation {
name = "isle-arch-pkg-${releaseName}-${cpuArch}";
nativeBuildInputs = [
pkgs.zstd
pkgs.pacman
pkgs.fakeroot
pkgs.libarchive
];
inherit pkgbuild;
src = appImage;
defaultDaemonYml = ../../../go/daemon/daecommon/daemon.yml;
systemdService = ../isle.service;
dontUnpack = true;
buildPhase = ''
mkdir -p root/etc/isle/
cp "$defaultDaemonYml" root/etc/isle/daemon.yml
mkdir -p root/usr/lib/sysusers.d/
cat >root/usr/lib/sysusers.d/isle.conf <<EOF
u isle - "isle Daemon"
EOF
mkdir -p root/usr/lib/systemd/system
cp "$systemdService" root/usr/lib/systemd/system/isle.service
cp $pkgbuild PKGBUILD
tar -cf src.tar.zst --zstd --mode=a+rX,u+w -C root .
cp "$src"/bin/isle isle
PKGEXT=".pkg.tar.zst" CARCH="${cpuArch}" makepkg \
--nodeps \
--config ${pkgs.pacman}/etc/makepkg.conf
'';
installPhase = ''
mkdir -p $out
cp *.pkg.tar.zst $out/
'';
# NOTE if https://github.com/NixOS/nixpkgs/issues/241911 is ever addressed
# it'd be nice to add an automatic check using namcap here.
}

16
dist/linux/isle.service vendored Normal file
View File

@ -0,0 +1,16 @@
[Unit]
Description=Isle
Requires=network.target
After=network.target
[Service]
User=isle
ExecStart=/usr/bin/isle daemon -c /etc/isle/daemon.yml
RuntimeDirectory=isle
RuntimeDirectoryMode=0700
StateDirectory=isle
StateDirectoryMode=0700
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target

View File

@ -1,34 +0,0 @@
# TODO implement this in go
set -e -o pipefail
cd "$APPDIR"
conf_path="$_RUNTIME_DIR_PATH"/dnsmasq.conf
cat etc/dnsmasq/base.conf > "$conf_path"
tmp="$(mktemp -d -t cryptic-net-dnsmasq-entrypoint-XXX)"
( trap "rm -rf '$tmp'" EXIT
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
echo "listen-address=$thisHostIP" >> "$conf_path"
ls -1 "$tmp"/hosts | while read hostYml; do
hostName=$(echo "$hostYml" | cut -d. -f1)
hostIP=$(cat "$tmp"/hosts/"$hostYml" | yq '.nebula.ip')
echo "address=/${hostName}.hosts.cryptic.io/$hostIP" >> "$conf_path"
done
)
cat "$_DAEMON_YML_PATH" | \
yq '.dns.resolvers | .[] | "server=" + .' \
>> "$conf_path"
exec bin/dnsmasq -d -C "$conf_path"

View File

@ -1,39 +0,0 @@
{
stdenv,
buildEnv,
glibcStatic,
rebase,
}: rec {
dnsmasq = stdenv.mkDerivation rec {
pname = "dnsmasq";
version = "2.85";
src = builtins.fetchurl {
url = "https://www.thekelleys.org.uk/dnsmasq/${pname}-${version}.tar.xz";
sha256 = "sha256-rZjTgD32h+W5OAgPPSXGKP5ByHh1LQP7xhmXh/7jEvo=";
};
nativeBuildInputs = [ glibcStatic ];
makeFlags = [
"LDFLAGS=-static"
"DESTDIR="
"BINDIR=$(out)/bin"
"MANDIR=$(out)/man"
"LOCALEDIR=$(out)/share/locale"
];
};
env = buildEnv {
name = "cryptic-net-dnsmasq";
paths = [
(rebase "cryptic-net-dnsmasq-bin" ./bin "bin")
(rebase "cryptic-net-dnsmasq-etc" ./etc "etc/dnsmasq")
dnsmasq
];
};
}

View File

@ -1,41 +0,0 @@
# Configuration file for dnsmasq.
#
# Format is one option per line, legal options are the same
# as the long options legal on the command line. See
# "/usr/sbin/dnsmasq --help" or "man 8 dnsmasq" for details.
# Listen on this specific port instead of the standard DNS port
# (53). Setting this to zero completely disables DNS function,
# leaving only DHCP and/or TFTP.
port=53
# If you don't want dnsmasq to read /etc/resolv.conf or any other
# file, getting its servers from this file instead (see below), then
# uncomment this.
no-resolv
# On systems which support it, dnsmasq binds the wildcard address,
# even when it is listening on only some interfaces. It then discards
# requests that it shouldn't reply to. This has the advantage of
# working even when interfaces come and go and change address. If you
# want dnsmasq to really bind only the interfaces it is listening on,
# uncomment this option. About the only time you may need this is when
# running another nameserver on the same machine.
bind-interfaces
# If you don't want dnsmasq to read /etc/hosts, uncomment the
# following line.
no-hosts
# Unset user and group so that dnsmasq doesn't drop privileges to another user.
# If this isn't done then dnsmasq fails to start up, since it fails to access
# /etc/passwd correctly, probably due to nix.
user=
group=
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
#
# Everything below is generated dynamically based on runtime configuration
#
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

View File

@ -4,20 +4,6 @@ This document guides an admin through adding a single host to the network. Keep
in mind that the steps described here must be done for _each_ host the user
wishes to add.
There are two ways for a user to add a host to the cryptic-net network.
- If the user is savy enough to obtain their own `cryptic-net` binary, they can
do so. The admin can then generate a `bootstrap.tgz` file for their host,
give that to the user, and the user can run `cryptic-net daemon` using that
bootstrap file.
- If the user is not so savy, the admin can generate a custom `cryptic-net`
binary with the `bootstrap.tgz` embedded into it. The user can be given this
binary and run `cryptic-net daemon` without any configuration on their end.
From the admin's perspective the only difference between these cases is one
extra step.
## Step 1: Choose Hostname
The user will need to provide you with a name for their host. The name should
@ -29,66 +15,20 @@ conform to the following rules:
* It should end with a letter or number.
## Step 2: Add Host to Network
## Step 2: Create a `bootstrap.json` File
The admin should choose an IP for the host. The IP you choose for the new host
should be one which is not yet used by any other host, and which is in the VPN's
set of allowed IPs.
The admin should perform the following command from their own host:
```
cryptic-net hosts add --name <name> --ip <ip>
```
## Step 3: Create a `bootstrap.tgz` File
Access to an `admin.tgz` file is required for this step.
To create a `bootstrap.tgz` file for the new host, the admin should perform the
To create a `bootstrap.json` file for the new host, the admin should perform the
following command from their own host:
```
cryptic-net hosts make-bootstrap \
--name <name> \
--admin-path <path to admin.tgz> \
> bootstrap.tgz
isle hosts create --hostname <name> >bootstrap.json
```
The resulting `bootstrap.tgz` file should be treated as a secret file that is
shared only with the user it was generated for. The `bootstrap.tgz` file should
not be re-used between hosts either.
The resulting `bootstrap.json` file should be treated as a secret file and
shared only with the user it was generated for. The `bootstrap.json` file should
not be re-used between hosts.
If the user already has access to a `cryptic-net` binary then the new
`bootstrap.tgz` file can be given to them as-is, and they can proceed with
running their host's `cryptic-net daemon`.
The user can now proceed with calling `isle network join`, as described in the
[Getting Started][getting-started] document.
### Encrypted `admin.tgz`
If `admin.tgz` is kept in an encrypted format on disk (it should be!) then the
decrypted form can be piped into `make-bootstrap` over stdin. For example, if
GPG is being used to secure `admin.tgz` then the following could be used to
generate a `bootstrap.tgz`:
```
gpg -d <path to admin.tgz.gpg> | cryptic-net hosts make-boostrap \
--name <name> \
--admin-path - \
> bootstrap.tgz
```
Note that the value of `--admin-path` is `-`, indicating that `admin.tgz` should
be read from stdin.
## Step 4: Optionally, Build Binary
If you wish to embed the `bootstrap.tgz` into a custom binary for the user (to
make installation _extremely_ easy for them) then you can run the following:
```
nix-build --arg bootstrap <path to bootstrap.tgz> -A appImage
```
The resulting binary can be found in the `result` directory which is created.
Note that this binary should be treated like a `bootstrap.tgz` in terms of its
uniqueness and sensitivity.
[getting-started]: ../user/getting-started.md

View File

@ -0,0 +1,101 @@
# Creating a New Network
This guide is for those who wish to start a new isle network of their own.
By starting a new isle network, you are becoming the administrator of a
network. Be aware that being a network administrator is not necessarily easy,
and the users of your network will frequently need your help in order to have a
good experience. It can be helpful to have others with which you are
administering the network, in order to share responsibilities.
## Requirements
Creating a network is done using a single host, which will become the first host
in the network.
The configuration used during network creation will be identical to that used
during normal operation of the host, so be prepared to commit to that
configuration for a non-trivial amount of time.
The requirements for this host are:
* A public static IP, or a dynamic public IP with [dDNS][ddns] set up.
* There should be UDP port which is accessible publicly over that IP/DNS name.
This may involve forwarding the UDP port in your gateway if the host is
behind a NAT, and/or allowing traffic on that UDP port in your hosts
firewall.
* At least 3 GB of disk storage space.
* At least 3 directories should be chosen, each of which will be committing at
least 1GB. Ideally these directories should be on different physical disks,
but if that's not possible it's ok. See the Next Steps section.
* None of the resources being used for this network (the UDP port or storage
locations) should be being used by other networks.
## Step 1: Configure the isle Daemon
Open `/etc/isle/daemon.yml` in a text editor and perform the following changes:
* Set the `vpn.public_addr` field to the `host:port` your host is accessible on,
where `host` is the static public IP/DNS name of your host, and `port` is the
UDP port which is publicly accessible.
* Configure 3 (at least) allocations in the `storage.allocations` section.
Save and close the file.
Run the following to restart the daemon with the new configuration:
```
sudo systemctl restart isle
```
## Step 2: Choose Parameters
There are some key parameters which must be chosen when creating a new network.
These will remain constant throughout the lifetime of the network, and so should
be chosen with care.
* Name: A human-readable name for the network. This will only be used for
display purposes.
* Subnet: The IP subnet (or CIDR) will look something like `10.10.0.0/16`, where
the `/16` indicates that all IPs from `10.10.0.0` to `10.10.255.255` are
included. It's recommended to choose from the [ranges reserved for private
networks](https://en.wikipedia.org/wiki/IPv4#Private_networks), but within
that selection the choice is up to you.
* Domain: isle is shipped with a DNS server which will automatically
configure itself with all hosts in the network, with each DNS entry taking the
form of `hostname.hosts.domain`, where `domain` is the domain chosen in this
step. The domain may be a valid public domain or not, it's up to you.
* Hostname: The hostname of your host, which will be the first host in the
network, must be chosen at this point. You can reference the [Adding a Host to
the Network](./adding-a-host-to-the-network.md) document for the constraints
on the hostname.
* IP: The IP of your host, which will be the first host in the network. This IP
must be within the chosen subnet range.
## Step 3: Create the Network
To create the network, run:
```
sudo isle network create \
--name <name> \
--ip-net <subnet> \
--domain <domain> \
--hostname <hostname>
```
At this point your host, and your network, are ready to go! To add other hosts
to the network you can reference the [Adding a Host to the Network][add-host]
document.
[add-host]: ./adding-a-host-to-the-network.md
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/

54
docs/command-line.md Normal file
View File

@ -0,0 +1,54 @@
# Command-line Usage
This documents provides examples of how to accomplish various tasks from Isle's
command-line interface.
Isle network members fall into different roles, depending on their level of
involvement and expertise within their particular network. The documentation is
broken down by these categories, so that the reader can easily decide which
documents they need to care about.
### User Docs
Users are members who use network resources, but do not provide any network
resources themselves. Users may be accessing the network from a mobile device,
and so are not expected to be online at any particular moment.
Documentation for users:
* [Joining a Network](./user/join-a-network.md)
* [Using DNS](./user/using-dns.md) (advanced)
* Restic example (TODO)
### Operator Docs
Operators are members who own a dedicated host which they can expect to be
always-online (to the extent that's possible in a residential environment).
Operator hosts will need at least one of the following to be useful:
* A static public IP, or a dynamic public IP with [dDNS][ddns] set up.
* At least 1GB of unused storage which can be reserved for the network.
Operators are expected to be familiar with server administration, and to not be
afraid of a terminal.
Documentation for operators:
* [Contributing Storage](./operator/contributing-storage.md)
* [Contributing a Lighthouse](./operator/contributing-a-lighthouse.md)
* [Managing garage](./operator/managing-garage.md)
* [Firewalls](./operator/firewall.md)
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
### Admin Docs
Admins are members who control membership within the network. They are likely
operators as well.
Documentation for admins:
* [Creating a New Network](./admin/creating-a-new-network.md)
* [Adding a Host to the Network](./admin/adding-a-host-to-the-network.md)
* Removing a Host From the Network (TODO)

25
docs/dev/architecture.md Normal file
View File

@ -0,0 +1,25 @@
# Architecture
The isle daemon is the central point around which all operations occur. Users of
the isle command-line tool (or, soon, a GUI) communicate with the daemon over a
local RPC socket to issue commands, for example to tell it to join a new network
or to retrieve the names of hosts in an already-joined network.
For every network which is joined, the isle daemon will create and manage
sub-processes to provide certain parts of its functionality. These include:
* A [nebula][nebula] instance to provide VPN functionality.
* A [dnsmasq][dnsmasq] instance to act as DNS server.
* Zero or more [garage][garage] instances, depending on how storage is
configured on the host, to provide the S3 storage layer.
The isle daemon considers the configuration and management of these
sub-processes to be an implementation detail. In other words, its RPC interface,
and therefore all user interfaces, do not expose members to the fact that these
sub-processes exist, or even that these projects are being used under the hood.
![Architectural diagram](./architecture.svg)
[nebula]: https://github.com/slackhq/nebula
[garage]: https://garagehq.deuxfleurs.fr/
[dnsmasq]: https://dnsmasq.org/doc.html

View File

@ -0,0 +1,49 @@
@startuml
skinparam componentStyle rectangle
[isle command line] as isleCommand
() "daemon.RPC" as isleDaemonRPC
[daemon.Client] as daemonClient
frame "isle daemon process" {
portin "RPC Socket" as rpcSocket
[RPC Server] as rpcServer
() "daemon.RPC" as daemonRPC
[daemon.Daemon] as daemon
[network.Network (A)] as networkA
[network.Network (B)] as networkB
rpcServer --> rpcSocket : handle
rpcServer ..> daemonRPC : dispatch
daemon - daemonRPC
daemon --> networkA
daemon --> networkB
}
isleCommand ..> isleDaemonRPC : issue commands
daemonClient - isleDaemonRPC
daemonClient --> rpcSocket
package "network A child processes" {
[nebula] as networkANebula
[garage (alloc 1)] as networkAGarage1
[garage (alloc 2)] as networkAGarage2
[dnsmasq] as networkADNSMasq
}
networkA --> networkANebula
networkA --> networkAGarage1
networkA --> networkAGarage2
networkA --> networkADNSMasq
package "network B child processes" {
[nebula] as networkBNebula
[dnsmasq] as networkBDNSMasq
}
networkB --> networkBNebula
networkB --> networkBDNSMasq
@enduml

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

47
docs/dev/building.md Normal file
View File

@ -0,0 +1,47 @@
# Building Isle
Building from source requires [nix][nix].
(*NOTE* The first time you run some of these builds a lot of things will be
built from scratch. If you have not otherwise configured it, nix might be using
a tmpfs as its build directory, and the capacity of this tmpfs will probably be
exceeded by this build. You can change your build directory to somewhere on-disk
by setting the TMPDIR environment variable for `nix-daemon` (see [this github
issue][tmpdir-gh].))
[nix]: https://nixos.wiki/wiki/Nix_package_manager
[tmpdir-gh]: https://github.com/NixOS/nix/issues/2098#issuecomment-383243838
## Current System
You can build an AppImage for your current system by running the following from
the project's root:
```
nix-build -A build.appImage
```
The resulting binary can be found under `result/bin`.
## Cross-Compile
An AppImage can be cross-compiled by passing the `hostSystem` argument:
```
nix-build --argstr hostSystem "x86_64-linux" -A build.appImage
```
Supported system strings are enumerated in `nix/pkgs.nix`.
## Alternative Build Targets
Besides AppImage, Isle supports building alternative package types which are
targetted at different specific operating systems. Each of these has its own
`build.*` target.
* AppImage (all OSs): `nix-build -A build.appImage`
* Arch Linux package: `nix-build -A build.archPkg`
All targets theoretically support passing in a `hostSystem` argument to
cross-compile to a different architecture or OS, but some targets may not make
sense with some `hostSystem` values.

View File

@ -1,68 +0,0 @@
@startuml
hide empty description
state "./cryptic-net daemon -c ./daemon.yml" as init
state AppDir {
note "All relative paths are relative to the root of the AppDir" as N1
state "./AppRun" as AppRun {
AppRun : * Set PATH to APPDIR/bin
}
state "./bin/cryptic-net-main entrypoint daemon -c ./daemon.yml" as entrypoint {
entrypoint : * Create runtime dir at $_RUNTIME_DIR_PATH
entrypoint : * Lock runtime dir
entrypoint : * Merge given and default daemon.yml files
entrypoint : * Copy bootstrap.tgz into $_DATA_DIR_PATH, if it's not there
entrypoint : * Merge daemon.yml config into bootstrap.tgz
entrypoint : * Run child processes
}
init --> AppRun : exec
AppRun --> entrypoint : exec
state "./bin/dnsmasq-entrypoint" as dnsmasqEntrypoint {
dnsmasqEntrypoint : * Create $_RUNTIME_DIR_PATH/dnsmasq.conf
}
state "./bin/dnsmasq -d -C $_RUNTIME_DIR_PATH/dnsmasq.conf" as dnsmasq
entrypoint --> dnsmasqEntrypoint : child
dnsmasqEntrypoint --> dnsmasq : exec
state "./bin/cryptic-net-main nebula-entrypoint" as nebulaEntrypoint {
nebulaEntrypoint : * Create $_RUNTIME_DIR_PATH/nebula.yml
}
state "./bin/nebula -config $_RUNTIME_DIR_PATH/nebula.yml" as nebula
entrypoint --> nebulaEntrypoint : child
nebulaEntrypoint --> nebula : exec
state "./bin/cryptic-net-main garage-entrypoint" as garageEntrypoint {
garageEntrypoint : * Create $_RUNTIME_DIR_PATH/garage-N.toml\n (one per storage allocation)
garageEntrypoint : * Run child processes
}
state "./bin/garage -c $_RUNTIME_DIR_PATH/garage-N.toml server" as garage
state "./bin/garage-apply-layout-diff" as garageApplyLayoutDiff {
garageApplyLayoutDiff : * Runs once then exits
garageApplyLayoutDiff : * Updates cluster topo
}
entrypoint --> garageEntrypoint : child (only if >1 storage allocation defined in daemon.yml)
garageEntrypoint --> garage : child (one per storage allocation)
garageEntrypoint --> garageApplyLayoutDiff : child
state "./bin/cryptic-net-main update-global-bucket" as updateGlobalBucket {
updateGlobalBucket : * Runs once then exits
updateGlobalBucket : * Updates the bootstrap data for the host in garage for other hosts to query
}
entrypoint --> updateGlobalBucket : child
}
@enduml

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@ -1,21 +1,20 @@
# Design Principles
The following points form the basis for all design decisions made within the
cryptic-net project.
Isle project.
* The UX is aggressively optimized to eliminate manual intervention by users.
* The UX is aggressively optimized to eliminate manual intervention by members.
All other concerns are secondary. The concept of "UX" extends beyond GUI
interfaces, and encompasses all interactions of any sort with a cryptic-net
interfaces, and encompasses all interactions of any sort with an isle
process.
* All resources within a cryptic-net are expected to be hosted on hardware owned
by community members, for example home media servers or gaming rigs. Thus, a
cryptic-net is fully autonomous.
* All resources within an isle network are expected to be hosted on hardware
owned by community members, for example home media servers or gaming rigs.
Thus, an isle network is fully autonomous.
* Hardware resources are expected to be heterogenous and geographically
dispersed.
* It is expected that a single host might be a part of multiple, independent
cryptic-net networks. These should not conflict with each other, nor share
resources.
isle networks. These should not conflict with each other, nor share resources.

25
docs/dev/glossary.md Normal file
View File

@ -0,0 +1,25 @@
# Glossary
The purpose of this document is define the specific terms which should be used
for various concepts, with the goal of establishing consistency throughout
documentation and source code.
- "Isle" - The name of this project, which is a proper noun and so should always
be capitalized.
- "isle" - The name of the binary or program produced by the Isle project. isle
is the name of the file itself, as well as an instance of the process, and so
is always lower-case.
- "host" - A computer or device on which isle is run.
- "isle network", "network" - A collection of hosts which communicate and share
resources with each other via the Isle project.
- "garage cluster" - Garage is one of the sub-processes which isle is able to
run. These garage process connect together to form a cluster. We use the
term "cluster" in the context of garage to stay consistent with garage's
documentation and command-line.
- "member" - A person who takes part in the usage, operation, or administration
of an isle network.

16
docs/dev/index.md Normal file
View File

@ -0,0 +1,16 @@
# Developer Documentation
This section of the documentation is targeted at those who are contributing to
the Isle codebase. It is recommended to start with the following pages, in order
to better understand how to navigate and work on the codebase.
* [Glossary](./glossary.md)
* [Design Principles](./design-principles.md)
* [Architecture](./architecture.md)
These pages can be helpful in specific situations.
* [Building Isle](./building.md)
* [Testing Isle](./testing.md)
* [Rebuilding Documentation](./rebuilding-documentation.md)
* [Releases](./releases.md)

View File

@ -1,6 +1,6 @@
# Rebuilding Documentation
Most documentation for cryptic-net takes the form of markdown (`.md`) files,
Most documentation for Isle takes the form of markdown (`.md`) files,
which do not require any build step. There are a few other kinds of files, such
as `.plantuml` files, which do require a build step. If these are changed then
their artifacts should be rebuilt by doing:

32
docs/dev/releases.md Normal file
View File

@ -0,0 +1,32 @@
# Releases
A release consists of:
- A full set of `isle` binaries for all supported platforms, compiled from the
same source.
- A text file containing hashes of each binary.
- A file containing a signature of the hash file, created by whoever is building
the release.
## Building
*NOTE: This has only been tested from an x86_64 linux machine*
To create a release only a functional nix installation is required. Simply run
the `./release.sh` script, and input a release name when prompted.
From here an `isle` binary will be cross-compiled for all supported
platforms. This will take a long time the first time you perform it on your
machine.
Once compilation is completed, the release will be signed using the default GPG
key on your machine, and you will be prompted for its password in order to
create the signature.
## Releasing
Releases are uploaded to the repository's Releases page, and release naming
follows the conventional semantic versioning system. Each release should be
accompanied by a set of changes which have occurred since the last release,
described both in the `CHANGELOG.md` file and in the description on the Release
itself.

39
docs/dev/testing.md Normal file
View File

@ -0,0 +1,39 @@
# Testing Isle
All tests are currently written as go tests, and as such can be run from the
`go` directory using the normal go testing tool.
```
cd go
go test -run Foo ./daemon
go test ./... # Test everything
```
## Integration Tests
Integration tests are those which require processes or state external to the
test itself. Integration tests are marked using the
`toolkit.MarkIntegrationTest` function, which will cause them to be skipped
unless being run in the integration test environment.
Besides a normal nix installation (like all Isle development needs), integration
tests also require `sudo` and [capsh][capsh] to be installed on the system.
[capsh]: https://www.man7.org/linux/man-pages/man1/capsh.1.html
By running tests using the `go/integration_test.sh` script the tests will be
automatically run in the integration test environment. All arguments will be
passed directly to the go testing tool.
```
cd go
./integration_test.sh -run Foo ./daemon
```
`integration_test.sh` wraps a call to `go test` in a bash shell which has all
required binaries available to it, and which has acquired necessary
[capabilities][capabilities] to use the binaries as needed. Acquiring
capabilities is done by elevating the user to root using `sudo`, and then
dropping them back down to a shell of the original user with capabilities set.
[capabilities]: https://wiki.archlinux.org/title/Capabilities

86
docs/install.md Normal file
View File

@ -0,0 +1,86 @@
# Installation
This document will guide you through the process of obtaining and installing
Isle on your machine.
NOTE currently only linux machines with the following architectures are
supported:
- `x86_64` (aka `amd64`)
- `aarch64` (aka `arm64`)
- `i686`
(`i686` has not been tested.)
More OSs and architectures coming soon!
## Install isle
How isle gets installed depends on which Linux distribution you are using.
### Archlinux (also Manjaro)
Download the latest `.pkg.tar.zst` package file for your platform from
[this link][latest].
Install the package using pacman:
```
sudo pacman -U /path/to/isle-*.pkg.tar.zst
```
### Other Distributions
If a package file is not available for your distribution you can still install
an AppImage directly. It is assumed that all commands below are run as root.
Download the latest `.AppImage` binary for your platform from
[this link][latest], and place it in your `/usr/bin` directory.
Create a `daemon.yml` file using default values by doing:
```
mkdir -p /etc/isle/
isle daemon --dump-config > /etc/isle/daemon.yml
```
Create a system user for the isle daemon to run as:
```
useradd -r -s /bin/false -C "isle Daemon" isle
```
If your distro uses systemd, download [the latest systemd service
file][serviceFile] and place it in `/etc/systemd/system`. Run `systemctl
daemon-reload` to ensure systemd has seen the new service file.
If your distro uses an init system other than systemd then you will need to
configure that yourself. You can use the systemd service file linked above as a
reference.
[latest]: https://code.betamike.com/micropelago/isle/releases/latest
[serviceFile]: https://code.betamike.com/micropelago/isle/src/branch/main/dist/linux/isle.service
### From Source
If you'd like to build your own `isle` binary from scratch, see the [Building
Isle](./dev/building.md) document.
## Add Users to the `isle` Group (Optional)
If you wish to run isle commands as a user other than root, you can add that
user to the `isle` group:
```
sudo usermod -aG isle username
```
## Start the isle Service
Once installed and bootstrapped you can enable and start the isle service by
doing:
```
sudo systemctl enable --now isle
```
(NOTE If your distro uses an init system other than systemd then you will need
to instead start isle according to that system's requirements.)

View File

@ -1,7 +1,7 @@
# Contributing a Lighthouse
The [nebula][nebula] project provides the VPN component which is used by
cryptic-net. Every nebula network requires at least one (but preferably more)
Isle. Every nebula network requires at least one (but preferably more)
publicly accessible hosts. These hosts are called lighthouses.
Lighthouses do _not_ route traffic between hosts on the VPN. Rather, they
@ -10,7 +10,7 @@ NAT punching through any NATs that hosts might be behind. As such, they are very
lightweight to run, and require no storage resources at all.
If your host machine has a public static IP, or a dynamic public IP with
[dDNS][ddns] set up, then it can contribute a lighthouse for cryptic-net.
[dDNS][ddns] set up, then it can contribute a lighthouse.
[nebula]: https://github.com/slackhq/nebula
[ddns]: https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/
@ -26,21 +26,14 @@ traffic on that port to your host.
Configure your host's firewall to allow all UDP traffic on that port.
## Create daemon.yml
First, if you haven't already, [create a `daemon.yml`
file](../user/creating-a-daemonyml-file.md). This will be used to
configure your `cryptic-net daemon` process with the public address that other
hosts can find your daemon on.
## Edit daemon.yml
Open your `daemon.yml` file in a text editor, and find the `vpn.public_addr`
field. Update that field to reflect your host's IP/DNS name and your chosen UDP
port.
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
`vpn.public_addr` field. Update that field to reflect your host's IP/DNS name
and your chosen UDP port.
## Restart the Daemon
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
With the `daemon.yml` configured, you should restart your `isle daemon`
process. On startup the daemon will add its public address to the global
configuration, which other hosts will pick up on and begin using.

View File

@ -1,19 +1,12 @@
# Contributing Storage
If your host machine can be reasonably sure of being online most, if not all, of
the time, and has 100GB or more of unused drive space you'd like to contribute
to the network, then this document is for you.
This document is for you if your host machine can be reliably be online at all
times and has 1GB or more of unused drive space you'd like to contribute to the
network.
## Create daemon.yml
## Edit `daemon.yml`
First, if you haven't already, [create a `daemon.yml`
file](../user/creating-a-daemonyml-file.md). This will be used to
configure your `cryptic-net daemon` process with the storage locations and
capacities you want to contribute.
## Edit daemon.yml
Open your `daemon.yml` file in a text editor, and find the
Open your `/etc/isle/daemon.yml` file in a text editor, and find the
`storage.allocations` section.
Each allocation in the allocations list describes the space being contributed
@ -23,7 +16,7 @@ one allocation listed.
The comments in the file should be self-explanatory, but ask your admin if you
need any clarification.
Here are an example set of allocations for a host which is contributing space
Here is an example set of allocations for a host which is contributing space
from two separate drives:
```
@ -31,37 +24,44 @@ storage:
allocations:
# 1.2 TB are being shared from drive1
- data_path: /mnt/drive1/cryptic-net/data
meta_path: /mnt/drive1/cryptic-net/meta
- data_path: /mnt/drive1/isle/data
meta_path: /mnt/drive1/isle/meta
capacity: 1200
api_port: 3900
rpc_port: 3901
web_port: 3902
# 100 GB (the minimum) are being shared from drive2
- data_path: /mnt/drive2/cryptic-net/data
meta_path: /mnt/drive2/cryptic-net/meta
# 100 GB are being shared from drive2
- data_path: /mnt/drive2/isle/data
meta_path: /mnt/drive2/isle/meta
capacity: 100
api_port: 3910
rpc_port: 3911
web_port: 3912
```
## Setup Firewall
## Set Up Your Firewall
You will need to configure your hosts's firewall to allow traffic from
cryptic-net IPs on the ports you specified in your allocations.
See the doc on [Firewalls](./firewalls.md), to be sure that your host's firewall
is properly set up for providing storage.
## Restart the Daemon
With the `daemon.yml` configured, you should restart your `cryptic-net daemon`
With the `daemon.yml` configured, you should restart your `isle daemon`
process.
The `isle daemon` will automatically allow the ports used for your
storage allocations in the vpn firewall.
## Removing Allocations
If you later decide to no longer provide storage simply remove the
`storage.allocations` item from your `/etc/isle/daemon.yml` file and restart the
`isle daemon` process.
Once removed, it is advisable to wait some time before removing storage
allocations from other hosts. This ensures that all data which was previously
on this host has had a chance to fully replicate to multiple other hosts.
## Further Reading
cryptic-net uses the [garage][garage] project for its storage system. See the
Isle uses the [garage][garage] project for its storage system. See the
[Managing Garage](managing-garage.md) document for more
information on how to interact directly with the garage instance being run by
cryptic-net.
isle.
[garage]: https://garagehq.deuxfleurs.fr/documentation/quick-start/

View File

@ -0,0 +1,48 @@
# Firewalls
When providing services on your host, whether
[network](./contributing-a-lighthouse.md) or
[storage](./contributing-storage.md), you will need to ensure that your host's
firewall is configured correctly to do so.
To make matters even more confusing, there are actually two firewalls at play:
the host's firewall, and the VPN firewall.
## VPN Firewall
Isle uses the [nebula](https://github.com/slackhq/nebula) project to
provide its VPN layer. Nebula ships with its own [builtin
firewall](https://nebula.defined.net/docs/config/firewall), which only applies
to connections coming in over the virtual network interface which it creates.
This firewall can be manually configured as part of the `/etc/isle/daemon.yml`
file.
Any storage instances which are defined as part of the `daemon.yml` file will
have their network ports automatically added to the VPN firewall by isle.
This means that you only need to configure the VPN firewall if you are hosting
services for your isle network besides storage.
## Host Firewall
The host you are running isle on will almost definitely have a firewall
running, separate from the VPN firewall. If you wish to provide services for
your isle network from your host, you will need to allow their ports in your
host's firewall.
**isle does _not_ automatically configure your host's firewall to any extent!**
One option is to open your host to all traffic from your isle network, and
allow the VPN firewall to be fully responsible for filtering traffic. To do this
on Linux using iptables, for example, you would add something like this to your
iptables configuration:
```
-A INPUT --source <network CIDR> --jump ACCEPT
```
being sure to replace the network CIDR with the one for you network.
If you don't feel comfortable allowing nebula to deal with all packet filtering,
you will need to manually determine and add the ports for each nebula service to
your host's firewall. It is recommended that you manually specify any storage
allocation ports defined in your `daemon.yml` if this is the approach you take.

View File

@ -1,11 +1,11 @@
# Managing Garage
The garage project provides the network storage component for
cryptic-net. If you're reading this document then you would likely benefit
Isle. If you're reading this document then you would likely benefit
greatly from reading the [garage documentation][garage] on their website. It's
extremely well written and concise.
Note that the `cryptic-net daemon` process will handle all setup steps described
Note that the `isle daemon` process will handle all setup steps described
in that documentation, but it's still good to have an understanding of how
garage works and what it can do.
@ -13,12 +13,12 @@ garage works and what it can do.
## Garage Runtime Note
There is an important thing to note regarding how cryptic-net runs garage. As
There is an important thing to note regarding how isle runs garage. As
described in the [Contributing Storage](contributing-storage.md) document, a
single `cryptic-net daemon` process can be configured to provide any number of
single `isle daemon` process can be configured to provide any number of
storage allocations.
For each allocation which is configured, `cryptic-net daemon` will configure and
For each allocation which is configured, `isle daemon` will configure and
run a separate `garage server` instance as a sub-process. Each garage will use
the host's name as its zone in the garage cluster layout, which means that the
cluster will prefer to not replicate the same data within the same host, but may
@ -26,14 +26,14 @@ do so if necessary.
## Garage CLI
Every `cryptic-net` binary contains a full `garage` binary embedded into it.
Every `isle` binary contains a full `garage` binary embedded into it.
This binary can be accessed directly like so:
```
sudo cryptic-net garage cli <subcmd> <args>
sudo isle garage cli <subcmd> <args>
```
Before handing off execution to the `garage` binary, the `cryptic-net` process
Before handing off execution to the `garage` binary, the `isle` process
will automatically set up the RPC host and secret environment variables.
If the host which is running the command has more than one allocation
@ -47,7 +47,7 @@ connected to.
To display the current layout of the garage cluster:
```
sudo cryptic-net garage cli layout show
sudo isle garage cli layout show
```
**(DO NOT CHANGE THE CLUSTER LAYOUT UNLESS YOU KNOW WHAT YOU'RE DOING!)**
@ -55,11 +55,11 @@ sudo cryptic-net garage cli layout show
To create a new bucket:
```
sudo cryptic-net garage cli bucket create new-bucket
sudo isle garage cli bucket create new-bucket
```
To list existing buckets:
```
sudo cryptic-net garage cli bucket list
sudo isle garage cli bucket list
```

View File

@ -1,100 +0,0 @@
# Roadmap
The following are rough outlines of upcoming work on the roadmap, roughly in the
order they will be implemented.
## Main quest
These items are listed more or less in the order they need to be completed, as
they generally depend on the items previous to them.
### Cross Compilation
Currently the only supported OS/CPU is Linux/amd64. This can be expanded
_theoretically_ quite easily, using nix's cross compilation tools. First target
should be OSX/arm64, but windows would also be quite the get.
### Bootstrap
This will be difficult. There's currently no way to bootstrap a new cryptic-net
network from scratch, only the currently existing one is supported. Support for
IPv6 internal CIDR should also be a part of this effort.
### Testing
Once bootstrap is generalized, we'll be able to write some automated tests.
## Side quests
These items aren't necessarily required by the main quest, and aren't dependent
on any other items being completed. They are nice-to-haves that we do want to
eventually complete, but aren't the main focus.
### DNS resolver settings
The daemon should update the resolver settings of the host, so that it
automatically serves DNS queries, unless set to not do so in `daemon.yml`.
### Install sub-command
It would be great to have a `cryptic-net install` sub-command which would
auto-detect the installed operating system and install the daemon automatically.
### Web server + interface
One idea is to have every `cryptic-net daemon` run a webserver as one of its
sub-processes. This server could serve multiple functions:
- Possible public gateway, if the host is configured to be public, into internal
cryptic-net services. This will require some sort of service discovery
mechanism that other hosts in the network can easily hook into via their
`daemon.yml`s. Ideally this mechanism would involve little beyond updating
entries in garage, which the public gateways then pick up on.
One wrinkle here will be TLS certificates. Ideally internal hosts could host
websites publicly via the gateways on their network, using arbitrary domains
that the users own. The gateways will need some way of obtaining certs for
these domains automatically (or as automatically as possible).
- Local interface for the `cryptic-net daemon` process. For example, status and
connectivity information for the local host could be provided via a simple web
interface, which the user can open in their browser. This saves us the effort
of needing to develop UIs for individual OSs. This could also make remotely
debugging hosts easier for admins.
### Mobile app
To start with a simple mobile app which provided connectivity to the network
would be great. Such a mobile app could be based on the existing
[mobile_nebula](https://github.com/DefinedNet/mobile_nebula). The main changes
needed would be:
- Allow importing a `bootstrap.tgz` file, rather than requiring manual setup by
users.
- Set device's DNS settings. There is an [open
PR](https://github.com/DefinedNet/mobile_nebula/pull/18) for android to do
this upstream.
- Rebranding and possibly submitting to Apple app store (bleh).
### Don't run as root
It's currently a pretty hard requirement for `cryptic-net daemon` to run as
root. This is due to:
- nebula's network interface root to be started.
- dnsmasq listening on port 53, generally a protected port.
If we can't figure out how to get these things running from the start as
non-privileged users, we at least need to get cryptic-net to drop priveleges
from root after initial startup.
### Plugins
It would not be difficult to spec out a plugin system using nix commands.
Existing components could be rigged to use this plugin system, and we could then
use the system to add future components which might prove useful. Once the
project is public such a system would be much appreciated I think, as it would
let other groups rig their binaries with all sorts of new functionality.

View File

@ -1,14 +1,20 @@
{
pkgs ? (import ../nix/pkgs.nix).stable,
buildSystem ? builtins.currentSystem,
hostSystem ? buildSystem,
pkgsNix ? (import ../nix/pkgs.nix),
}: pkgs.mkShell {
name = "cryptic-net-build-docs";
buildInputs = [ pkgs.plantuml ];
shellHook = ''
set -e
plantuml -tsvg ./dev/*.plantuml
exit 0
'';
}
}: let
pkgs = pkgsNix.default {
inherit buildSystem hostSystem;
};
in
pkgs.mkShell {
name = "isle-build-docs";
buildInputs = [ pkgs.plantuml ];
shellHook = ''
set -e
plantuml -tsvg ./dev/*.plantuml
exit 0
'';
}

View File

@ -1,32 +0,0 @@
# Creating a daemon.yml File
The `cryptic-net daemon` process has generally sane defaults and does not need
to be configured for most users. However, in some cases it does, so this
document describes how to use the `daemon.yml` file to handle those cases.
## Create daemon.yml
First, create a `daemon.yml` file. You can create a new `daemon.yml` with
default values filled in by doing:
```
cryptic-net daemon --dump-config > /path/to/daemon.yml
```
If you open that file in a text editor you can view all default values that
`cryptic-net daemon` ships with, as well as documentation for all configurable
parameters. Feel free to edit this file as needed.
## Using daemon.yml
With the `daemon.yml` created and configured, you can configure your daemon
process to use it by passing it as the `-c` argument:
```
sudo cryptic-net daemon -c /path/to/daemon.yml
```
If you are an operator then your host should be running its `cryptic-net daemon`
process in systemd (see [Getting Started](getting-started.md) if
not), and you will need to modify the `cryptic-net.service` accordingly.

View File

@ -1,117 +0,0 @@
# Getting Started
This document will guide you through the process of obtaining a cryptic-net
binary and joining the network.
NOTE currently only linux machines with amd64/x86_64 processors are supported.
More OSs and architectures coming soon!
## Obtaining a cryptic-net Binary
Every host can have a binary built for it which has all configuration for that
host embedded directly into it. Such binaries require no extra configuration by
the user to use, and have no dependencies on anything else in the user's system.
The process of obtaining a custom binary for your host is quite simple: ask an
admin of the network you'd like to join to give you one!
Note that if you'd like to join the network on multiple devices, each device
will needs its own binary, so be sure to tell your admin how many you want to
add and their names.
### Obtaining a cryptic-net Binary, the Hard Way
Alternatively, you can build your own binary by running the following from the
project's root:
```
nix-build -A appImage
```
(*NOTE* Dependencies of `cryptic-net` seemingly compile all of musl and rust
from scratch (it's not clear why, blame garage!). If you have not otherwise
configured it, nix might be using a tmpfs as its build directory, and the
capacity of this tmpfs will probably be exceeded by this build. You can change
your build directory to somewhere on-disk by setting the TMPDIR environment
variable for `nix-daemon` (see [this github issue][tmpdir-gh].))
The resulting binary can be found in the `result` directory which is created.
In this case you will need an admin to provide you with a `bootstrap.tgz` for
your host, rather than a custom binary. When running the daemon in the following
steps you will need to provide the `--bootstrap-path` CLI argument to the daemon
process.
[tmpdir-gh]: https://github.com/NixOS/nix/issues/2098#issuecomment-383243838
## Running the Daemon
Once you have a binary, you will need to run the `daemon` sub-command as the
root user. This can most easily be done using the `sudo` command, in a terminal:
```
sudo /path/to/cryptic-net daemon
```
This will start the daemon process, which will keep running until you kill it
with `ctrl-c`.
You can double check that the daemon is running properly by pinging a private IP
from the network in a separate terminal:
```
ping 10.10.0.1
```
If the pings are successful then your daemon is working!
## Installing the Daemon as a Systemd Service
NOTE in the future we will introduce an `install` sub-command which will
automate most of this section.
Rather than running the daemon manually, you can install it as a systemd
service. This way your daemon will automatically start in the background on
startup, and will be restarted if it has any issues.
To do so, create a file at `/etc/systemd/system/cryptic-net.service` with the
following contents:
```
[Unit]
Description=cryptic-net
Requires=network.target
After=network.target
[Service]
Restart=always
RestartSec=1s
User=root
ExecStart=/path/to/cryptic-net daemon
[Install]
WantedBy=multi-user.target
```
Remember to change the `/path/to/cryptic-net` part to the actual absolute path
to your binary!
Once created, perform the following commands in a terminal to enable the
service:
```
sudo systemctl daemon-reload
sudo systemctl enable --now cryptic-net
```
You can check the service's status by doing:
```
sudo systemctl status cryptic-net
```
and you can view its full logs by doing:
```
sudo journalctl -lu cryptic-net
```

View File

@ -0,0 +1,20 @@
# Join a Network
This document guide you through the process of joining an existing network
of isle hosts. If instead you wish to create a new network for others to join
then see the [Creating a New Network][creating-a-new-network] page.
To join an existing network you will need to first obtain a `bootstrap.json`
file. The `bootstrap.json` file contains all information required for your
particular host to join the network, and must be generated and provided to you
by an admin for the network.
Once obtained, you can join the network by doing:
```
isle network join --bootstrap-path /path/to/bootstrap.json
```
After a few moments you will have successfully joined the network!
[creating-a-new-network]: ../admin/creating-a-new-network.md

View File

@ -1,36 +1,35 @@
# Using DNS
Every `cryptic-net daemon` process ships with a DNS server which runs
Every `isle daemon` process ships with a DNS server which runs
automatically. This server will listen on port 53 on the VPN IP of that
particular host.
The server will serve requests for `<hostname>.hosts.cryptic.io` hostnames,
where `<hostname>` is any host's name in the `bootstrap/nebula/hosts` directory.
The returned IP will be the corresponding IP for the host, as listed in the
host's `bootstrap/nebula/hosts` file.
The server will serve requests for `<hostname>.hosts.<domain>` hostnames,
where `<hostname>` is the name of any host in the network, and `<domain`> is the
network's domain name.
If a request for a non `.cryptic.io` hostname is received then the server will
forward the request to a pre-configured public resolver. The set of public
resolvers used can be configured using the
[daemon.yml](creating-a-daemonyml-file.md) file.
If a request for a hostname not within the network's domain is received then the
server will forward the request to a pre-configured public resolver. The set of
public resolvers used can be configured in the `/etc/isle/daemon.yml` file.
This DNS server is an optional feature of cryptic-net, and not required in
general for making use of the network.
This DNS server is an optional feature of Isle, and not required in general for
making use of the network.
## Example
As an example of how to make use of this DNS server, let's say my host's IP on
the network is `10.10.1.1`. In order to configure the host to use the
cryptic-net DNS server for all DNS requests, I could do something like this:
the network is `10.10.1.1`, and my network's domain is `cool.internal`.
In order to configure the host to use the isle DNS server for all DNS
requests, I could do something like this:
```
sudo su
echo "nameserver 10.10.1.1" > /etc/resolv.conf
```
From that point, all DNS requests on my host would hit the cryptic-net DNS
server. If I request `my-host.cryptic.io`, it would respond with the appropriate
private IP.
From that point, all DNS requests on my host would hit the isle DNS
server. If I request `my-host.hosts.cool.internal`, it would respond with the
appropriate private IP.
NOTE that configuration of dns resolvers is very OS-specific, even amongst Linux
distributions, so ensure you know how your resolver configuration works before

7
flake.lock Normal file
View File

@ -0,0 +1,7 @@
{
"nodes": {
"root": {}
},
"root": "root",
"version": 7
}

38
flake.nix Normal file
View File

@ -0,0 +1,38 @@
# TODO this is currently broken, garage is using builtins.currentSystem for some
# reason.
{
description = "isle provides the foundation for an autonomous community cloud infrastructure";
outputs = {
self,
}: let
supportedSystems = (import ./nix/pkgs.nix).supportedSystems;
mkPkg = (buildSystem: hostSystem: let
defaultAttrs = (import ./default.nix) {
inherit hostSystem buildSystem;
revision = if self ? rev then self.rev else "dirty";
releaseName = "flake";
};
in
defaultAttrs.build.appImage
);
pkgsForBuildSystem = (buildSystem: {
default = mkPkg buildSystem buildSystem;
});
in {
packages = (builtins.foldl'
(pkgs: buildSystem:
pkgs // { "${buildSystem}" = pkgsForBuildSystem buildSystem; })
{}
supportedSystems
);
};
}

View File

@ -1,34 +0,0 @@
{
fetchgit,
buildEnv,
minio-client,
}: let
version = "0.8.0-rc1";
src = fetchgit {
name = "garage-v${version}";
url = "https://git.deuxfleurs.fr/Deuxfleurs/garage.git";
rev = "2197753dfdb25944e55c25d911ae6d14b8506c4d";
sha256 = "sha256-Rzwx1/vl3xg5bj4Chxj8VLBZ25zlPawOc+uMl3AHhkw=";
};
in rec {
garage = (import "${src}/default.nix") { git_version = version; };
minioClient = minio-client;
env = buildEnv {
name = "cryptic-net-garage";
paths = [
garage
minioClient
./src
];
};
}

View File

@ -1,24 +0,0 @@
set -e -o pipefail
tmp="$(mktemp -d -t cryptic-net-garage-apply-layout-diff-XXX)"
( trap "rm -rf '$tmp'" EXIT
tar xzf "$_BOOTSTRAP_PATH" -C "$tmp" ./hosts
thisHostName=$(tar xzf "$_BOOTSTRAP_PATH" --to-stdout ./hostname)
thisHostIP=$(cat "$tmp"/hosts/"$thisHostName".yml | yq '.nebula.ip')
firstRPCPort=$(cat "$_DAEMON_YML_PATH" | yq '.storage.allocations[0].rpc_port')
firstPeerID=$(cryptic-net-main garage-peer-keygen -danger -ip "$thisHostIP" -port "$firstRPCPort")
export GARAGE_RPC_HOST="$firstPeerID"@"$thisHostIP":"$firstRPCPort"
export GARAGE_RPC_SECRET=$(tar -xzf "$_BOOTSTRAP_PATH" --to-stdout "./garage/rpc-secret.txt")
garage layout show | cryptic-net-main garage-layout-diff | while read diffLine; do
echo "> $diffLine"
$diffLine
done
)

View File

@ -1,9 +0,0 @@
# go-workspace
This module is used for building all custom go binaries within the cryptic-net
project.
The reason binaries are contained here, and not under the sub-directory for the
sub-process the correspond to like most other code in this project, is that nix
makes it difficult to compose multiple modules defined locally. If nix ever
fixes this we should split this out.

View File

@ -1,18 +0,0 @@
{
buildGoModule,
}: let
build = subPackage: buildGoModule {
pname = "cryptic-net-" + (builtins.baseNameOf subPackage);
version = "unstable";
src = ./src;
vendorSha256 = "sha256-UqMxbu/v/zDR4yJgUYiLs7HuHkvsZc/MJiWgw/8g+xk=";
subPackages = [
subPackage
];
};
in {
crypticNetMain = build "cmd/cryptic-net-main";
}

View File

@ -1,122 +0,0 @@
// Package admin deals with the parsing and creation of admin.tgz files.
package admin
import (
"cryptic-net/garage"
"cryptic-net/nebula"
"cryptic-net/tarutil"
"cryptic-net/yamlutil"
"fmt"
"io"
"io/fs"
"gopkg.in/yaml.v3"
)
const (
nebulaCertsCACertPath = "nebula/certs/ca.crt"
nebulaCertsCAKeyPath = "nebula/certs/ca.key"
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
garageAdminBucketKeyYmlPath = "garage/cryptic-net-admin-bucket-key.yml"
garageRPCSecretPath = "garage/rpc-secret.txt"
)
// Admin is used for accessing all information contained within an admin.tgz.
type Admin struct {
NebulaCACert nebula.CACert
GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials
GarageAdminBucketS3APICredentials garage.S3APICredentials
}
// FromFS loads an Admin instance from the given fs.FS, which presumably
// represents the file structure of an admin.tgz file.
func FromFS(adminFS fs.FS) (Admin, error) {
var a Admin
filesToLoadAsYAML := []struct {
into interface{}
path string
}{
{&a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
{&a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
}
for _, f := range filesToLoadAsYAML {
if err := yamlutil.LoadYamlFSFile(f.into, adminFS, f.path); err != nil {
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
}
filesToLoadAsString := []struct {
into *string
path string
}{
{&a.NebulaCACert.CACert, nebulaCertsCACertPath},
{&a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{&a.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToLoadAsString {
body, err := fs.ReadFile(adminFS, f.path)
if err != nil {
return Admin{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
*f.into = string(body)
}
return a, nil
}
// FromReader reads an admin.tgz file from the given io.Reader.
func FromReader(r io.Reader) (Admin, error) {
fs, err := tarutil.FSFromReader(r)
if err != nil {
return Admin{}, fmt.Errorf("reading admin.tgz: %w", err)
}
return FromFS(fs)
}
// WriteTo writes the Admin as a new admin.tgz to the given io.Writer.
func (a Admin) WriteTo(into io.Writer) error {
w := tarutil.NewTGZWriter(into)
filesToWriteAsYAML := []struct {
value interface{}
path string
}{
{a.GarageGlobalBucketS3APICredentials, garageGlobalBucketKeyYmlPath},
{a.GarageAdminBucketS3APICredentials, garageAdminBucketKeyYmlPath},
}
for _, f := range filesToWriteAsYAML {
b, err := yaml.Marshal(f.value)
if err != nil {
return fmt.Errorf("yaml encoding data for %q: %w", f.path, err)
}
w.WriteFileBytes(f.path, b)
}
filesToWriteAsString := []struct {
value string
path string
}{
{a.NebulaCACert.CACert, nebulaCertsCACertPath},
{a.NebulaCACert.CAKey, nebulaCertsCAKeyPath},
{a.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToWriteAsString {
w.WriteFileBytes(f.path, []byte(f.value))
}
return w.Close()
}

View File

@ -1,191 +0,0 @@
// Package bootstrap deals with the parsing and creation of bootstrap.tgz files.
// It also contains some helpers which rely on bootstrap data.
package bootstrap
import (
"cryptic-net/garage"
"cryptic-net/nebula"
"cryptic-net/tarutil"
"cryptic-net/yamlutil"
"crypto/sha512"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"gopkg.in/yaml.v3"
)
// Paths within the bootstrap FS which for general data.
const (
hostNamePath = "hostname"
)
// Bootstrap is used for accessing all information contained within a
// bootstrap.tgz file.
type Bootstrap struct {
Hosts map[string]Host
HostName string
NebulaHostCert nebula.HostCert
GarageRPCSecret string
GarageGlobalBucketS3APICredentials garage.S3APICredentials
}
// FromFS loads a Boostrap instance from the given fs.FS, which presumably
// represents the file structure of a bootstrap.tgz file.
func FromFS(bootstrapFS fs.FS) (Bootstrap, error) {
var (
b Bootstrap
err error
)
if b.Hosts, err = loadHosts(bootstrapFS); err != nil {
return Bootstrap{}, fmt.Errorf("loading hosts info from fs: %w", err)
}
if err = yamlutil.LoadYamlFSFile(
&b.GarageGlobalBucketS3APICredentials,
bootstrapFS,
garageGlobalBucketKeyYmlPath,
); err != nil {
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", garageGlobalBucketKeyYmlPath, err)
}
filesToLoadAsString := []struct {
into *string
path string
}{
{&b.HostName, hostNamePath},
{&b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{&b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{&b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{&b.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToLoadAsString {
body, err := fs.ReadFile(bootstrapFS, f.path)
if err != nil {
return Bootstrap{}, fmt.Errorf("loading %q from fs: %w", f.path, err)
}
*f.into = string(body)
}
return b, nil
}
// FromReader reads a bootstrap.tgz file from the given io.Reader.
func FromReader(r io.Reader) (Bootstrap, error) {
fs, err := tarutil.FSFromReader(r)
if err != nil {
return Bootstrap{}, fmt.Errorf("reading bootstrap.tgz: %w", err)
}
return FromFS(fs)
}
// FromFile reads a bootstrap.tgz from a file at the given path.
func FromFile(path string) (Bootstrap, error) {
f, err := os.Open(path)
if err != nil {
return Bootstrap{}, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
return FromReader(f)
}
// WriteTo writes the Bootstrap as a new bootstrap.tgz to the given io.Writer.
func (b Bootstrap) WriteTo(into io.Writer) error {
w := tarutil.NewTGZWriter(into)
filesToWriteAsString := []struct {
value string
path string
}{
{b.HostName, hostNamePath},
{b.NebulaHostCert.CACert, nebulaCertsCACertPath},
{b.NebulaHostCert.HostCert, nebulaCertsHostCertPath},
{b.NebulaHostCert.HostKey, nebulaCertsHostKeyPath},
{b.GarageRPCSecret, garageRPCSecretPath},
}
for _, f := range filesToWriteAsString {
w.WriteFileBytes(f.path, []byte(f.value))
}
garageGlobalBucketKeyB, err := yaml.Marshal(b.GarageGlobalBucketS3APICredentials)
if err != nil {
return fmt.Errorf("yaml encoding garage global bucket creds: %w", err)
}
w.WriteFileBytes(garageGlobalBucketKeyYmlPath, garageGlobalBucketKeyB)
for _, host := range b.Hosts {
hostB, err := yaml.Marshal(host)
if err != nil {
return fmt.Errorf("yaml encoding host %#v: %w", host, err)
}
path := filepath.Join(hostsDirPath, host.Name+".yml")
w.WriteFileBytes(path, hostB)
}
return w.Close()
}
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
// HostName isn't found in the Hosts map.
func (b Bootstrap) ThisHost() Host {
host, ok := b.Hosts[b.HostName]
if !ok {
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.HostName))
}
return host
}
// Hash returns a deterministic hash of the given hosts map.
func HostsHash(hostsMap map[string]Host) ([]byte, error) {
hosts := make([]Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
h := sha512.New()
if err := yaml.NewEncoder(h).Encode(hosts); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
// WithHosts returns a copy of the Bootstrap with the given set of Hosts applied
// to it. It will _not_ overwrite the Host for _this_ host, however.
func (b Bootstrap) WithHosts(hosts map[string]Host) Bootstrap {
hostsCopy := make(map[string]Host, len(hosts))
for name, host := range hosts {
hostsCopy[name] = host
}
hostsCopy[b.HostName] = b.ThisHost()
b.Hosts = hostsCopy
return b
}

View File

@ -1,86 +0,0 @@
package bootstrap
import (
"cryptic-net/garage"
"fmt"
)
// Paths within the bootstrap FS related to garage.
const (
garageGlobalBucketKeyYmlPath = "garage/cryptic-net-global-bucket-key.yml"
garageRPCSecretPath = "garage/rpc-secret.txt"
)
// GaragePeers returns a Peer for each known garage instance in the network.
func (b Bootstrap) GaragePeers() []garage.Peer {
var peers []garage.Peer
for _, host := range b.Hosts {
if host.Garage == nil {
continue
}
for _, instance := range host.Garage.Instances {
peer := garage.Peer{
IP: host.Nebula.IP,
RPCPort: instance.RPCPort,
S3APIPort: instance.S3APIPort,
}
peers = append(peers, peer)
}
}
return peers
}
// GarageRPCPeerAddrs returns the full RPC peer address for each known garage
// instance in the network.
func (b Bootstrap) GarageRPCPeerAddrs() []string {
var addrs []string
for _, peer := range b.GaragePeers() {
addrs = append(addrs, peer.RPCPeerAddr())
}
return addrs
}
// ChooseGaragePeer returns a Peer for a garage instance from the network. It
// will prefer a garage instance on this particular host, if there is one, but
// will otherwise return a random endpoint.
func (b Bootstrap) ChooseGaragePeer() garage.Peer {
thisHost := b.ThisHost()
if thisHost.Garage != nil && len(thisHost.Garage.Instances) > 0 {
inst := thisHost.Garage.Instances[0]
return garage.Peer{
IP: thisHost.Nebula.IP,
RPCPort: inst.RPCPort,
S3APIPort: inst.S3APIPort,
}
}
for _, peer := range b.GaragePeers() {
return peer
}
panic("no garage instances configured")
}
// GlobalBucketS3APIClient returns an S3 client pre-configured with access to
// the global bucket.
func (b Bootstrap) GlobalBucketS3APIClient() (garage.S3APIClient, error) {
addr := b.ChooseGaragePeer().S3APIAddr()
creds := b.GarageGlobalBucketS3APICredentials
client, err := garage.NewS3APIClient(addr, creds)
if err != nil {
return nil, fmt.Errorf("connecting to garage S3 API At %q: %w", addr, err)
}
return client, err
}

View File

@ -1,111 +0,0 @@
package bootstrap
import (
"bytes"
"context"
"cryptic-net/garage"
"fmt"
"log"
"path/filepath"
"github.com/minio/minio-go/v7"
"gopkg.in/yaml.v3"
)
// Paths within garage's global bucket
const (
garageGlobalBucketBootstrapHostsDirPath = "bootstrap/hosts"
)
// PutGarageBoostrapHost places the <hostname>.yml file for the given host into
// garage so that other hosts are able to see relevant configuration for it.
//
// The given client should be for the global bucket.
func PutGarageBoostrapHost(
ctx context.Context, client garage.S3APIClient, host Host,
) error {
buf := new(bytes.Buffer)
if err := yaml.NewEncoder(buf).Encode(host); err != nil {
log.Fatalf("yaml encoding host data: %v", err)
}
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, host.Name+".yml")
_, err := client.PutObject(
ctx, garage.GlobalBucket, filePath, buf, int64(buf.Len()),
minio.PutObjectOptions{},
)
if err != nil {
return fmt.Errorf("writing to %q in global bucket: %w", filePath, err)
}
return nil
}
// RemoveGarageBootstrapHost removes the <hostname>.yml for the given host from
// garage.
//
// The given client should be for the global bucket.
func RemoveGarageBootstrapHost(
ctx context.Context, client garage.S3APIClient, hostName string,
) error {
filePath := filepath.Join(garageGlobalBucketBootstrapHostsDirPath, hostName+".yml")
return client.RemoveObject(
ctx, garage.GlobalBucket, filePath,
minio.RemoveObjectOptions{},
)
}
// GetGarageBootstrapHosts loads the <hostname>.yml file for all hosts stored in
// garage.
//
// The given client should be for the global bucket.
func GetGarageBootstrapHosts(
ctx context.Context, client garage.S3APIClient,
) (
map[string]Host, error,
) {
hosts := map[string]Host{}
objInfoCh := client.ListObjects(
ctx, garage.GlobalBucket,
minio.ListObjectsOptions{
Prefix: garageGlobalBucketBootstrapHostsDirPath,
Recursive: true,
},
)
for objInfo := range objInfoCh {
if objInfo.Err != nil {
return nil, fmt.Errorf("listing objects: %w", objInfo.Err)
}
obj, err := client.GetObject(
ctx, garage.GlobalBucket, objInfo.Key, minio.GetObjectOptions{},
)
if err != nil {
return nil, fmt.Errorf("retrieving object %q: %w", objInfo.Key, err)
}
var host Host
err = yaml.NewDecoder(obj).Decode(&host)
obj.Close()
if err != nil {
return nil, fmt.Errorf("yaml decoding object %q: %w", objInfo.Key, err)
}
hosts[host.Name] = host
}
return hosts, nil
}

View File

@ -1,82 +0,0 @@
package bootstrap
import (
"fmt"
"io/fs"
"path/filepath"
"strings"
"gopkg.in/yaml.v3"
)
const (
hostsDirPath = "hosts"
)
// NebulaHost describes the nebula configuration of a Host which is relevant for
// other hosts to know.
type NebulaHost struct {
IP string `yaml:"ip"`
PublicAddr string `yaml:"public_addr,omitempty"`
}
// GarageHost describes a single garage instance in the GarageHost.
type GarageHostInstance struct {
RPCPort int `yaml:"rpc_port"`
S3APIPort int `yaml:"s3_api_port"`
WebPort int `yaml:"web_port"`
}
// GarageHost describes the garage configuration of a Host which is relevant for
// other hosts to know.
type GarageHost struct {
Instances []GarageHostInstance `yaml:"instances"`
}
// Host consolidates all information about a single host from the bootstrap
// file.
type Host struct {
Name string `yaml:"name"`
Nebula NebulaHost `yaml:"nebula"`
Garage *GarageHost `yaml:"garage,omitempty"`
}
func loadHosts(bootstrapFS fs.FS) (map[string]Host, error) {
hosts := map[string]Host{}
readAsYaml := func(into interface{}, path string) error {
b, err := fs.ReadFile(bootstrapFS, path)
if err != nil {
return fmt.Errorf("reading file from fs: %w", err)
}
return yaml.Unmarshal(b, into)
}
globPath := filepath.Join(hostsDirPath, "*.yml")
hostPaths, err := fs.Glob(bootstrapFS, globPath)
if err != nil {
return nil, fmt.Errorf("listing host files at %q in fs: %w", globPath, err)
}
for _, hostPath := range hostPaths {
hostName := filepath.Base(hostPath)
hostName = strings.TrimSuffix(hostName, filepath.Ext(hostName))
var host Host
if err := readAsYaml(&host, hostPath); err != nil {
return nil, fmt.Errorf("reading %q as yaml: %w", hostPath, err)
}
hosts[hostName] = host
}
if len(hosts) == 0 {
return nil, fmt.Errorf("failed to load any hosts from fs")
}
return hosts, nil
}

View File

@ -1,8 +0,0 @@
package bootstrap
// Paths within the bootstrap FS related to nebula.
const (
nebulaCertsCACertPath = "nebula/certs/ca.crt"
nebulaCertsHostCertPath = "nebula/certs/host.crt"
nebulaCertsHostKeyPath = "nebula/certs/host.key"
)

View File

@ -1,81 +0,0 @@
package main
//
// This binary acts as a wrapper around other programs which would otherwise
// form their own binaries. We do this for two reasons:
//
// * Nix makes it difficult to determine which individuals binaries need to be
// rebuilt upon changes, so it rebuilds all of them no matter what changed. This
// makes development slow. By wrapping everything in a sinble binary we only
// ever have to build that binary.
//
// * If we have N binaries total, then we have N copies of the go runtime in our
// final AppImage. By bundling the binaries into a single one we can reduce the
// number go runtime copies to 1.
//
import (
"cryptic-net/cmd/entrypoint"
garage_entrypoint "cryptic-net/cmd/garage-entrypoint"
garage_layout_diff "cryptic-net/cmd/garage-layout-diff"
garage_peer_keygen "cryptic-net/cmd/garage-peer-keygen"
nebula_entrypoint "cryptic-net/cmd/nebula-entrypoint"
update_global_bucket "cryptic-net/cmd/update-global-bucket"
"fmt"
"os"
)
type mainFn struct {
name string
fn func()
}
var mainFns = []mainFn{
{"entrypoint", entrypoint.Main},
{"garage-entrypoint", garage_entrypoint.Main},
{"garage-layout-diff", garage_layout_diff.Main},
{"garage-peer-keygen", garage_peer_keygen.Main},
{"nebula-entrypoint", nebula_entrypoint.Main},
{"update-global-bucket", update_global_bucket.Main},
}
var mainFnsMap = func() map[string]mainFn {
m := map[string]mainFn{}
for _, mainFn := range mainFns {
m[mainFn.name] = mainFn
}
return m
}()
func usage() {
fmt.Fprintf(os.Stderr, "USAGE: %s <cmd>\n\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Commands:\n\n")
for _, mainFn := range mainFns {
fmt.Fprintf(os.Stderr, "%s\n", mainFn.name)
}
os.Stderr.Sync()
os.Exit(1)
}
func main() {
if len(os.Args) < 2 {
usage()
}
mainFn, ok := mainFnsMap[os.Args[1]]
if !ok {
usage()
}
// remove os.Args[1] from the arg list, so that other commands which consume
// args don't get confused
os.Args = append(os.Args[:1], os.Args[2:]...)
mainFn.fn()
}

View File

@ -1,423 +0,0 @@
package entrypoint
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"os"
"path/filepath"
"strconv"
"sync"
"time"
crypticnet "cryptic-net"
"cryptic-net/bootstrap"
"cryptic-net/garage"
"cryptic-net/yamlutil"
"github.com/cryptic-io/pmux/pmuxlib"
"github.com/imdario/mergo"
"gopkg.in/yaml.v3"
)
// The daemon sub-command deals with starting an actual cryptic-net daemon
// process, which is required to be running for most other cryptic-net
// functionality. The sub-command does the following:
//
// * Creates and locks the runtime directory.
//
// * Creates the data directory and copies the appdir bootstrap file into there,
// if it's not already there.
//
// * Merges the user-provided daemon.yml file with the default, and writes the
// result to the runtime dir.
//
// * Merges daemon.yml configuration into the bootstrap configuration, and
// rewrites the bootstrap file.
//
// * Sets up environment variables that all other sub-processes then use, based
// on the runtime dir.
//
// * Dynamically creates the root pmux config and runs pmux.
//
// * (On exit) cleans up the runtime directory.
func writeDaemonYml(userDaemonYmlPath, builtinDaemonYmlPath, runtimeDirPath string) error {
var fullDaemonYml map[string]interface{}
if err := yamlutil.LoadYamlFile(&fullDaemonYml, builtinDaemonYmlPath); err != nil {
return fmt.Errorf("parsing builtin daemon.yml file: %w", err)
}
if userDaemonYmlPath != "" {
var daemonYml map[string]interface{}
if err := yamlutil.LoadYamlFile(&daemonYml, userDaemonYmlPath); err != nil {
return fmt.Errorf("parsing %q: %w", userDaemonYmlPath, err)
}
err := mergo.Merge(&fullDaemonYml, daemonYml, mergo.WithOverride)
if err != nil {
return fmt.Errorf("merging contents of file %q: %w", userDaemonYmlPath, err)
}
}
fullDaemonYmlB, err := yaml.Marshal(fullDaemonYml)
if err != nil {
return fmt.Errorf("yaml marshaling daemon config: %w", err)
}
daemonYmlPath := filepath.Join(runtimeDirPath, "daemon.yml")
if err := ioutil.WriteFile(daemonYmlPath, fullDaemonYmlB, 0400); err != nil {
return fmt.Errorf("writing daemon.yml file to %q: %w", daemonYmlPath, err)
}
return nil
}
func copyBootstrapToDataDir(env *crypticnet.Env, r io.Reader) error {
path := env.DataDirBootstrapPath()
dirPath := filepath.Dir(path)
if err := os.MkdirAll(dirPath, 0700); err != nil {
return fmt.Errorf("creating directory %q: %w", dirPath, err)
}
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating file %q: %w", path, err)
}
_, err = io.Copy(f, r)
f.Close()
if err != nil {
return fmt.Errorf("copying bootstrap file to %q: %w", path, err)
}
if err := env.LoadBootstrap(path); err != nil {
return fmt.Errorf("loading bootstrap from %q: %w", path, err)
}
return nil
}
// creates a new bootstrap file using available information from the network. If
// the new bootstrap file is different than the existing one, the existing one
// is overwritten, ReloadBootstrap is called on env, true is returned.
func reloadBootstrap(env *crypticnet.Env, s3Client garage.S3APIClient) (bool, error) {
newHosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, s3Client)
if err != nil {
return false, fmt.Errorf("getting hosts from garage: %w", err)
}
newHostsHash, err := bootstrap.HostsHash(newHosts)
if err != nil {
return false, fmt.Errorf("calculating hash of new hosts: %w", err)
}
currHostsHash, err := bootstrap.HostsHash(env.Bootstrap.Hosts)
if err != nil {
return false, fmt.Errorf("calculating hash of current hosts: %w", err)
}
if bytes.Equal(newHostsHash, currHostsHash) {
return false, nil
}
buf := new(bytes.Buffer)
if err := env.Bootstrap.WithHosts(newHosts).WriteTo(buf); err != nil {
return false, fmt.Errorf("writing new bootstrap file to buffer: %w", err)
}
if err := copyBootstrapToDataDir(env, buf); err != nil {
return false, fmt.Errorf("copying new bootstrap file to data dir: %w", err)
}
return true, nil
}
// runs a single pmux process ofor daemon, returning only once the env.Context
// has been canceled or bootstrap info has been changed. This will always block
// until the spawned pmux has returned.
func runDaemonPmuxOnce(env *crypticnet.Env, s3Client garage.S3APIClient) error {
thisHost := env.Bootstrap.ThisHost()
thisDaemon := env.ThisDaemon()
fmt.Fprintf(os.Stderr, "host name is %q, ip is %q\n", thisHost.Name, thisHost.Nebula.IP)
pmuxProcConfigs := []pmuxlib.ProcessConfig{
{
Name: "nebula",
Cmd: "cryptic-net-main",
Args: []string{
"nebula-entrypoint",
},
},
{
Name: "dnsmasq",
Cmd: "bash",
Args: []string{
"wait-for-ip",
thisHost.Nebula.IP,
"dnsmasq-entrypoint",
},
},
}
{
var args []string
if allocs := thisDaemon.Storage.Allocations; len(allocs) > 0 {
for _, alloc := range allocs {
args = append(
args,
"wait-for",
net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
"--",
)
}
} else {
args = []string{
"wait-for-ip",
thisHost.Nebula.IP,
}
}
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: "update-global-bucket",
Cmd: "bash",
Args: append(args, "update-global-bucket"),
NoRestartOn: []int{0},
})
}
if len(thisDaemon.Storage.Allocations) > 0 {
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: "garage",
Cmd: "bash",
Args: []string{
"wait-for-ip",
thisHost.Nebula.IP,
"cryptic-net-main", "garage-entrypoint",
},
// garage can take a while to clean up
SigKillWait: (1 * time.Minute) + (10 * time.Second),
})
}
pmuxConfig := pmuxlib.Config{Processes: pmuxProcConfigs}
doneCh := env.Context.Done()
var wg sync.WaitGroup
defer wg.Wait()
ctx, cancel := context.WithCancel(env.Context)
defer cancel()
wg.Add(1)
go func() {
defer wg.Done()
pmuxlib.Run(ctx, pmuxConfig)
}()
ticker := time.NewTicker(3 * time.Minute)
defer ticker.Stop()
for {
select {
case <-doneCh:
return env.Context.Err()
case <-ticker.C:
fmt.Fprintln(os.Stderr, "checking for changes to bootstrap")
if changed, err := reloadBootstrap(env, s3Client); err != nil {
return fmt.Errorf("reloading bootstrap: %w", err)
} else if changed {
fmt.Fprintln(os.Stderr, "bootstrap info has changed, restarting all processes")
return nil
}
}
}
}
var subCmdDaemon = subCmd{
name: "daemon",
descr: "Runs the cryptic-net daemon (Default if no sub-command given)",
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
daemonYmlPath := flags.StringP(
"config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.",
)
dumpConfig := flags.Bool(
"dump-config", false,
"Write the default configuration file to stdout and exit.",
)
bootstrapPath := flags.StringP(
"bootstrap-path", "b", "",
`Path to a bootstrap.tgz file. This only needs to be provided the first time the daemon is started, after that it is ignored. If the cryptic-net binary has a bootstrap built into it then this argument is always optional.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
env := subCmdCtx.env
s3Client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
appDirPath := env.AppDirPath
builtinDaemonYmlPath := filepath.Join(appDirPath, "etc", "daemon.yml")
if *dumpConfig {
builtinDaemonYml, err := os.ReadFile(builtinDaemonYmlPath)
if err != nil {
return fmt.Errorf("reading default daemon.yml at %q: %w", builtinDaemonYmlPath, err)
}
if _, err := os.Stdout.Write(builtinDaemonYml); err != nil {
return fmt.Errorf("writing default daemon.yml to stdout: %w", err)
}
return nil
}
runtimeDirPath := env.RuntimeDirPath
fmt.Fprintf(os.Stderr, "will use runtime directory %q for temporary state\n", runtimeDirPath)
if err := os.MkdirAll(runtimeDirPath, 0700); err != nil {
return fmt.Errorf("creating directory %q: %w", runtimeDirPath, err)
} else if err := crypticnet.NewProcLock(runtimeDirPath).WriteLock(); err != nil {
return err
}
// do not defer the cleaning of the runtime directory until the lock has
// been obtained, otherwise we might delete the directory out from under
// the feet of an already running daemon
defer func() {
fmt.Fprintf(os.Stderr, "cleaning up runtime directory %q\n", runtimeDirPath)
if err := os.RemoveAll(runtimeDirPath); err != nil {
fmt.Fprintf(os.Stderr, "error removing temporary directory %q: %v", runtimeDirPath, err)
}
}()
// If the bootstrap file is not being stored in the data dir, move it
// there and reload the bootstrap info
if env.BootstrapPath != env.DataDirBootstrapPath() {
path := env.BootstrapPath
// If there's no BootstrapPath then no bootstrap file could be
// found. In this case we require the user to provide one on the
// command-line.
if path == "" {
if *bootstrapPath == "" {
return errors.New("No bootstrap.tgz file could be found, and one is not provided with --bootstrap-path")
}
path = *bootstrapPath
}
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("opening file %q: %w", env.BootstrapPath, err)
}
err = copyBootstrapToDataDir(env, f)
f.Close()
if err != nil {
return fmt.Errorf("copying bootstrap file from %q: %w", path, err)
}
}
if err := writeDaemonYml(*daemonYmlPath, builtinDaemonYmlPath, runtimeDirPath); err != nil {
return fmt.Errorf("generating daemon.yml file: %w", err)
}
{
// we update this Host's data using whatever configuration has been
// provided by daemon.yml. This way the daemon has the most
// up-to-date possible bootstrap. This updated bootstrap will later
// get updated in garage using update-global-bucket, so other hosts
// will see it as well.
// ThisDaemon can only be called after writeDaemonYml.
daemon := env.ThisDaemon()
host := env.Bootstrap.ThisHost()
host.Nebula.PublicAddr = daemon.VPN.PublicAddr
host.Garage = nil
if allocs := daemon.Storage.Allocations; len(allocs) > 0 {
host.Garage = new(bootstrap.GarageHost)
for _, alloc := range allocs {
host.Garage.Instances = append(host.Garage.Instances, bootstrap.GarageHostInstance{
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
WebPort: alloc.WebPort,
})
}
}
env.Bootstrap.Hosts[host.Name] = host
buf := new(bytes.Buffer)
if err := env.Bootstrap.WithHosts(env.Bootstrap.Hosts).WriteTo(buf); err != nil {
return fmt.Errorf("writing new bootstrap file to buffer: %w", err)
}
if err := copyBootstrapToDataDir(env, buf); err != nil {
return fmt.Errorf("copying new bootstrap file to data dir: %w", err)
}
}
for key, val := range env.ToMap() {
if err := os.Setenv(key, val); err != nil {
return fmt.Errorf("failed to set %q to %q: %w", key, val, err)
}
}
for {
if err := runDaemonPmuxOnce(env, s3Client); errors.Is(err, context.Canceled) {
return nil
} else if err != nil {
return fmt.Errorf("running pmux for daemon: %w", err)
}
}
},
}

View File

@ -1,118 +0,0 @@
package entrypoint
import (
"fmt"
"os"
"syscall"
)
var subCmdGarageMC = subCmd{
name: "mc",
descr: "Runs the mc (minio-client) binary. The cryptic-net garage can be accessed under the `garage` alias",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(true)
keyID := flags.StringP(
"key-id", "i", "",
"Optional key ID to use, defaults to that of the shared cryptic-net-global key",
)
keySecret := flags.StringP(
"key-secret", "s", "",
"Optional key secret to use, defaults to that of the shared cryptic-net-global key",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
env := subCmdCtx.env
s3APIAddr := env.Bootstrap.ChooseGaragePeer().S3APIAddr()
if *keyID == "" || *keySecret == "" {
if *keyID == "" {
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.ID
}
if *keySecret == "" {
*keyID = env.Bootstrap.GarageGlobalBucketS3APICredentials.Secret
}
}
args := flags.Args()
if i := flags.ArgsLenAtDash(); i >= 0 {
args = args[i:]
}
args = append([]string{"mc"}, args...)
var (
binPath = env.BinPath("mc")
cliEnv = append(
os.Environ(),
fmt.Sprintf(
"MC_HOST_garage=http://%s:%s@%s",
*keyID, *keySecret, s3APIAddr,
),
// The garage docs say this is necessary, though nothing bad
// seems to happen if we leave it out *shrug*
"MC_REGION=garage",
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarageCLI = subCmd{
name: "cli",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
env := subCmdCtx.env
var (
binPath = env.BinPath("garage")
args = append([]string{"garage"}, subCmdCtx.args...)
cliEnv = append(
os.Environ(),
"GARAGE_RPC_HOST="+env.Bootstrap.ChooseGaragePeer().RPCAddr(),
"GARAGE_RPC_SECRET="+env.Bootstrap.GarageRPCSecret,
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarage = subCmd{
name: "garage",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running cryptic-net daemon",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdGarageCLI,
subCmdGarageMC,
)
},
}

View File

@ -1,246 +0,0 @@
package entrypoint
import (
"cryptic-net/admin"
"cryptic-net/bootstrap"
"cryptic-net/nebula"
"errors"
"fmt"
"net"
"os"
"regexp"
"sort"
"gopkg.in/yaml.v3"
)
var hostNameRegexp = regexp.MustCompile(`^[a-z][a-z0-9\-]*$`)
func validateHostName(name string) error {
if !hostNameRegexp.MatchString(name) {
return errors.New("a host's name must start with a letter and only contain letters, numbers, and dashes")
}
return nil
}
var subCmdHostsAdd = subCmd{
name: "add",
descr: "Adds a host to the network",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the new host",
)
ip := flags.StringP(
"ip", "i", "",
"IP of the new host",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *ip == "" {
return errors.New("--name and --ip are required")
}
if err := validateHostName(*name); err != nil {
return fmt.Errorf("invalid hostname %q: %w", *name, err)
}
if net.ParseIP(*ip) == nil {
return fmt.Errorf("invalid ip %q", *ip)
}
// TODO validate that the IP is in the correct CIDR
env := subCmdCtx.env
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
host := bootstrap.Host{
Name: *name,
Nebula: bootstrap.NebulaHost{
IP: *ip,
},
}
return bootstrap.PutGarageBoostrapHost(env.Context, client, host)
},
}
var subCmdHostsList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
env := subCmdCtx.env
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
hostsMap, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
if err != nil {
return fmt.Errorf("retrieving hosts from garage: %w", err)
}
hosts := make([]bootstrap.Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
return yaml.NewEncoder(os.Stdout).Encode(hosts)
},
}
var subCmdHostsDelete = subCmd{
name: "delete",
descr: "Deletes a host from the network",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the host to delete",
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" {
return errors.New("--name is required")
}
env := subCmdCtx.env
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
return bootstrap.RemoveGarageBootstrapHost(env.Context, client, *name)
},
}
func readAdmin(path string) (admin.Admin, error) {
if path == "-" {
adm, err := admin.FromReader(os.Stdin)
if err != nil {
return admin.Admin{}, fmt.Errorf("parsing admin.tgz from stdin: %w", err)
}
return adm, nil
}
f, err := os.Open(path)
if err != nil {
return admin.Admin{}, fmt.Errorf("opening file: %w", err)
}
defer f.Close()
return admin.FromReader(f)
}
var subCmdHostsMakeBootstrap = subCmd{
name: "make-bootstrap",
descr: "Creates a new bootstrap.tgz file for a particular host and writes it to stdout",
checkLock: true,
do: func(subCmdCtx subCmdCtx) error {
flags := subCmdCtx.flagSet(false)
name := flags.StringP(
"name", "n", "",
"Name of the host to generate bootstrap.tgz for",
)
adminPath := flags.StringP(
"admin-path", "a", "",
`Path to admin.tgz file. If the given path is "-" then stdin is used.`,
)
if err := flags.Parse(subCmdCtx.args); err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" || *adminPath == "" {
return errors.New("--name and --admin-path are required")
}
env := subCmdCtx.env
adm, err := readAdmin(*adminPath)
if err != nil {
return fmt.Errorf("reading admin.tgz with --admin-path of %q: %w", *adminPath, err)
}
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
return fmt.Errorf("creating client for global bucket: %w", err)
}
// NOTE this isn't _technically_ required, but if the `hosts add`
// command for this host has been run recently then it might not have
// made it into the bootstrap file yet, and so won't be in
// `env.Bootstrap`.
hosts, err := bootstrap.GetGarageBootstrapHosts(env.Context, client)
if err != nil {
return fmt.Errorf("retrieving host info from garage: %w", err)
}
host, ok := hosts[*name]
if !ok {
return fmt.Errorf("couldn't find host into for %q in garage, has `cryptic-net hosts add` been run yet?", *name)
}
nebulaHostCert, err := nebula.NewHostCert(adm.NebulaCACert, host.Name, host.Nebula.IP)
if err != nil {
return fmt.Errorf("creating new nebula host key/cert: %w", err)
}
newBootstrap := bootstrap.Bootstrap{
Hosts: hosts,
HostName: *name,
NebulaHostCert: nebulaHostCert,
GarageRPCSecret: adm.GarageRPCSecret,
GarageGlobalBucketS3APICredentials: adm.GarageGlobalBucketS3APICredentials,
}
return newBootstrap.WriteTo(os.Stdout)
},
}
var subCmdHosts = subCmd{
name: "hosts",
descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(subCmdCtx subCmdCtx) error {
return subCmdCtx.doSubCmd(
subCmdHostsAdd,
subCmdHostsList,
subCmdHostsDelete,
subCmdHostsMakeBootstrap,
)
},
}

View File

@ -1,36 +0,0 @@
package entrypoint
import (
"fmt"
"os"
crypticnet "cryptic-net"
)
// The purpose of this binary is to act as the entrypoint of the cryptic-net
// process. It processes the command-line arguments which are passed in, and
// then passes execution along to an appropriate binary housed in AppDir/bin
// (usually a bash script, which is more versatile than a go program).
func Main() {
env, err := crypticnet.NewEnv(true)
if err != nil {
panic(fmt.Sprintf("loading environment: %v", err))
}
err = subCmdCtx{
args: os.Args[1:],
env: env,
}.doSubCmd(
subCmdDaemon,
subCmdHosts,
subCmdGarage,
subCmdVersion,
)
if err != nil {
panic(err)
}
}

View File

@ -1,121 +0,0 @@
package entrypoint
import (
crypticnet "cryptic-net"
"fmt"
"os"
"strings"
"github.com/spf13/pflag"
)
// subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct {
subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
env *crypticnet.Env
}
type subCmd struct {
name string
descr string
checkLock bool
do func(subCmdCtx) error
}
func (ctx subCmdCtx) usagePrefix() string {
subCmdNamesStr := strings.Join(ctx.subCmdNames, " ")
if subCmdNamesStr != "" {
subCmdNamesStr += " "
}
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
}
func (ctx subCmdCtx) flagSet(withPassthrough bool) *pflag.FlagSet {
flags := pflag.NewFlagSet(ctx.subCmd.name, pflag.ExitOnError)
flags.Usage = func() {
var passthroughStr string
if withPassthrough {
passthroughStr = " [--] [args...]"
}
fmt.Fprintf(
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
ctx.usagePrefix(), ctx.subCmd.name, passthroughStr,
)
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(ctx.subCmd.name))
fmt.Fprintln(os.Stderr, flags.FlagUsages())
os.Stderr.Sync()
os.Exit(2)
}
return flags
}
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
printUsageExit := func(subCmdName string) {
fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName)
fmt.Fprintf(
os.Stderr,
"%s<subCmd> [-h|--help] [sub-command flags...]\n",
ctx.usagePrefix(),
)
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
for _, subCmd := range subCmds {
fmt.Fprintf(os.Stderr, " %s\t%s\n", subCmd.name, subCmd.descr)
}
fmt.Fprintf(os.Stderr, "\n")
os.Stderr.Sync()
os.Exit(2)
}
args := ctx.args
if len(args) == 0 {
printUsageExit("")
}
subCmdsMap := map[string]subCmd{}
for _, subCmd := range subCmds {
subCmdsMap[subCmd.name] = subCmd
}
subCmdName, args := args[0], args[1:]
subCmd, ok := subCmdsMap[subCmdName]
if !ok {
printUsageExit(subCmdName)
}
if subCmd.checkLock {
err := crypticnet.NewProcLock(ctx.env.RuntimeDirPath).AssertLock()
if err != nil {
return fmt.Errorf("checking lock file: %w", err)
}
}
err := subCmd.do(subCmdCtx{
subCmd: subCmd,
args: args,
subCmdNames: append(ctx.subCmdNames, subCmdName),
env: ctx.env,
})
if err != nil {
return err
}
return nil
}

View File

@ -1,129 +0,0 @@
package garage_entrypoint
import (
"fmt"
"log"
"net"
"os"
"path/filepath"
"strconv"
"time"
crypticnet "cryptic-net"
"cryptic-net/garage"
"github.com/cryptic-io/pmux/pmuxlib"
)
func writeChildConf(
env *crypticnet.Env,
alloc crypticnet.DaemonYmlStorageAllocation,
) (string, error) {
if err := os.MkdirAll(alloc.MetaPath, 0750); err != nil {
return "", fmt.Errorf("making directory %q: %w", alloc.MetaPath, err)
}
thisHost := env.Bootstrap.ThisHost()
peer := garage.Peer{
IP: thisHost.Nebula.IP,
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
}
pubKey, privKey := peer.RPCPeerKey()
nodeKeyPath := filepath.Join(alloc.MetaPath, "node_key")
nodeKeyPubPath := filepath.Join(alloc.MetaPath, "node_keypub")
if err := os.WriteFile(nodeKeyPath, privKey, 0400); err != nil {
return "", fmt.Errorf("writing private key to %q: %w", nodeKeyPath, err)
} else if err := os.WriteFile(nodeKeyPubPath, pubKey, 0440); err != nil {
return "", fmt.Errorf("writing public key to %q: %w", nodeKeyPubPath, err)
}
garageTomlPath := filepath.Join(
env.RuntimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
)
err := garage.WriteGarageTomlFile(garageTomlPath, garage.GarageTomlData{
MetaPath: alloc.MetaPath,
DataPath: alloc.DataPath,
RPCSecret: env.Bootstrap.GarageRPCSecret,
RPCAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
APIAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.S3APIPort)),
WebAddr: net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.WebPort)),
BootstrapPeers: env.Bootstrap.GarageRPCPeerAddrs(),
})
if err != nil {
return "", fmt.Errorf("creating garage.toml file at %q: %w", garageTomlPath, err)
}
return garageTomlPath, nil
}
func waitForArgs(env *crypticnet.Env, bin string, binArgs ...string) []string {
thisHost := env.Bootstrap.ThisHost()
var args []string
for _, alloc := range env.ThisDaemon().Storage.Allocations {
args = append(
args,
"wait-for",
net.JoinHostPort(thisHost.Nebula.IP, strconv.Itoa(alloc.RPCPort)),
"--",
)
}
args = append(args, bin)
args = append(args, binArgs...)
return args
}
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
log.Fatalf("reading envvars: %v", err)
}
var pmuxProcConfigs []pmuxlib.ProcessConfig
for _, alloc := range env.ThisDaemon().Storage.Allocations {
childConfPath, err := writeChildConf(env, alloc)
if err != nil {
log.Fatalf("writing child config file for alloc %+v: %v", alloc, err)
}
log.Printf("wrote config file %q", childConfPath)
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: fmt.Sprintf("garage-%d", alloc.RPCPort),
Cmd: "garage",
Args: []string{"-c", childConfPath, "server"},
SigKillWait: 1 * time.Minute,
})
}
pmuxProcConfigs = append(pmuxProcConfigs, pmuxlib.ProcessConfig{
Name: "garage-apply-layout-diff",
Cmd: "bash",
Args: waitForArgs(env, "bash", "garage-apply-layout-diff"),
NoRestartOn: []int{0},
})
pmuxlib.Run(env.Context, pmuxlib.Config{Processes: pmuxProcConfigs})
}

View File

@ -1,256 +0,0 @@
package garage_layout_diff
// This binary accepts the output of `garage layout show` into its stdout, and
// it outputs a newline-delimited set of `garage layout $cmd` strings on
// stdout. The layout commands which are output will, if run, bring the current
// node's layout on the cluster up-to-date with what's in daemon.yml.
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
crypticnet "cryptic-net"
"cryptic-net/garage"
)
type clusterNode struct {
ID string
Zone string
Capacity int
}
type clusterNodes []clusterNode
func (n clusterNodes) get(id string) (clusterNode, bool) {
var ok bool
for _, node := range n {
if len(node.ID) > len(id) {
ok = strings.HasPrefix(node.ID, id)
} else {
ok = strings.HasPrefix(id, node.ID)
}
if ok {
return node, true
}
}
return clusterNode{}, false
}
var currClusterLayoutVersionB = []byte("Current cluster layout version:")
func readCurrNodes(r io.Reader) (clusterNodes, int, error) {
input, err := io.ReadAll(r)
if err != nil {
return nil, 0, fmt.Errorf("reading stdin: %w", err)
}
// NOTE I'm not sure if this check should be turned on or not. It simplifies
// things to turn it off and just say that no one should ever be manually
// messing with the layout, but on the other hand maybe someone might?
//
//if i := bytes.Index(input, []byte("==== STAGED ROLE CHANGES ====")); i >= 0 {
// return nil, 0, errors.New("cluster layout has staged changes already, won't modify")
//}
/* The first section of input will always be something like this:
```
==== CURRENT CLUSTER LAYOUT ====
ID Tags Zone Capacity
AAA ZZZ 1
BBB ZZZ 1
CCC ZZZ 1
Current cluster layout version: N
```
There may be more, depending on if the cluster already has changes staged,
but this will definitely be first. */
i := bytes.Index(input, currClusterLayoutVersionB)
if i < 0 {
return nil, 0, errors.New("no current cluster layout found in input")
}
input, tail := input[:i], input[i:]
var currNodes clusterNodes
for inputBuf := bufio.NewReader(bytes.NewBuffer(input)); ; {
line, err := inputBuf.ReadString('\n')
if errors.Is(err, io.EOF) {
break
} else if err != nil {
return nil, 0, fmt.Errorf("reading input line by line from buffer: %w", err)
}
fields := strings.Fields(line)
if len(fields) < 3 {
continue
}
id := fields[0]
// The ID will always be given ending in this fucked up ellipses
if trimmedID := strings.TrimSuffix(id, "…"); id == trimmedID {
continue
} else {
id = trimmedID
}
zone := fields[1]
capacity, err := strconv.Atoi(fields[2])
if err != nil {
return nil, 0, fmt.Errorf("parsing capacity %q: %w", fields[2], err)
}
currNodes = append(currNodes, clusterNode{
ID: id,
Zone: zone,
Capacity: capacity,
})
}
// parse current cluster version from tail
tail = bytes.TrimPrefix(tail, currClusterLayoutVersionB)
if i := bytes.Index(tail, []byte("\n")); i > 0 {
tail = tail[:i]
}
tail = bytes.TrimSpace(tail)
version, err := strconv.Atoi(string(tail))
if err != nil {
return nil, 0, fmt.Errorf("parsing version string from %q: %w", tail, err)
}
return currNodes, version, nil
}
func readExpNodes(env *crypticnet.Env) clusterNodes {
thisHost := env.Bootstrap.ThisHost()
var expNodes clusterNodes
for _, alloc := range env.ThisDaemon().Storage.Allocations {
peer := garage.Peer{
IP: thisHost.Nebula.IP,
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
}
id := peer.RPCPeerID()
expNodes = append(expNodes, clusterNode{
ID: id,
Zone: env.Bootstrap.HostName,
Capacity: alloc.Capacity / 100,
})
}
return expNodes
}
// NOTE: The id formatting for currNodes and expNodes is different; expNodes has
// fully expanded ids, currNodes are abbreviated.
func diff(currNodes, expNodes clusterNodes) []string {
var lines []string
for _, node := range currNodes {
if _, ok := expNodes.get(node.ID); !ok {
lines = append(
lines,
fmt.Sprintf("garage layout remove %s", node.ID),
)
}
}
for _, expNode := range expNodes {
currNode, ok := currNodes.get(expNode.ID)
currNode.ID = expNode.ID // so that equality checking works
if ok && currNode == expNode {
continue
}
lines = append(
lines,
fmt.Sprintf(
"garage layout assign %s -z %s -c %d",
expNode.ID,
expNode.Zone,
expNode.Capacity,
),
)
}
return lines
}
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
panic(fmt.Errorf("reading environment: %w", err))
}
currNodes, currVersion, err := readCurrNodes(os.Stdin)
if err != nil {
panic(fmt.Errorf("reading current layout from stdin: %w", err))
}
thisCurrNodes := make(clusterNodes, 0, len(currNodes))
for _, node := range currNodes {
if env.Bootstrap.HostName != node.Zone {
continue
}
thisCurrNodes = append(thisCurrNodes, node)
}
expNodes := readExpNodes(env)
lines := diff(thisCurrNodes, expNodes)
if len(lines) == 0 {
return
}
for _, line := range lines {
fmt.Println(line)
}
fmt.Printf("garage layout apply --version %d\n", currVersion+1)
}

View File

@ -1,137 +0,0 @@
package garage_layout_diff
import (
"bytes"
"reflect"
"strconv"
"testing"
)
func TestReadCurrNodes(t *testing.T) {
expNodes := clusterNodes{
{
ID: "AAA",
Zone: "XXX",
Capacity: 1,
},
{
ID: "BBB",
Zone: "YYY",
Capacity: 2,
},
{
ID: "CCC",
Zone: "ZZZ",
Capacity: 3,
},
}
expVersion := 666
tests := []struct {
input string
expNodes clusterNodes
expVersion int
}{
{
input: `
==== CURRENT CLUSTER LAYOUT ====
ID Tags Zone Capacity
AAA XXX 1
BBB YYY 2
CCC ZZZ 3
Current cluster layout version: 666
`,
expNodes: expNodes,
expVersion: expVersion,
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
gotNodes, gotVersion, err := readCurrNodes(
bytes.NewBufferString(test.input),
)
if err != nil {
t.Fatal(err)
}
if gotVersion != test.expVersion {
t.Fatalf(
"expected version %d, got %d",
test.expVersion,
gotVersion,
)
}
if !reflect.DeepEqual(gotNodes, test.expNodes) {
t.Fatalf(
"expected nodes: %#v,\ngot nodes: %#v",
gotNodes,
test.expNodes,
)
}
})
}
}
func TestDiff(t *testing.T) {
currNodes := clusterNodes{
{
ID: "1",
Zone: "zone",
Capacity: 1,
},
{
ID: "2",
Zone: "zone",
Capacity: 1,
},
{
ID: "3",
Zone: "zone",
Capacity: 1,
},
{
ID: "4",
Zone: "zone",
Capacity: 1,
},
}
expNodes := clusterNodes{
{
ID: "111",
Zone: "zone",
Capacity: 1,
},
{
ID: "222",
Zone: "zone2",
Capacity: 1,
},
{
ID: "333",
Zone: "zone",
Capacity: 10,
},
}
expLines := []string{
`garage layout remove 4`,
`garage layout assign 222 -z zone2 -c 1`,
`garage layout assign 333 -z zone -c 10`,
}
gotLines := diff(currNodes, expNodes)
if !reflect.DeepEqual(gotLines, expLines) {
t.Fatalf("expected lines: %#v,\ngot lines: %#v", expLines, gotLines)
}
}

View File

@ -1,65 +0,0 @@
package garage_peer_keygen
/*
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! !!
!! DANGER !!
!! !!
!! This script will deterministically produce public/private keys given some !!
!! arbitrary input. This is NEVER what you want. It's only being used in !!
!! cryptic-net for a very specific purpose for which I think it's ok and is !!
!! very necessary, and people are probably _still_ going to yell at me. !!
!! !!
!! DONT USE THIS. !!
!! !!
!! - Brian !!
!! !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
*/
import (
"encoding/hex"
"flag"
"fmt"
"io/ioutil"
"os"
"cryptic-net/garage"
)
func Main() {
ip := flag.String("ip", "", "Internal IP address of the node to generate a key for")
port := flag.Int("port", 0, "RPC port number for the garage instance to generate a key for")
outPriv := flag.String("out-priv", "", "The path to the private key which should be created, if given.")
outPub := flag.String("out-pub", "", "The path to the public key which should be created, if given.")
danger := flag.Bool("danger", false, "Set this flag to indicate you understand WHY this binary should NEVER be used (see source code).")
flag.Parse()
if len(*ip) == 0 || *port == 0 || !*danger {
panic("The arguments -ip, -port, and -danger are required")
}
peer := garage.Peer{
IP: *ip,
RPCPort: *port,
}
pubKey, privKey := peer.RPCPeerKey()
fmt.Fprintln(os.Stdout, hex.EncodeToString(pubKey))
if *outPub != "" {
if err := ioutil.WriteFile(*outPub, pubKey, 0444); err != nil {
panic(fmt.Errorf("writing public key to %q: %w", *outPub, err))
}
}
if *outPriv != "" {
if err := ioutil.WriteFile(*outPriv, privKey, 0400); err != nil {
panic(fmt.Errorf("writing private key to %q: %w", *outPriv, err))
}
}
}

View File

@ -1,134 +0,0 @@
package nebula_entrypoint
import (
"cryptic-net/yamlutil"
"log"
"net"
"os"
"path/filepath"
"strconv"
"syscall"
crypticnet "cryptic-net"
)
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
log.Fatalf("reading envvars: %v", err)
}
var (
lighthouseHostIPs []string
staticHostMap = map[string][]string{}
)
for _, host := range env.Bootstrap.Hosts {
if host.Nebula.PublicAddr == "" {
continue
}
lighthouseHostIPs = append(lighthouseHostIPs, host.Nebula.IP)
staticHostMap[host.Nebula.IP] = []string{host.Nebula.PublicAddr}
}
config := map[string]interface{}{
"pki": map[string]string{
"ca": env.Bootstrap.NebulaHostCert.CACert,
"cert": env.Bootstrap.NebulaHostCert.HostCert,
"key": env.Bootstrap.NebulaHostCert.HostKey,
},
"static_host_map": staticHostMap,
"punchy": map[string]bool{
"punch": true,
"respond": true,
},
"tun": map[string]interface{}{
"dev": "cryptic-nebula1",
},
}
if err != nil {
log.Fatal(err)
}
if publicAddr := env.ThisDaemon().VPN.PublicAddr; publicAddr == "" {
config["listen"] = map[string]string{
"host": "0.0.0.0",
"port": "0",
}
config["lighthouse"] = map[string]interface{}{
"hosts": lighthouseHostIPs,
}
} else {
_, port, err := net.SplitHostPort(publicAddr)
if err != nil {
log.Fatalf("parsing public address %q: %v", publicAddr, err)
}
config["listen"] = map[string]string{
"host": "0.0.0.0",
"port": port,
}
config["lighthouse"] = map[string]interface{}{
"hosts": []string{},
"am_lighthouse": true,
}
}
thisDaemon := env.ThisDaemon()
var firewallInbound []crypticnet.ConfigFirewallRule
for _, alloc := range thisDaemon.Storage.Allocations {
firewallInbound = append(
firewallInbound,
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.S3APIPort),
Proto: "tcp",
Host: "any",
},
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.RPCPort),
Proto: "tcp",
Host: "any",
},
crypticnet.ConfigFirewallRule{
Port: strconv.Itoa(alloc.WebPort),
Proto: "tcp",
Host: "any",
},
)
}
firewall := thisDaemon.VPN.Firewall
firewall.Inbound = append(firewallInbound, firewall.Inbound...)
config["firewall"] = firewall
nebulaYmlPath := filepath.Join(env.RuntimeDirPath, "nebula.yml")
if err := yamlutil.WriteYamlFile(config, nebulaYmlPath); err != nil {
log.Fatalf("writing nebula.yml to %q: %v", nebulaYmlPath, err)
}
var (
binPath = env.BinPath("nebula")
args = []string{"nebula", "-config", nebulaYmlPath}
cliEnv = os.Environ()
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
log.Fatalf("calling exec(%q, %#v, %#v)", binPath, args, cliEnv)
}
}

View File

@ -1,30 +0,0 @@
package update_global_bucket
import (
crypticnet "cryptic-net"
"cryptic-net/bootstrap"
"log"
)
func Main() {
env, err := crypticnet.ReadEnv()
if err != nil {
log.Fatalf("reading envvars: %v", err)
}
client, err := env.Bootstrap.GlobalBucketS3APIClient()
if err != nil {
log.Fatalf("creating client for global bucket: %v", err)
}
err = bootstrap.PutGarageBoostrapHost(
env.Context,
client,
env.Bootstrap.ThisHost(),
)
if err != nil {
log.Fatal(err)
}
}

View File

@ -1,51 +0,0 @@
package crypticnet
type ConfigFirewall struct {
Conntrack ConfigConntrack `yaml:"conntrack"`
Outbound []ConfigFirewallRule `yaml:"outbound"`
Inbound []ConfigFirewallRule `yaml:"inbound"`
}
type ConfigConntrack struct {
TCPTimeout string `yaml:"tcp_timeout"`
UDPTimeout string `yaml:"udp_timeout"`
DefaultTimeout string `yaml:"default_timeout"`
MaxConnections int `yaml:"max_connections"`
}
type ConfigFirewallRule struct {
Port string `yaml:"port,omitempty"`
Code string `yaml:"code,omitempty"`
Proto string `yaml:"proto,omitempty"`
Host string `yaml:"host,omitempty"`
Group string `yaml:"group,omitempty"`
Groups []string `yaml:"groups,omitempty"`
CIDR string `yaml:"cidr,omitempty"`
CASha string `yaml:"ca_sha,omitempty"`
CAName string `yaml:"ca_name,omitempty"`
}
// DaemonYmlStorageAllocation describes the structure of each storage allocation
// within the daemon.yml file.
type DaemonYmlStorageAllocation struct {
DataPath string `yaml:"data_path"`
MetaPath string `yaml:"meta_path"`
Capacity int `yaml:"capacity"`
S3APIPort int `yaml:"api_port"` // TODO fix field name here
RPCPort int `yaml:"rpc_port"`
WebPort int `yaml:"web_port"`
}
// DaemonYml describes the structure of the daemon.yml file.
type DaemonYml struct {
DNS struct {
Resolvers []string `yaml:"resolvers"`
} `yaml:"dns"`
VPN struct {
PublicAddr string `yaml:"public_addr"`
Firewall ConfigFirewall `yaml:"firewall"`
} `yaml:"vpn"`
Storage struct {
Allocations []DaemonYmlStorageAllocation
} `yaml:"storage"`
}

View File

@ -1,3 +0,0 @@
// Package globals defines global constants and variables which are valid
// across all cryptic-net processes and sub-processes.
package crypticnet

View File

@ -1,226 +0,0 @@
package crypticnet
import (
"context"
"cryptic-net/bootstrap"
"cryptic-net/yamlutil"
"errors"
"fmt"
"io/fs"
"os"
"os/signal"
"path/filepath"
"sync"
"syscall"
"github.com/adrg/xdg"
)
// Names of various environment variables which get set by the entrypoint.
const (
DaemonYmlPathEnvVar = "_DAEMON_YML_PATH"
BootstrapPathEnvVar = "_BOOTSTRAP_PATH"
RuntimeDirPathEnvVar = "_RUNTIME_DIR_PATH"
DataDirPathEnvVar = "_DATA_DIR_PATH"
)
// Env contains the values of environment variables, as well as other entities
// which are useful across all processes.
type Env struct {
Context context.Context
AppDirPath string
DaemonYmlPath string
RuntimeDirPath string
DataDirPath string
// If NewEnv is called with bootstrapOptional, and a bootstrap file is not
// found, then these fields will not be set.
BootstrapPath string
Bootstrap bootstrap.Bootstrap
thisDaemon DaemonYml
thisDaemonOnce sync.Once
}
func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
if appDirPath == "" {
appDirPath = "."
}
return appDirPath
}
// NewEnv calculates an Env instance based on the APPDIR and XDG envvars.
//
// If bootstrapOptional is true then NewEnv will first check if a bootstrap file
// can be found in the expected places, and if not then it will not populate
// BootstrapFS or any other fields based on it.
func NewEnv(bootstrapOptional bool) (*Env, error) {
runtimeDirPath := filepath.Join(xdg.RuntimeDir, "cryptic-net")
appDirPath := getAppDirPath()
env := &Env{
AppDirPath: appDirPath,
DaemonYmlPath: filepath.Join(runtimeDirPath, "daemon.yml"),
RuntimeDirPath: runtimeDirPath,
DataDirPath: filepath.Join(xdg.DataHome, "cryptic-net"),
}
return env, env.init(bootstrapOptional)
}
// ReadEnv reads an Env from the process's environment variables, rather than
// calculating like NewEnv does.
func ReadEnv() (*Env, error) {
var err error
readEnv := func(key string) string {
if err != nil {
return ""
}
val := os.Getenv(key)
if val == "" {
err = fmt.Errorf("envvar %q not set", key)
}
return val
}
env := &Env{
AppDirPath: getAppDirPath(),
DaemonYmlPath: readEnv(DaemonYmlPathEnvVar),
RuntimeDirPath: readEnv(RuntimeDirPathEnvVar),
DataDirPath: readEnv(DataDirPathEnvVar),
}
if err != nil {
return nil, err
}
return env, env.init(false)
}
// DataDirBootstrapPath returns the path to the bootstrap file within the user's
// data dir. If the file does not exist there it will be found in the AppDirPath
// by ReloadBootstrap.
func (e *Env) DataDirBootstrapPath() string {
return filepath.Join(e.DataDirPath, "bootstrap.tgz")
}
// LoadBootstrap sets BootstrapPath to the given value, and loads BootstrapFS
// and all derived fields based on that.
func (e *Env) LoadBootstrap(path string) error {
var err error
if e.Bootstrap, err = bootstrap.FromFile(path); err != nil {
return fmt.Errorf("parsing bootstrap.tgz at %q: %w", path, err)
}
e.BootstrapPath = path
return nil
}
func (e *Env) initBootstrap(bootstrapOptional bool) error {
exists := func(path string) (bool, error) {
if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {
return false, nil
} else if err != nil {
return false, fmt.Errorf("stat'ing %q: %w", path, err)
}
return true, nil
}
// start by checking if a bootstrap can be found in the user's data
// directory. This will only not be the case if daemon has never been
// successfully started.
{
bootstrapPath := e.DataDirBootstrapPath()
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
// fallback to checking within the AppDir for a bootstrap which has been
// embedded into the binary.
{
bootstrapPath := filepath.Join(e.AppDirPath, "share/bootstrap.tgz")
if exists, err := exists(bootstrapPath); err != nil {
return fmt.Errorf("determining if %q exists: %w", bootstrapPath, err)
} else if !exists && !bootstrapOptional {
return fmt.Errorf("boostrap file not found at %q", bootstrapPath)
} else if exists {
return e.LoadBootstrap(bootstrapPath)
}
}
return nil
}
func (e *Env) init(bootstrapOptional bool) error {
var cancel context.CancelFunc
e.Context, cancel = context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalCh
cancel()
fmt.Fprintf(os.Stderr, "got signal %v, will exit gracefully\n", sig)
sig = <-signalCh
fmt.Fprintf(os.Stderr, "second interrupt signal %v received, force quitting, there may be zombie children left behind, good luck!\n", sig)
os.Stderr.Sync()
os.Exit(1)
}()
if err := e.initBootstrap(bootstrapOptional); err != nil {
return fmt.Errorf("initializing bootstrap data: %w", err)
}
return nil
}
// ToMap returns the Env as a map of key/value strings. If this map is set into
// a process's environment, then that process can read it back using ReadEnv.
func (e *Env) ToMap() map[string]string {
return map[string]string{
DaemonYmlPathEnvVar: e.DaemonYmlPath,
BootstrapPathEnvVar: e.BootstrapPath,
RuntimeDirPathEnvVar: e.RuntimeDirPath,
DataDirPathEnvVar: e.DataDirPath,
}
}
// ThisDaemon returns the DaemonYml (loaded from DaemonYmlPath) for the
// currently running process.
func (e *Env) ThisDaemon() DaemonYml {
e.thisDaemonOnce.Do(func() {
if err := yamlutil.LoadYamlFile(&e.thisDaemon, e.DaemonYmlPath); err != nil {
panic(err)
}
})
return e.thisDaemon
}
// BinPath returns the absolute path to a binary in the AppDir.
func (e *Env) BinPath(name string) string {
return filepath.Join(e.AppDirPath, "bin", name)
}

View File

@ -1,34 +0,0 @@
package garage
import (
"errors"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
// IsKeyNotFound returns true if the given error is the result of a key not
// being found in a bucket.
func IsKeyNotFound(err error) bool {
var mErr minio.ErrorResponse
return errors.As(err, &mErr) && mErr.Code == "NoSuchKey"
}
// S3APIClient is a client used to interact with garage's S3 API.
type S3APIClient = *minio.Client
// S3APICredentials describe data fields necessary for authenticating with a
// garage S3 API endpoint.
type S3APICredentials struct {
ID string `yaml:"id"`
Secret string `yaml:"secret"`
}
// NewS3APIClient returns a minio client configured to use the given garage S3 API
// endpoint.
func NewS3APIClient(addr string, creds S3APICredentials) (S3APIClient, error) {
return minio.New(addr, &minio.Options{
Creds: credentials.NewStaticV4(creds.ID, creds.Secret, ""),
Region: Region,
})
}

View File

@ -1,13 +0,0 @@
// Package garage contains helper functions and types which are useful for
// setting up garage configs, processes, and deployments.
package garage
const (
// Region is the region which garage is configured with.
Region = "garage"
// GlobalBucket is the name of the global garage bucket which is
// accessible to all hosts in the network.
GlobalBucket = "cryptic-net-global"
)

View File

@ -1,41 +0,0 @@
package garage
import "io"
type infiniteReader struct {
b []byte
i int
}
// NewInfiniteReader returns a reader which will produce the given bytes in
// repetition. len(b) must be greater than 0.
func NewInfiniteReader(b []byte) io.Reader {
if len(b) == 0 {
panic("len(b) must be greater than 0")
}
return &infiniteReader{b: b}
}
func (r *infiniteReader) Read(b []byte) (int, error) {
// here, have a puzzle
var n int
for {
n += copy(b[n:], r.b[r.i:])
if r.i > 0 {
n += copy(b[n:], r.b[:r.i])
}
r.i = (r.i + n) % len(r.b)
if n >= len(b) {
return n, nil
}
}
}

View File

@ -1,101 +0,0 @@
package garage
import (
"bytes"
"strconv"
"testing"
)
func TestInfiniteReader(t *testing.T) {
tests := []struct {
in []byte
size int
exp []string
}{
{
in: []byte("a"),
size: 1,
exp: []string{"a"},
},
{
in: []byte("ab"),
size: 1,
exp: []string{"a", "b"},
},
{
in: []byte("ab"),
size: 2,
exp: []string{"ab"},
},
{
in: []byte("ab"),
size: 3,
exp: []string{"aba", "bab"},
},
{
in: []byte("ab"),
size: 4,
exp: []string{"abab"},
},
{
in: []byte("ab"),
size: 5,
exp: []string{"ababa", "babab"},
},
{
in: []byte("abc"),
size: 1,
exp: []string{"a", "b", "c"},
},
{
in: []byte("abc"),
size: 2,
exp: []string{"ab", "ca", "bc"},
},
{
in: []byte("abc"),
size: 3,
exp: []string{"abc"},
},
{
in: []byte("abc"),
size: 4,
exp: []string{"abca", "bcab", "cabc"},
},
{
in: []byte("abc"),
size: 5,
exp: []string{"abcab", "cabca", "bcabc"},
},
}
for i, test := range tests {
t.Run(strconv.Itoa(i), func(t *testing.T) {
r := NewInfiniteReader(test.in)
buf := make([]byte, test.size)
assertRead := func(expBuf []byte) {
n, err := r.Read(buf)
if !bytes.Equal(buf, expBuf) {
t.Fatalf("expected bytes %q, got %q", expBuf, buf)
} else if n != len(buf) {
t.Fatalf("expected n %d, got %d", len(buf), n)
} else if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
for i := 0; i < 3; i++ {
for _, expStr := range test.exp {
assertRead([]byte(expStr))
}
}
})
}
}

View File

@ -1,66 +0,0 @@
package garage
import (
"crypto/ed25519"
"encoding/hex"
"fmt"
"net"
"strconv"
)
// Peer describes all information necessary to connect to a given garage node.
type Peer struct {
IP string
RPCPort int
S3APIPort int
}
// RPCPeerKey deterministically generates a public/private keys which can
// be used as a garage node key.
//
// DANGER: This function will deterministically produce public/private keys
// given some arbitrary input. This is NEVER what you want. It's only being used
// in cryptic-net for a very specific purpose for which I think it's ok and is
// very necessary, and people are probably _still_ going to yell at me.
//
func (p Peer) RPCPeerKey() (pubKey, privKey []byte) {
input := []byte(net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort)))
// Append the length of the input to the input, so that the input "foo"
// doesn't generate the same key as the input "foofoo".
input = strconv.AppendInt(input, int64(len(input)), 10)
pubKey, privKey, err := ed25519.GenerateKey(NewInfiniteReader(input))
if err != nil {
panic(err)
}
return pubKey, privKey
}
// RPCPeerID returns the peer ID of the garage node for use in communicating
// over RPC.
//
// DANGER: See warning on RPCPeerKey.
func (p Peer) RPCPeerID() string {
pubKey, _ := p.RPCPeerKey()
return hex.EncodeToString(pubKey)
}
// RPCAddr returns the address of the peer's RPC port.
func (p Peer) RPCAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.RPCPort))
}
// RPCPeerAddr returns the full peer address (e.g. "id@ip:port") of the garage
// node for use in communicating over RPC.
//
// DANGER: See warning on RPCPeerKey.
func (p Peer) RPCPeerAddr() string {
return fmt.Sprintf("%s@%s", p.RPCPeerID(), p.RPCAddr())
}
// S3APIAddr returns the address of the peer's S3 API port.
func (p Peer) S3APIAddr() string {
return net.JoinHostPort(p.IP, strconv.Itoa(p.S3APIPort))
}

View File

@ -1,76 +0,0 @@
package garage
import (
"fmt"
"io"
"os"
"text/template"
)
// GarageTomlData describes all fields needed for rendering a garage.toml
// file via this package's template.
type GarageTomlData struct {
MetaPath string
DataPath string
RPCSecret string
RPCAddr string
APIAddr string
WebAddr string
BootstrapPeers []string
}
var garageTomlTpl = template.Must(template.New("").Parse(`
metadata_dir = "{{ .MetaPath }}"
data_dir = "{{ .DataPath }}"
replication_mode = "3"
rpc_secret = "{{ .RPCSecret }}"
rpc_bind_addr = "{{ .RPCAddr }}"
rpc_public_addr = "{{ .RPCAddr }}"
bootstrap_peers = [{{- range .BootstrapPeers }}
"{{ . }}",
{{ end -}}]
[s3_api]
api_bind_addr = "{{ .APIAddr }}"
s3_region = "garage"
[s3_web]
bind_addr = "{{ .WebAddr }}"
root_domain = ".example.com"
`))
// RenderGarageToml renders a garage.toml using the given data into the writer.
func RenderGarageToml(into io.Writer, data GarageTomlData) error {
return garageTomlTpl.Execute(into, data)
}
// WriteGarageTomlFile renders a garage.toml using the given data to a new file
// at the given path.
func WriteGarageTomlFile(path string, data GarageTomlData) error {
file, err := os.OpenFile(
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640,
)
if err != nil {
return fmt.Errorf("creating file: %w", err)
}
defer file.Close()
err = RenderGarageToml(file, data)
if err != nil {
return fmt.Errorf("rendering template to file: %w", err)
}
return nil
}

View File

@ -1,160 +0,0 @@
// Package nebula contains helper functions and types which are useful for
// setting up nebula configs, processes, and deployments.
package nebula
import (
"crypto/ed25519"
"crypto/rand"
"fmt"
"io"
"net"
"time"
"github.com/slackhq/nebula/cert"
"golang.org/x/crypto/curve25519"
)
// TODO this should one day not be hardcoded
var ipCIDRMask = func() net.IPMask {
_, ipNet, err := net.ParseCIDR("10.10.0.0/16")
if err != nil {
panic(err)
}
return ipNet.Mask
}()
// HostCert contains the certificate and private key files which will need to
// be present on a particular host. Each file is PEM encoded.
type HostCert struct {
CACert string
HostKey string
HostCert string
}
// CACert contains the certificate and private files which can be used to create
// HostCerts. Each file is PEM encoded.
type CACert struct {
CACert string
CAKey string
}
// NewHostCert generates a new key/cert for a nebula host using the CA key
// which will be found in the adminFS.
func NewHostCert(
caCert CACert, hostName, hostIP string,
) (
HostCert, error,
) {
// The logic here is largely based on
// https://github.com/slackhq/nebula/blob/v1.4.0/cmd/nebula-cert/sign.go
caKey, _, err := cert.UnmarshalEd25519PrivateKey([]byte(caCert.CAKey))
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.key: %w", err)
}
caCrt, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(caCert.CACert))
if err != nil {
return HostCert{}, fmt.Errorf("unmarshaling ca.crt: %w", err)
}
issuer, err := caCrt.Sha256Sum()
if err != nil {
return HostCert{}, fmt.Errorf("getting ca.crt issuer: %w", err)
}
expireAt := caCrt.Details.NotAfter.Add(-1 * time.Second)
ip := net.ParseIP(hostIP)
if ip == nil {
return HostCert{}, fmt.Errorf("invalid host ip %q", hostIP)
}
ipNet := &net.IPNet{
IP: ip,
Mask: ipCIDRMask,
}
var hostPub, hostKey []byte
{
var pubkey, privkey [32]byte
if _, err := io.ReadFull(rand.Reader, privkey[:]); err != nil {
return HostCert{}, fmt.Errorf("reading random bytes to form private key: %w", err)
}
curve25519.ScalarBaseMult(&pubkey, &privkey)
hostPub, hostKey = pubkey[:], privkey[:]
}
hostCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: hostName,
Ips: []*net.IPNet{ipNet},
NotBefore: time.Now(),
NotAfter: expireAt,
PublicKey: hostPub,
IsCA: false,
Issuer: issuer,
},
}
if err := hostCrt.CheckRootConstrains(caCrt); err != nil {
return HostCert{}, fmt.Errorf("validating certificate constraints: %w", err)
}
if err := hostCrt.Sign(caKey); err != nil {
return HostCert{}, fmt.Errorf("signing host cert with ca.key: %w", err)
}
hostKeyPEM := cert.MarshalX25519PrivateKey(hostKey)
hostCrtPEM, err := hostCrt.MarshalToPEM()
if err != nil {
return HostCert{}, fmt.Errorf("marshalling host.crt: %w", err)
}
return HostCert{
CACert: caCert.CACert,
HostKey: string(hostKeyPEM),
HostCert: string(hostCrtPEM),
}, nil
}
// NewCACert generates a CACert. The domain should be the network's root domain,
// and is included in the signing certificate's Name field.
func NewCACert(domain string) (CACert, error) {
pubKey, privKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
panic(fmt.Errorf("generating ed25519 key: %w", err))
}
now := time.Now()
expireAt := now.Add(2 * 365 * 24 * time.Hour)
caCrt := cert.NebulaCertificate{
Details: cert.NebulaCertificateDetails{
Name: fmt.Sprintf("%s cryptic-net root cert", domain),
NotBefore: now,
NotAfter: expireAt,
PublicKey: pubKey,
IsCA: true,
},
}
if err := caCrt.Sign(privKey); err != nil {
return CACert{}, fmt.Errorf("signing caCrt: %w", err)
}
caKeyPEM := cert.MarshalEd25519PrivateKey(privKey)
caCrtPem, err := caCrt.MarshalToPEM()
if err != nil {
return CACert{}, fmt.Errorf("marshaling caCrt: %w", err)
}
return CACert{
CACert: string(caCrtPem),
CAKey: string(caKeyPEM),
}, nil
}

View File

@ -1,99 +0,0 @@
package crypticnet
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"github.com/shirou/gopsutil/process"
)
var errDaemonNotRunning = errors.New("no cryptic-net daemon process running")
// ProcLock is used to lock a process.
type ProcLock interface {
// WriteLock creates a new lock, or errors if the lock is alread held.
WriteLock() error
// AssertLock returns an error if the lock already exists.
AssertLock() error
}
type procLock struct {
dir string
}
// NewProcLock returns a ProcLock which will use a file in the given directory
// to lock the process.
func NewProcLock(dir string) ProcLock {
return &procLock{dir: dir}
}
func (pl *procLock) path() string {
return filepath.Join(pl.dir, "lock")
}
func (pl *procLock) WriteLock() error {
lockFilePath := pl.path()
lockFile, err := os.OpenFile(
lockFilePath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400,
)
if errors.Is(err, os.ErrExist) {
return fmt.Errorf(
"lock file %q already exists, if the cryptic-net daemon is not already running you can safely delete this file",
lockFilePath,
)
} else if err != nil {
return fmt.Errorf("opening lockfile %q: %w", lockFilePath, err)
}
defer lockFile.Close()
if _, err := fmt.Fprintf(lockFile, "%d\n", os.Getpid()); err != nil {
return fmt.Errorf("writing pid to %q: %w", lockFilePath, err)
}
return nil
}
// checks that the lock file exists and that the process which created it also
// still exists.
func (pl *procLock) AssertLock() error {
lockFilePath := pl.path()
lockFile, err := os.Open(lockFilePath)
if errors.Is(err, fs.ErrNotExist) {
return errDaemonNotRunning
} else if err != nil {
return fmt.Errorf("checking lock file %q: %w", lockFilePath, err)
}
defer lockFile.Close()
var pid int32
if _, err := fmt.Fscan(lockFile, &pid); err != nil {
return fmt.Errorf("scanning pid from lock file %q: %w", lockFilePath, err)
}
procExists, err := process.PidExists(pid)
if err != nil {
return fmt.Errorf("checking if process %d exists: %w", pid, err)
} else if !procExists {
return errDaemonNotRunning
}
return nil
}

View File

@ -1,24 +0,0 @@
// Package tarutil implements utilities which are useful for interacting with
// tar and tgz files.
package tarutil
import (
"compress/gzip"
"fmt"
"io"
"io/fs"
"github.com/nlepage/go-tarfs"
)
// FSFromReader returns a FS instance which will read the contents of a tgz
// file from the given Reader.
func FSFromReader(r io.Reader) (fs.FS, error) {
gf, err := gzip.NewReader(r)
if err != nil {
return nil, fmt.Errorf("un-gziping: %w", err)
}
defer gf.Close()
return tarfs.New(gf)
}

View File

@ -1,112 +0,0 @@
package tarutil
import (
"archive/tar"
"bytes"
"compress/gzip"
"fmt"
"io"
"path/filepath"
"strings"
)
// TGZWriter is a utility for writing tgz files. If an internal error is
// encountered by any method then all subsequent methods will be no-ops, and
// Close() will return that error (after closing out resources).
type TGZWriter struct {
gzipW *gzip.Writer
tarW *tar.Writer
err error
dirsWritten map[string]bool
}
// NewTGZWriter initializes and returns a new instance of TGZWriter which will
// write all data to the given io.Writer.
func NewTGZWriter(w io.Writer) *TGZWriter {
gzipW := gzip.NewWriter(w)
tarW := tar.NewWriter(gzipW)
return &TGZWriter{
gzipW: gzipW,
tarW: tarW,
dirsWritten: map[string]bool{},
}
}
// Close cleans up all open resources being held by TGZWriter, and returns the
// first internal error which was encountered during its operation (if any).
func (w *TGZWriter) Close() error {
w.tarW.Close()
w.gzipW.Close()
return w.err
}
func (w *TGZWriter) writeDir(path string) {
if w.err != nil {
return
} else if path != "." {
w.writeDir(filepath.Dir(path))
}
if path == "." {
path = "./"
} else {
path = "./" + strings.TrimPrefix(path, "./")
path = path + "/"
}
if w.dirsWritten[path] {
return
}
err := w.tarW.WriteHeader(&tar.Header{
Name: path,
Mode: 0700,
})
if err != nil {
w.err = fmt.Errorf("writing header for directory %q: %w", path, err)
return
}
w.dirsWritten[path] = true
}
// WriteFile writes a file to the tgz archive. The file will automatically be
// rooted to the "." directory, and any sub-directories the file exists in
// should have already been created.
func (w *TGZWriter) WriteFile(path string, size int64, body io.Reader) {
if w.err != nil {
return
}
path = "./" + strings.TrimPrefix(path, "./")
w.writeDir(filepath.Dir(path))
err := w.tarW.WriteHeader(&tar.Header{
Name: path,
Size: size,
Mode: 0400,
})
if err != nil {
w.err = fmt.Errorf("writing header for file %q: %w", path, err)
return
}
if _, err := io.Copy(w.tarW, body); err != nil {
w.err = fmt.Errorf("writing file body of file %q: %w", path, err)
return
}
}
// WriteFileBytes is a shortcut for calling WriteFile with the given byte slice
// being used as the file body.
func (w *TGZWriter) WriteFileBytes(path string, body []byte) {
bodyR := bytes.NewReader(body)
w.WriteFile(path, bodyR.Size(), bodyR)
}

View File

@ -1,67 +0,0 @@
package yamlutil
import (
"fmt"
"io/fs"
"os"
"gopkg.in/yaml.v3"
)
// LoadYamlFile reads the file at the given path and unmarshals it into the
// given pointer.
func LoadYamlFile(into interface{}, path string) error {
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
defer file.Close()
if err = yaml.NewDecoder(file).Decode(into); err != nil {
return fmt.Errorf("decoding yaml: %w", err)
}
return nil
}
// WriteYamlFile encodes the given data as a yaml document, and writes it to the
// given file path, overwriting any previous data.
func WriteYamlFile(data interface{}, path string) error {
file, err := os.OpenFile(
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0640,
)
if err != nil {
return fmt.Errorf("opening file: %w", err)
}
err = yaml.NewEncoder(file).Encode(data)
file.Close()
if err != nil {
return fmt.Errorf("writing/encoding file: %w", err)
}
return nil
}
// LoadYamlFSFile is like LoadYamlFile, but it will read the file from the given
// fs.FS instance.
func LoadYamlFSFile(into interface{}, f fs.FS, path string) error {
body, err := fs.ReadFile(f, path)
if err != nil {
return fmt.Errorf("reading file from FS: %w", err)
}
if err := yaml.Unmarshal(body, into); err != nil {
return fmt.Errorf("yaml unmarshaling: %w", err)
}
return nil
}

4
go/.golangci.yml Normal file
View File

@ -0,0 +1,4 @@
# https://github.com/golangci/golangci-lint/issues/4733
linters-settings:
errcheck:
ignore : ""

195
go/bootstrap/bootstrap.go Normal file
View File

@ -0,0 +1,195 @@
// Package bootstrap deals with the parsing and creation of bootstrap.json
// files. It also contains some helpers which rely on bootstrap data.
package bootstrap
import (
"crypto/sha512"
"encoding/json"
"fmt"
"isle/nebula"
"isle/toolkit"
"maps"
"net/netip"
"path/filepath"
"sort"
"strings"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
)
// StateDirPath returns the path within the user's state directory where the
// bootstrap file is stored.
func StateDirPath(dataDirPath string) string {
return filepath.Join(dataDirPath, "bootstrap.json")
}
// CreationParams are general parameters used when creating a new network. These
// are available to all hosts within the network via their bootstrap files.
type CreationParams struct {
ID string
Name string
Domain string
}
// NewCreationParams instantiates and returns a CreationParams.
func NewCreationParams(name, domain string) CreationParams {
return CreationParams{
ID: toolkit.RandStr(32),
Name: name,
Domain: domain,
}
}
// Annotate implements the mctx.Annotator interface.
func (p CreationParams) Annotate(aa mctx.Annotations) {
aa["networkID"] = p.ID
aa["networkName"] = p.Name
aa["networkDomain"] = p.Domain
}
// Matches returns true if the given string matches some aspect of the
// CreationParams.
func (p CreationParams) Matches(str string) bool {
if strings.HasPrefix(p.ID, str) {
return true
}
if strings.EqualFold(p.Name, str) {
return true
}
if strings.EqualFold(p.Domain, str) {
return true
}
return false
}
// Conflicts returns true if either CreationParams has some parameter which
// overlaps with that of the other.
func (p CreationParams) Conflicts(p2 CreationParams) bool {
if p.ID == p2.ID {
return true
}
if strings.EqualFold(p.Name, p2.Name) {
return true
}
if strings.EqualFold(p.Domain, p2.Domain) {
return true
}
return false
}
// Bootstrap contains all information which is needed by a host daemon to join a
// network on boot.
type Bootstrap struct {
NetworkCreationParams CreationParams
CAPublicCredentials nebula.CAPublicCredentials
PrivateCredentials nebula.HostPrivateCredentials
HostAssigned `json:"-"`
SignedHostAssigned nebula.Signed[HostAssigned] // signed by CA
Hosts map[nebula.HostName]Host
}
// New initializes and returns a new Bootstrap file for a new host.
//
// TODO in the resulting bootstrap only include this host and hosts which are
// necessary for connecting to nebula/garage. Remember to immediately re-poll
// garage for the full hosts list during network joining.
func New(
caCreds nebula.CACredentials,
adminCreationParams CreationParams,
existingHosts map[nebula.HostName]Host,
name nebula.HostName,
ip netip.Addr,
) (
Bootstrap, error,
) {
hostPubCreds, hostPrivCreds, err := nebula.NewHostCredentials(
caCreds, name, ip,
)
if err != nil {
return Bootstrap{}, fmt.Errorf("generating host credentials: %w", err)
}
assigned := HostAssigned{
Name: name,
PublicCredentials: hostPubCreds,
}
signedAssigned, err := nebula.Sign(assigned, caCreds.SigningPrivateKey)
if err != nil {
return Bootstrap{}, fmt.Errorf("signing assigned fields: %w", err)
}
existingHosts = maps.Clone(existingHosts)
existingHosts[name] = Host{
HostAssigned: assigned,
}
return Bootstrap{
NetworkCreationParams: adminCreationParams,
CAPublicCredentials: caCreds.Public,
PrivateCredentials: hostPrivCreds,
HostAssigned: assigned,
SignedHostAssigned: signedAssigned,
Hosts: existingHosts,
}, nil
}
// UnmarshalJSON implements the json.Unmarshaler interface. It will
// automatically populate the HostAssigned field by unwrapping the
// SignedHostAssigned field.
func (b *Bootstrap) UnmarshalJSON(data []byte) error {
type inner Bootstrap
err := json.Unmarshal(data, (*inner)(b))
if err != nil {
return err
}
b.HostAssigned, err = b.SignedHostAssigned.Unwrap(
b.CAPublicCredentials.SigningKey,
)
if err != nil {
return fmt.Errorf("unwrapping HostAssigned: %w", err)
}
return nil
}
// ThisHost is a shortcut for b.Hosts[b.HostName], but will panic if the
// HostName isn't found in the Hosts map.
func (b Bootstrap) ThisHost() Host {
host, ok := b.Hosts[b.Name]
if !ok {
panic(fmt.Sprintf("hostname %q not defined in bootstrap's hosts", b.Name))
}
return host
}
// Hash returns a deterministic hash of the given hosts map.
func HostsHash(hostsMap map[nebula.HostName]Host) ([]byte, error) {
hosts := make([]Host, 0, len(hostsMap))
for _, host := range hostsMap {
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool {
return hosts[i].Name < hosts[j].Name
})
h := sha512.New()
if err := json.NewEncoder(h).Encode(hosts); err != nil {
return nil, err
}
return h.Sum(nil), nil
}

30
go/bootstrap/garage.go Normal file
View File

@ -0,0 +1,30 @@
package bootstrap
import (
"isle/garage"
)
// GarageNodes returns a Node for each known garage instance in the network.
func (b Bootstrap) GarageNodes() []garage.RemoteNode {
var nodes []garage.RemoteNode
for _, host := range b.Hosts {
nodes = append(nodes, host.GarageNodes()...)
}
return nodes
}
// ChooseGarageNode returns a RemoteNode for a garage instance from the network.
// It will prefer a garage instance on this particular host, if there is one,
// but will otherwise return a random endpoint.
func (b Bootstrap) ChooseGarageNode() garage.RemoteNode {
thisHost := b.ThisHost()
if len(thisHost.Garage.Instances) > 0 {
return thisHost.GarageNodes()[0]
}
for _, node := range b.GarageNodes() {
return node
}
panic("no garage instances configured")
}

109
go/bootstrap/hosts.go Normal file
View File

@ -0,0 +1,109 @@
package bootstrap
import (
"fmt"
"isle/garage"
"isle/nebula"
"net/netip"
)
// NebulaHost describes the nebula configuration of a Host which is relevant for
// other hosts to know.
type NebulaHost struct {
PublicAddr string
}
// GarageHost describes a single garage instance in the GarageHost.
type GarageHostInstance struct {
ID string
RPCPort int
S3APIPort int
}
// GarageHost describes the garage configuration of a Host which is relevant for
// other hosts to know.
type GarageHost struct {
Instances []GarageHostInstance
}
// HostAssigned are all fields related to a host which were assigned to it by an
// admin.
type HostAssigned struct {
Name nebula.HostName
PublicCredentials nebula.HostPublicCredentials
}
// HostConfigured are all the fields a host can configure for itself.
type HostConfigured struct {
Nebula NebulaHost `json:",omitempty"`
Garage GarageHost `json:",omitempty"`
}
// AuthenticatedHost wraps all the data about a host which other hosts may know
// about it, such that those hosts can authenticate that the data is valid and
// approved by an admin.
type AuthenticatedHost struct {
Assigned nebula.Signed[HostAssigned] // signed by CA
Configured nebula.Signed[HostConfigured] // signed by host
}
// Unwrap attempts to authenticate and unwrap the Host embedded in this
// instance. nebula.ErrInvalidSignature is returned if any signatures are
// invalid.
func (ah AuthenticatedHost) Unwrap(caCreds nebula.CAPublicCredentials) (Host, error) {
assigned, err := ah.Assigned.Unwrap(caCreds.SigningKey)
if err != nil {
return Host{}, fmt.Errorf("unwrapping assigned fields using CA public key: %w", err)
}
configured, err := ah.Configured.Unwrap(assigned.PublicCredentials.SigningKey)
if err != nil {
return Host{}, fmt.Errorf("unwrapping configured fields using host public key: %w", err)
}
return Host{assigned, configured}, nil
}
// Host contains all data bout a host which other hosts may know about it.
//
// A Host should only be obtained over the network as an AuthenticatedHost, and
// subsequently Unwrapped.
type Host struct {
HostAssigned
HostConfigured
}
// IP returns the IP address encoded in the Host's nebula certificate, or panics
// if there is an error.
//
// This assumes that the Host and its data has already been verified against the
// CA signing key.
func (h Host) IP() netip.Addr {
cert := h.PublicCredentials.Cert.Unwrap()
if len(cert.Details.Ips) == 0 {
panic(fmt.Sprintf("host %q not configured with any ips: %+v", h.Name, h))
}
ip := cert.Details.Ips[0].IP
addr, ok := netip.AddrFromSlice(ip)
if !ok {
panic(fmt.Sprintf("ip %q (%#v) is not valid, somehow", ip, ip))
}
return addr
}
// GarageNodes returns a RemoteNode for each garage instance advertised by this
// Host.
func (h Host) GarageNodes() []garage.RemoteNode {
var nodes []garage.RemoteNode
for _, instance := range h.Garage.Instances {
nodes = append(nodes, garage.RemoteNode{
ID: instance.ID,
IP: h.IP().String(),
RPCPort: instance.RPCPort,
S3APIPort: instance.S3APIPort,
})
}
return nodes
}

View File

@ -0,0 +1,14 @@
package main
import (
"fmt"
"isle/bootstrap"
)
func (ctx subCmdCtx) getHosts() ([]bootstrap.Host, error) {
res, err := newDaemonRPCClient().GetHosts(ctx)
if err != nil {
return nil, fmt.Errorf("calling GetHosts: %w", err)
}
return res, nil
}

View File

@ -0,0 +1,87 @@
package main
import (
"context"
"fmt"
"os"
"isle/daemon"
"isle/daemon/daecommon"
)
// TODO it would be good to have an `isle daemon config-check` kind of command,
// which could be run prior to a systemd service restart to make sure we don't
// restart the service into a configuration that will definitely fail.
var subCmdDaemon = subCmd{
name: "daemon",
descr: "Runs the isle daemon (Default if no sub-command given)",
noNetwork: true,
do: func(ctx subCmdCtx) error {
daemonConfigPath := ctx.flags.StringP(
"config-path", "c", "",
"Optional path to a daemon.yml file to load configuration from.",
)
dumpConfig := ctx.flags.Bool(
"dump-config", false,
"Write the default configuration file to stdout and exit.",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *dumpConfig {
return daecommon.CopyDefaultConfig(os.Stdout)
}
logger := ctx.logger()
defer logger.Close()
// TODO check that daemon is either running as root, or that the
// required linux capabilities are set.
// TODO check that the tun module is loaded (for nebula).
daemonConfig, err := daecommon.LoadConfig(*daemonConfigPath)
if err != nil {
return fmt.Errorf("loading daemon config: %w", err)
}
daemonInst, err := daemon.New(
ctx, logger, daemonConfig, envBinDirPath, nil,
)
if err != nil {
return fmt.Errorf("starting daemon: %w", err)
}
defer func() {
logger.Info(ctx, "Stopping child processes")
if err := daemonInst.Shutdown(); err != nil {
logger.Error(ctx, "Shutting down daemon cleanly failed, there may be orphaned child processes", err)
}
logger.Info(ctx, "Child processes successfully stopped")
}()
{
logger := logger.WithNamespace("http")
httpSrv, err := newHTTPServer(
ctx, logger, daemonInst,
)
if err != nil {
return fmt.Errorf("starting HTTP server: %w", err)
}
defer func() {
// see comment in daemonInst shutdown logic regarding background
// context.
logger.Info(ctx, "Shutting down HTTP socket")
if err := httpSrv.Shutdown(context.Background()); err != nil {
logger.Error(ctx, "Failed to cleanly shutdown http server", err)
}
}()
}
<-ctx.Done()
return nil
},
}

View File

@ -0,0 +1,74 @@
package main
import (
"context"
"errors"
"fmt"
"io/fs"
"isle/daemon"
"isle/daemon/jsonrpc2"
"net"
"net/http"
"os"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
const daemonHTTPRPCPath = "/rpc/v0.json"
func newDaemonRPCClient() daemon.RPC {
return daemon.RPCFromClient(
jsonrpc2.NewUnixHTTPClient(
daemon.HTTPSocketPath(), daemonHTTPRPCPath,
),
)
}
func newHTTPServer(
ctx context.Context, logger *mlog.Logger, daemonInst *daemon.Daemon,
) (
*http.Server, error,
) {
socketPath := daemon.HTTPSocketPath()
ctx = mctx.Annotate(ctx, "socketPath", socketPath)
if err := os.Remove(socketPath); errors.Is(err, fs.ErrNotExist) {
// No problem
} else if err != nil {
return nil, fmt.Errorf(
"removing %q prior to listening: %w", socketPath, err,
)
} else {
logger.WarnString(
ctx, "Deleted existing socket file prior to listening, it's possible a previous daemon failed to shutdown gracefully",
)
}
l, err := net.Listen("unix", socketPath)
if err != nil {
return nil, fmt.Errorf(
"listening on socket %q: %w", socketPath, err,
)
}
if err := os.Chmod(socketPath, 0660); err != nil {
return nil, fmt.Errorf(
"setting permissions of %q to 0660: %w", socketPath, err,
)
}
logger.Info(ctx, "HTTP server socket created")
httpMux := http.NewServeMux()
httpMux.Handle(daemonHTTPRPCPath, daemonInst.HTTPRPCHandler())
srv := &http.Server{Handler: httpMux}
go func() {
if err := srv.Serve(l); !errors.Is(err, http.ErrServerClosed) {
logger.Fatal(ctx, "HTTP server unexpectedly shut down", err)
}
}()
return srv, nil
}

View File

@ -0,0 +1,64 @@
package main
import (
"encoding"
"errors"
"fmt"
"isle/nebula"
"isle/toolkit"
"net/netip"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
type textUnmarshaler[T any] interface {
encoding.TextUnmarshaler
*T
}
type textUnmarshalerFlag[T encoding.TextMarshaler, P textUnmarshaler[T]] struct {
V T
}
func (f *textUnmarshalerFlag[T, P]) Set(v string) error {
return P(&(f.V)).UnmarshalText([]byte(v))
}
func (f *textUnmarshalerFlag[T, P]) String() string {
b, err := f.V.MarshalText()
if err != nil {
panic(fmt.Sprintf("calling MarshalText on %#v: %v", f.V, err))
}
return string(b)
}
func (f *textUnmarshalerFlag[T, P]) Type() string { return "string" }
////////////////////////////////////////////////////////////////////////////////
type (
hostNameFlag = textUnmarshalerFlag[nebula.HostName, *nebula.HostName]
ipNetFlag = textUnmarshalerFlag[nebula.IPNet, *nebula.IPNet]
ipFlag = textUnmarshalerFlag[netip.Addr, *netip.Addr]
)
type logLevelFlag struct {
mlog.Level
}
func (f *logLevelFlag) Set(v string) error {
f.Level = toolkit.LogLevelFromString(v)
if f.Level == nil {
return errors.New("not a valid log level")
}
return nil
}
func (f *logLevelFlag) String() string {
if f.Level == nil {
return "UNKNOWN"
}
return f.Level.String()
}
func (f *logLevelFlag) Type() string { return "string" }

163
go/cmd/entrypoint/garage.go Normal file
View File

@ -0,0 +1,163 @@
package main
import (
"errors"
"fmt"
"isle/daemon/daecommon"
"os"
"path/filepath"
"syscall"
)
// minio-client keeps a configuration directory which contains various pieces of
// information which may or may not be useful. Unfortunately when it initializes
// this directory it likes to print some annoying logs, so we pre-initialize in
// order to prevent it from doing so.
func initMCConfigDir(envVars daecommon.EnvVars) (string, error) {
var (
path = filepath.Join(envVars.StateDir.Path, "mc")
sharePath = filepath.Join(path, "share")
configJSONPath = filepath.Join(path, "config.json")
)
if err := os.MkdirAll(sharePath, 0700); err != nil {
return "", fmt.Errorf("creating %q: %w", sharePath, err)
}
if err := os.WriteFile(configJSONPath, []byte(`{}`), 0600); err != nil {
return "", fmt.Errorf("writing %q: %w", configJSONPath, err)
}
return path, nil
}
var subCmdGarageMC = subCmd{
name: "mc",
descr: "Runs the mc (minio-client) binary. The isle garage can be accessed under the `garage` alias",
passthroughArgs: true,
do: func(ctx subCmdCtx) error {
keyID := ctx.flags.StringP(
"key-id", "i", "",
"Optional key ID to use, defaults to that of the shared global key",
)
keySecret := ctx.flags.StringP(
"key-secret", "s", "",
"Optional key secret to use, defaults to that of the shared global key",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
clientParams, err := newDaemonRPCClient().GetGarageClientParams(ctx)
if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err)
}
s3APIAddr := clientParams.Node.S3APIAddr()
if *keyID == "" {
*keyID = clientParams.GlobalBucketS3APICredentials.ID
}
if *keySecret == "" {
*keySecret = clientParams.GlobalBucketS3APICredentials.Secret
}
args := ctx.flags.Args()
if i := ctx.flags.ArgsLenAtDash(); i >= 0 {
args = args[i:]
}
envVars := daecommon.GetEnvVars()
configDir, err := initMCConfigDir(envVars)
if err != nil {
return fmt.Errorf("initializing minio-client config directory: %w", err)
}
args = append([]string{
binPath("mc"),
"--config-dir", configDir,
}, args...)
var (
mcHostVar = fmt.Sprintf(
"MC_HOST_garage=http://%s:%s@%s",
*keyID, *keySecret, s3APIAddr,
)
binPath = binPath("mc")
cliEnv = append(
os.Environ(),
mcHostVar,
// The garage docs say this is necessary, though nothing bad
// seems to happen if we leave it out *shrug*
"MC_REGION=garage",
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarageCLI = subCmd{
name: "cli",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
clientParams, err := newDaemonRPCClient().GetGarageClientParams(ctx)
if err != nil {
return fmt.Errorf("calling GetGarageClientParams: %w", err)
}
if clientParams.RPCSecret == "" {
return errors.New("this host does not have the garage RPC secret")
}
var (
binPath = binPath("garage")
args = append([]string{"garage"}, ctx.args...)
cliEnv = append(
os.Environ(),
"GARAGE_RPC_HOST="+clientParams.Node.RPCNodeAddr(),
"GARAGE_RPC_SECRET="+clientParams.RPCSecret,
)
)
if err := syscall.Exec(binPath, args, cliEnv); err != nil {
return fmt.Errorf(
"calling exec(%q, %#v, %#v): %w",
binPath, args, cliEnv, err,
)
}
return nil
},
}
var subCmdGarage = subCmd{
name: "garage",
descr: "Runs the garage binary, automatically configured to point to the garage sub-process of a running isle daemon",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdGarageCLI,
subCmdGarageMC,
)
},
}

143
go/cmd/entrypoint/host.go Normal file
View File

@ -0,0 +1,143 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"isle/bootstrap"
"isle/daemon/network"
"isle/jsonutil"
"os"
"sort"
)
var subCmdHostCreate = subCmd{
name: "create",
descr: "Creates a new host in the network, writing its new bootstrap.json to stdout",
do: func(ctx subCmdCtx) error {
var (
hostName hostNameFlag
ip ipFlag
)
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of the host to generate bootstrap.json for",
)
ctx.flags.VarP(&ip, "ip", "i", "IP of the new host. An available IP will be chosen if none is given.")
canCreateHosts := ctx.flags.Bool(
"can-create-hosts",
false,
"The new host should have the ability to create hosts too",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed {
return errors.New("--hostname is required")
}
res, err := newDaemonRPCClient().CreateHost(
ctx, hostName.V, network.CreateHostOpts{
IP: ip.V,
CanCreateHosts: *canCreateHosts,
},
)
if err != nil {
return fmt.Errorf("calling CreateHost: %w", err)
}
return json.NewEncoder(os.Stdout).Encode(res)
},
}
var subCmdHostList = subCmd{
name: "list",
descr: "Lists all hosts in the network, and their IPs",
do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
hostsRes, err := ctx.getHosts()
if err != nil {
return fmt.Errorf("calling GetHosts: %w", err)
}
type host struct {
Name string
VPN struct {
IP string
}
Storage bootstrap.GarageHost `json:",omitempty"`
}
hosts := make([]host, 0, len(hostsRes))
for _, h := range hostsRes {
host := host{
Name: string(h.Name),
Storage: h.Garage,
}
host.VPN.IP = h.IP().String()
hosts = append(hosts, host)
}
sort.Slice(hosts, func(i, j int) bool { return hosts[i].Name < hosts[j].Name })
return jsonutil.WriteIndented(os.Stdout, hosts)
},
}
var subCmdHostRemove = subCmd{
name: "remove",
descr: "Removes a host from the network",
do: func(ctx subCmdCtx) error {
var (
hostName hostNameFlag
)
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of the host to remove",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed {
return errors.New("--hostname is required")
}
if err := newDaemonRPCClient().RemoveHost(ctx, hostName.V); err != nil {
return fmt.Errorf("calling RemoveHost: %w", err)
}
return nil
},
}
var subCmdHost = subCmd{
name: "host",
plural: "s",
descr: "Sub-commands having to do with configuration of hosts in the network",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdHostCreate,
subCmdHostRemove,
subCmdHostList,
)
},
}

68
go/cmd/entrypoint/main.go Normal file
View File

@ -0,0 +1,68 @@
package main
import (
"context"
"os"
"os/signal"
"path/filepath"
"syscall"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
func getAppDirPath() string {
appDirPath := os.Getenv("APPDIR")
if appDirPath == "" {
appDirPath = "."
}
return appDirPath
}
var (
envAppDirPath = getAppDirPath()
envBinDirPath = filepath.Join(envAppDirPath, "bin")
)
func binPath(name string) string {
return filepath.Join(envBinDirPath, name)
}
func main() {
logger := mlog.NewLogger(nil)
defer logger.Close()
ctx, cancel := context.WithCancel(context.Background())
signalCh := make(chan os.Signal, 2)
signal.Notify(signalCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-signalCh
cancel()
ctx := mctx.Annotate(ctx, "signal", sig.String())
logger.Info(ctx, "got signal, exiting gracefully")
sig = <-signalCh
ctx = mctx.Annotate(ctx, "signal", sig.String())
logger.FatalString(ctx, "second signal received, force quitting, there may be zombie children left behind, good luck!")
}()
err := subCmdCtx{
Context: ctx,
args: os.Args[1:],
}.doSubCmd(
subCmdDaemon,
subCmdGarage,
subCmdHost,
subCmdNebula,
subCmdNetwork,
subCmdVersion,
)
if err != nil {
logger.Fatal(ctx, "error running command", err)
}
}

140
go/cmd/entrypoint/nebula.go Normal file
View File

@ -0,0 +1,140 @@
package main
import (
"errors"
"fmt"
"isle/jsonutil"
"isle/nebula"
"os"
)
var subCmdNebulaCreateCert = subCmd{
name: "create-cert",
descr: "Creates a signed nebula certificate file for an existing host and writes it to stdout",
do: func(ctx subCmdCtx) error {
var hostName hostNameFlag
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of the host to generate a certificate for",
)
pubKeyPath := ctx.flags.StringP(
"public-key-path", "p", "",
`Path to PEM file containing public key which will be embedded in the cert.`,
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if !hostNameF.Changed || *pubKeyPath == "" {
return errors.New("--hostname and --pub-key-path are required")
}
hostPubPEM, err := os.ReadFile(*pubKeyPath)
if err != nil {
return fmt.Errorf("reading public key from %q: %w", *pubKeyPath, err)
}
var hostPub nebula.EncryptingPublicKey
if err := hostPub.UnmarshalNebulaPEM(hostPubPEM); err != nil {
return fmt.Errorf("unmarshaling public key as PEM: %w", err)
}
res, err := newDaemonRPCClient().CreateNebulaCertificate(
ctx, hostName.V, hostPub,
)
if err != nil {
return fmt.Errorf("calling CreateNebulaCertificate: %w", err)
}
nebulaHostCertPEM, err := res.Unwrap().MarshalToPEM()
if err != nil {
return fmt.Errorf("marshaling cert to PEM: %w", err)
}
if _, err := os.Stdout.Write([]byte(nebulaHostCertPEM)); err != nil {
return fmt.Errorf("writing to stdout: %w", err)
}
return nil
},
}
var subCmdNebulaShow = subCmd{
name: "show",
descr: "Writes nebula network information to stdout in JSON format",
do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
hosts, err := ctx.getHosts()
if err != nil {
return fmt.Errorf("getting hosts: %w", err)
}
caPublicCreds, err := newDaemonRPCClient().GetNebulaCAPublicCredentials(ctx)
if err != nil {
return fmt.Errorf("calling GetNebulaCAPublicCredentials: %w", err)
}
caCert := caPublicCreds.Cert
caCertDetails := caCert.Unwrap().Details
if len(caCertDetails.Subnets) != 1 {
return fmt.Errorf(
"malformed ca.crt, contains unexpected subnets %#v",
caCertDetails.Subnets,
)
}
subnet := caCertDetails.Subnets[0]
type outLighthouse struct {
PublicAddr string
IP string
}
out := struct {
CACert nebula.Certificate
SubnetCIDR string
Lighthouses []outLighthouse
}{
CACert: caCert,
SubnetCIDR: subnet.String(),
}
for _, h := range hosts {
if h.Nebula.PublicAddr == "" {
continue
}
out.Lighthouses = append(out.Lighthouses, outLighthouse{
PublicAddr: h.Nebula.PublicAddr,
IP: h.IP().String(),
})
}
if err := jsonutil.WriteIndented(os.Stdout, out); err != nil {
return fmt.Errorf("encoding to stdout: %w", err)
}
return nil
},
}
var subCmdNebula = subCmd{
name: "nebula",
descr: "Sub-commands related to the nebula VPN",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdNebulaCreateCert,
subCmdNebulaShow,
)
},
}

View File

@ -0,0 +1,126 @@
package main
import (
"errors"
"fmt"
"isle/daemon/network"
"isle/jsonutil"
"os"
)
var subCmdNetworkCreate = subCmd{
name: "create",
descr: "Create's a new network, with this host being the first host in that network.",
noNetwork: true,
do: func(ctx subCmdCtx) error {
var (
ipNet ipNetFlag
hostName hostNameFlag
)
name := ctx.flags.StringP(
"name", "N", "",
"Human-readable name to identify the network as.",
)
domain := ctx.flags.StringP(
"domain", "d", "",
"Domain name that should be used as the root domain in the network.",
)
ipNetF := ctx.flags.VarPF(
&ipNet, "ip-net", "i",
`An IP subnet, in CIDR form, which will be the overall range of`+
` possible IPs in the network. The first IP in this network`+
` range will become this first host's IP.`,
)
hostNameF := ctx.flags.VarPF(
&hostName,
"hostname", "n",
"Name of this host, which will be the first host in the network",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *name == "" ||
*domain == "" ||
!ipNetF.Changed ||
!hostNameF.Changed {
return errors.New("--name, --domain, --ip-net, and --hostname are required")
}
err = newDaemonRPCClient().CreateNetwork(
ctx, *name, *domain, ipNet.V, hostName.V,
)
if err != nil {
return fmt.Errorf("creating network: %w", err)
}
return nil
},
}
var subCmdNetworkJoin = subCmd{
name: "join",
descr: "Joins this host to an existing network",
noNetwork: true,
do: func(ctx subCmdCtx) error {
bootstrapPath := ctx.flags.StringP(
"bootstrap-path", "b", "", "Path to a bootstrap.json file.",
)
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
if *bootstrapPath == "" {
return errors.New("--bootstrap-path is required")
}
var newBootstrap network.JoiningBootstrap
if err := jsonutil.LoadFile(&newBootstrap, *bootstrapPath); err != nil {
return fmt.Errorf(
"loading bootstrap from %q: %w", *bootstrapPath, err,
)
}
return newDaemonRPCClient().JoinNetwork(ctx, newBootstrap)
},
}
var subCmdNetworkList = subCmd{
name: "list",
descr: "Lists all networks which have been joined",
noNetwork: true,
do: func(ctx subCmdCtx) error {
ctx, err := ctx.withParsedFlags()
if err != nil {
return fmt.Errorf("parsing flags: %w", err)
}
creationParams, err := newDaemonRPCClient().GetNetworks(ctx)
if err != nil {
return fmt.Errorf("getting joined networks: %w", err)
}
return jsonutil.WriteIndented(os.Stdout, creationParams)
},
}
var subCmdNetwork = subCmd{
name: "network",
descr: "Sub-commands related to network membership",
plural: "s",
do: func(ctx subCmdCtx) error {
return ctx.doSubCmd(
subCmdNetworkCreate,
subCmdNetworkJoin,
subCmdNetworkList,
)
},
}

View File

@ -0,0 +1,193 @@
package main
import (
"context"
"fmt"
"isle/daemon"
"os"
"strings"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"github.com/spf13/pflag"
)
type flagSet struct {
*pflag.FlagSet
network string
logLevel logLevelFlag
}
type subCmd struct {
name string
descr string
do func(subCmdCtx) error
// If set then the name will be allowed to be suffixed with this string.
plural string
// noNetwork, if true, means the call doesn't require a network to be
// specified on the command-line if there are more than one networks
// configured.
noNetwork bool
// Extra arguments on the command-line will be passed through to some
// underlying command.
passthroughArgs bool
}
// subCmdCtx contains all information available to a subCmd's do method.
type subCmdCtx struct {
context.Context
subCmd subCmd // the subCmd itself
args []string // command-line arguments, excluding the subCmd itself.
subCmdNames []string // names of subCmds so far, including this one
flags *flagSet
}
func newSubCmdCtx(
ctx context.Context,
subCmd subCmd,
args []string,
subCmdNames []string,
) subCmdCtx {
flags := pflag.NewFlagSet(subCmd.name, pflag.ExitOnError)
flags.Usage = func() {
var passthroughStr string
if subCmd.passthroughArgs {
passthroughStr = " [--] [args...]"
}
fmt.Fprintf(
os.Stderr, "%s[-h|--help] [%s flags...]%s\n\n",
usagePrefix(subCmdNames), subCmd.name, passthroughStr,
)
fmt.Fprintf(os.Stderr, "%s FLAGS:\n\n", strings.ToUpper(subCmd.name))
fmt.Fprintln(os.Stderr, flags.FlagUsages())
os.Stderr.Sync()
os.Exit(2)
}
fs := &flagSet{
FlagSet: flags,
logLevel: logLevelFlag{mlog.LevelInfo},
}
if !subCmd.noNetwork {
fs.FlagSet.StringVar(
&fs.network, "network", "", "Which network to perform the command against, if more than one is joined. Can be an ID, name, or domain.",
)
}
fs.FlagSet.VarP(
&fs.logLevel,
"log-level", "l",
"Maximum log level to output. Can be DEBUG, CHILD, INFO, WARN, ERROR, or FATAL.",
)
return subCmdCtx{
Context: ctx,
subCmd: subCmd,
args: args,
subCmdNames: subCmdNames,
flags: fs,
}
}
func usagePrefix(subCmdNames []string) string {
subCmdNamesStr := strings.Join(subCmdNames, " ")
if subCmdNamesStr != "" {
subCmdNamesStr += " "
}
return fmt.Sprintf("\nUSAGE: %s %s", os.Args[0], subCmdNamesStr)
}
func (ctx subCmdCtx) logger() *mlog.Logger {
return mlog.NewLogger(&mlog.LoggerOpts{
MaxLevel: ctx.flags.logLevel.Int(),
})
}
func (ctx subCmdCtx) withParsedFlags() (subCmdCtx, error) {
ctx.flags.VisitAll(func(f *pflag.Flag) {
if f.Shorthand == "h" {
panic(fmt.Sprintf("flag %+v has reserved shorthand `-h`", f))
}
if f.Name == "help" {
panic(fmt.Sprintf("flag %+v has reserved name `--help`", f))
}
})
if err := ctx.flags.Parse(ctx.args); err != nil {
return ctx, err
}
ctx.Context = daemon.WithNetwork(ctx.Context, ctx.flags.network)
return ctx, nil
}
func (ctx subCmdCtx) doSubCmd(subCmds ...subCmd) error {
printUsageExit := func(subCmdName string) {
fmt.Fprintf(os.Stderr, "unknown sub-command %q\n", subCmdName)
fmt.Fprintf(
os.Stderr,
"%s<subCmd> [-h|--help] [sub-command flags...]\n",
usagePrefix(ctx.subCmdNames),
)
fmt.Fprintf(os.Stderr, "\nSUB-COMMANDS:\n\n")
for _, subCmd := range subCmds {
name := subCmd.name
if subCmd.plural != "" {
name += "(" + subCmd.plural + ")"
}
fmt.Fprintf(os.Stderr, " %s\t%s\n", name, subCmd.descr)
}
fmt.Fprintf(os.Stderr, "\n")
os.Stderr.Sync()
os.Exit(2)
}
args := ctx.args
if len(args) == 0 {
printUsageExit("")
}
subCmdsMap := map[string]subCmd{}
for _, subCmd := range subCmds {
subCmdsMap[subCmd.name] = subCmd
if subCmd.plural != "" {
subCmdsMap[subCmd.name+subCmd.plural] = subCmd
}
}
subCmdName, args := args[0], args[1:]
subCmd, ok := subCmdsMap[subCmdName]
if !ok {
printUsageExit(subCmdName)
}
nextSubCmdCtx := newSubCmdCtx(
ctx.Context,
subCmd,
args,
append(ctx.subCmdNames, subCmdName),
)
if err := subCmd.do(nextSubCmdCtx); err != nil {
return err
}
return nil
}

View File

@ -1,4 +1,4 @@
package entrypoint
package main
import (
"fmt"
@ -7,11 +7,12 @@ import (
)
var subCmdVersion = subCmd{
name: "version",
descr: "Dumps version and build info to stdout",
do: func(subCmdCtx subCmdCtx) error {
name: "version",
descr: "Dumps version and build info to stdout",
noNetwork: true,
do: func(ctx subCmdCtx) error {
versionPath := filepath.Join(subCmdCtx.env.AppDirPath, "share/version")
versionPath := filepath.Join(envAppDirPath, "share/version")
version, err := os.ReadFile(versionPath)

View File

@ -0,0 +1,289 @@
// Package children manages the creation, lifetime, and shutdown of child
// processes created by the daemon.
package children
import (
"context"
"errors"
"fmt"
"code.betamike.com/micropelago/pmux/pmuxlib"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"isle/bootstrap"
"isle/daemon/daecommon"
"isle/secrets"
"isle/toolkit"
)
// Opts are optional parameters which can be passed in when initializing a new
// Children instance. A nil Opts is equivalent to a zero value.
type Opts struct{}
func (o *Opts) withDefaults() *Opts {
if o == nil {
o = new(Opts)
}
return o
}
// Children manages all child processes of a network. Child processes are
// comprised of:
// - nebula
// - dnsmasq
// - garage (0 or more, depending on configured storage allocations)
type Children struct {
logger *mlog.Logger
binDirPath string
runtimeDir toolkit.Dir
garageAdminToken string
opts Opts
garageRPCSecret string
nebulaProc *pmuxlib.Process
dnsmasqProc *pmuxlib.Process
garageProcs map[string]*pmuxlib.Process
}
// New initializes and returns a Children instance. If initialization fails an
// error is returned.
func New(
ctx context.Context,
logger *mlog.Logger,
binDirPath string,
secretsStore secrets.Store,
networkConfig daecommon.NetworkConfig,
runtimeDir toolkit.Dir,
garageAdminToken string,
hostBootstrap bootstrap.Bootstrap,
opts *Opts,
) (
*Children, error,
) {
opts = opts.withDefaults()
logger.Info(ctx, "Loading secrets")
garageRPCSecret, err := daecommon.GetGarageRPCSecret(ctx, secretsStore)
if err != nil && !errors.Is(err, secrets.ErrNotFound) {
return nil, fmt.Errorf("loading garage RPC secret: %w", err)
}
c := &Children{
logger: logger,
binDirPath: binDirPath,
runtimeDir: runtimeDir,
garageAdminToken: garageAdminToken,
opts: *opts,
garageRPCSecret: garageRPCSecret,
}
if c.nebulaProc, err = nebulaPmuxProc(
ctx,
c.logger,
c.runtimeDir.Path,
c.binDirPath,
networkConfig,
hostBootstrap,
); err != nil {
return nil, fmt.Errorf("starting nebula: %w", err)
}
if err := waitForNebula(ctx, c.logger, hostBootstrap); err != nil {
logger.Warn(ctx, "Failed waiting for nebula to initialize, shutting down child processes", err)
c.Shutdown()
return nil, fmt.Errorf("waiting for nebula to start: %w", err)
}
if c.dnsmasqProc, err = dnsmasqPmuxProc(
ctx,
c.logger,
c.runtimeDir.Path,
c.binDirPath,
networkConfig,
hostBootstrap,
); err != nil {
logger.Warn(ctx, "Failed to start dnsmasq, shutting down child processes", err)
c.Shutdown()
return nil, fmt.Errorf("starting dnsmasq: %w", err)
}
// TODO wait for dnsmasq to come up
if c.garageProcs, err = garagePmuxProcs(
ctx,
c.logger,
garageRPCSecret,
c.runtimeDir.Path,
c.binDirPath,
networkConfig,
garageAdminToken,
hostBootstrap,
); err != nil {
logger.Warn(ctx, "Failed to start garage processes, shutting down child processes", err)
c.Shutdown()
return nil, fmt.Errorf("starting garage processes: %w", err)
}
if err := waitForGarage(
ctx, c.logger, networkConfig, garageAdminToken, hostBootstrap,
); err != nil {
logger.Warn(ctx, "Failed waiting for garage processes to initialize, shutting down child processes", err)
c.Shutdown()
return nil, fmt.Errorf("waiting for garage processes to initialize: %w", err)
}
return c, nil
}
// TODO block until process has been confirmed to have come back up
// successfully.
func (c *Children) reloadDNSMasq(
ctx context.Context,
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) error {
if _, changed, err := dnsmasqWriteConfig(
ctx, c.logger, c.runtimeDir.Path, networkConfig, hostBootstrap,
); err != nil {
return fmt.Errorf("writing new dnsmasq config: %w", err)
} else if !changed {
c.logger.Info(ctx, "No changes to dnsmasq config file")
return nil
}
c.logger.Info(ctx, "dnsmasq config file has changed, restarting process")
c.dnsmasqProc.Restart()
return nil
}
func (c *Children) reloadNebula(
ctx context.Context,
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) error {
if _, changed, err := nebulaWriteConfig(
ctx, c.logger, c.runtimeDir.Path, networkConfig, hostBootstrap,
); err != nil {
return fmt.Errorf("writing a new nebula config: %w", err)
} else if !changed {
c.logger.Info(ctx, "No changes to nebula config file")
return nil
}
c.logger.Info(ctx, "nebula config file has changed, restarting process")
c.nebulaProc.Restart()
if err := waitForNebula(ctx, c.logger, hostBootstrap); err != nil {
return fmt.Errorf("waiting for nebula to start: %w", err)
}
return nil
}
func (c *Children) reloadGarage(
ctx context.Context,
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) error {
allocs := networkConfig.Storage.Allocations
if len(allocs) == 0 {
return nil
}
var anyChanged bool
for _, alloc := range allocs {
var (
procName = garagePmuxProcName(alloc)
ctx = mctx.Annotate(
ctx,
"garageProcName", procName,
"garageDataPath", alloc.DataPath,
)
)
// TODO it's possible that the config changed, but only the bootstrap
// peers, in which case we don't need to restart the node.
childConfigPath, changed, err := garageWriteChildConfig(
ctx,
c.logger,
c.garageRPCSecret,
c.runtimeDir.Path,
c.garageAdminToken,
hostBootstrap,
alloc,
)
if err != nil {
return fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
} else if !changed {
c.logger.Info(ctx, "No changes to garage config file")
continue
}
anyChanged = true
if proc, ok := c.garageProcs[procName]; ok {
c.logger.Info(ctx, "garage config has changed, restarting process")
proc.Restart()
continue
}
c.logger.Info(ctx, "garage config has been added, creating process")
c.garageProcs[procName] = garagePmuxProc(
ctx, c.logger, c.binDirPath, procName, childConfigPath,
)
}
if anyChanged {
if err := waitForGarage(
ctx, c.logger, networkConfig, c.garageAdminToken, hostBootstrap,
); err != nil {
return fmt.Errorf("waiting for garage to start: %w", err)
}
}
return nil
}
// Reload applies a ReloadDiff to the Children, using the given bootstrap which
// must be the same one which was passed to CalculateReloadDiff.
func (c *Children) Reload(
ctx context.Context,
newNetworkConfig daecommon.NetworkConfig,
newBootstrap bootstrap.Bootstrap,
) error {
if err := c.reloadNebula(ctx, newNetworkConfig, newBootstrap); err != nil {
return fmt.Errorf("reloading nebula: %w", err)
}
var errs []error
if err := c.reloadDNSMasq(ctx, newNetworkConfig, newBootstrap); err != nil {
errs = append(errs, fmt.Errorf("reloading dnsmasq: %w", err))
}
if err := c.reloadGarage(ctx, newNetworkConfig, newBootstrap); err != nil {
errs = append(errs, fmt.Errorf("reloading garage: %w", err))
}
return errors.Join(errs...)
}
// Shutdown blocks until all child processes have gracefully shut themselves
// down.
func (c *Children) Shutdown() {
for _, proc := range c.garageProcs {
proc.Stop()
}
if c.dnsmasqProc != nil {
c.dnsmasqProc.Stop()
}
if c.nebulaProc != nil {
c.nebulaProc.Stop()
}
}

View File

@ -0,0 +1,84 @@
package children
import (
"context"
"fmt"
"isle/bootstrap"
"isle/daemon/daecommon"
"isle/dnsmasq"
"path/filepath"
"code.betamike.com/micropelago/pmux/pmuxlib"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
func dnsmasqWriteConfig(
ctx context.Context,
logger *mlog.Logger,
runtimeDirPath string,
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) (
string, bool, error,
) {
hosts := make([]dnsmasq.ConfDataHost, 0, len(hostBootstrap.Hosts))
for _, host := range hostBootstrap.Hosts {
hosts = append(hosts, dnsmasq.ConfDataHost{
Name: string(host.Name),
IP: host.IP().String(),
})
}
var (
confPath = filepath.Join(runtimeDirPath, "dnsmasq.conf")
confData = dnsmasq.ConfData{
Resolvers: networkConfig.DNS.Resolvers,
Domain: hostBootstrap.NetworkCreationParams.Domain,
IP: hostBootstrap.ThisHost().IP().String(),
Hosts: hosts,
}
)
changed, err := dnsmasq.WriteConfFile(ctx, logger, confPath, confData)
if err != nil {
return "", false, fmt.Errorf(
"writing dnsmasq.conf to %q: %w", confPath, err,
)
}
return confPath, changed, nil
}
// TODO consider a shared dnsmasq across all the daemon's networks.
// This would have a few benefits:
// - Less processes, less problems
// - Less configuration for the user in the case of more than one network.
// - Can listen on 127.0.0.x:53, rather than on the nebula address. This
// allows DNS to come up before nebula, which is helpful when nebula depends
// on DNS.
func dnsmasqPmuxProc(
ctx context.Context,
logger *mlog.Logger,
runtimeDirPath, binDirPath string,
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) (
*pmuxlib.Process, error,
) {
confPath, _, err := dnsmasqWriteConfig(
ctx, logger, runtimeDirPath, networkConfig, hostBootstrap,
)
if err != nil {
return nil, fmt.Errorf(
"writing dnsmasq config: %w", err,
)
}
cfg := pmuxlib.ProcessConfig{
Cmd: filepath.Join(binDirPath, "dnsmasq"),
Args: []string{"-d", "-C", confPath},
}
cfg = withPmuxLoggers(ctx, logger, "dnsmasq", cfg)
return pmuxlib.NewProcess(cfg), nil
}

View File

@ -0,0 +1,178 @@
package children
import (
"context"
"errors"
"fmt"
"isle/bootstrap"
"isle/daemon/daecommon"
"isle/garage"
"isle/garage/garagesrv"
"net"
"path/filepath"
"strconv"
"code.betamike.com/micropelago/pmux/pmuxlib"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
func garageAdminClientLogger(logger *mlog.Logger) *mlog.Logger {
return logger.WithNamespace("garageAdminClient")
}
func waitForGarage(
ctx context.Context,
logger *mlog.Logger,
networkConfig daecommon.NetworkConfig,
adminToken string,
hostBootstrap bootstrap.Bootstrap,
) error {
allocs := networkConfig.Storage.Allocations
if len(allocs) == 0 {
return nil
}
adminClientLogger := garageAdminClientLogger(logger)
for _, alloc := range allocs {
adminAddr := net.JoinHostPort(
hostBootstrap.ThisHost().IP().String(),
strconv.Itoa(alloc.AdminPort),
)
adminClient := garage.NewAdminClient(
adminClientLogger, adminAddr, adminToken,
)
ctx := mctx.Annotate(
ctx, "garageAdminAddr", adminAddr, "garageDataPath", alloc.DataPath,
)
logger.Info(ctx, "Waiting for garage instance to be healthy")
if err := adminClient.Wait(ctx); err != nil {
return fmt.Errorf("waiting for garage instance %q to start up: %w", adminAddr, err)
}
adminClient.Close()
}
return nil
}
func garageWriteChildConfig(
ctx context.Context,
logger *mlog.Logger,
rpcSecret, runtimeDirPath, adminToken string,
hostBootstrap bootstrap.Bootstrap,
alloc daecommon.ConfigStorageAllocation,
) (
string, bool, error,
) {
var (
thisHost = hostBootstrap.ThisHost()
id = daecommon.BootstrapGarageHostForAlloc(thisHost, alloc).ID
node = garage.LocalNode{
RemoteNode: garage.RemoteNode{
ID: id,
IP: thisHost.IP().String(),
RPCPort: alloc.RPCPort,
S3APIPort: alloc.S3APIPort,
},
AdminPort: alloc.AdminPort,
}
garageTomlPath = filepath.Join(
runtimeDirPath, fmt.Sprintf("garage-%d.toml", alloc.RPCPort),
)
)
changed, err := garagesrv.WriteGarageTomlFile(
ctx,
logger,
garageTomlPath,
garagesrv.GarageTomlData{
MetaPath: alloc.MetaPath,
DataPath: alloc.DataPath,
RPCSecret: rpcSecret,
AdminToken: adminToken,
LocalNode: node,
BootstrapPeers: hostBootstrap.GarageNodes(),
},
)
if err != nil {
return "", false, fmt.Errorf(
"creating garage.toml file at %q: %w", garageTomlPath, err,
)
}
return garageTomlPath, changed, nil
}
func garagePmuxProcName(alloc daecommon.ConfigStorageAllocation) string {
return fmt.Sprintf("garage-%d", alloc.RPCPort)
}
func garagePmuxProc(
ctx context.Context,
logger *mlog.Logger,
binDirPath string,
procName string,
childConfigPath string,
) *pmuxlib.Process {
cfg := pmuxlib.ProcessConfig{
Cmd: filepath.Join(binDirPath, "garage"),
Args: []string{"-c", childConfigPath, "server"},
}
cfg = withPmuxLoggers(ctx, logger, procName, cfg)
return pmuxlib.NewProcess(cfg)
}
func garagePmuxProcs(
ctx context.Context,
logger *mlog.Logger,
rpcSecret, runtimeDirPath, binDirPath string,
networkConfig daecommon.NetworkConfig,
adminToken string,
hostBootstrap bootstrap.Bootstrap,
) (
map[string]*pmuxlib.Process, error,
) {
var (
pmuxProcs = map[string]*pmuxlib.Process{}
allocs = networkConfig.Storage.Allocations
)
if len(allocs) > 0 && rpcSecret == "" {
return nil, errors.New("Storage allocations defined, but garage RPC secret is not available")
}
for _, alloc := range allocs {
childConfigPath, _, err := garageWriteChildConfig(
ctx,
logger,
rpcSecret, runtimeDirPath, adminToken,
hostBootstrap,
alloc,
)
if err != nil {
return nil, fmt.Errorf("writing child config file for alloc %+v: %w", alloc, err)
}
procName := garagePmuxProcName(alloc)
pmuxProcs[procName] = garagePmuxProc(
ctx, logger, binDirPath, procName, childConfigPath,
)
}
return pmuxProcs, nil
}

View File

@ -0,0 +1,30 @@
package children
import (
"context"
"time"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
// until keeps trying fn until it returns nil, returning true. If the context is
// canceled then until returns false.
func until(
ctx context.Context,
logger *mlog.Logger,
descr string,
fn func(context.Context) error,
) bool {
for {
logger.Info(ctx, descr)
err := fn(ctx)
if err == nil {
return true
} else if ctxErr := ctx.Err(); ctxErr != nil {
return false
}
logger.Warn(ctx, descr+" failed, retrying in one second", err)
time.Sleep(1 * time.Second)
}
}

View File

@ -0,0 +1,59 @@
package children
import (
"context"
"fmt"
"isle/toolkit"
"code.betamike.com/micropelago/pmux/pmuxlib"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
)
type pmuxLogger struct {
ctx context.Context
logger *mlog.Logger
}
func newPmuxStdoutLogger(
ctx context.Context, logger *mlog.Logger, name string,
) pmuxlib.Logger {
return &pmuxLogger{ctx, logger.WithNamespace(name).WithNamespace("out")}
}
func newPmuxStderrLogger(
ctx context.Context, logger *mlog.Logger, name string,
) pmuxlib.Logger {
return &pmuxLogger{ctx, logger.WithNamespace(name).WithNamespace("err")}
}
func newPmuxSysLogger(
ctx context.Context, logger *mlog.Logger, name string,
) pmuxlib.Logger {
return &pmuxLogger{ctx, logger.WithNamespace(name).WithNamespace("sys")}
}
func (l *pmuxLogger) Println(line string) {
l.logger.Log(mlog.Message{
Context: l.ctx,
Level: toolkit.LogLevelChild,
Description: line,
})
}
func (l *pmuxLogger) Printf(format string, args ...any) {
l.Println(fmt.Sprintf(format, args...))
}
////////////////////////////////////////////////////////////////////////////////
func withPmuxLoggers(
ctx context.Context,
logger *mlog.Logger,
name string,
cfg pmuxlib.ProcessConfig,
) pmuxlib.ProcessConfig {
cfg.StdoutLogger = newPmuxStdoutLogger(ctx, logger, name)
cfg.StderrLogger = newPmuxStderrLogger(ctx, logger, name)
cfg.SysLogger = newPmuxSysLogger(ctx, logger, name)
return cfg
}

View File

@ -0,0 +1,200 @@
package children
import (
"context"
"fmt"
"io"
"isle/bootstrap"
"isle/daemon/daecommon"
"isle/toolkit"
"net"
"path/filepath"
"code.betamike.com/micropelago/pmux/pmuxlib"
"dev.mediocregopher.com/mediocre-go-lib.git/mctx"
"dev.mediocregopher.com/mediocre-go-lib.git/mlog"
"github.com/slackhq/nebula/cert"
"gopkg.in/yaml.v3"
)
// waitForNebula waits for the nebula interface to have been started up. It does
// this by attempting to create a UDP connection which has the nebula IP set as
// its source. If this succeeds we can assume that at the very least the nebula
// interface has been initialized.
func waitForNebula(
ctx context.Context, logger *mlog.Logger, hostBootstrap bootstrap.Bootstrap,
) error {
var (
ip = net.IP(hostBootstrap.ThisHost().IP().AsSlice())
lUDPAddr = &net.UDPAddr{IP: ip, Port: 0}
rUDPAddr = &net.UDPAddr{IP: ip, Port: 45535}
)
ctx = mctx.Annotate(ctx, "lUDPAddr", lUDPAddr, "rUDPAddr", rUDPAddr)
until(
ctx,
logger,
"Checking if nebula is online by creating UDP socket from nebula IP",
func(context.Context) error {
conn, err := net.DialUDP("udp", lUDPAddr, rUDPAddr)
if err != nil {
return err
}
conn.Close()
return nil
},
)
return ctx.Err()
}
// TODO this needs to be produce a deterministic config value.
func nebulaConfig(
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) (
map[string]any, error,
) {
var (
lighthouseHostIPs []string
staticHostMap = map[string][]string{}
)
for _, host := range hostBootstrap.Hosts {
if host.Nebula.PublicAddr == "" {
continue
}
ip := host.IP().String()
lighthouseHostIPs = append(lighthouseHostIPs, ip)
staticHostMap[ip] = []string{host.Nebula.PublicAddr}
}
caCertPEM, err := hostBootstrap.CAPublicCredentials.Cert.Unwrap().MarshalToPEM()
if err != nil {
return nil, fmt.Errorf("marshaling CA cert to PEM: :%w", err)
}
hostCertPEM, err := hostBootstrap.PublicCredentials.Cert.Unwrap().MarshalToPEM()
if err != nil {
return nil, fmt.Errorf("marshaling host cert to PEM: :%w", err)
}
hostKeyPEM := cert.MarshalX25519PrivateKey(
hostBootstrap.PrivateCredentials.EncryptingPrivateKey.Bytes(),
)
config := map[string]any{
"pki": map[string]string{
"ca": string(caCertPEM),
"cert": string(hostCertPEM),
"key": string(hostKeyPEM),
},
"static_host_map": staticHostMap,
"punchy": map[string]bool{
"punch": true,
"respond": true,
},
"tun": map[string]any{
"dev": networkConfig.VPN.Tun.Device,
},
"firewall": networkConfig.VPN.Firewall,
}
if publicAddr := networkConfig.VPN.PublicAddr; publicAddr == "" {
config["listen"] = map[string]string{
"host": "0.0.0.0",
"port": "0",
}
config["lighthouse"] = map[string]any{
"hosts": lighthouseHostIPs,
}
} else {
host, port, err := net.SplitHostPort(publicAddr)
if err != nil {
return nil, fmt.Errorf(
"parsing public address %q: %w", publicAddr, err,
)
}
// This helps with integration testing, so we can set a test to listen
// on some local IP without conflicting with something else running on
// the host.
if hostIP := net.ParseIP(host); hostIP == nil || !hostIP.IsLoopback() {
host = "0.0.0.0"
}
config["listen"] = map[string]string{
"host": host,
"port": port,
}
config["lighthouse"] = map[string]any{
"hosts": []string{},
"am_lighthouse": true,
}
}
return config, nil
}
func nebulaWriteConfig(
ctx context.Context,
logger *mlog.Logger,
runtimeDirPath string,
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) (
string, bool, error,
) {
config, err := nebulaConfig(networkConfig, hostBootstrap)
if err != nil {
return "", false, fmt.Errorf("creating nebula config: %w", err)
}
nebulaYmlPath := filepath.Join(runtimeDirPath, "nebula.yml")
changed, err := toolkit.WriteFileCheckChanged(
ctx, logger, nebulaYmlPath, 0600, func(w io.Writer) error {
return yaml.NewEncoder(w).Encode(config)
},
)
if err != nil {
return "", false, fmt.Errorf(
"writing nebula.yml to %q: %w", nebulaYmlPath, err,
)
}
return nebulaYmlPath, changed, nil
}
func nebulaPmuxProc(
ctx context.Context,
logger *mlog.Logger,
runtimeDirPath, binDirPath string,
networkConfig daecommon.NetworkConfig,
hostBootstrap bootstrap.Bootstrap,
) (
*pmuxlib.Process, error,
) {
nebulaYmlPath, _, err := nebulaWriteConfig(
ctx, logger, runtimeDirPath, networkConfig, hostBootstrap,
)
if err != nil {
return nil, fmt.Errorf("writing nebula config: %w", err)
}
cfg := pmuxlib.ProcessConfig{
Cmd: filepath.Join(binDirPath, "nebula"),
Args: []string{"-config", nebulaYmlPath},
}
cfg = withPmuxLoggers(ctx, logger, "nebula", cfg)
return pmuxlib.NewProcess(cfg), nil
}

136
go/daemon/client.go Normal file
View File

@ -0,0 +1,136 @@
// Code generated by gowrap. DO NOT EDIT.
// template: jsonrpc2/client_gen.tpl
// gowrap: http://github.com/hexdigest/gowrap
package daemon
//go:generate gowrap gen -p isle/daemon -i RPC -t jsonrpc2/client_gen.tpl -o client.go -l ""
import (
"context"
"isle/bootstrap"
"isle/daemon/daecommon"
"isle/daemon/jsonrpc2"
"isle/daemon/network"
"isle/nebula"
)
type rpcClient struct {
client jsonrpc2.Client
}
// RPCFromClient wraps a Client so that it implements the
// RPC interface.
func RPCFromClient(client jsonrpc2.Client) RPC {
return &rpcClient{client}
}
func (c *rpcClient) CreateHost(ctx context.Context, h1 nebula.HostName, c2 network.CreateHostOpts) (j1 network.JoiningBootstrap, err error) {
err = c.client.Call(
ctx,
&j1,
"CreateHost",
h1,
c2,
)
return
}
func (c *rpcClient) CreateNebulaCertificate(ctx context.Context, h1 nebula.HostName, e1 nebula.EncryptingPublicKey) (c2 nebula.Certificate, err error) {
err = c.client.Call(
ctx,
&c2,
"CreateNebulaCertificate",
h1,
e1,
)
return
}
func (c *rpcClient) CreateNetwork(ctx context.Context, name string, domain string, ipNet nebula.IPNet, hostName nebula.HostName) (err error) {
err = c.client.Call(
ctx,
nil,
"CreateNetwork",
name,
domain,
ipNet,
hostName,
)
return
}
func (c *rpcClient) GetConfig(ctx context.Context) (n1 daecommon.NetworkConfig, err error) {
err = c.client.Call(
ctx,
&n1,
"GetConfig",
)
return
}
func (c *rpcClient) GetGarageClientParams(ctx context.Context) (g1 network.GarageClientParams, err error) {
err = c.client.Call(
ctx,
&g1,
"GetGarageClientParams",
)
return
}
func (c *rpcClient) GetHosts(ctx context.Context) (ha1 []bootstrap.Host, err error) {
err = c.client.Call(
ctx,
&ha1,
"GetHosts",
)
return
}
func (c *rpcClient) GetNebulaCAPublicCredentials(ctx context.Context) (c2 nebula.CAPublicCredentials, err error) {
err = c.client.Call(
ctx,
&c2,
"GetNebulaCAPublicCredentials",
)
return
}
func (c *rpcClient) GetNetworks(ctx context.Context) (ca1 []bootstrap.CreationParams, err error) {
err = c.client.Call(
ctx,
&ca1,
"GetNetworks",
)
return
}
func (c *rpcClient) JoinNetwork(ctx context.Context, j1 network.JoiningBootstrap) (err error) {
err = c.client.Call(
ctx,
nil,
"JoinNetwork",
j1,
)
return
}
func (c *rpcClient) RemoveHost(ctx context.Context, hostName nebula.HostName) (err error) {
err = c.client.Call(
ctx,
nil,
"RemoveHost",
hostName,
)
return
}
func (c *rpcClient) SetConfig(ctx context.Context, n1 daecommon.NetworkConfig) (err error) {
err = c.client.Call(
ctx,
nil,
"SetConfig",
n1,
)
return
}

103
go/daemon/config.go Normal file
View File

@ -0,0 +1,103 @@
package daemon
import (
"errors"
"fmt"
"io/fs"
"isle/bootstrap"
"isle/daemon/daecommon"
"os"
"path/filepath"
"slices"
"strings"
"sync"
)
func getDefaultHTTPSocketDirPath() string {
path, err := firstExistingDir(
"/tmp",
// TODO it's possible the daemon process can't actually write to these
"/run",
"/var/run",
"/dev/shm",
)
if err != nil {
panic(fmt.Sprintf("Failed to find directory for HTTP socket: %v", err))
}
return path
}
// HTTPSocketPath returns the path to the daemon's HTTP socket which is used for
// RPC and other functionality.
var HTTPSocketPath = sync.OnceValue(func() string {
return envOr(
"ISLE_DAEMON_HTTP_SOCKET_PATH",
func() string {
return filepath.Join(
getDefaultHTTPSocketDirPath(), "isle-daemon.sock",
)
},
)
})
func pickNetworkConfig(
daemonConfig daecommon.Config, creationParams bootstrap.CreationParams,
) (
daecommon.NetworkConfig, bool,
) {
if len(daemonConfig.Networks) == 1 { // DEPRECATED
if c, ok := daemonConfig.Networks[daecommon.DeprecatedNetworkID]; ok {
return c, true
}
}
for searchStr, networkConfig := range daemonConfig.Networks {
if creationParams.Matches(searchStr) {
return networkConfig, true
}
}
return daecommon.NetworkConfig{}, false
}
////////////////////////////////////////////////////////////////////////////////
// Jigs
func envOr(name string, fallback func() string) string {
if v := os.Getenv(name); v != "" {
return v
}
return fallback()
}
func firstExistingDir(paths ...string) (string, error) {
var errs []error
for _, path := range paths {
stat, err := os.Stat(path)
switch {
case errors.Is(err, fs.ErrExist):
continue
case err != nil:
errs = append(
errs, fmt.Errorf("checking if path %q exists: %w", path, err),
)
case !stat.IsDir():
errs = append(
errs, fmt.Errorf("path %q exists but is not a directory", path),
)
default:
return path, nil
}
}
err := fmt.Errorf(
"no directory found at any of the following paths: %s",
strings.Join(paths, ", "),
)
if len(errs) > 0 {
err = errors.Join(slices.Insert(errs, 0, err)...)
}
return "", err
}

Some files were not shown because too many files have changed in this diff Show More