diff --git a/.devcontainer/init-cmd.sh b/.devcontainer/init-cmd.sh index a0c3b916..1ff1547c 100755 --- a/.devcontainer/init-cmd.sh +++ b/.devcontainer/init-cmd.sh @@ -14,13 +14,20 @@ until PGPASSWORD=$POSTGRES_PASSWORD psql -h "$POSTGRES_HOST" -U "$POSTGRES_USER" sleep 1 done +# Install openssl +wget http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.1_1.1.1n-0+deb10u6_amd64.deb +sudo dpkg -i libssl1.1_1.1.1n-0+deb10u6_amd64.deb + # Check architecture and copy the corresponding file if it exists ARCH=$(uname -m) -if [ "$ARCH" = "x86_64" ] && [ -f "/workspaces/ztnodeid/build/linux_amd64/ztmkworld" ]; then - cp /workspaces/ztnodeid/build/linux_amd64/ztmkworld /usr/local/bin/ztmkworld -elif [ "$ARCH" = "aarch64" ] && [ -f "/workspaces/ztnodeid/build/linux_arm64/ztmkworld" ]; then - cp /workspaces/ztnodeid/build/linux_arm64/ztmkworld /usr/local/bin/ztmkworld +if [ "$ARCH" = "x86_64" ] && [ -f "/workspaces/bin/mkworld/build/linux_amd64/ztmkworld" ]; then + cp /workspaces/bin/mkworld/build/linux_amd64/ztmkworld /usr/local/bin/ztmkworld + cp /workspaces/bin/idtool/build/linux_amd64/zerotier-idtool /usr/local/bin/zerotier-idtool +elif [ "$ARCH" = "aarch64" ] && [ -f "/workspaces/bin/mkworld/build/linux_arm64/ztmkworld" ]; then + cp /workspaces/bin/mkworld/build/linux_arm64/ztmkworld /usr/local/bin/ztmkworld + cp /workspaces/bin/idtool/build/linux_arm64/zerotier-idtool /usr/local/bin/zerotier-idtool fi + chmod +x /usr/local/bin/ztmkworld # apply migrations to the database diff --git a/Dockerfile b/Dockerfile index 14aa6016..ca8f5b25 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,16 +35,24 @@ RUN SKIP_ENV_VALIDATION=1 npm run build # Copy the ztmkworld binary based on the target platform architecture FROM base AS ztmkworld_builder ARG TARGETPLATFORM + WORKDIR /app -COPY ztnodeid/build/linux_amd64/ztmkworld ztmkworld_amd64 -COPY ztnodeid/build/linux_arm64/ztmkworld ztmkworld_arm64 -RUN \ - case "${TARGETPLATFORM}" in \ - "linux/amd64") cp ztmkworld_amd64 /usr/local/bin/ztmkworld ;; \ - "linux/arm64") cp ztmkworld_arm64 /usr/local/bin/ztmkworld ;; \ + +COPY bin/mkworld/build/linux_amd64/ztmkworld ztmkworld_amd64 +COPY bin/mkworld/build/linux_arm64/ztmkworld ztmkworld_arm64 +COPY bin/idtool/build/linux_amd64/zerotier-idtool zerotier-idtool_amd64 +COPY bin/idtool/build/linux_arm64/zerotier-idtool zerotier-idtool_arm64 + +RUN case "${TARGETPLATFORM}" in \ + "linux/amd64") \ + cp ztmkworld_amd64 /usr/local/bin/ztmkworld && \ + cp zerotier-idtool_amd64 /usr/local/bin/zerotier-idtool ;; \ + "linux/arm64") \ + cp ztmkworld_arm64 /usr/local/bin/ztmkworld && \ + cp zerotier-idtool_arm64 /usr/local/bin/zerotier-idtool ;; \ *) echo "Unsupported architecture" && exit 1 ;; \ esac && \ - chmod +x /usr/local/bin/ztmkworld + chmod +x /usr/local/bin/ztmkworld /usr/local/bin/zerotier-idtool # Production image, copy all the files and run next FROM $NODEJS_IMAGE AS runner @@ -65,7 +73,24 @@ ENV NEXT_TELEMETRY_DISABLED 1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -RUN apt update && apt install -y curl sudo postgresql-client && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt update && apt install -y curl wget sudo postgresql-client && apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Openssl 1.1.1n is required for the zerotier-idtool +RUN ARCH=$(dpkg --print-architecture) && \ + if [ "$ARCH" = "amd64" ]; then \ + wget http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.1_1.1.1n-0+deb10u6_amd64.deb; \ + dpkg -i libssl1.1_1.1.1n-0+deb10u6_amd64.deb; \ + rm libssl1.1_1.1.1n-0+deb10u6_amd64.deb; \ + elif [ "$ARCH" = "arm64" ]; then \ + wget http://security.debian.org/debian-security/pool/updates/main/o/openssl/libssl1.1_1.1.1n-0+deb10u6_arm64.deb; \ + dpkg -i libssl1.1_1.1.1n-0+deb10u6_arm64.deb; \ + rm libssl1.1_1.1.1n-0+deb10u6_arm64.deb; \ + else \ + echo "Unsupported architecture: $ARCH"; \ + exit 1; \ + fi + + # need to install these package for seeding the database RUN npm install @prisma/client @paralleldrive/cuid2 RUN npm install -g prisma ts-node @@ -82,6 +107,7 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma COPY --from=builder --chown=nextjs:nodejs /app/init-db.sh ./init-db.sh COPY --from=ztmkworld_builder /usr/local/bin/ztmkworld /usr/local/bin/ztmkworld +COPY --from=ztmkworld_builder /usr/local/bin/zerotier-idtool /usr/local/bin/zerotier-idtool # prepeare .env file for the init-db.sh script RUN touch .env && chown nextjs:nodejs .env diff --git a/bin/idtool/Dockerfile b/bin/idtool/Dockerfile new file mode 100644 index 00000000..095ab2d9 --- /dev/null +++ b/bin/idtool/Dockerfile @@ -0,0 +1,28 @@ +# syntax=docker/dockerfile:1 + +# Build stage +FROM --platform=$TARGETPLATFORM ubuntu:20.04 AS builder + +ARG TARGETPLATFORM +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y \ + curl \ + gnupg \ + lsb-release \ + && rm -rf /var/lib/apt/lists/* + +# Add ZeroTier repository and install +RUN curl -s 'https://raw.githubusercontent.com/zerotier/ZeroTierOne/master/doc/contact%40zerotier.com.gpg' | gpg --dearmor > /usr/share/keyrings/zerotier-archive-keyring.gpg +RUN echo "deb [signed-by=/usr/share/keyrings/zerotier-archive-keyring.gpg] http://download.zerotier.com/debian/$(lsb_release -cs) $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/zerotier.list + +RUN apt-get update && apt-get install -y zerotier-one + +# Final stage +FROM scratch AS export-stage + +# Copy zerotier-idtool from the builder stage +COPY --from=builder /usr/sbin/zerotier-idtool . + +# Build command: +# docker buildx build --platform linux/amd64,linux/arm64 --target export-stage -o type=local,dest=./build . \ No newline at end of file diff --git a/bin/idtool/build/linux_amd64/zerotier-idtool b/bin/idtool/build/linux_amd64/zerotier-idtool new file mode 100755 index 00000000..b97e99c0 Binary files /dev/null and b/bin/idtool/build/linux_amd64/zerotier-idtool differ diff --git a/bin/idtool/build/linux_arm64/zerotier-idtool b/bin/idtool/build/linux_arm64/zerotier-idtool new file mode 100755 index 00000000..85677c76 Binary files /dev/null and b/bin/idtool/build/linux_arm64/zerotier-idtool differ diff --git a/ztnodeid/Dockerfile b/bin/mkworld/Dockerfile similarity index 100% rename from ztnodeid/Dockerfile rename to bin/mkworld/Dockerfile diff --git a/ztnodeid/assets/mkworld.config.json b/bin/mkworld/assets/mkworld.config.json similarity index 100% rename from ztnodeid/assets/mkworld.config.json rename to bin/mkworld/assets/mkworld.config.json diff --git a/ztnodeid/build/freebsd_amd64/ztmkworld b/bin/mkworld/build/freebsd_amd64/ztmkworld similarity index 100% rename from ztnodeid/build/freebsd_amd64/ztmkworld rename to bin/mkworld/build/freebsd_amd64/ztmkworld diff --git a/ztnodeid/build/linux_amd64/ztmkworld b/bin/mkworld/build/linux_amd64/ztmkworld similarity index 100% rename from ztnodeid/build/linux_amd64/ztmkworld rename to bin/mkworld/build/linux_amd64/ztmkworld diff --git a/ztnodeid/build/linux_arm64/ztmkworld b/bin/mkworld/build/linux_arm64/ztmkworld similarity index 100% rename from ztnodeid/build/linux_arm64/ztmkworld rename to bin/mkworld/build/linux_arm64/ztmkworld diff --git a/ztnodeid/cmd/mkworld/main.go b/bin/mkworld/cmd/mkworld/main.go similarity index 100% rename from ztnodeid/cmd/mkworld/main.go rename to bin/mkworld/cmd/mkworld/main.go diff --git a/ztnodeid/go.mod b/bin/mkworld/go.mod similarity index 100% rename from ztnodeid/go.mod rename to bin/mkworld/go.mod diff --git a/ztnodeid/go.sum b/bin/mkworld/go.sum similarity index 100% rename from ztnodeid/go.sum rename to bin/mkworld/go.sum diff --git a/ztnodeid/pkg/node/errs.go b/bin/mkworld/pkg/node/errs.go similarity index 100% rename from ztnodeid/pkg/node/errs.go rename to bin/mkworld/pkg/node/errs.go diff --git a/ztnodeid/pkg/node/identity.go b/bin/mkworld/pkg/node/identity.go similarity index 100% rename from ztnodeid/pkg/node/identity.go rename to bin/mkworld/pkg/node/identity.go diff --git a/ztnodeid/pkg/node/node.go b/bin/mkworld/pkg/node/node.go similarity index 100% rename from ztnodeid/pkg/node/node.go rename to bin/mkworld/pkg/node/node.go diff --git a/ztnodeid/pkg/node/world.go b/bin/mkworld/pkg/node/world.go similarity index 100% rename from ztnodeid/pkg/node/world.go rename to bin/mkworld/pkg/node/world.go diff --git a/ztnodeid/pkg/ztcrypto/identity.go b/bin/mkworld/pkg/ztcrypto/identity.go similarity index 100% rename from ztnodeid/pkg/ztcrypto/identity.go rename to bin/mkworld/pkg/ztcrypto/identity.go diff --git a/docs/docs/Installation/FreeBSD.md b/docs/docs/Installation/FreeBSD.md index 15294b3c..fa261d7c 100644 --- a/docs/docs/Installation/FreeBSD.md +++ b/docs/docs/Installation/FreeBSD.md @@ -100,7 +100,7 @@ setenv PRISMA_QUERY_ENGINE_LIBRARY /root/prisma-engines/target/release/libquery_ 8. Copy mkworld binary: ```bash - cp ztnodeid/build/freebsd_amd64/ztmkworld /usr/local/bin/ztmkworld + cp bin/mkworld/build/freebsd_amd64/ztmkworld /usr/local/bin/ztmkworld ``` 9. Run server: ```bash diff --git a/install.ztnet/bash/ztnet.sh b/install.ztnet/bash/ztnet.sh index c5fb0482..29355e3b 100755 --- a/install.ztnet/bash/ztnet.sh +++ b/install.ztnet/bash/ztnet.sh @@ -984,7 +984,7 @@ pull_checkout_ztnet check_postgres_access_and_prompt_password "$POSTGRES_USER" "$POSTGRES_PASSWORD" "$POSTGRES_DB" # Copy mkworld binary -cp "$TEMP_REPO_DIR/ztnodeid/build/linux_$ARCH/ztmkworld" /usr/local/bin/ztmkworld +cp "$TEMP_REPO_DIR/bin/mkworld/build/linux_$ARCH/ztmkworld" /usr/local/bin/ztmkworld NEXT_PUBLIC_APP_VERSION="${CUSTOM_VERSION:-$latestTag}" diff --git a/prisma/migrations/20240820103254_moon/migration.sql b/prisma/migrations/20240820103254_moon/migration.sql new file mode 100644 index 00000000..91e603ee --- /dev/null +++ b/prisma/migrations/20240820103254_moon/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Planet" ADD COLUMN "isMoon" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "RootNodes" ADD COLUMN "isMoon" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 34acaf36..bbe886b2 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -60,6 +60,7 @@ model Planet { plID BigInt @default(0) plBirth BigInt @default(0) plRecommend Boolean @default(false) + isMoon Boolean @default(false) rootNodes RootNodes[] globalOptions GlobalOptions? } @@ -68,6 +69,7 @@ model RootNodes { id Int @id @default(autoincrement()) Planet Planet @relation(fields: [PlanetId], references: [id], onDelete: Cascade) PlanetId Int + isMoon Boolean @default(false) comments String? identity String endpoints Json diff --git a/src/components/adminPage/controller/privateRoot.tsx b/src/components/adminPage/controller/privateRoot.tsx index 988b447b..86f96f2e 100644 --- a/src/components/adminPage/controller/privateRoot.tsx +++ b/src/components/adminPage/controller/privateRoot.tsx @@ -97,6 +97,11 @@ const PrivateRoot = () => { toast.error(error.message); }); }; + + // generate the moon id from the first node that has ismoon set to true + const isMoonIdentity = getPlanet?.rootNodes?.filter((node) => node.isMoon); + const moonId = isMoonIdentity?.[0]?.identity.trim().split(":")[0]; + return (
@@ -124,7 +129,10 @@ const PrivateRoot = () => {
{getPlanet?.rootNodes?.map((node, i) => ( -
+
{!node.endpoints.toString().includes("9993") ? (
{
) : null} -

Root #{i + 1}

-

- Comments: {node.comments} -

-

- Endpoints: {node.endpoints.toString()} -

-

- Identity: {node.identity.substring(0, 50)} -

+
+
+

Root #{i + 1}

+ {node.isMoon ? ( + + {`zerotier-cli orbit ${moonId} ${ + node.identity.split(":")[0] + }`} + + ) : null} +
+

+ Comments: {node.comments} +

+

+ Endpoints: {node.endpoints.toString()} +

+

+ Identity: {node.identity.substring(0, 50)} +

+

+ WorldType: {node.isMoon ? "Moon" : "Planet"} +

+
+
+ {node.isMoon ? ( +
+
+

+ Moon file is available for download at the following URL: +

+ + api/moon + +
+
+ ) : ( + + {/* {t("controller.generatePlanet.downloadPlanetInfo")} */} +

+ Planet file is available for download at the following URL: +

+ + {t("controller.generatePlanet.downloadPlanetUrl")} + +
+ )} +
))}
-
-

- {t("controller.generatePlanet.downloadPlanetInfo")}{" "} - - {t("controller.generatePlanet.downloadPlanetUrl")} - -

-
) : null} +
+
+ Is Moon? + handleMoonToggle(i)} + /> +
+

