diff --git a/.gitignore b/.gitignore index 0a354404..4f8d0146 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ temp .env.test.local .env.production.local +.idea + # Created by https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node # Edit at https://www.toptal.com/developers/gitignore?templates=vscode,yarn,react,node @@ -156,9 +158,9 @@ sketch # if you are NOT using Zero-installs, then: # comment the following lines -!.yarn/cache +#!.yarn/cache # and uncomment the following lines -# .pnp.* +.pnp.* # End of https://www.toptal.com/developers/gitignore/api/vscode,yarn,react,node \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 056fe5c2..fd7d0552 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -33,6 +33,54 @@ jspm_packages/ # Yarn Integrity file .yarn-integrity +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + # Misc .DS_Store .env.local diff --git a/backend/services/network.js b/backend/services/network.js index c2a0958d..a3a4ba94 100644 --- a/backend/services/network.js +++ b/backend/services/network.js @@ -4,6 +4,7 @@ const axios = require("axios"); const api = require("../utils/controller-api"); const db = require("../utils/db"); const constants = require("../utils/constants"); +const zns = require("../utils/zns"); async function getNetworkAdditionalData(data) { let additionalData = db @@ -21,11 +22,30 @@ async function getNetworkAdditionalData(data) { delete data.remoteTraceLevel; delete data.remoteTraceTarget; + //let ad = { ...additionalData.value() }; + let ad_ = additionalData.value(); + let ad = JSON.parse(JSON.stringify(ad_)); + data.dns = { + domain: ad_.dnsDomain, + servers: [], + }; + if (ad_.dnsIP) data.dns["servers"].push(ad_.dnsIP); + console.log( + `*** ad_="${JSON.stringify(ad_, null, 3)}" -> ad="${JSON.stringify( + ad, + null, + 3 + )}" -> ${JSON.stringify(data.dns, null, 3)}` + ); + delete ad.dnsIP; + delete ad.dnsDomain; + delete ad.dnsEnable; + delete ad.dnsWildcard; return { id: data.id, type: "Network", clock: Math.floor(new Date().getTime() / 1000), - ...additionalData.value(), + ...ad, config: data, }; } @@ -46,13 +66,11 @@ async function getNetworksData(nwids) { return []; }); - let data = Promise.all( + return Promise.all( multipleRes.map((el) => { return getNetworkAdditionalData(el.data); }) ); - - return data; } exports.createNetworkAdditionalData = createNetworkAdditionalData; @@ -62,6 +80,9 @@ async function createNetworkAdditionalData(nwid) { additionalConfig: { description: "", rulesSource: constants.defaultRulesSource, + dnsEnable: false, + dnsDomain: "", + dnsWildcard: false, }, members: [], }; @@ -79,6 +100,21 @@ async function updateNetworkAdditionalData(nwid, data) { if (data.hasOwnProperty("rulesSource")) { additionalData.rulesSource = data.rulesSource; } + if (data.hasOwnProperty("dnsEnable")) { + if (data.dnsEnable) { + //TODO: start ZeroNSd and get its IP address + additionalData.dnsIP = "127.0.0.1"; + } else { + additionalData.dnsIP = null; + } + additionalData.dnsEnable = data.dnsEnable; + } + if (data.hasOwnProperty("dnsDomain")) { + additionalData.dnsDomain = data.dnsDomain; + } + if (data.hasOwnProperty("dnsWildcard")) { + additionalData.dnsWildcard = data.dnsWildcard; + } if (additionalData) { db.get("networks") @@ -86,6 +122,10 @@ async function updateNetworkAdditionalData(nwid, data) { .map("additionalConfig") .map((additionalConfig) => _.assign(additionalConfig, additionalData)) .write(); + + if (data.hasOwnProperty("dnsEnable")) { + zns.handleNet(db.get("networks").filter({ id: nwid }).value()[0]); + } } } diff --git a/backend/utils/zns.js b/backend/utils/zns.js new file mode 100644 index 00000000..65cc4287 --- /dev/null +++ b/backend/utils/zns.js @@ -0,0 +1,147 @@ +const cp = require("child_process"); +const path = require("path"); +const fs = require("fs"); + +const db = require("../utils/db"); + +//TODO: does this kind of "optimization" make sense in Node.js? +let token = null; +function getToken() { + if (!token) + try { + token = db.get("users").value()[0].token; + } catch { + console.warn("*** token retrieval failed"); + } + return token; +} + +function setPid(nwid, pid) { + db.get("networks") + .filter({ id: nwid }) + .map("additionalConfig") + .map((additionalConfig) => (additionalConfig.pidDNS = pid)) + .write(); +} + +const isRunning = (query, pid) => { + return new Promise(function (resolve) { + //FIXME: Check if pgrep is available + cp.exec(`pgrep ${query}`, (err, stdout) => { + resolve(stdout.indexOf(`${pid}`) > -1); + }); + }); +}; + +function startDNS(token, nwid, conf) { + //FIXME: check it does the right thing when conf.pidDNS is null/undefined + isRunning("zeronsd", conf.pidDNS).then((ok) => { + if (ok) { + console.log( + `startDNS(${token}, ${nwid}): already active on PID ${conf.pidDNS}` + ); + } else { + let cmd = "zeronsd"; + let opts = Array(); + if (process.geteuid() === 0) { + // production in Docker container + } else { + // we are debugging + let myLocal = "/home/mcon/.cargo/bin"; + let pth = process.env.PATH.split(path.delimiter); + if (!pth.includes(myLocal)) pth.push(myLocal); + if ( + !process.env.PATH.split(path.delimiter).some(function (d) { + let e = path.resolve(d, cmd); + console.log(`*** PATH testing: "${d}" -> "${e}"`); + try { + fs.accessSync(e, fs.constants.X_OK); + console.log(" is executable"); + cmd = "sudo"; + opts.push("-E", e); + return true; + } catch (e) { + console.warn(" cannot execute"); + return false; + } + }) + ) { + console.error(`*** zeronsd not found in PATH (${process.env.PATH})`); + return; + } + } + opts.push("start"); + if (conf.hasOwnProperty("dnsWildcard") && conf.dnsWildcard) { + opts.push("-w"); + } + if (conf.hasOwnProperty("dnsDomain") && !!conf.dnsDomain) { + opts.push("-d", conf.dnsDomain); + } + opts.push(nwid); + process.env.ZEROTIER_CENTRAL_TOKEN = token; + console.log(`*** PATH: "${process.env.PATH}"`); + console.log( + `*** ZEROTIER_CENTRAL_TOKEN: "${process.env.ZEROTIER_CENTRAL_TOKEN}"` + ); + let dns = cp.spawn(cmd, opts, { detached: true }); + dns.on("spawn", () => { + console.log( + `zeronsd successfully spawned [${dns.pid}](${dns.spawnargs})` + ); + setPid(nwid, dns.pid); + }); + dns.stdout.on("data", (data) => { + console.log(`zeronsd spawn stdout: ${data}`); + }); + dns.stderr.on("data", (data) => { + console.error(`zeronsd spawn stderr: ${data}`); + }); + dns.on("error", (error) => { + console.log(`zeronsd spawn ERROR: [${error}](${dns.spawnargs})`); + }); + dns.on("close", (code) => { + console.log(`zeronsd exited: [${code}](${dns.spawnargs})`); + setPid(nwid, null); + }); + } + }); +} + +function stopDNS(nwid, conf) { + let pid = conf.pidDNS; + if (pid) { + isRunning("zeronsd", pid).then((ok) => { + if (ok) { + console.log(`stopDNS(${nwid}): stopping PID ${pid}`); + try { + process.kill(pid); + } catch (e) { + console.error(`stopDNS(${nwid}): stopping PID ${pid} FAILED (${e})`); + } + } else { + console.log(`stopDNS(${nwid}): PID ${pid} is stale`); + } + }); + setPid(nwid, null); + } else { + console.log(`stopDNS(${nwid}): net has no PID`); + } +} + +exports.handleNet = handleNet; +function handleNet(net) { + let cfg = net.additionalConfig; + if (cfg.dnsEnable) { + startDNS(getToken(), net.id, cfg); + } else { + stopDNS(net.id, cfg); + } +} + +exports.scan = scan; +function scan() { + let nets = db.get("networks").value(); + nets.forEach((net) => { + handleNet(net); + }); +} diff --git a/docker/all-in-one/Dockerfile b/docker/all-in-one/Dockerfile new file mode 100644 index 00000000..04814530 --- /dev/null +++ b/docker/all-in-one/Dockerfile @@ -0,0 +1,81 @@ +# ---- initialize build stage +FROM node:current-alpine as builder + +# ---- build ZeroTier-One +RUN apk add --update --no-cache alpine-sdk linux-headers \ + && git clone --quiet https://github.com/zerotier/ZeroTierOne.git /src \ + && make -C /src -f make-linux.mk + +# ---- build Zero-UI +ENV INLINE_RUNTIME_CHUNK=false +ENV GENERATE_SOURCEMAP=false + +RUN yarn set version berry + +WORKDIR /app/frontend +COPY ./frontend/package*.json /app/frontend +COPY ./frontend/yarn.lock /app/frontend +RUN yarn install + +COPY ./frontend /app/frontend +RUN yarn build + +# ---- build ZeroNSd \ +FROM rust:alpine as rbuild + +ARG IS_LOCAL=0 +ARG VERSION=main +ARG IS_TAG=0 + +RUN apk add --update --no-cache git libressl-dev musl-dev \ + && git clone https://github.com/zerotier/zeronsd.git \ + && cd zeronsd \ + && sh cargo-docker.sh + +# ---- initialize deploy stage +FROM node:current-alpine + +LABEL description="ZeroTier One as Docker Image" +LABEL org.opencontainers.image.authors="mcondarelli@soft-in.com" + +# ---- copy ZeroTier-One +ARG ZT_VERSION + +LABEL version="${ZT_VERSION}" + +RUN apk add --update --no-cache libc6-compat libstdc++ + +COPY --from=builder /src/zerotier-one /usr/sbin/ +RUN mkdir -p /var/lib/zerotier-one \ + && ln -s /usr/sbin/zerotier-one /usr/sbin/zerotier-idtool \ + && ln -s /usr/sbin/zerotier-one /usr/sbin/zerotier-cli + +EXPOSE 9993/udp + +# ---- copy Zero-UI +WORKDIR /app/frontend/build +COPY --from=builder /app/frontend/build /app/frontend/build/ + +WORKDIR /app/backend +COPY ./backend/package*.json /app/backend +COPY ./backend/yarn.lock /app/backend +RUN yarn install + +COPY ./backend /app/backend + +EXPOSE 4000 +ENV NODE_ENV=production +ENV ZU_SECURE_HEADERS=true +ENV ZU_SERVE_FRONTEND=true + +# ---- copy ZeroNSd +COPY --from=rbuild /usr/local/cargo/bin/zeronsd /usr/sbin/ + +# ---- final setup + +VOLUME /var/lib/zerotier + +COPY docker/all-in-one/entrypoint.sh /entrypoint.sh +RUN chmod 755 /entrypoint.sh + +CMD /entrypoint.sh diff --git a/docker/all-in-one/Dockerfile.debian b/docker/all-in-one/Dockerfile.debian new file mode 100644 index 00000000..31830b80 --- /dev/null +++ b/docker/all-in-one/Dockerfile.debian @@ -0,0 +1,75 @@ +# ---- initialize build stage +FROM node:current-bullseye as builder + +# ---- build ZeroTier-One +RUN apt-get update && apt-get install -y build-essential \ + && git clone --quiet https://github.com/zerotier/ZeroTierOne.git /src \ + && make -C /src -f make-linux.mk + +# ---- build Zero-UI +ENV INLINE_RUNTIME_CHUNK=false +ENV GENERATE_SOURCEMAP=false + +RUN yarn set version berry + +WORKDIR /app/frontend +COPY ./frontend/package*.json /app/frontend +COPY ./frontend/yarn.lock /app/frontend +RUN yarn install + +COPY ./frontend /app/frontend +RUN yarn build + +# ---- build ZeroNSd \ +FROM rust:bullseye as rbuild + +ARG IS_LOCAL=0 +ARG VERSION=main +ARG IS_TAG=0 + +COPY ./docker/all-in-one/zeronsd.patch /tmp/zeronsd.patch +RUN apt-get update && apt-get install --no-install-recommends -y git libssl-dev \ + && git clone https://github.com/zerotier/zeronsd.git \ + && cd zeronsd \ + && patch -p 1 /dev/null | grep -q zerotier-one + return $? +} + +echo "starting zerotier" +setsid /usr/sbin/zerotier-one & + +while ! grepzt +do + echo "zerotier hasn't started, waiting a second" + sleep 1 +done + +echo "joining networks" + +for i in "$@" +do + echo "joining $i" + + while ! zerotier-cli join "$i" + do + echo "joining $i failed; trying again in 1s" + sleep 1 + done +done + +echo "starting node" +node ./bin/www +echo "at end" diff --git a/docker/all-in-one/zeronsd.patch b/docker/all-in-one/zeronsd.patch new file mode 100644 index 00000000..81627c31 --- /dev/null +++ b/docker/all-in-one/zeronsd.patch @@ -0,0 +1,12 @@ +diff --git a/src/utils.rs b/src/utils.rs +index 0f62d52..0f90e69 100644 +--- a/src/utils.rs ++++ b/src/utils.rs +@@ -21,6 +21,7 @@ pub(crate) fn central_config(token: String) -> Configuration { + let mut config = Configuration::default(); + config.user_agent = Some(version()); + config.bearer_access_token = Some(token); ++ config.base_path = "http://localhost:3000/api".to_string(); + return config; + } + diff --git a/frontend/.gitignore b/frontend/.gitignore index 67dc8992..ab2c4010 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -9,6 +9,54 @@ coverage # Production build +# dotenv environment variables file +.env +.env.test +.env.production + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + # Misc .DS_Store .env.local diff --git a/frontend/src/components/NetworkSettings/NetworkSettings.jsx b/frontend/src/components/NetworkSettings/NetworkSettings.jsx index 9378a35f..860767dd 100644 --- a/frontend/src/components/NetworkSettings/NetworkSettings.jsx +++ b/frontend/src/components/NetworkSettings/NetworkSettings.jsx @@ -8,6 +8,7 @@ import { Typography, TextField, Select, + List, } from "@material-ui/core"; import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; @@ -27,19 +28,24 @@ function NetworkSettings({ network, setNetwork }) { } }; - const handleChange = (key1, key2, mode = "text", additionalData = null) => ( - event - ) => { - const value = parseValue(event, mode, additionalData); + const handleChange = + (key1, key2, mode = "text", additionalData = null) => + (event) => { + const value = parseValue(event, mode, additionalData); - let updatedNetwork = replaceValue({ ...network }, key1, key2, value); - setNetwork(updatedNetwork); + let updatedNetwork = replaceValue({ ...network }, key1, key2, value); + setNetwork(updatedNetwork); - let data = setValue({}, key1, key2, value); + let data = setValue({}, key1, key2, value); - sendReq(data); - }; + sendReq(data); + }; + console.log( + `*** dns="${JSON.stringify(network)}" -> ${JSON.stringify( + network["config"] + )}` + ); return ( }> @@ -89,6 +95,56 @@ function NetworkSettings({ network, setNetwork }) { + + ZeroDNS setup + + + + Enable DNS + + + + + + + + + Use wildcards + +