+ Check this box if this root should be a moon. Moons are user-defined root + servers that operate alongside ZeroTier's default root servers (planets). + They don't make your network private, but can improve performance, + especially in regions with poor connectivity to the default roots. Moons can + also provide continuity if the default roots are unreachable. +

+
))}
@@ -99,12 +122,12 @@ const RootForm: React.FC<{ onClose: () => void }> = ({ onClose }) => { rootNodes: [ { endpoints: [], + isMoon: false, comments: "", identity: "", }, ] as Partial[], }); - useEffect(() => { setWorld((prev) => { // Use existing data from getOptions.rootNodes if available @@ -121,6 +144,7 @@ const RootForm: React.FC<{ onClose: () => void }> = ({ onClose }) => { return { ...prev, // Spread the previous state + isMoon: getPlanet?.isMoon, plRecommend: getPlanet?.plRecommend !== undefined ? getPlanet?.plRecommend @@ -209,7 +233,18 @@ const RootForm: React.FC<{ onClose: () => void }> = ({ onClose }) => { e.preventDefault(); setWorld((prev) => ({ ...prev, - rootNodes: [...prev.rootNodes, { endpoints: "", identity: "", comments: "" }], + rootNodes: [ + ...prev.rootNodes, + { endpoints: "", identity: "", comments: "", isMoon: false }, + ], + })); + }; + const handleMoonToggle = (index) => { + setWorld((prev) => ({ + ...prev, + rootNodes: prev.rootNodes.map((node, i) => + i === index ? { ...node, isMoon: !node.isMoon } : node, + ), })); }; const handleRemoveClick = (e, index) => { @@ -222,6 +257,7 @@ const RootForm: React.FC<{ onClose: () => void }> = ({ onClose }) => { rootNodes: [...roots], })); }; + return ( <> {/* Display list of root nodes */} @@ -277,6 +313,7 @@ const RootForm: React.FC<{ onClose: () => void }> = ({ onClose }) => { handleEndpointArrayChange={handleEndpointArrayChange} handleAddClick={handleAddClick} handleRemoveClick={handleRemoveClick} + handleMoonToggle={handleMoonToggle} />
diff --git a/src/pages/api/mkworld/config.ts b/src/pages/api/mkworld/config.ts index 669220f3..d852ad44 100644 --- a/src/pages/api/mkworld/config.ts +++ b/src/pages/api/mkworld/config.ts @@ -30,6 +30,7 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "GET") { try { const folderPath = path.resolve(`${ZT_FOLDER}/zt-mkworld`); + const moonsPath = path.resolve(`${ZT_FOLDER}/moon`); // Check if the directory exists if (!fs.existsSync(folderPath) || !fs.statSync(folderPath).isDirectory()) { @@ -64,6 +65,19 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { // Append all files from the directory to the zip archive.directory(folderPath, false); + + // Check if the moons.d directory exists and has files + if (fs.existsSync(moonsPath) && fs.statSync(moonsPath).isDirectory()) { + const moonFiles = fs.readdirSync(moonsPath); + + // Add each moon file to the archive + for (const file of moonFiles) { + const filePath = path.join(moonsPath, file); + if (fs.statSync(filePath).isFile()) { + archive.file(filePath, { name: `moon/${file}` }); + } + } + } archive.finalize(); } catch (error) { console.error(error); diff --git a/src/pages/api/moon.ts b/src/pages/api/moon.ts new file mode 100644 index 00000000..05bae6f1 --- /dev/null +++ b/src/pages/api/moon.ts @@ -0,0 +1,97 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import fs from "fs"; +import path from "path"; +import { ZT_FOLDER } from "~/utils/ztApi"; +import archiver from "archiver"; + +export const config = { + api: { + bodyParser: false, + }, +}; + +export default async (req: NextApiRequest, res: NextApiResponse) => { + if (req.method !== "GET") { + return res.status(405).send("Method Not Allowed"); + } + + try { + const moonPath = path.resolve(ZT_FOLDER, "moon"); + + if (!fs.existsSync(moonPath) || !fs.statSync(moonPath).isDirectory()) { + return res.status(404).send("Moon directory not found."); + } + + const moonFiles = fs.readdirSync(moonPath); + const compiledMoonFile = moonFiles.find((file) => file.startsWith("000000")); + if (!compiledMoonFile) { + return res.status(404).send("Compiled moon file not found."); + } + + const compiledMoonPath = path.join(moonPath, compiledMoonFile); + if (!fs.statSync(compiledMoonPath).isFile()) { + return res.status(404).send("Compiled moon file is not a valid file."); + } + + // Create a zip archive + const archive = archiver("zip", { + zlib: { level: 9 }, // Sets the compression level. + }); + + // Set the headers + res.setHeader("Content-Disposition", "attachment; filename=moon_package.zip"); + res.setHeader("Content-Type", "application/zip"); + + // Pipe archive data to the response + archive.pipe(res); + + // Add moon file to the archive + archive.file(compiledMoonPath, { name: compiledMoonFile }); + + // Create and add README file + const readmeContent = ` + How to Use the Moon File + + There are two ways to add this moon to your ZeroTier nodes: + + Method 1: Using the moon file + + 1. Place the moon file (${compiledMoonFile}) in your ZeroTier directory: + - On Linux/macOS: /var/lib/zerotier-one/moons.d/ + - On Windows: C:\\ProgramData\\ZeroTier\\One\\moons.d\\ + + 2. Restart the ZeroTier service: + - On Linux: sudo systemctl restart zerotier-one + - On macOS: sudo launchctl stop com.zerotier.one && sudo launchctl start com.zerotier.one + - On Windows: Restart the ZeroTier One service in the Services application + + Method 2: Using the zerotier-cli orbit command + + 1. Use the following command on each node where you want to add the moon: + zerotier-cli orbit + + Replace with the 10-digit ID of your moon (the part after '000000' in the filename). + For example, if the moon file is named '000000deadbeef00.moon', the command would be: + zerotier-cli orbit deadbeef00 deadbeef00 + + This command will contact the root and obtain the full world definition from it if it's online and reachable. + + Verifying the moon: + + To verify that the moon has been added, use the following command: + zerotier-cli listmoons + + Remember, moons operate alongside ZeroTier's default root servers. They can improve performance and provide continuity if the default roots are unreachable, but they don't make your network private. + + For more detailed information, visit: https://docs.zerotier.com/zerotier/moons + `; + + archive.append(readmeContent, { name: "README.txt" }); + + // Finalize the archive and send the response + await archive.finalize(); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error."); + } +}; diff --git a/src/pages/api/planet.ts b/src/pages/api/planet.ts index 181cdb8f..63893fbb 100644 --- a/src/pages/api/planet.ts +++ b/src/pages/api/planet.ts @@ -17,19 +17,14 @@ export default async (req: NextApiRequest, res: NextApiResponse) => { if (req.method === "GET") { try { const folderPath = path.resolve(`${ZT_FOLDER}/zt-mkworld`); - const filePath = path.join(folderPath, "planet.custom"); + const planetPath = path.join(folderPath, "planet.custom"); - // Check if the directory and file exist - if ( - !fs.existsSync(folderPath) || - !fs.statSync(folderPath).isDirectory() || - !fs.existsSync(filePath) - ) { - return res.status(404).send("Folder or file not found."); + if (!fs.existsSync(planetPath) || !fs.statSync(planetPath).isFile()) { + return res.status(404).send("Planet file not found."); } // Read the file and stream it to the response - const fileStream = fs.createReadStream(filePath); + const fileStream = fs.createReadStream(planetPath); // Set the headers res.setHeader("Content-Disposition", "attachment; filename=planet.custom"); diff --git a/src/server/api/routers/adminRoute.ts b/src/server/api/routers/adminRoute.ts index d76f0bb8..3af4144e 100644 --- a/src/server/api/routers/adminRoute.ts +++ b/src/server/api/routers/adminRoute.ts @@ -27,6 +27,7 @@ import { ZT_FOLDER } from "~/utils/ztApi"; import { isRunningInDocker } from "~/utils/docker"; import { getNetworkClassCIDR } from "~/utils/IPv4gen"; import { InvitationLinkType } from "~/types/invitation"; +import { createMoon } from "../services/adminService"; type WithError = T & { error?: boolean; message?: string }; @@ -896,6 +897,7 @@ export const adminRouter = createTRPCRouter({ plBirth: true, plRecommend: true, rootNodes: true, + isMoon: true, }, }, }, @@ -926,6 +928,7 @@ export const adminRouter = createTRPCRouter({ rootNodes: z.array( z.object({ identity: z.string().min(1, "Identity must have a value."), + isMoon: z.boolean().default(false), endpoints: z .any() .refine( @@ -964,6 +967,12 @@ export const adminRouter = createTRPCRouter({ const mkworldDir = `${ZT_FOLDER}/zt-mkworld`; const planetPath = `${ZT_FOLDER}/planet`; const backupDir = `${ZT_FOLDER}/planet_backup`; + const moonPath = `${ZT_FOLDER}/moon`; + // binary path + const ztmkworldBinPath = "/usr/local/bin/ztmkworld"; + + // Ensure the moon identity is created if it doesn't exist + const identityPath = `${ZT_FOLDER}/identity.secret`; // Check for write permission on the directory try { @@ -981,12 +990,10 @@ export const adminRouter = createTRPCRouter({ } // Check if identity.public exists - if (!fs.existsSync(`${ZT_FOLDER}/identity.public`)) { + if (!fs.existsSync(identityPath)) { throwError("identity.public file does NOT exist, cannot generate planet file."); } - // Check if ztmkworld executable exists - const ztmkworldBinPath = "/usr/local/bin/ztmkworld"; if (!fs.existsSync(ztmkworldBinPath)) { throwError("ztmkworld executable does not exist at the specified location."); } @@ -995,6 +1002,11 @@ export const adminRouter = createTRPCRouter({ fs.mkdirSync(mkworldDir); } + // make sure the moon directory exists + if (!fs.existsSync(moonPath)) { + fs.mkdirSync(moonPath); + } + // Backup existing planet file if it exists if (fs.existsSync(planetPath)) { // we only backup the orginal planet file once @@ -1018,6 +1030,7 @@ export const adminRouter = createTRPCRouter({ comments: node.comments || "ztnet.network", identity: node.identity, endpoints: node.endpoints, + isMoon: node.isMoon, })), signing: ["previous.c25519", "current.c25519"], output: "planet.custom", @@ -1026,42 +1039,59 @@ export const adminRouter = createTRPCRouter({ plRecommend: input.plRecommend, }; - fs.writeFileSync(`${mkworldDir}/mkworld.config.json`, JSON.stringify(config)); - - /* - * - * Update local.conf file with the new port number - * - */ - // Extract the port numbers from the first endpoint string - const portNumbers = input.rootNodes[0].endpoints[0] - .split(",") - .map((endpoint) => parseInt(endpoint.split("/").pop() || "", 10)); - - try { - await updateLocalConf(portNumbers); - } catch (error) { - throwError(error); + // Process moons + if (input.rootNodes.some((node) => node.isMoon)) { + await createMoon(input.rootNodes, moonPath); } - /* - * - * Generate planet file using mkworld - * - */ - try { - execSync( - // "cd /etc/zt-mkworld && /usr/local/bin/ztmkworld -c /etc/zt-mkworld/mkworld.config.json", - // use mkworldDir - `cd ${mkworldDir} && ${ztmkworldBinPath} -c ${mkworldDir}/mkworld.config.json`, - ); - } catch (_error) { - throwError( - "Could not create planet file. Please make sure your config is valid.", + // Check if there are any non-moon roots + const planetRoots = input.rootNodes.filter((node) => !node.isMoon); + + if (planetRoots.length > 0) { + // Create planet file + const config: WorldConfig = { + rootNodes: planetRoots.map((node) => ({ + comments: node.comments || "ztnet.network", + identity: node.identity, + endpoints: node.endpoints, + })), + signing: ["previous.c25519", "current.c25519"], + output: "planet.custom", + plID: input.plID || 0, + plBirth: input.plBirth || 0, + plRecommend: input.plRecommend, + }; + + fs.writeFileSync( + `${mkworldDir}/mkworld.config.json`, + JSON.stringify(config, null, 2), ); + + // Update local.conf with the port number from the first non-moon root + const portNumbers = planetRoots[0].endpoints[0] + .split(",") + .map((endpoint) => parseInt(endpoint.split("/").pop() || "", 10)); + + try { + await updateLocalConf(portNumbers); + } catch (error) { + throwError(error); + } + + // Generate planet file + try { + execSync( + `cd ${mkworldDir} && ${ztmkworldBinPath} -c ${mkworldDir}/mkworld.config.json`, + ); + } catch (_error) { + throwError( + "Could not create planet file. Please make sure your config is valid.", + ); + } + + // Copy generated planet file + fs.copyFileSync(`${mkworldDir}/planet.custom`, planetPath); } - // Copy generated planet file - fs.copyFileSync(`${mkworldDir}/planet.custom`, planetPath); /* * @@ -1116,6 +1146,8 @@ export const adminRouter = createTRPCRouter({ const paths = { backupDir: `${ZT_FOLDER}/planet_backup`, planetPath: `${ZT_FOLDER}/planet`, + moonPath: `${ZT_FOLDER}/moon`, + moonsDPath: `${ZT_FOLDER}/moons.d`, mkworldDir: `${ZT_FOLDER}/zt-mkworld`, }; @@ -1145,6 +1177,9 @@ export const adminRouter = createTRPCRouter({ // Clean up backup and mkworld directories fs.rmSync(paths.backupDir, { recursive: true, force: true }); fs.rmSync(paths.mkworldDir, { recursive: true, force: true }); + fs.rmSync(paths.moonPath, { recursive: true, force: true }); + fs.rmSync(paths.moonsDPath, { recursive: true, force: true }); + /* * * Reset local.conf with default port number diff --git a/src/server/api/services/adminService.ts b/src/server/api/services/adminService.ts new file mode 100644 index 00000000..6711c3a8 --- /dev/null +++ b/src/server/api/services/adminService.ts @@ -0,0 +1,90 @@ +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import { ZT_FOLDER } from "~/utils/ztApi"; + +export interface RootNode { + endpoints?: string[]; + comments?: string; + identity?: string; + isMoon?: boolean; +} + +export async function createMoon(nodes: RootNode[], moonPath: string) { + const identityPath = `${ZT_FOLDER}/identity.secret`; + const publicIdentityPath = `${ZT_FOLDER}/identity.public`; + + //binary path + const ZT_IDTOOL = "zerotier-idtool"; + + // Generate moon identity if it doesn't exist + if (!fs.existsSync(identityPath) || !fs.existsSync(publicIdentityPath)) { + execSync(`${ZT_IDTOOL} generate ${identityPath} ${publicIdentityPath}`); + } + + const currentControllerId = fs + .readFileSync(publicIdentityPath, "utf-8") + .trim() + .split(":")[0]; + // Filter nodes that are marked as moons + const moonNodes = nodes.filter((node) => node.isMoon); + const moonId = moonNodes[0].identity.trim().split(":")[0]; + + const moonConfig = { + id: moonId, + roots: moonNodes.map((node) => ({ + identity: node.identity, + endpoints: node.endpoints, + })), + }; + + // Generate the initial moon file + const initMoonFilePath = `${moonPath}/initmoon_${moonId}.json`; + execSync(`${ZT_IDTOOL} initmoon ${identityPath} > ${initMoonFilePath}`); + + // Read the initmoon.json file + const initMoonConfig = JSON.parse(fs.readFileSync(initMoonFilePath, "utf-8")); + + // Merge the roots from moonConfig into initMoonConfig + initMoonConfig.roots = moonConfig.roots.map((root) => ({ + identity: root.identity, + stableEndpoints: root.endpoints, + })); + + // Write the updated config back to initmoon.json + fs.writeFileSync(initMoonFilePath, JSON.stringify(initMoonConfig, null, 4)); + + // Generate the final moon file + execSync(`cd ${moonPath} && ${ZT_IDTOOL} genmoon ${initMoonFilePath}`); + + // Check if one of the nodes is the current controller + const isCurrentControllerMoon = moonNodes.some((node) => + node.identity.includes(currentControllerId), + ); + + if (isCurrentControllerMoon) { + // Find the generated moon file + const moonFiles = fs.readdirSync(moonPath); + const generatedMoonFile = moonFiles.find( + (file) => file.startsWith("000000") && file.endsWith(".moon"), + ); + + if (generatedMoonFile) { + const sourcePath = path.join(moonPath, generatedMoonFile); + const destPath = path.join(ZT_FOLDER, "moons.d", generatedMoonFile); + + // Ensure the moons.d directory exists + const moonsDirPath = path.join(ZT_FOLDER, "moons.d"); + if (!fs.existsSync(moonsDirPath)) { + fs.mkdirSync(moonsDirPath, { recursive: true }); + } + + // Copy the moon file to the moons.d folder + fs.copyFileSync(sourcePath, destPath); + } else { + console.error("Generated moon file not found"); + } + } + + return currentControllerId; +}