├── .dockerignore ├── .github ├── FUNDING.yml └── workflows │ ├── build-and-publish-multi-arch-images.yml │ ├── build-website.yml │ ├── test-system.yml │ └── test-unit.yml ├── .gitignore ├── .version ├── Build-Automation ├── System-Tests │ ├── Dockerfile │ ├── compose │ │ ├── download_folder │ │ │ └── server.conf │ │ ├── entrypoint.sh │ │ ├── test-Corefile │ │ ├── test-server-conf │ │ ├── test-server.conf │ │ └── test.yml │ └── wait-for.sh ├── Unit-Tests │ ├── .dockerignore │ ├── Dockerfile │ └── test.yml ├── Website │ ├── Dockerfile │ └── nginx │ │ └── nginx.conf └── WirtBot │ ├── Dockerfile │ ├── compose │ ├── dev.yml │ ├── example.yml │ ├── test-Corefile │ └── test-server.conf │ ├── entrypoint.sh │ ├── nginx │ ├── nginx.http │ └── nginx.https │ ├── s6-overlay-aarch64-installer-2.1.0.2 │ ├── s6-overlay-amd64-installer-2.1.0.2 │ └── service-files │ ├── Corefile │ ├── clean-up │ ├── core │ ├── coredns-plugins │ ├── dns │ ├── finisher │ ├── firewall │ ├── fix-dns-permissions │ ├── fix-wireguard-permissions │ ├── initial-wireguard-config │ ├── interface │ ├── wireguard-metrics │ └── wireguard-restarter ├── CHANGELOG.md ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── Core ├── .dockerignore ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── README.md ├── conf.test ├── dns.test ├── server.conf.example ├── src │ ├── api │ │ └── mod.rs │ ├── crypto │ │ └── mod.rs │ ├── filesystem │ │ ├── managed_dns.rs │ │ ├── mod.rs │ │ └── wireguard_config.rs │ └── main.rs └── wireguard-restarter.sh ├── Documentation ├── Architecture.md ├── Local-Development.md └── Releasing.md ├── Interface ├── .env ├── .env.development ├── .env.localtest ├── .gitignore ├── Architecture_diagram.jpg ├── Architecture_diagram.odg ├── LICENSE ├── LICENSE.md ├── README.md ├── babel.config.js ├── jest.config.js ├── package-lock.json ├── package.json ├── pkg │ └── index.js ├── public │ ├── arrow-thick-left.svg │ ├── arrow-thick-right.svg │ ├── index.html │ ├── logo.svg │ └── wirt-bot.svg ├── src │ ├── App.vue │ ├── Widgets │ │ └── Dashboard │ │ │ ├── DNS.vue │ │ │ ├── Devices.vue │ │ │ ├── Network.vue │ │ │ ├── Server.vue │ │ │ └── Settings.vue │ ├── api │ │ └── index.js │ ├── assets │ │ ├── IBMPlexSans-Regular.ttf │ │ └── logo.svg │ ├── components │ │ ├── AccentedCard.vue │ │ ├── Alerts.vue │ │ ├── Button.vue │ │ ├── Card.vue │ │ ├── DeviceRow.vue │ │ ├── DeviceTable.vue │ │ ├── Footer.vue │ │ ├── Header.vue │ │ ├── Inputs │ │ │ ├── CheckBox.vue │ │ │ ├── IP.vue │ │ │ ├── IpOrHostname.vue │ │ │ ├── Number.vue │ │ │ ├── Select.vue │ │ │ └── Text.vue │ │ └── Modal.vue │ ├── i18n.js │ ├── icons.js │ ├── lib │ │ ├── backup.js │ │ ├── crate │ │ │ ├── Cargo.lock │ │ │ ├── Cargo.toml │ │ │ ├── src │ │ │ │ └── lib.rs │ │ │ └── webdriver.json │ │ ├── crypto.js │ │ ├── dns.js │ │ ├── dns.spec.js │ │ ├── download.js │ │ ├── helpers.js │ │ ├── wireguard.js │ │ └── wireguard.spec.js │ ├── locales │ │ ├── de.json │ │ └── en.json │ ├── main.js │ ├── pages │ │ ├── Dashboard │ │ │ └── Dashboard.vue │ │ └── FirstUse │ │ │ └── FirstUse.vue │ ├── routes.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ └── alerts.js │ └── styles │ │ ├── icons.scss │ │ ├── normalize.css │ │ ├── styles.scss │ │ └── variables.scss └── vue.config.js ├── LICENSE.md ├── Makefile ├── README.md ├── SECURITY.md ├── System-Tests ├── .eslintrc.js ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── run_all_tests.sh └── tests │ ├── backups │ ├── 1.4.5.json │ ├── 2.3.3.json │ ├── 2.3.4.json │ └── 2.6.0.json │ ├── test_IP_input.mjs │ ├── test_backups.spec.mjs │ ├── test_backups_overwrite_old_config.spec.mjs │ ├── test_creating_complex_network.spec.mjs │ ├── test_creating_simple_network.spec.mjs │ ├── test_deleting_device.spec.mjs │ ├── test_device_input_validations.spec.mjs │ ├── test_dns.spec.mjs │ ├── test_dns_ignored_zones.spec.mjs │ ├── test_initial_setup.mjs │ ├── test_square_qr_codes.mjs │ ├── tests.mjs │ └── widgets │ ├── devices.mjs │ ├── dns.mjs │ ├── initial_setup.mjs │ ├── network.mjs │ ├── server.mjs │ └── settings.mjs ├── Website ├── .gitignore ├── LICENSE.md ├── README.md ├── content │ ├── .vuepress │ │ ├── components │ │ │ ├── AccentedCard.vue │ │ │ ├── Button.vue │ │ │ ├── Card.vue │ │ │ ├── HomePageEnglish.vue │ │ │ └── Modal.vue │ │ ├── config.js │ │ ├── public │ │ │ ├── browser.svg │ │ │ ├── devices.svg │ │ │ ├── glass.svg │ │ │ ├── lightning.svg │ │ │ ├── logo.svg │ │ │ └── wirt-bot.svg │ │ └── styles │ │ │ ├── palette.styl │ │ │ └── variables.scss │ ├── README.md │ ├── announcements │ │ ├── 11-01-23.md │ │ ├── 21-10-20.md │ │ ├── 22-11-20.md │ │ ├── 31-10-20.md │ │ ├── 8-11-20.md │ │ ├── README.md │ │ ├── update-3.md │ │ ├── update-4.md │ │ ├── update-5.md │ │ ├── update-6.md │ │ ├── update-7.md │ │ ├── update-8.md │ │ └── update-9.md │ ├── documentation │ │ ├── README.md │ │ ├── faq.md │ │ ├── images │ │ │ ├── android1.jpg │ │ │ ├── android2.jpg │ │ │ ├── android3.jpg │ │ │ ├── android4.jpg │ │ │ ├── android5.jpg │ │ │ ├── android6.jpg │ │ │ ├── android7.jpg │ │ │ ├── windows1.jpg │ │ │ ├── windows2.jpg │ │ │ ├── windows3.jpg │ │ │ ├── windows4.jpg │ │ │ └── windows5.jpg │ │ ├── issues.md │ │ ├── join-a-network.md │ │ ├── setup.md │ │ ├── system-overview.md │ │ └── what-can-i-do-with-wirt.md │ ├── images │ │ └── dashboard.jpg │ └── screenshots.md ├── package-lock.json ├── package.json └── system-overview.md ├── convenience-scripts ├── generate_interface_keys.py ├── update-dependencies.sh └── update-version.sh ├── dashboard.jpg └── testwirtbot.conf.example /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/target/ 3 | **/crate/pkg/ 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: b-m-f # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/build-and-publish-multi-arch-images.yml: -------------------------------------------------------------------------------- 1 | name: Build multi arch docker images and push to dockerhub 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | multi: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v1 17 | 18 | - name: Set up Docker Buildx 19 | uses: docker/setup-buildx-action@v1 20 | 21 | - name: Login to DockerHub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKERHUB_USERNAME }} 25 | password: ${{ secrets.DOCKERHUB_TOKEN }} 26 | 27 | - name: Docker meta 28 | id: docker_meta 29 | uses: crazy-max/ghaction-docker-meta@v1 30 | with: 31 | images: bmff/wirtbot 32 | tag-semver: | 33 | {{version}} 34 | 35 | - name: Build and push amd64,arm64 36 | uses: docker/build-push-action@v2 37 | with: 38 | context: . 39 | file: ./Build-Automation/WirtBot/Dockerfile 40 | platforms: linux/amd64,linux/arm64 41 | push: true 42 | tags: ${{ steps.docker_meta.outputs.tags }} 43 | labels: ${{ steps.docker_meta.outputs.labels }} 44 | -------------------------------------------------------------------------------- /.github/workflows/build-website.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy website 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | build-and-deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 🛎️ 11 | uses: actions/checkout@v2.3.1 12 | 13 | - name: Install and Build 🔧 14 | run: | 15 | cd Website && npm install 16 | npm run build 17 | 18 | - name: Deploy 🚀 19 | uses: JamesIves/github-pages-deploy-action@4.1.5 20 | with: 21 | branch: gh-pages 22 | folder: Website/content/.vuepress/dist 23 | -------------------------------------------------------------------------------- /.github/workflows/test-system.yml: -------------------------------------------------------------------------------- 1 | name: run WirtBot system tests 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | pull_request: 7 | branches: [master] 8 | paths: 9 | - "Interface/**" 10 | - "WirtBot/**" 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | build: 15 | runs-on: ubuntu-20.04 16 | 17 | steps: 18 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 19 | - uses: actions/checkout@v2 20 | - name: Run test suite for the whole System 21 | run: | 22 | make test-system 23 | -------------------------------------------------------------------------------- /.github/workflows/test-unit.yml: -------------------------------------------------------------------------------- 1 | name: run WirtBot unit tests 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | paths: 7 | - "Core/**" 8 | - "Interface/**" 9 | 10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 17 | - uses: actions/checkout@v2 18 | - name: Trigger tests 19 | run: | 20 | make test-unit-ci 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/target 3 | testwirtbot.conf 4 | 5 | -------------------------------------------------------------------------------- /.version: -------------------------------------------------------------------------------- 1 | 3.9.4 2 | -------------------------------------------------------------------------------- /Build-Automation/System-Tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/playwright:jammy 2 | 3 | # install node gyp deps 4 | RUN apt-get update 5 | RUN apt-get install -yy g++ build-essential 6 | 7 | # build the app 8 | WORKDIR '/app' 9 | COPY System-Tests/package.json . 10 | COPY System-Tests/package-lock.json . 11 | RUN npm ci 12 | 13 | 14 | COPY System-Tests/tests/ tests/ 15 | COPY System-Tests/run_all_tests.sh run_all_tests.sh 16 | CMD ["npm", "run", "test"] 17 | -------------------------------------------------------------------------------- /Build-Automation/System-Tests/compose/download_folder/server.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.10.0.1 3 | ListenPort = 1233 4 | PrivateKey = aIY75yhDnUvdk2Jv0oKhOlvIsrtoT2/r1DD7qv9teFI= 5 | 6 | [Peer] 7 | AllowedIPs = 10.10.0.2/32 8 | PublicKey = izKgv4WqLerPUVde8sSI6o4hvDgrHzsjC6JUEmb+RSI= 9 | [Peer] 10 | AllowedIPs = 10.10.0.3/32 11 | PublicKey = bF2yMwOz3ql4Bjz34mYI9RuVuaLof2CKp+iVx0rHBB4= -------------------------------------------------------------------------------- /Build-Automation/System-Tests/compose/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # Modified entrypoint that does not actually set up Firewall rules or spawn the WireGuard interface 4 | 5 | set -eo pipefail 6 | 7 | if [[ -z "${SSL_INTERFACE}" ]]; then 8 | # Env variable is not defined 9 | ## || true is added so it does not crash when the config file doesnt exist yet 10 | rm /etc/nginx/nginx.conf || true 11 | mv /etc/nginx/nginx.http /etc/nginx/nginx.conf 12 | else 13 | # Env variable is defined 14 | echo "Preparing NGINX for SSL" 15 | rm /etc/nginx/nginx.conf || true 16 | mv /etc/nginx/nginx.https /etc/nginx/nginx.conf 17 | fi 18 | 19 | # Start all services needed for WirtBot wit s6-overlay supervision 20 | /init 21 | -------------------------------------------------------------------------------- /Build-Automation/System-Tests/compose/test-Corefile: -------------------------------------------------------------------------------- 1 | . { 2 | reload 3 | prometheus 0.0.0.0:9153 4 | forward . tls://1.1.1.1 { 5 | except test fritz.box lan local home 6 | tls_servername cloudflare-dns.com 7 | health_check 5s 8 | } 9 | cache 30 10 | } 11 | test { 12 | hosts { 13 | 10.10.0.1 wirtbot.test 14 | 1010:1010:1010:1010:0000:0000:0000:1 wirtbot.test 15 | 10.10.0.2 test-1.test 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Build-Automation/System-Tests/compose/test-server-conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.11.0.1 3 | ListenPort = 1234 4 | PrivateKey = aEYrLAUI9YaGN8zm1Dkj/5n4m7PKY3f05BulPIIUXVs= 5 | 6 | [Peer] 7 | AllowedIPs = 10.11.0.2/32 8 | PublicKey = rQC0eUNPDOb3nEmcgkkf13ishklyZS0ex8R0x0Ak5GQ= -------------------------------------------------------------------------------- /Build-Automation/System-Tests/compose/test-server.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.10.0.1 3 | ListenPort = 1234 4 | PrivateKey = SG61J3enwG68I5pKRKj/ndIhofLCrLBmolstdQeOFlE= 5 | 6 | [Peer] 7 | AllowedIPs = 10.10.0.2/32 8 | PublicKey = 8pLTvLBnzLRJdzD/XIB8/eQeCnp6wBUyIHLsqsQW1Ws= -------------------------------------------------------------------------------- /Build-Automation/System-Tests/compose/test.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | test-runner: 4 | build: 5 | dockerfile: Build-Automation/System-Tests/Dockerfile 6 | context: ../../.. 7 | container_name: test-runner 8 | command: ["npm", "run", "test:ci"] 9 | depends_on: ["test-wirtbot"] 10 | volumes: 11 | - ./test-Corefile:/tmp/WirtBotTests/Corefile 12 | - ./test-server.conf:/tmp/WirtBotTests/server.conf 13 | test-wirtbot: 14 | entrypoint: ["/bin/bash", "/test-entrypoint.sh"] 15 | build: 16 | dockerfile: Build-Automation/WirtBot/Dockerfile 17 | context: ../../.. 18 | args: 19 | - environment=test 20 | container_name: wirtbot.test 21 | volumes: 22 | - ./test-server.conf:/etc/wireguard/server.conf:z 23 | - ./test-Corefile:/etc/coredns/Corefile:z 24 | - ./entrypoint.sh:/test-entrypoint.sh:z 25 | ports: 26 | - 10101:10101/udp 27 | environment: 28 | - "PUBLIC_KEY=1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI=" 29 | - "PORT=3030" 30 | - "ALLOWED_ORIGIN=http://wirtbot.test" 31 | - "RUST_LOG=debug" 32 | - "MANAGED_DNS_ENABLED=1" 33 | - "MANAGED_DNS_DEVICE_FILE=/etc/coredns/Corefile" 34 | - "CONFIG_PATH=/etc/wireguard/server.conf" 35 | 36 | volumes: 37 | download_folder: 38 | -------------------------------------------------------------------------------- /Build-Automation/Unit-Tests/.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules/ 2 | **/target/ 3 | **/crate/pkg/ 4 | -------------------------------------------------------------------------------- /Build-Automation/Unit-Tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:buster 2 | ENV DEBIAN_FRONTEND=noninteractive 3 | RUN apt-get update && apt-get upgrade -yy 4 | RUN apt-get install -yy build-essential make 5 | 6 | # install taken from https://github.com/nodejs/docker-node/blob/main/16/buster/Dockerfile 7 | ENV NODE_VERSION 16.12.0 8 | 9 | RUN ARCH= && dpkgArch="$(dpkg --print-architecture)" \ 10 | && case "${dpkgArch##*-}" in \ 11 | amd64) ARCH='x64';; \ 12 | ppc64el) ARCH='ppc64le';; \ 13 | s390x) ARCH='s390x';; \ 14 | arm64) ARCH='arm64';; \ 15 | armhf) ARCH='armv7l';; \ 16 | i386) ARCH='x86';; \ 17 | *) echo "unsupported architecture"; exit 1 ;; \ 18 | esac \ 19 | # gpg keys listed at https://github.com/nodejs/node#release-keys 20 | && set -ex \ 21 | && for key in \ 22 | 4ED778F539E3634C779C87C6D7062848A1AB005C \ 23 | 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ 24 | 74F12602B6F1C4E913FAA37AD3A89613643B6201 \ 25 | 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ 26 | 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \ 27 | C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \ 28 | C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C \ 29 | DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ 30 | A48C2BEE680E841632CD4E44F07496B3EB3C1762 \ 31 | 108F52B48DB57BB0CC439B2997B01419BD92F80A \ 32 | B9E2F5981AA6E0CD28160D9FF13993A75599653C \ 33 | ; do \ 34 | gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" || \ 35 | gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ 36 | done \ 37 | && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" \ 38 | && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ 39 | && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ 40 | && grep " node-v$NODE_VERSION-linux-$ARCH.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ 41 | && tar -xJf "node-v$NODE_VERSION-linux-$ARCH.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \ 42 | && rm "node-v$NODE_VERSION-linux-$ARCH.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \ 43 | && ln -s /usr/local/bin/node /usr/local/bin/nodejs 44 | 45 | 46 | RUN mkdir /etc/wireguard 47 | RUN mkdir /etc/coredns/ 48 | RUN touch /etc/wireguard/server.conf 49 | RUN touch /etc/coredns/Corefile 50 | 51 | 52 | COPY Core/ Core/ 53 | COPY Website/ Website/ 54 | COPY Interface Interface 55 | COPY Makefile . 56 | 57 | RUN npm i -g wasm-pack 58 | RUN make dev-setup 59 | 60 | ENTRYPOINT ["make", "test-unit"] -------------------------------------------------------------------------------- /Build-Automation/Unit-Tests/test.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | wirtbot_unit_tests: 5 | build: 6 | context: ../.. 7 | dockerfile: Build-Automation/Unit-Tests/Dockerfile 8 | restart: always 9 | environment: 10 | "PUBLIC_KEY": "1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI=" 11 | "ALLOWED_ORIGIN": "http://localhost:8080" 12 | "RUST_LOG": "debug" 13 | "MANAGED_DNS_ENABLED": 1 14 | container_name: wirtbot_unit_tests 15 | -------------------------------------------------------------------------------- /Build-Automation/Website/Dockerfile: -------------------------------------------------------------------------------- 1 | # APP 2 | FROM node:buster as app 3 | 4 | 5 | # build the app 6 | WORKDIR '/website' 7 | COPY Website/package.json . 8 | COPY Website/package-lock.json . 9 | ENV NODE_ENV production 10 | RUN npm ci 11 | 12 | COPY Website/styles content 13 | COPY Website/components content 14 | RUN npm run build 15 | 16 | 17 | # Create small Nginx image with the production ready application 18 | FROM nginx:alpine 19 | 20 | COPY --from=app /website/content/.vuepress/dist /website 21 | COPY Build-Automation/Website/nginx/nginx.conf /etc/nginx/nginx.conf 22 | -------------------------------------------------------------------------------- /Build-Automation/Website/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user nobody nobody; 2 | worker_processes 1; 3 | error_log stderr error; 4 | worker_rlimit_nofile 8192; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | 11 | http { 12 | access_log /dev/stdout combined; 13 | 14 | server { 15 | listen 80 default_server; 16 | listen [::]:80 default_server; 17 | server_name _; 18 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; 19 | add_header X-Frame-Options DENY; 20 | add_header X-Content-Type-Options nosniff; 21 | add_header X-XSS-Protection "1; mode=block"; 22 | include /etc/nginx/mime.types; 23 | 24 | root /website; 25 | error_page 404 /404.html; 26 | location /404.html { 27 | internal; 28 | } 29 | 30 | location / { 31 | try_files $uri $uri/ $uri.html =404; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /Build-Automation/WirtBot/compose/dev.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | wirtbot: 5 | build: 6 | context: ../../.. 7 | dockerfile: Build-Automation/WirtBot/DevDockerfile 8 | ports: [3030:3030] 9 | restart: always 10 | volumes: ["../..:/app"] 11 | environment: 12 | "PUBLIC_KEY": "1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI=" 13 | "ALLOWED_ORIGIN": "http://localhost:8080" 14 | "RUST_LOG": "debug" 15 | "MANAGED_DNS_ENABLED": 1 16 | cap_add: 17 | - NET_ADMIN 18 | volumes: 19 | - ./test-server.conf:/etc/wireguard/server.conf 20 | - ./test-Corefile:/dns/Corefile 21 | ports: 22 | - 10101:10101/udp 23 | container_name: development_wirtbot 24 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/compose/example.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | wirtbot: 5 | build: 6 | context: ../../.. 7 | dockerfile: Build-Automation/WirtBot/Dockerfile 8 | 9 | ports: [3030:3030] 10 | restart: always 11 | environment: 12 | "PUBLIC_KEY": "1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI=" 13 | "ALLOWED_ORIGIN": "http://localhost:8080" 14 | "RUST_LOG": "debug" 15 | "MANAGED_DNS_ENABLED": 1 16 | cap_add: 17 | - NET_ADMIN 18 | volumes: 19 | - ./test-server.conf:/etc/wireguard/server.conf 20 | - ./test-Corefile:/dns/Corefile 21 | ports: 22 | - 80:80 23 | - 3030:3030 24 | - 10101:10101/udp 25 | container_name: development_wirtbot 26 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/compose/test-Corefile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Build-Automation/WirtBot/compose/test-Corefile -------------------------------------------------------------------------------- /Build-Automation/WirtBot/compose/test-server.conf: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.10.4.1 3 | ListenPort = 10101 4 | PrivateKey = 8F17gFH1GXCdtikT78dQ81O1ch9XsKUzscnfyS97KkY= 5 | 6 | [Peer] 7 | AllowedIPs = 10.10.4.2/32 8 | PublicKey = Mmn3hPQTp+rYXGTaag8LvE/R4HNTxf2/pSk3MdFicS8= -------------------------------------------------------------------------------- /Build-Automation/WirtBot/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -eo pipefail 4 | 5 | if [[ -z "${SSL_INTERFACE}" ]]; then 6 | # Env variable is not defined 7 | ## || true is added so it does not crash when the config file doesnt exist yet 8 | rm /etc/nginx/nginx.conf || true 9 | mv /etc/nginx/nginx.http /etc/nginx/nginx.conf 10 | else 11 | # Env variable is defined 12 | echo "Preparing NGINX for SSL" 13 | rm /etc/nginx/nginx.conf || true 14 | mv /etc/nginx/nginx.https /etc/nginx/nginx.conf 15 | fi 16 | 17 | # Start wireguard interface 18 | wg-quick up server 19 | 20 | # Start firewall for masquerading traffic when routing traffic from clients in the network 21 | /firewall.sh 22 | 23 | # Start all services needed for WirtBot wit s6-overlay supervision 24 | /init 25 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/nginx/nginx.http: -------------------------------------------------------------------------------- 1 | user interface interface; 2 | daemon off; 3 | worker_processes 1; 4 | worker_rlimit_nofile 8192; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | 11 | http { 12 | 13 | server { 14 | listen 80 default_server; 15 | listen [::]:80 default_server; 16 | server_name _; 17 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; 18 | add_header X-Frame-Options DENY; 19 | add_header X-Content-Type-Options nosniff; 20 | add_header X-XSS-Protection "1; mode=block"; 21 | include /etc/nginx/mime.types; 22 | types { 23 | application/wasm wasm; 24 | } 25 | 26 | root /interface; 27 | 28 | location / { 29 | try_files $uri $uri/ /index.html; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /Build-Automation/WirtBot/nginx/nginx.https: -------------------------------------------------------------------------------- 1 | user interface interface; 2 | daemon off; 3 | worker_processes 1; 4 | worker_rlimit_nofile 8192; 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | 11 | http { 12 | 13 | server { 14 | # https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=modern&openssl=1.1.1k&guideline=5.6 15 | listen 443 ssl http2; 16 | server_name _; 17 | 18 | ssl_certificate /interface/public_key; 19 | ssl_certificate_key /interface/private_key; 20 | 21 | ssl_protocols TLSv1.2 TLSv1.3; 22 | ssl_prefer_server_ciphers on; 23 | ssl_session_timeout 1d; 24 | ssl_session_cache shared:MozSSL:10m; # about 40000 sessions 25 | ssl_session_tickets off; 26 | 27 | add_header Strict-Transport-Security "max-age=63072000" always; 28 | add_header X-Frame-Options DENY; 29 | add_header X-Content-Type-Options nosniff; 30 | add_header X-XSS-Protection "1; mode=block"; 31 | include /etc/nginx/mime.types; 32 | types { 33 | application/wasm wasm; 34 | } 35 | 36 | root /interface; 37 | 38 | location / { 39 | try_files $uri $uri/ /index.html; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Build-Automation/WirtBot/s6-overlay-aarch64-installer-2.1.0.2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Build-Automation/WirtBot/s6-overlay-aarch64-installer-2.1.0.2 -------------------------------------------------------------------------------- /Build-Automation/WirtBot/s6-overlay-amd64-installer-2.1.0.2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Build-Automation/WirtBot/s6-overlay-amd64-installer-2.1.0.2 -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/Corefile: -------------------------------------------------------------------------------- 1 | . { 2 | reload 10s 3 | prometheus 4 | } 5 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/clean-up: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | wg-quick down server -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/core: -------------------------------------------------------------------------------- 1 | #!/usr/bin/execlineb -P 2 | with-contenv 3 | s6-setuidgid core wirtbot-core -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/coredns-plugins: -------------------------------------------------------------------------------- 1 | metadata:metadata 2 | cancel:cancel 3 | tls:tls 4 | reload:reload 5 | debug:debug 6 | health:health 7 | prometheus:metrics 8 | errors:errors 9 | log:log 10 | cache:cache 11 | hosts:hosts 12 | forward:forward 13 | whoami:whoami 14 | local:local 15 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/dns: -------------------------------------------------------------------------------- 1 | #!/usr/bin/with-contenv sh 2 | coredns -conf /etc/coredns/Corefile 3 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/finisher: -------------------------------------------------------------------------------- 1 | #!/usr/bin/execlineb -S1 2 | s6-svscanctl -t /var/run/s6/services 3 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/firewall: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eou pipefail 4 | 5 | # Making sure that the rule only exists once in the table. 6 | # || true is added so the script does not exit with error on first run when no rule can be flushed 7 | nft flush chain nat postrouting || true 8 | 9 | # check https://wiki.nftables.org/wiki-nftables/index.php/Performing_Network_Address_Translation_(NAT) 10 | nft add table nat 11 | nft 'add chain nat postrouting { type nat hook postrouting priority 100 ; }' 12 | nft add rule nat postrouting oifname eth0 masquerade fully-random 13 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/fix-dns-permissions: -------------------------------------------------------------------------------- 1 | /etc/coredns true coredns:coredns 0660 0770 -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/fix-wireguard-permissions: -------------------------------------------------------------------------------- 1 | /etc/wireguard true core 0600 0700 -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/initial-wireguard-config: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.10.0.0/24 3 | PrivateKey = yAnz5TF+lXXJte14tji3zlMNq+hd2rYUIgJBgB3fBmk= 4 | ListenPort = 10101 -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/interface: -------------------------------------------------------------------------------- 1 | #!/usr/bin/execlineb -P 2 | nginx -c /etc/nginx/nginx.conf 3 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/wireguard-metrics: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | prometheus_wireguard_exporter -n /etc/wireguard/server.conf -i server 4 | 5 | 6 | -------------------------------------------------------------------------------- /Build-Automation/WirtBot/service-files/wireguard-restarter: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | wireguard-restarter 4 | 5 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | Have fun. 4 | Dont disrespect other contributors. 5 | Try to help each other out, no need for competition here. 6 | Report any behaviour that you think crosses the line as a Github issue! 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Git 4 | 5 | ### Branches 6 | 7 | The main branch is `master`. 8 | 9 | Base your work on it and chose it as the target for Merge Requests. 10 | 11 | ## Testing 12 | 13 | Make sure to include tests with your changes. 14 | If the code you submit adds or changes functionality this should be covered by a test. 15 | 16 | Pull requests without tests might be rejected for that reason. 17 | 18 | ### Running system-tests 19 | 20 | Run `make -j2 dev-tests` which runs the Core with a pre-seeded Public key. 21 | 22 | Now head into the System-Tests folder and run the provided script with the following Environment variables: 23 | 24 | - URL: url of the Interface (default it localhost:8080) 25 | - API: url of the API (default it localhost:3030) 26 | 27 | Alternatively you can run `make systems-tests` to run all tests in a dockerized environment. 28 | 29 | ## Development environment 30 | 31 | ### Dependencies 32 | 33 | Make sure that `npm`, `g++`, `rustup`, `cargo-watch` and `wasm-pack` are installed. 34 | 35 | - For `npm` I recommend [nvm](https://github.com/nvm-sh/nvm). 36 | - To install `g++` please check how to do this on your OS. 37 | - To install `rustup` run `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh` and add `$HOME/.cargo/bin` to your PATH 38 | - To install `wasm-pack` run ` curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh` 39 | - `cargo-watch` can be installed with `cargo install cargo-watch` 40 | 41 | To install all the necessary dependencies first run `make dev-setup`. 42 | 43 | You can find all available functionality in the `Makefile` in the root of the repository. 44 | To get started run `make -j2 dev` to get a local build up and running 45 | 46 | ### Reading the WirtBot logs 47 | 48 | - Find out WirtBot container name with `docker ps` 49 | - attach to WirtBot with `docker logs -f $CONTAINER_NAME` 50 | -------------------------------------------------------------------------------- /Core/.dockerignore: -------------------------------------------------------------------------------- 1 | target/ -------------------------------------------------------------------------------- /Core/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | public.key 3 | -------------------------------------------------------------------------------- /Core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wirtbot" 3 | version = "3.9.4" 4 | authors = ["b-m-f "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | tokio = { version = "1.1.0", features = ["macros", "rt-multi-thread"] } 9 | warp = {version = "0.3", features = ["tls"]} 10 | serde = {version = "1.0.116", features = ["derive"]} 11 | serde_json = "1.0" 12 | log = "0.4" 13 | pretty_env_logger = "0.4" 14 | ed25519-dalek = {version = "1.0.1", features = ["serde"]} 15 | base64 = "0.13.0" 16 | rand = "0.7" 17 | -------------------------------------------------------------------------------- /Core/README.md: -------------------------------------------------------------------------------- 1 | # WirtBot 2 | 3 | This is the WirtBot Core that is responsible for: 4 | 5 | - listening to incomming requests from the Interface that come via HTTP/S 6 | - automatically update the WireGuard configuration 7 | - automatically update the CoreDNS configuration 8 | 9 | in this order. 10 | 11 | To guarantee that only the user who owns the WirtBot can update it, the Interface's public key has to be provided when running the WirtBot to verify payloads with the Interface's signature. 12 | 13 | ## Compilation 14 | 15 | - Install rust and cargo with https://rustup.rs/ 16 | - `cargo build --release` 17 | 18 | ## Configuration options 19 | 20 | The WirtBot is configured with environment variables: 21 | 22 | ### Optional 23 | 24 | - **PUBLIC_KEY**: Public key of Interface. Will create a keypair if ommited 25 | - **HOST**: the host address to listen on 26 | - **PORT**: the port to listen on 27 | - **CONFIG_PATH**: path to WireGuard configuration 28 | - **MANAGED_DNS_DEVICE_FILE**: File to write DNS entries to 29 | - **SSL_CORE**: Actives SSL for the core and expectes keys at `/core/public_key` and `/core/private_key` 30 | 31 | ## Logging 32 | 33 | To enable logging run the program with the wanted environment variable. 34 | Check [env-logger](https://docs.rs/env_logger/0.7.1/env_logger/) for more information. 35 | 36 | ## Payloads 37 | `LOG_PAYLOADS=true` 38 | 39 | 40 | ### Server logs 41 | 42 | `RUST_LOG=wirt::api`. For example `RUST_LOG=wirt::api cargo run` 43 | 44 | ### Info logs 45 | 46 | `RUST_LOG=info` 47 | -------------------------------------------------------------------------------- /Core/conf.test: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.10.0.1 3 | ListenPort = 1234 4 | PrivateKey = uFOce8aMDBCJyIIAXE9bLrCmKu1DdNo58KzGQzE+/kk= -------------------------------------------------------------------------------- /Core/dns.test: -------------------------------------------------------------------------------- 1 | . { 2 | reload 3 | local 4 | prometheus 0.0.0.0:9153 5 | forward . tls://1.2.3.4 { 6 | except wirt.internal fritz.box lan local home test 7 | tls_servername testdns.test 8 | health_check 5s 9 | } 10 | cache 30 11 | } 12 | test { 13 | hosts { 14 | 10.11.0.1 test.test 15 | 1001::1000:1 test.test 16 | 10.11.0.2 test-1.test 17 | 10.11.0.3 test-2.test 18 | 1001::1000:fffe test-2.test 19 | 1001::1000:fffa test-3.test 20 | } 21 | } -------------------------------------------------------------------------------- /Core/server.conf.example: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.10.0.1 3 | ListenPort = 10101 4 | PrivateKey = wKcOYzxjaQV03gIy0uBov+WgeR1U20XY8o63r1ljuUc= -------------------------------------------------------------------------------- /Core/src/crypto/mod.rs: -------------------------------------------------------------------------------- 1 | use base64::{decode, encode}; 2 | use ed25519_dalek::{Keypair, PublicKey, Signature, PUBLIC_KEY_LENGTH, SIGNATURE_LENGTH}; 3 | use rand::rngs::OsRng; 4 | use serde_json::json; 5 | use std::env; 6 | use std::fmt; 7 | 8 | const PUBLIC_KEY: &str = "PUBLIC_KEY"; 9 | 10 | /// Errors that can occur while decoding. 11 | #[derive(Clone, Debug, PartialEq, Eq)] 12 | pub enum DecodeError { 13 | NotAPublicKey(String), 14 | NotASignature(String), 15 | Decode(String) 16 | } 17 | 18 | impl fmt::Display for DecodeError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 20 | match &*self { 21 | DecodeError::NotAPublicKey(string) => { 22 | write!(f, "{}", string) 23 | } 24 | DecodeError::NotASignature(string) => { 25 | write!(f, "{}", string) 26 | } 27 | DecodeError::Decode(string) => { 28 | write!(f, "{}", string) 29 | } 30 | } 31 | } 32 | } 33 | 34 | pub fn decode_public_key_base64(public_key_base64: String) -> Result { 35 | let mut raw_public_key_buffer = [0; PUBLIC_KEY_LENGTH]; 36 | let raw_public_key_vector = match decode(&public_key_base64) { 37 | Ok(vec) => vec, 38 | Err(_) => { 39 | return Err(DecodeError::NotAPublicKey( 40 | "Data provided was not a base64 encoded public key".into(), 41 | )) 42 | } 43 | }; 44 | let raw_public_key_bytes = &raw_public_key_vector[..raw_public_key_buffer.len()]; 45 | raw_public_key_buffer.copy_from_slice(raw_public_key_bytes); 46 | match PublicKey::from_bytes(&raw_public_key_buffer) { 47 | Ok(key) => Ok(key), 48 | Err(_) => Err(DecodeError::NotAPublicKey( 49 | "Data provided was not a base64 encoded public key".into(), 50 | )), 51 | } 52 | } 53 | 54 | pub fn decode_signature_base64(signature_base64: String) -> Result { 55 | let raw_signature_vector = match decode(&signature_base64) { 56 | Ok(vec) => vec, 57 | Err(_) => { 58 | return Err(DecodeError::NotASignature( 59 | "Data provided was not a base64 encoded signature".into(), 60 | )) 61 | } 62 | }; 63 | if raw_signature_vector.len() != SIGNATURE_LENGTH { 64 | return Err(DecodeError::NotASignature( 65 | "Data provided was not a base64 encoded signature".into(), 66 | )); 67 | } 68 | match Signature::from_bytes(&raw_signature_vector){ 69 | Ok(val) => return Ok(val), 70 | Err(e) => { 71 | return Err(DecodeError::Decode( 72 | e.to_string(), 73 | )); 74 | } 75 | } 76 | } 77 | 78 | pub fn get_key() -> String { 79 | match env::var(PUBLIC_KEY) { 80 | Ok(val) => return val, 81 | Err(_) => { 82 | let mut csprng = OsRng {}; 83 | let keypair: Keypair = Keypair::generate(&mut csprng); 84 | std::println!("A new keypair for communication between Core and UI was generated"); 85 | 86 | let new_conf = json!({ 87 | "keys": { 88 | "private": encode(keypair.secret.to_bytes()), 89 | "public": encode(keypair.public.to_bytes()) 90 | }, 91 | }); 92 | std::println!("Please import the following text into your dashboard to take control of this WirtBot"); 93 | std::println!("{}", encode(new_conf.to_string())); 94 | return encode(keypair.public.to_bytes()); 95 | } 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | mod test { 101 | use super::*; 102 | 103 | use ed25519_dalek::Signer; 104 | use std::any::type_name; 105 | 106 | fn type_of(_: T) -> &'static str { 107 | type_name::() 108 | } 109 | 110 | #[test] 111 | fn get_key_from_env_and_without_env_and_decoding() { 112 | // These cant be seperate tests, as they both manipulate the environment 113 | env::set_var(PUBLIC_KEY, "test"); 114 | assert_eq!(get_key(), "test"); 115 | env::remove_var(PUBLIC_KEY); 116 | let key = get_key(); 117 | let bytes = decode(&key).unwrap(); 118 | assert_eq!(bytes.len(), PUBLIC_KEY_LENGTH); 119 | PublicKey::from_bytes(&bytes).unwrap(); 120 | let key = get_key(); 121 | let pubkey = decode_public_key_base64(key).unwrap(); 122 | assert_eq!(type_of(pubkey), "ed25519_dalek::public::PublicKey"); 123 | } 124 | 125 | #[test] 126 | fn test_write_devices_replaces_previous_content() { 127 | let mut csprng = OsRng {}; 128 | let keypair: Keypair = Keypair::generate(&mut csprng); 129 | let message: &[u8] = b"test"; 130 | let signature: Signature = keypair.sign(message); 131 | let encoded_signature = encode(signature); 132 | let decoded_signature = decode_signature_base64(encoded_signature).unwrap(); 133 | assert_eq!(decoded_signature, signature); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Core/src/filesystem/managed_dns.rs: -------------------------------------------------------------------------------- 1 | pub fn write_device_file(devices: String, path: String) -> std::io::Result<()> { 2 | return std::fs::write(path, devices.as_bytes()); 3 | } 4 | -------------------------------------------------------------------------------- /Core/src/filesystem/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod wireguard_config; 2 | pub mod managed_dns; -------------------------------------------------------------------------------- /Core/src/filesystem/wireguard_config.rs: -------------------------------------------------------------------------------- 1 | use std::fs::OpenOptions; 2 | use std::io::prelude::*; 3 | use std::io::Result as IOResult; 4 | 5 | pub fn write_config_file(config: String, path: String) -> IOResult<()> { 6 | match OpenOptions::new() 7 | .read(true) 8 | .write(true) 9 | .truncate(true) 10 | .create(true) 11 | .open(path) 12 | { 13 | Ok(mut file) => { 14 | file.write_all(config.as_bytes())?; 15 | return Ok(()); 16 | } 17 | Err(e) => { 18 | return Err(e); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Core/src/main.rs: -------------------------------------------------------------------------------- 1 | use pretty_env_logger; 2 | 3 | mod api; 4 | mod crypto; 5 | mod filesystem; 6 | 7 | #[macro_use] 8 | extern crate log; 9 | 10 | #[tokio::main] 11 | async fn main() { 12 | // Setup server 13 | pretty_env_logger::init(); 14 | api::start_api().await; 15 | } 16 | -------------------------------------------------------------------------------- /Core/wireguard-restarter.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | while inotifywait -e close_write /etc/wireguard/server.conf; do 3 | wg-quick down server; 4 | wg-quick up server; 5 | echo "WireGuard interface has been restartd"; 6 | echo "Old Peers were removed"; 7 | echo "New Peers were added"; 8 | done 9 | -------------------------------------------------------------------------------- /Documentation/Architecture.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | ## Flow 4 | ### Secure Setup 5 | 1. Set up the WirtBot to get a keypair 6 | 2. Restart the WirtBot, providing the newly generated public key 7 | 3. Add devices to the WirtBot, such as the computer used for the setup 8 | 4. Download the WireGuard config and join the private network 9 | 5. Try reaching the WirtBot at its internal IP address 10 | 6. On success Restart the WirtBot, making sure not to bind the Interface Port 80 11 | 7. All configuration can now be done inside the safe internal network 12 | 8. The network and interface are still secured to only be changeable by the admin 13 | 14 | ## Interface 15 | 16 | The interface is written with Vue.js, using the [vue-cli](https://cli.vuejs.org/) tooling. 17 | Key generation is done with WASM that was compiled from Rust. 18 | 19 | Check the **Interface** directory in the repository to read about details regarding the stack. 20 | 21 | ## Core 22 | 23 | The Core API is written in Rust. 24 | 25 | ## Secure Communication between Interface and Core 26 | 27 | All communication is based on Public/Private key encryption. 28 | Every payload from the Interface is signed with its private key. 29 | 30 | The Core, which is provided with the Interface\`s public key at start, verifies the signature on every incomming request. 31 | 32 | In case the signature is invalid the request is dropped. 33 | 34 | ## Containerization 35 | 36 | The Container uses [s6-overlay](https://github.com/just-containers/s6-overlay) to run all necessary processes. 37 | 38 | This approach is chosen to encapsulate the functionality of the whole WirtBot into 1 container. 39 | This allows for easy stopping and starting of the whole system without needing all the inside knowledge when setting it up. 40 | 41 | For details check the **Build-automation** directory. 42 | 43 | -------------------------------------------------------------------------------- /Documentation/Local-Development.md: -------------------------------------------------------------------------------- 1 | # Barebones setup 2 | 3 | `make -j2 dev` 4 | 5 | This will spawn the Interface and server in their default configuration. 6 | They are not set up to allow communication between each other, so that things such as the necessity to ensure proper key exchange between Core and Interface are not hidden. 7 | 8 | # Preconfigured setup 9 | 10 | `make -j2 dev-tests` 11 | 12 | This will spawn a Core and Interface that are preconfigured with a keypair to ensure they can communicate. 13 | 14 | Using this you can more easily write tests and also test ideas. 15 | -------------------------------------------------------------------------------- /Documentation/Releasing.md: -------------------------------------------------------------------------------- 1 | # Releasing 2 | 3 | - Update the version in `.version`. 4 | - Run `make update-versions` 5 | - Run `make tag-release` 6 | - Run `git push --tags` 7 | 8 | Once the release is tagged the **CI** will take over and build images for ARM and AMD64 and push them to Dockerhub. 9 | 10 | ## Manual release 11 | 12 | If the CI has issues there is a possibility to release manually. 13 | 14 | For now you need access credentials for Dockerhub => `docker login` 15 | 16 | Now install the dependencies for multiarch builds: 17 | 18 | ``` 19 | # Fedora 20 | qemu-user-static 21 | qemu 22 | buildah 23 | ``` 24 | 25 | 26 | Now its possible to build and push with `make build-and-release` 27 | -------------------------------------------------------------------------------- /Interface/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_BASE_URL= 2 | VUE_APP_I18N_LOCALE=en 3 | VUE_APP_I18N_FALLBACK_LOCALE=en 4 | -------------------------------------------------------------------------------- /Interface/.env.development: -------------------------------------------------------------------------------- 1 | VUE_APP_BASE_URL="http://localhost:3030" 2 | VUE_APP_HTTP_MODE=true 3 | VUE_APP_PUBLIC_KEY= 4 | VUE_APP_PRIVATE_KEY= -------------------------------------------------------------------------------- /Interface/.env.localtest: -------------------------------------------------------------------------------- 1 | VUE_APP_BASE_URL="http://localhost:3030" 2 | VUE_APP_HTTP_MODE=true 3 | VUE_APP_PUBLIC_KEY=1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI= 4 | VUE_APP_PRIVATE_KEY=5mulHuvASgsqAR282LC4nTKoALXpqJWfOTcpQseXRYg= -------------------------------------------------------------------------------- /Interface/.gitignore: -------------------------------------------------------------------------------- 1 | tests_output 2 | geckodriver.log 3 | chromedriver.log 4 | node_modules 5 | dist 6 | -------------------------------------------------------------------------------- /Interface/Architecture_diagram.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Interface/Architecture_diagram.jpg -------------------------------------------------------------------------------- /Interface/Architecture_diagram.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Interface/Architecture_diagram.odg -------------------------------------------------------------------------------- /Interface/README.md: -------------------------------------------------------------------------------- 1 | # Interface 2 | 3 | ![Architecture diagram](Architecture_diagram.jpg) 4 | 5 | The architecture of the Interface is intended to give clear roles to different parts. Responsibility for tasks should be easy to attribute to certain ares of the code. 6 | 7 | ## Configuration 8 | 9 | You can set the following environment variables: 10 | 11 | - **SSL_INTERFACE**: set to true to enable SSL for the interface. Keys are expected at `/interface/public_key` and `/interface/private_key` 12 | 13 | ## State 14 | 15 | All the state that this application uses is defined and stored in the `src/store` directory. 16 | 17 | This allows for a good overview of what is going on in the application and unifies the interface for changing things. 18 | 19 | Refactoring becomes easier and inside of your components you do not have to worry about things like accessing data from other components - **everything has to go through the single store**. 20 | 21 | [vuex](https://vuex.vuejs.org/) is used as the library to provide this functionality. If you want to change anything on the stores, you should familiarize yourself with the concepts. 22 | 23 | ## Styling 24 | 25 | 26 | ### Globals 27 | Global **variables** are coded in `src/styles/variables.scss` 28 | 29 | These include things like: 30 | 31 | - colors 32 | - spacings 33 | - mixins 34 | 35 | Global **styles** can be changed in `src/styles/styles.scss`. 36 | This includes things such as: 37 | 38 | - Styling all `` 39 | - Styling all `input[type='number']` 40 | 41 | ### Components 42 | 43 | All styles in the application should be [scoped](https://vuejs.org/v2/style-guide/#Component-style-scoping-essential) to their respective component and use `SASS`. 44 | In the vue components this is done with the following `` section. 45 | 46 | ```vue 47 | 48 | 49 | ``` 50 | 51 | All the **global variables** will be available. 52 | 53 | -------------------------------------------------------------------------------- /Interface/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Interface/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "@vue/cli-plugin-unit-jest", 3 | testMatch: ["**/src/**/*.spec.js", "**/tests/unit/**/*.spec.js"] 4 | }; 5 | -------------------------------------------------------------------------------- /Interface/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wirtbot/interface", 3 | "version": "3.9.4", 4 | "author": "b-m-f ", 5 | "license": "AGPL-3", 6 | "engines": { 7 | "node": ">= 14.0.0" 8 | }, 9 | "scripts": { 10 | "build": "vue-cli-service build", 11 | "build:test": "vue-cli-service build --mode=test", 12 | "test": "vue-cli-service test:unit --passWithNoTests", 13 | "audit": "npm audit --production", 14 | "lint": "vue-cli-service lint", 15 | "dev": "vue-cli-service serve --mode=development", 16 | "dev-test": "vue-cli-service serve --mode=localtest", 17 | "i18n:report": "vue-cli-service i18n:report --src './src/**/*.?(js|vue)' --locales './src/locales/**/*.json'" 18 | }, 19 | "dependencies": { 20 | "@babel/eslint-parser": "^7.18.9", 21 | "@fortawesome/fontawesome-svg-core": "6.2.0", 22 | "@fortawesome/free-solid-svg-icons": "6.2.0", 23 | "@fortawesome/vue-fontawesome": "^3.0.0-5", 24 | "@intlify/vue-i18n-loader": "^4.2.0", 25 | "@kazupon/vue-i18n-loader": "0.5.0", 26 | "@vue/cli-plugin-babel": "5.0.8", 27 | "@vue/cli-plugin-eslint": "5.0.8", 28 | "@vue/cli-plugin-unit-jest": "5.0.8", 29 | "@vue/cli-service": "5.0.8", 30 | "@wasm-tool/wasm-pack-plugin": "1.6.0", 31 | "core-js": "3.25.1", 32 | "eslint": "8.23.0", 33 | "eslint-plugin-vue": "9.4.0", 34 | "html-webpack-plugin": "5.5.0", 35 | "jszip": "3.10.1", 36 | "lodash-es": "4.17.21", 37 | "qrcode": "1.5.1", 38 | "sass": "1.54.9", 39 | "sass-loader": "^13", 40 | "text-encoding": "0.7.0", 41 | "vue": "3.2.39", 42 | "vue-cli-plugin-i18n": "^2.3.1", 43 | "vue-i18n": "^9.2.2", 44 | "vue-loader": "17.0.0", 45 | "vue-markdown-loader": "2.5.0", 46 | "vue-router": "4.1.5", 47 | "vue-template-compiler": "2.7.10", 48 | "vuex": "4.0.2", 49 | "vuex-persistedstate": "4.1.0" 50 | }, 51 | "devDependencies": { 52 | "@vue/test-utils": "2.0.2", 53 | "@vue/vue3-jest": "^27.0.0" 54 | }, 55 | "eslintConfig": { 56 | "root": true, 57 | "env": { 58 | "node": true 59 | }, 60 | "extends": [ 61 | "plugin:vue/essential", 62 | "eslint:recommended" 63 | ], 64 | "rules": { 65 | "no-console": "warn", 66 | "no-debugger": "warn", 67 | "vue/multi-word-component-names": "warn", 68 | "vue/no-reserved-component-names": "warn" 69 | }, 70 | "overrides": [ 71 | { 72 | "files": [ 73 | "**/src/**/*.spec.js" 74 | ], 75 | "env": { 76 | "jest": true 77 | } 78 | } 79 | ] 80 | }, 81 | "eslintIgnore": [ 82 | "**/crate/*" 83 | ], 84 | "browserslist": [ 85 | "> 1%", 86 | "last 2 versions" 87 | ] 88 | } 89 | -------------------------------------------------------------------------------- /Interface/pkg/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Interface/pkg/index.js -------------------------------------------------------------------------------- /Interface/public/arrow-thick-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Interface/public/arrow-thick-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /Interface/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | Wirt 15 | 16 | 17 | 23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Interface/public/logo.svg: -------------------------------------------------------------------------------- 1 | wirt-logo -------------------------------------------------------------------------------- /Interface/public/wirt-bot.svg: -------------------------------------------------------------------------------- 1 | wirt-bot-logo -------------------------------------------------------------------------------- /Interface/src/App.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 51 | 52 | 87 | -------------------------------------------------------------------------------- /Interface/src/Widgets/Dashboard/DNS.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 110 | 111 | 126 | -------------------------------------------------------------------------------- /Interface/src/Widgets/Dashboard/Devices.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 54 | 55 | 60 | -------------------------------------------------------------------------------- /Interface/src/Widgets/Dashboard/Network.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 44 | 45 | 54 | -------------------------------------------------------------------------------- /Interface/src/Widgets/Dashboard/Settings.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 97 | 98 | 136 | -------------------------------------------------------------------------------- /Interface/src/api/index.js: -------------------------------------------------------------------------------- 1 | import { sign } from "../lib/crypto"; 2 | import store from "../store"; 3 | import i18n from "../i18n"; 4 | import debounce from "lodash/debounce"; 5 | 6 | // Loaded with https://cli.vuejs.org/guide/mode-and-env.html 7 | // const BASE_URL = process.env.VUE_APP_BASE_URL + "/api"; 8 | 9 | function asyncDebounce(func, wait) { 10 | const debounced = debounce((resolve, reject, args) => { 11 | func(...args).then(resolve).catch(reject); 12 | }, wait); 13 | return (...args) => 14 | new Promise((resolve, reject) => { 15 | debounced(resolve, reject, args); 16 | }); 17 | } 18 | 19 | // Check the MDN docs for more info on how the API is implemented: 20 | // https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch 21 | 22 | async function post(endpoint, data) { 23 | const response = await fetch(endpoint, { 24 | method: "POST", 25 | mode: "cors", 26 | cache: "no-cache", 27 | credentials: "same-origin", 28 | headers: { 29 | "Content-Type": "application/json", 30 | }, 31 | referrer: "no-referrer", 32 | body: JSON.stringify(data), 33 | }); 34 | if (response.status >= 400 && response.status < 600) { 35 | const err = new Error("Error when talking to API"); 36 | err.status = response.status; 37 | err.text = response.text; 38 | throw err; 39 | } 40 | } 41 | 42 | const debouncedPost = asyncDebounce(post, 300); 43 | 44 | function handleError(e) { 45 | if (e.status === 401) { 46 | store.dispatch("alerts/addError", `${i18n.global.t("errors.signature")}`); 47 | return; 48 | } 49 | if (e.status === 405) { 50 | store.dispatch("alerts/addError", `${i18n.global.t("errors.cors")}`); 51 | return; 52 | } 53 | store.dispatch("alerts/addError", `${i18n.global.t("errors.updateFail")}`); 54 | } 55 | 56 | export async function updateServerConfig(config, host) { 57 | try { 58 | let keys = store.state.keys; 59 | if (process.env.VUE_APP_PUBLIC_KEY && process.env.VUE_APP_PRIVATE_KEY) { 60 | keys.public = process.env.VUE_APP_PUBLIC_KEY; 61 | keys.private = process.env.VUE_APP_PRIVATE_KEY; 62 | } 63 | const messageWithSignature = await sign(config, keys); 64 | if (location.protocol === 'https:') { 65 | await debouncedPost(`https://${host}/update`, messageWithSignature); 66 | } else { 67 | await debouncedPost(`http://${host}/update`, messageWithSignature); 68 | } 69 | store.dispatch( 70 | "alerts/addSuccess", 71 | `${i18n.global.t("success.updateSuccessConfig")}` 72 | ); 73 | } catch (e) { 74 | handleError(e); 75 | } 76 | } 77 | 78 | export async function updateDNSConfig(config, host) { 79 | try { 80 | let keys = store.state.keys; 81 | if (process.env.VUE_APP_PUBLIC_KEY && process.env.VUE_APP_PRIVATE_KEY) { 82 | keys.public = process.env.VUE_APP_PUBLIC_KEY; 83 | keys.private = process.env.VUE_APP_PRIVATE_KEY; 84 | } 85 | const messageWithSignature = await sign(config, keys); 86 | if (location.protocol === 'https:') { 87 | await debouncedPost( 88 | `https://${host}/update-device-dns-entries`, 89 | messageWithSignature 90 | ); 91 | } else { 92 | await debouncedPost( 93 | `http://${host}/update-device-dns-entries`, 94 | messageWithSignature 95 | ); 96 | } 97 | store.dispatch( 98 | "alerts/addSuccess", 99 | `${i18n.global.t("success.updateSuccessDNS")}` 100 | ); 101 | } catch (e) { 102 | handleError(e); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /Interface/src/assets/IBMPlexSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Interface/src/assets/IBMPlexSans-Regular.ttf -------------------------------------------------------------------------------- /Interface/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | Layer 2 -------------------------------------------------------------------------------- /Interface/src/components/AccentedCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /Interface/src/components/Alerts.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 26 | 27 | 54 | -------------------------------------------------------------------------------- /Interface/src/components/Button.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 60 | -------------------------------------------------------------------------------- /Interface/src/components/Card.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /Interface/src/components/Footer.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 36 | 37 | 75 | -------------------------------------------------------------------------------- /Interface/src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 38 | 39 | 125 | -------------------------------------------------------------------------------- /Interface/src/components/Inputs/CheckBox.vue: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 25 | 26 | 28 | -------------------------------------------------------------------------------- /Interface/src/components/Inputs/IP.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 48 | -------------------------------------------------------------------------------- /Interface/src/components/Inputs/IpOrHostname.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 53 | 54 | 63 | -------------------------------------------------------------------------------- /Interface/src/components/Inputs/Number.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 78 | 79 | 84 | -------------------------------------------------------------------------------- /Interface/src/components/Inputs/Select.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Interface/src/components/Inputs/Text.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 90 | 91 | 97 | -------------------------------------------------------------------------------- /Interface/src/components/Modal.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Interface/src/components/Modal.vue -------------------------------------------------------------------------------- /Interface/src/i18n.js: -------------------------------------------------------------------------------- 1 | import {createI18n} from 'vue-i18n'; 2 | 3 | 4 | function loadLocaleMessages () { 5 | const locales = require.context('./locales', true, /[A-Za-z0-9-_,\s]+\.json$/i) 6 | const messages = {} 7 | locales.keys().forEach(key => { 8 | const matched = key.match(/([A-Za-z0-9-_]+)\./i) 9 | if (matched && matched.length > 1) { 10 | const locale = matched[1] 11 | messages[locale] = locales(key).default 12 | } 13 | }) 14 | return messages 15 | 16 | } 17 | 18 | export default createI18n({ 19 | locale: process.env.VUE_APP_I18N_LOCALE || 'en', 20 | fallbackLocale: process.env.VUE_APP_I18N_FALLBACK_LOCALE || 'en', 21 | allowComposition: true, 22 | messages: loadLocaleMessages() 23 | }) 24 | -------------------------------------------------------------------------------- /Interface/src/icons.js: -------------------------------------------------------------------------------- 1 | import { library } from "@fortawesome/fontawesome-svg-core"; 2 | import { 3 | faFileDownload, 4 | faTrash, 5 | faBars, 6 | faCheckSquare, 7 | faCheck, 8 | faPlus, 9 | faCogs, 10 | faServer, 11 | faLaptop, 12 | faMobileAlt 13 | } from "@fortawesome/free-solid-svg-icons"; 14 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 15 | 16 | library.add( 17 | faFileDownload, 18 | faTrash, 19 | faBars, 20 | faCheckSquare, 21 | faCheck, 22 | faPlus, 23 | faCogs, 24 | faServer, 25 | faLaptop, 26 | faMobileAlt 27 | ); 28 | 29 | export default FontAwesomeIcon; 30 | -------------------------------------------------------------------------------- /Interface/src/lib/backup.js: -------------------------------------------------------------------------------- 1 | import store from "../store"; 2 | import i18n from "../i18n"; 3 | import { guidGenerator } from "./helpers"; 4 | 5 | // Returns upgraded Backup as JSON string 6 | export function upgradeBackup(backupAsJSONString) { 7 | const backup = JSON.parse(backupAsJSONString); 8 | 9 | const appVersion = store.state.version || 1.0; 10 | const backupVersion = backup.version; 11 | let updatedBackup = backup; 12 | 13 | if (appVersion < backupVersion) { 14 | throw Error(i18n.t("errors.backupNotCompatible")); 15 | } 16 | 17 | if (backupVersion < "2.3.4") { 18 | updatedBackup.server.ip.v4 = backup.server.ip.v4.join("."); 19 | updatedBackup.network.dns.ip.v4 = backup.network.dns.ip.v4.join("."); 20 | } 21 | if (backupVersion < "2.5.0") { 22 | updatedBackup.server.subnet.v4.slice(0, -1); 23 | updatedBackup.server.subnet.v6.slice(0, -1); 24 | updatedBackup.network.dns.ignoredZones = [ 25 | "fritz.box", 26 | "home", 27 | "lan", 28 | "local", 29 | ]; 30 | } 31 | 32 | // Set to the current version of the app where it is now being imported in 33 | updatedBackup.version = appVersion; 34 | 35 | // Check that all devices have an ID 36 | // This should only trigger when backups are changed manually 37 | updatedBackup.devices = backup.devices.map((device) => { 38 | if (!device.id) { 39 | device.id = `${guidGenerator()}-generated-at-import`; 40 | } 41 | return device; 42 | }); 43 | 44 | return JSON.stringify(Object.assign({}, store.state, updatedBackup)); 45 | } 46 | -------------------------------------------------------------------------------- /Interface/src/lib/crate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wirtbot-crypto-rust-wasm" 3 | version = "3.9.4" 4 | authors = ["b-m-f "] 5 | edition = "2018" 6 | license = "AGPL-3" 7 | repository = "https://github.com/b-m-f/WirtBot" 8 | 9 | [lib] 10 | crate-type = ["cdylib", "rlib"] 11 | 12 | 13 | 14 | [package.metadata.wasm-pack.profile.release] 15 | wasm-opt = false 16 | 17 | [profile.release] 18 | # This makes the compiled code faster and smaller, but it makes compiling slower, 19 | # so it's only enabled in release mode. 20 | lto = true 21 | 22 | [dev-dependencies] 23 | wasm-bindgen-test = "0.3.18" 24 | 25 | [dependencies] 26 | wasm-bindgen = "0.2.68" 27 | serde = { version = "1.0.116", features = ["derive"] } 28 | serde_json = "1.0.58" 29 | rand = {version = "0.7.3", features = ["wasm-bindgen"]} 30 | getrandom = {version = "0.2.0", features=["js"]} 31 | sha2 = "0.9.1" 32 | x25519-dalek = {version = "1.1", default-features = false, features = ["u32_backend"]} 33 | base64 = "0.13.0" 34 | ed25519-dalek = {version = "1.0.1", default-features = false, features = ["serde", "batch_deterministic", "u32_backend"]} 35 | bincode = "1.3.1" 36 | -------------------------------------------------------------------------------- /Interface/src/lib/crate/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate ed25519_dalek; 2 | extern crate rand; 3 | 4 | use base64::{decode, encode}; 5 | use ed25519_dalek::{ 6 | Digest, ExpandedSecretKey, Keypair, PublicKey, SecretKey, Sha512, PUBLIC_KEY_LENGTH, 7 | SECRET_KEY_LENGTH, 8 | }; 9 | use rand::rngs::OsRng; 10 | use serde::{Deserialize, Serialize}; 11 | use wasm_bindgen::prelude::*; 12 | use x25519_dalek::PublicKey as x25519_PublicKey; 13 | use x25519_dalek::StaticSecret; 14 | 15 | #[wasm_bindgen] 16 | pub fn generate_key_pair() -> String { 17 | #[derive(Serialize)] 18 | struct Pair { 19 | public_key: String, 20 | private_key: String, 21 | } 22 | let mut csprng: OsRng = OsRng {}; 23 | let secret_raw = StaticSecret::new(&mut csprng); 24 | let public_raw = x25519_PublicKey::from(&secret_raw); 25 | 26 | // length defined here: https://docs.rs/x25519-dalek/0.6.0/x25519_dalek/struct.PublicKey.html 27 | let pub_key: &[u8; 32] = public_raw.as_bytes(); 28 | // length defined here: https://docs.rs/x25519-dalek/0.6.0/x25519_dalek/struct.StaticSecret.html 29 | let priv_key: [u8; 32] = secret_raw.to_bytes(); 30 | 31 | // WireGuard keys are base64 encoded 32 | // 17.3.2020 from https://github.com/WireGuard/wireguard-tools/blob/master/src/encoding.c 33 | let pub_key_string = base64::encode(pub_key); 34 | let priv_key_string = base64::encode(priv_key); 35 | 36 | let pair = Pair { 37 | public_key: pub_key_string, 38 | private_key: priv_key_string, 39 | }; 40 | serde_json::to_string(&pair).unwrap() 41 | } 42 | 43 | #[wasm_bindgen] 44 | pub fn generate_signature_keys() -> String { 45 | let mut csprng = OsRng {}; 46 | let keypair: Keypair = Keypair::generate(&mut csprng); 47 | let public_key = keypair.public; 48 | let private_key = keypair.secret; 49 | let raw_public_key = public_key.to_bytes(); 50 | let raw_private_key = private_key.to_bytes(); 51 | 52 | #[derive(Serialize)] 53 | struct Pair { 54 | private_key: String, 55 | public_key: String, 56 | } 57 | let pair = Pair { 58 | private_key: base64::encode(raw_private_key), 59 | public_key: base64::encode(raw_public_key), 60 | }; 61 | return serde_json::to_string(&pair).unwrap(); 62 | } 63 | 64 | // returns the signature for a message encoded in base64 65 | #[wasm_bindgen] 66 | pub fn sign_message(keypair: String, message: String) -> String { 67 | #[derive(Deserialize)] 68 | struct Pair { 69 | private: String, 70 | public: String, 71 | } 72 | // Keys are in Base64. See above. 73 | // Base64 decode will return a vector that we need to put back into a bounded array 74 | // of size SECRET_KEY_LENGTH/PUBLIC_KEY_LENGTH 75 | // Since we created the keys we know that this will work. 76 | let pair: Pair = serde_json::from_str(&keypair).unwrap(); 77 | 78 | let mut raw_private_key_buffer = [0; SECRET_KEY_LENGTH]; 79 | let raw_private_key_vector = base64::decode(&pair.private).unwrap(); 80 | let raw_private_key_bytes = &raw_private_key_vector[..raw_private_key_buffer.len()]; 81 | raw_private_key_buffer.copy_from_slice(raw_private_key_bytes); 82 | let decoded_private_key = SecretKey::from_bytes(&raw_private_key_buffer).unwrap(); 83 | 84 | let mut raw_public_key_buffer = [0; PUBLIC_KEY_LENGTH]; 85 | let raw_public_key_vector = base64::decode(&pair.public).unwrap(); 86 | let raw_public_key_bytes = &raw_public_key_vector[..raw_public_key_buffer.len()]; 87 | raw_public_key_buffer.copy_from_slice(raw_public_key_bytes); 88 | let decoded_public_key = PublicKey::from_bytes(&raw_public_key_buffer).unwrap(); 89 | 90 | let expanded_private_key = ExpandedSecretKey::from(&decoded_private_key); 91 | 92 | let mut prehashed: Sha512 = Sha512::default(); 93 | prehashed.update(message); 94 | 95 | let signature = expanded_private_key 96 | .sign_prehashed(prehashed, &decoded_public_key, Some(b"wirtbot")) 97 | .unwrap(); 98 | 99 | let signature_bytes = signature.to_bytes(); 100 | 101 | let signature_base_64 = base64::encode(signature_bytes.to_vec()); 102 | 103 | signature_base_64 104 | } 105 | 106 | #[cfg(test)] 107 | mod test { 108 | extern crate serde; 109 | extern crate serde_json; 110 | extern crate wasm_bindgen_test; 111 | 112 | use super::*; 113 | use serde::{Deserialize, Serialize}; 114 | use wasm_bindgen_test::*; 115 | 116 | wasm_bindgen_test_configure!(run_in_browser); 117 | 118 | #[wasm_bindgen_test] 119 | fn test_generate_key_pair() { 120 | #[derive(Serialize, Deserialize)] 121 | struct Pair { 122 | public_key: String, 123 | private_key: String, 124 | } 125 | let _key_pair: Pair = serde_json::from_str(&generate_key_pair()).unwrap(); 126 | // TODO: figure out a way to test that both struct fields have values 127 | 128 | assert!(true) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Interface/src/lib/crate/webdriver.json: -------------------------------------------------------------------------------- 1 | { 2 | "moz:firefoxOptions": { 3 | "prefs": {}, 4 | "args": [] 5 | }, 6 | "goog:chromeOptions": { 7 | "hostname": "localhost", 8 | "args": [ 9 | "--whitelisted-ips" 10 | ] 11 | } 12 | } -------------------------------------------------------------------------------- /Interface/src/lib/crypto.js: -------------------------------------------------------------------------------- 1 | export async function generateSigningKeys() { 2 | try { 3 | const wasm = import("./crate/pkg"); 4 | const generateKeypair = (await wasm).generate_signature_keys; 5 | const pair = JSON.parse(generateKeypair()); 6 | return { private: pair.private_key, public: pair.public_key }; 7 | } catch (error) { 8 | console.error(error); 9 | throw `WebAssembly key generation: ${error}`; 10 | } 11 | } 12 | 13 | export async function sign(message, keys) { 14 | try { 15 | const wasm = import("./crate/pkg"); 16 | const sign = (await wasm).sign_message; 17 | const signature = sign(JSON.stringify(keys), message); 18 | return { signature, message }; 19 | } catch (error) { 20 | console.error(error); 21 | throw `Error when signing message: ${error}`; 22 | } 23 | } 24 | 25 | export async function getKeys() { 26 | try { 27 | const wasm = import("./crate/pkg"); 28 | const generateKeypair = (await wasm).generate_key_pair; 29 | const pair = JSON.parse(generateKeypair()); 30 | return { private: pair.private_key, public: pair.public_key }; 31 | } catch (error) { 32 | throw `WebAssembly key generation: ${error}`; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Interface/src/lib/dns.js: -------------------------------------------------------------------------------- 1 | // This function creates an RFC 1035 DNS master file 2 | export function generateDNSFile(server, clients, network) { 3 | const tls = network.dns.tls; 4 | const tlsName = network.dns.tlsName; 5 | const dnsV4 = network.dns.ip && network.dns.ip.v4; 6 | const dnsV6 = network.dns.ip && network.dns.ip.v6; 7 | const ignoredZones = network.dns.ignoredZones; 8 | const dnsHostname = network.dns.hostname; 9 | 10 | const subnetv4 = 11 | server.subnet.v4 && server.subnet.v4[server.subnet.v4.length - 1] === "." 12 | ? `${server.subnet.v4}`.slice(0, -1) 13 | : server.subnet.v4; 14 | 15 | const subnetv6 = 16 | server.subnet.v6 && 17 | server.subnet.v6[server.subnet.v6.length - 1] === ":" && 18 | server.subnet.v6[server.subnet.v6.length - 2] === ":" 19 | ? `${server.subnet.v6}`.slice(0, -1) 20 | : server.subnet.v6; 21 | 22 | const deviceNames = clients.map((client) => { 23 | client.name = client.name.split(" ").join("-"); 24 | if (!server.name) { 25 | server.name = "wirtbot"; 26 | } 27 | const additionalNames = client.additionalNames ? client.additionalNames : []; 28 | 29 | if (client.ip.v6 && client.ip.v4) { 30 | let names = `` 31 | for (let name of additionalNames){ 32 | names = names + ` 33 | ${subnetv4}.${client.ip.v4} ${name}.${network.dns.name} 34 | ${subnetv6}:${client.ip.v6} ${name}.${network.dns.name}\n` 35 | } 36 | names = names.trim() 37 | if (names !== ""){ 38 | return `${subnetv4}.${client.ip.v4} ${client.name}.${network.dns.name} 39 | ${subnetv6}:${client.ip.v6} ${client.name}.${network.dns.name} 40 | ${names}`; 41 | } else { 42 | return `${subnetv4}.${client.ip.v4} ${client.name}.${network.dns.name} 43 | ${subnetv6}:${client.ip.v6} ${client.name}.${network.dns.name}`; 44 | } 45 | } 46 | if (client.ip.v6 && !client.ip.v4) { 47 | let names = `` 48 | for (let name of additionalNames){ 49 | names = names + ` 50 | ${subnetv6}:${client.ip.v6} ${name}.${network.dns.name}\n` 51 | } 52 | names = names.trim() 53 | if (names !== ""){ 54 | return `${subnetv6}:${client.ip.v6} ${client.name}.${network.dns.name} 55 | ${names}`; 56 | } else { 57 | return `${subnetv6}:${client.ip.v6} ${client.name}.${network.dns.name}`; 58 | } 59 | } 60 | if (!client.ip.v6 && client.ip.v4) { 61 | let names = `` 62 | for (let name of additionalNames){ 63 | names = names + ` 64 | ${subnetv4}.${client.ip.v4} ${name}.${network.dns.name}` 65 | } 66 | names = names.trim() 67 | if (names !== ""){ 68 | return `${subnetv4}.${client.ip.v4} ${client.name}.${network.dns.name} 69 | ${names}`; 70 | } 71 | else { 72 | return `${subnetv4}.${client.ip.v4} ${client.name}.${network.dns.name}`; 73 | } 74 | 75 | } 76 | }); 77 | 78 | const serverName = () => { 79 | if (subnetv6 && subnetv4) { 80 | return `${subnetv4}.1 ${server.name}.${network.dns.name} 81 | ${subnetv6}:1 ${server.name}.${network.dns.name}`; 82 | } 83 | if (subnetv6 && !subnetv4) { 84 | return `${subnetv6}:1 ${server.name}.${network.dns.name}`; 85 | } 86 | if (!subnetv6 && subnetv4) { 87 | return `${subnetv4}.1 ${server.name}.${network.dns.name}`; 88 | } 89 | }; 90 | 91 | const forwardConfig = () => { 92 | if (tls) { 93 | return `forward . ${dnsV4 ? `tls://` + dnsV4 + " " : ""}${ 94 | dnsV6 ? `tls://` + dnsV6 + "" : "" 95 | }{ 96 | except ${ignoredZones.join(" ")} 97 | tls_servername ${tlsName} 98 | health_check 5s 99 | }`; 100 | } else { 101 | return `forward . ${dnsV4 ? dnsV4 + " " : ""}${dnsV6 ? dnsV6 + " " : ""}${ 102 | dnsHostname ? dnsHostname : "" 103 | } { 104 | except ${ignoredZones.join(" ")} 105 | health_check 5s 106 | }`; 107 | } 108 | }; 109 | 110 | let masterFile = `. { 111 | reload 112 | local 113 | prometheus 0.0.0.0:9153 114 | ${forwardConfig()} 115 | cache 30 116 | } 117 | ${network.dns.name} { 118 | hosts { 119 | ${serverName()} 120 | ${deviceNames.join("\n ")} 121 | } 122 | }`; 123 | return masterFile.trim(); 124 | } 125 | -------------------------------------------------------------------------------- /Interface/src/lib/download.js: -------------------------------------------------------------------------------- 1 | export function downloadText(text, filename) { 2 | const blob = new Blob([text], { type: "octet/stream" }); 3 | if (window.navigator.msSaveOrOpenBlob) { 4 | window.navigator.msSaveBlob(blob, filename); 5 | } else { 6 | const a = document.createElement("a"); 7 | a.style = "display: none"; 8 | document.body.appendChild(a); 9 | a.style = "display: none"; 10 | 11 | const url = window.URL.createObjectURL(blob); 12 | a.href = url; 13 | a.download = filename; 14 | a.click(); 15 | window.URL.revokeObjectURL(url); 16 | document.body.removeChild(a); 17 | } 18 | 19 | } 20 | 21 | export function downloadFile(file, filename) { 22 | if (window.navigator.msSaveOrOpenBlob) { 23 | window.navigator.msSaveBlob(file, filename); 24 | } else { 25 | const elem = window.document.createElement("a"); 26 | elem.href = window.URL.createObjectURL(file); 27 | elem.download = filename; 28 | document.body.appendChild(elem); 29 | elem.click(); 30 | document.body.removeChild(elem); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Interface/src/lib/helpers.js: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/questions/6860853/generate-random-string-for-div-id/6860916#6860916 2 | export function guidGenerator() { 3 | var S4 = function () { 4 | return (((1 + Math.random()) * 0x10000) | 0) 5 | .toString(16) 6 | .substring(1); 7 | }; 8 | return ( 9 | S4() + 10 | S4() + 11 | "-" + 12 | S4() + 13 | "-" + 14 | S4() + 15 | "-" + 16 | S4() + 17 | "-" + 18 | S4() + 19 | S4() + 20 | S4() 21 | ); 22 | } 23 | 24 | export function validateIPv4(ip){ 25 | let newValue; 26 | let valid = true; 27 | try { 28 | newValue = ip.split(".").map((val, index) => { 29 | const number = parseInt(val); 30 | if (!number && number != 0) { 31 | valid = false; 32 | return 0; 33 | } 34 | if (number > 255) { 35 | valid = false; 36 | return number; 37 | } 38 | if ((index == 3 || index == 0) && number < 1) { 39 | valid = false; 40 | return number; 41 | } 42 | if (number < 0) { 43 | valid = false; 44 | return number; 45 | } 46 | return number; 47 | }); 48 | 49 | if (newValue.length < 4) { 50 | valid = false; 51 | } 52 | return valid; 53 | } catch (error) { 54 | return false; 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /Interface/src/locales/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "Wirt" 4 | }, 5 | "global": { 6 | "words": { 7 | "no": "nein", 8 | "yes": "ja" 9 | } 10 | }, 11 | "dashboard": { 12 | "widgets": { 13 | "devices": { 14 | "title": "Geräte", 15 | "labels": { 16 | "routed": "Den ganzen Traffic durch WireGuard tunneln" 17 | } 18 | } 19 | }, 20 | "server": "Server", 21 | "addDevice": "Neues Geraet hinzufuegen" 22 | }, 23 | "device": { 24 | "name": "Name", 25 | "ipInPrivateNetwork": "IP im Privat-Netzwerk", 26 | "deviceType": "Gerätetyp", 27 | "downloadConfig": "Konfigurationsdatei herunterladen" 28 | }, 29 | "header": { 30 | "quickstart": "Schnellstart" 31 | } 32 | } -------------------------------------------------------------------------------- /Interface/src/main.js: -------------------------------------------------------------------------------- 1 | import {createApp,h} from "vue"; 2 | import App from "./App.vue"; 3 | 4 | import router from "./routes"; 5 | import store from "./store"; 6 | import i18n from "./i18n"; 7 | import icons from "./icons"; 8 | 9 | const app = createApp({ 10 | router, 11 | store, 12 | i18n, 13 | render: () => h(App) 14 | }) 15 | 16 | app.component("font-awesome-icon", icons); 17 | 18 | app.config.productionTip = false; 19 | 20 | app.use(router); 21 | app.use(store); 22 | app.use(i18n); 23 | app.use(icons); 24 | 25 | app.mount("#app"); 26 | -------------------------------------------------------------------------------- /Interface/src/pages/Dashboard/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 117 | 118 | 163 | -------------------------------------------------------------------------------- /Interface/src/pages/FirstUse/FirstUse.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 52 | 53 | 69 | -------------------------------------------------------------------------------- /Interface/src/routes.js: -------------------------------------------------------------------------------- 1 | import {createRouter, createWebHashHistory} from "vue-router"; 2 | 3 | import Dashboard from "./pages/Dashboard/Dashboard"; 4 | 5 | const routes = [ 6 | { path: "/", component: Dashboard, name: "dashboard" }, 7 | ]; 8 | 9 | const router = new createRouter({ 10 | history: createWebHashHistory(), 11 | routes, 12 | }); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /Interface/src/store/modules/alerts.js: -------------------------------------------------------------------------------- 1 | import takeRight from "lodash/takeRight"; 2 | 3 | export default { 4 | namespaced: true, 5 | state: { alerts: [] }, 6 | mutations: { 7 | add(state, alert) { 8 | if (state.alerts.length >= 5) { 9 | state.alerts = [...takeRight(state.alerts, 5), alert]; 10 | } else { 11 | state.alerts = [...state.alerts, alert]; 12 | } 13 | }, 14 | remove(state, id) { 15 | state.alerts = state.alerts.filter((alert) => { 16 | return alert.id !== id; 17 | }); 18 | }, 19 | clean(state) { 20 | state.alerts = []; 21 | }, 22 | }, 23 | actions: { 24 | add({ state, commit, dispatch }, { message, type }) { 25 | const id = new Date().getTime(); 26 | const existingAlert = state.alerts.find( 27 | (item) => item.message === message 28 | ); 29 | if (existingAlert) { 30 | commit("remove", existingAlert.id); 31 | dispatch("add", { message, type }); 32 | } else { 33 | commit("add", { message, type, id }); 34 | window.setTimeout(() => { 35 | commit("remove", id); 36 | }, 2000); 37 | } 38 | }, 39 | addInfo({ dispatch }, message) { 40 | dispatch("add", { type: "info", message }); 41 | }, 42 | addError({ dispatch }, message) { 43 | dispatch("add", { type: "error", message }); 44 | }, 45 | addSuccess({ dispatch }, message) { 46 | dispatch("add", { type: "success", message }); 47 | }, 48 | clean({ commit }) { 49 | commit("clean"); 50 | }, 51 | }, 52 | getters: { 53 | alerts: (state) => { 54 | return state.alerts; 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /Interface/src/styles/icons.scss: -------------------------------------------------------------------------------- 1 | .fa-check-square { 2 | color: $success; 3 | } 4 | 5 | .fa-check { 6 | color: $success; 7 | } 8 | 9 | .fa-plus { 10 | color: $success; 11 | } 12 | 13 | .fa-trash { 14 | color: $error; 15 | } 16 | 17 | .fa-file-download { 18 | color: $secondary; 19 | } 20 | -------------------------------------------------------------------------------- /Interface/src/styles/normalize.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, 7 | body, 8 | div, 9 | span, 10 | applet, 11 | object, 12 | iframe, 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6, 19 | p, 20 | blockquote, 21 | pre, 22 | a, 23 | abbr, 24 | acronym, 25 | address, 26 | big, 27 | cite, 28 | code, 29 | del, 30 | dfn, 31 | em, 32 | img, 33 | ins, 34 | kbd, 35 | q, 36 | s, 37 | samp, 38 | small, 39 | strike, 40 | strong, 41 | sub, 42 | sup, 43 | tt, 44 | var, 45 | b, 46 | u, 47 | i, 48 | center, 49 | dl, 50 | dt, 51 | dd, 52 | ol, 53 | ul, 54 | li, 55 | fieldset, 56 | form, 57 | label, 58 | legend, 59 | table, 60 | caption, 61 | tbody, 62 | tfoot, 63 | thead, 64 | tr, 65 | th, 66 | td, 67 | article, 68 | aside, 69 | canvas, 70 | details, 71 | embed, 72 | figure, 73 | figcaption, 74 | footer, 75 | header, 76 | hgroup, 77 | menu, 78 | nav, 79 | output, 80 | ruby, 81 | section, 82 | summary, 83 | time, 84 | mark, 85 | audio, 86 | video { 87 | margin: 0; 88 | padding: 0; 89 | border: 0; 90 | font-size: 100%; 91 | font: inherit; 92 | vertical-align: baseline; 93 | } 94 | /* HTML5 display-role reset for older browsers */ 95 | article, 96 | aside, 97 | details, 98 | figcaption, 99 | figure, 100 | footer, 101 | header, 102 | hgroup, 103 | menu, 104 | nav, 105 | section { 106 | display: block; 107 | } 108 | body { 109 | line-height: 1; 110 | } 111 | ol, 112 | ul { 113 | list-style: none; 114 | } 115 | blockquote, 116 | q { 117 | quotes: none; 118 | } 119 | blockquote:before, 120 | blockquote:after, 121 | q:before, 122 | q:after { 123 | content: ""; 124 | content: none; 125 | } 126 | table { 127 | border-collapse: collapse; 128 | border-spacing: 0; 129 | } 130 | -------------------------------------------------------------------------------- /Interface/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $background: #fbf8f3; 2 | $secondary: #e07a5f; 3 | $primary: #0f4b81; 4 | $accent: red; 5 | $grey-dark: #eff3f7; 6 | $grey-light: #eeeeee; 7 | 8 | $success: green; 9 | $error: red; 10 | $info: $background; 11 | $white: #ffffff; 12 | $black: #000000; 13 | 14 | $spacing-x-small: 2px; 15 | $spacing-small: 0.3rem; 16 | $spacing-medium: 1.3rem; 17 | $spacing-large: 2rem; 18 | $spacing-x-large: 4rem; 19 | 20 | $heading-large: 2.09rem; 21 | $heading-medium: 1.45rem; 22 | 23 | $font-medium: 1rem; 24 | $font-small: 0.73rem; 25 | 26 | $border-large: 1rem; 27 | $border-medium: 0.3rem; 28 | $border-small: 1px; 29 | 30 | @mixin box-shadow-light { 31 | box-shadow: $spacing-x-small $spacing-small $spacing-medium 0px 32 | rgba($black, 0.4); 33 | } 34 | 35 | $mobile-width: 600px; 36 | $tablet-width: 1000px; 37 | -------------------------------------------------------------------------------- /Interface/vue.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const BundleAnalyzerPlugin = require("webpack-bundle-analyzer") 4 | .BundleAnalyzerPlugin; 5 | const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin"); 6 | 7 | module.exports = { 8 | publicPath: "./", 9 | transpileDependencies: [], 10 | css: { 11 | loaderOptions: { 12 | scss: { 13 | additionalData: `@import "~@/styles/variables.scss";`, 14 | }, 15 | }, 16 | }, 17 | 18 | pluginOptions: { 19 | i18n: { 20 | locale: "en", 21 | fallbackLocale: "en", 22 | localeDir: "locales", 23 | enableInSFC: true, 24 | }, 25 | }, 26 | configureWebpack: config => { 27 | config.experiments = { 28 | asyncWebAssembly: true 29 | } 30 | 31 | }, 32 | chainWebpack: (config) => { 33 | config 34 | // TODO: 16.3 check if still needed for Edge browser https://rustwasm.github.io/docs/wasm-bindgen/examples/hello-world.html 35 | .plugin("text-encoder") 36 | .use(webpack.ProvidePlugin) 37 | .init( 38 | (Plugin) => 39 | new Plugin({ 40 | TextDecoder: ["text-encoding", "TextDecoder"], 41 | TextEncoder: ["text-encoding", "TextEncoder"], 42 | }) 43 | ) 44 | .end() 45 | .plugin("bundle analyzer") 46 | .use(BundleAnalyzerPlugin) 47 | .init( 48 | (Plugin) => 49 | new Plugin({ 50 | analyzerMode: "static", 51 | reportFilename: "bundle-size-analysis", 52 | openAnalyzer: false, 53 | }) 54 | ) 55 | .end() 56 | .plugin("wasm") 57 | .use( 58 | new WasmPackPlugin({ 59 | crateDirectory: `${__dirname}/src/lib/crate`, 60 | }) 61 | ) 62 | .end() 63 | .module.rule("md") 64 | .test(/\.md/) 65 | .use("vue-loader") 66 | .loader("vue-loader") 67 | .end() 68 | .use("vue-markdown-loader") 69 | .loader("vue-markdown-loader/lib/markdown-compiler") 70 | .options({ 71 | raw: true, 72 | }); 73 | 74 | config.resolve.alias 75 | .set("components", path.join(__dirname, "src/components")); 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | ## Development 4 | dev: dev-client dev-server 5 | dev-tests: dev-client-test dev-tests-server 6 | dev-server: 7 | rm -rf /tmp/WirtBotTests && mkdir /tmp/WirtBotTests && \ 8 | touch /tmp/WirtBotTests/server.conf && \ 9 | touch /tmp/WirtBotTests/Corefile && \ 10 | cd ./Core && \ 11 | ALLOWED_ORIGIN=http://localhost:8080 \ 12 | RUST_LOG=debug MANAGED_DNS_ENABLED=1 \ 13 | MANAGED_DNS_DEVICE_FILE=/tmp/WirtBotTests/Corefile \ 14 | CONFIG_PATH=/tmp/WirtBotTests/server.conf cargo watch -x run || cd - 15 | dev-tests-server: 16 | rm -rf /tmp/WirtBotTests && mkdir /tmp/WirtBotTests && \ 17 | touch /tmp/WirtBotTests/server.conf && \ 18 | touch /tmp/WirtBotTests/Corefile && \ 19 | cd ./Core && \ 20 | PUBLIC_KEY=1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI= \ 21 | ALLOWED_ORIGIN=http://localhost:8080 \ 22 | RUST_LOG=debug MANAGED_DNS_ENABLED=1 \ 23 | MANAGED_DNS_DEVICE_FILE=/tmp/WirtBotTests/Corefile \ 24 | CONFIG_PATH=/tmp/WirtBotTests/server.conf cargo watch -x run || cd - 25 | dev-client: 26 | cd ./Interface && npm run dev 27 | dev-client-test: 28 | cd ./Interface && npm run dev-test 29 | dev-website: 30 | cd ./Website && npm run dev 31 | dev-setup: 32 | cd ./Website && npm ci && cd - && \ 33 | cd ./Interface && npm ci && cd - 34 | 35 | ## Run complete WirtBot 36 | run-test-wirtbot: 37 | sudo chown $$USER Build-Automation/WirtBot/compose/test-server.conf && \ 38 | docker-compose -f Build-Automation/WirtBot/compose/example.yml up --abort-on-container-exit --build --remove-orphans && \ 39 | sudo chown $$USER Build-Automation/WirtBot/compose/test-server.conf 40 | connect-test-wirtbot: 41 | cp testwirtbot.conf.example testwirtbot.conf && \ 42 | sed -i "s@Endpoint = development_wirtbot:10101@Endpoint = $(shell docker inspect development_wirtbot | grep -e "IPAddress\": \"[0-9].*\"" | cut -d '"' -f 4):10101@" testwirtbot.conf && \ 43 | sudo mv testwirtbot.conf /etc/wireguard/testwirtbot.conf && \ 44 | sudo wg-quick up testwirtbot 45 | 46 | ## Tests 47 | ### This cleans up the server conf, because the container changes it with chmod 48 | test-system: 49 | sudo chown $$USER Build-Automation/System-Tests/compose/test-server.conf && \ 50 | sudo chown $$USER Build-Automation/System-Tests/compose/test-Corefile && \ 51 | docker-compose -f Build-Automation/System-Tests/compose/test.yml up --abort-on-container-exit --build --remove-orphans && \ 52 | sudo chown $$USER Build-Automation/System-Tests/compose/test-Corefile && \ 53 | sudo chown $$USER Build-Automation/System-Tests/compose/test-server.conf 54 | test-unit: 55 | cd ./Website && npm run test && cd - && \ 56 | cd ./Interface && npm run test && cd - && \ 57 | cd ./Core && cargo test 58 | 59 | test-unit-ci: 60 | docker-compose -f Build-Automation/Unit-Tests/test.yml up --abort-on-container-exit --build --remove-orphans 61 | 62 | 63 | ## Maintenance 64 | update-versions: 65 | ./convenience-scripts/update-version.sh 66 | tag-release: 67 | git tag v$$(cat .version) 68 | update-dependencies: 69 | ./convenience-scripts/update-dependencies.sh 70 | 71 | ## Build and release 72 | build-and-release: 73 | buildah bud --dns=none --network=host --platform linux/arm64,linux/amd64 --manifest docker.io/bmff/wirtbot-$$(cat .version) -f Build-Automation/WirtBot/Dockerfile . && \ 74 | buildah manifest push --all docker.io/bmff/wirtbot-$$(cat .version) docker://docker.io/bmff/wirtbot:latest && \ 75 | buildah manifest push --all docker.io/bmff/wirtbot-$$(cat .version) docker://docker.io/bmff/wirtbot:$$(cat .version) 76 | 77 | ## DNS is included since rootless networking inside a toolbox on Fedora seemed to kill DNS resolution 78 | buildah-and-release-test: 79 | ## Require buildah and qemu-user-static 80 | buildah bud -m=12G --dns=none --network=host --manifest wirtbot:$$(cat .version) --platform=linux/amd64 -f Build-Automation/WirtBot/Dockerfile . && \ 81 | buildah manifest push wirtbot:$$(cat .version)-test "docker://bmff/wirtbot:$$(cat .version)-test" 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![WirtBot logo](Interface/public/logo.svg) 2 | 3 | # ARCHIVED 4 | 5 | WirtBot is feature complete and can be used without problems on properly secure networks, such as a home network behind a router. 6 | 7 | It is archived as it is feature complete. If anyone wants to take it back from the archives and maintain it contact me via the website linked in my profile. 8 | 9 | # What is a WirtBot? 10 | 11 | WirtBot is a combination of software tools to manage small to medium sized private networks. 12 | It uses state of the art technologies of the Linux and Cloud-native world to provide great speed and security. 13 | 14 | Whether you would like to make your self-hosted services available to friends and family, or want to create an intranet for your company - WirtBot has your back. 15 | 16 | On top of all that the setup is straight forward and very simple if you have used Docker before. 17 | 18 | It is a real Virtual Private Network to serve as a base layer for whatever project you have. 19 | 20 | Oh, and it is completely free. My way of paying back to the OSS community for all the knowledge it shares with the world. 21 | 22 | ## What is included? 23 | 24 | - Dashboard to create and manage WireGuard based networks 25 | - DNS zones for all connected devices 26 | - DNS forwarding, including DNSoverTLS 27 | - Real time metrics to be used with Prometheus 28 | - Port **9586**: [WireGuard](https://github.com/MindFlavor/prometheus_wireguard_exporter) 29 | - Port **9153**: [CoreDNS](https://coredns.io/plugins/metrics/) 30 | - Backup functionality 31 | - Modern crypto-aware API to keep the WirtBot only under your control 32 | - Extensive testing of the System is done on each change to the code to ensure correctness 33 | - Works on ARM64 and X64 34 | 35 | ## Setup 36 | 37 | Follow the instructions on the [website](https://b-m-f.github.io/WirtBot/). 38 | 39 | ## Screenshot 40 | 41 | ![Dashboard Screenshot](dashboard.jpg) 42 | 43 | ## Projects that make this possible 44 | 45 | These are some of the giants on whose shoulders the WirtBot is build 46 | 47 | - [WireGuard® ](https://wireguard.com) 48 | - [coredns](https://coredns.io/) 49 | - [VueJs](https://v3.vuejs.org/) 50 | - [VuePress](https://vuepress.vuejs.org/) 51 | - [docker](https://www.docker.com/) 52 | - [playwright](https://playwright.dev/) 53 | - [wasm-pack](https://rustwasm.github.io/docs/wasm-pack/) 54 | - [warp](https://github.com/seanmonstar/warp) 55 | - [ed25519-dalek](https://github.com/dalek-cryptography/ed25519-dalek) 56 | 57 | ## Support 58 | 59 | - [reddit](https://reddit.com/r/WirtBot) 60 | 61 | ## Documentation 62 | 63 | ### Users 64 | 65 | - [WirtBot Documentation](https://b-m-f.github.io/WirtBot/documentation/) 66 | 67 | ### Admins & Developers 68 | 69 | Check out the [Documentation](./Documentation) directory. 70 | 71 | ## LICENSE 72 | 73 | The licenses were chosen to ensure that the project will stay free for everyone, and allows safer networks for people to build things on. 74 | 75 | The WirtBot and Interface are licensed with `GNU Affero General Public License v3.0` 76 | 77 | The documentation and WirtBot logo are provided under `Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)` 78 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | Latest only 5 | 6 | ## Reporting a Vulnerability 7 | 8 | Please open up an issue. 9 | 10 | Thanks for the help and spending time on this project. Much appreciated! 11 | -------------------------------------------------------------------------------- /System-Tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 12, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "indent": [ 13 | "error", 14 | 4 15 | ], 16 | "linebreak-style": [ 17 | "error", 18 | "unix" 19 | ], 20 | "quotes": [ 21 | "error", 22 | "double" 23 | ], 24 | "semi": [ 25 | "error", 26 | "always" 27 | ] 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /System-Tests/.gitignore: -------------------------------------------------------------------------------- 1 | chromedriver.log 2 | tests_output 3 | tmp -------------------------------------------------------------------------------- /System-Tests/README.md: -------------------------------------------------------------------------------- 1 | # System tests 2 | 3 | The system tests are trying to test the while WirtBot system. 4 | This is achieved by creating a simulated test environment with multiple docker containers. 5 | 6 | All components - **Interface**, **WirtBot** and **Test-Runner** - run in Docker containers that are linked together. 7 | 8 | By using [docker volumes](https://docs.docker.com/storage/volumes/), it is possible for the **Test-Runner** to have access to all necessary files. 9 | 10 | This way it is possible to change the configuration on the Interface and then test if the changes were written to disk correctly, or rejected. 11 | 12 | 13 | ## Linking Interface and WirtBot Core 14 | 15 | Interface and WirtBot are linked together in the same way as during the development - by spawning them both with predefined keys to enable communcation. 16 | 17 | 18 | ## Local testing 19 | Make sure to install `firefox` and `chromium`. 20 | Now set `headless: false` in `tests.mjs`. This will make it easy to use the available `run_all_tests.sh` and inspect tests while they run. 21 | **Make sure to enable headless mode again when you are done testing!** 22 | -------------------------------------------------------------------------------- /System-Tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wirtbot/system-tests", 3 | "version": "3.9.4", 4 | "description": "This package is responsible for testing the WirtBot as a whole", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "URL=http://localhost:8080 API=localhost:3030 ./run_all_tests.sh", 8 | "test:ci": "URL=http://wirtbot.test API=wirtbot.test:3030 ./run_all_tests.sh" 9 | }, 10 | "dependencies": { 11 | "chromedriver": "105.0.0", 12 | "eslint": "8.23.0", 13 | "fibers": "5.0.3", 14 | "playwright": "1.25.2", 15 | "webpack-bundle-analyzer": "4.6.1" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/b-m-f/WirtBot.git" 20 | }, 21 | "keywords": [ 22 | "WirtBot" 23 | ], 24 | "author": "b-m-f ", 25 | "license": "AGPL-3.0", 26 | "bugs": { 27 | "url": "https://github.com/b-m-f/WirtBot/issues" 28 | }, 29 | "homepage": "https://github.com/b-m-f/WirtBot#readme", 30 | "devDependencies": {} 31 | } 32 | -------------------------------------------------------------------------------- /System-Tests/run_all_tests.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -ex 3 | 4 | 5 | cd tests/ 6 | for test in test_* 7 | do 8 | # capture external ENV variables if present or set project defaults 9 | API=${API:-localhost:3030} 10 | URL=${URL:-localhost:8080} 11 | 12 | URL=$URL API=$API node tests.mjs $test 13 | done 14 | cd - 15 | -------------------------------------------------------------------------------- /System-Tests/tests/test_IP_input.mjs: -------------------------------------------------------------------------------- 1 | import process from "process"; 2 | import assert from "assert"; 3 | 4 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 5 | import { addServer, getConfig as getServerConfig } from "./widgets/server.mjs"; 6 | import { 7 | addNewDevice, 8 | downloadConfig as downloadDeviceConfig, 9 | } from "./widgets/devices.mjs"; 10 | import { setDNSIP, getConfig as getDNSConfig } from "./widgets/dns.mjs"; 11 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 12 | import { promises as fsPromises } from "fs"; 13 | const { readFile } = fsPromises; 14 | 15 | // This is where the Core writes its updates 16 | const wirtBotFileDir = "/tmp/WirtBotTests"; 17 | 18 | export default async (browser) => { 19 | try { 20 | const page = await browser.newPage(); 21 | await page.goto(process.env.URL); 22 | await skipInitialConfig(page); 23 | 24 | await setAPIHost(page, process.env.API); 25 | 26 | 27 | await setDNSName(page, "test"); 28 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 29 | // Check the Build-Automation directory for more info 30 | 31 | // Test that the server IP wont update for the devices on incorrect input 32 | await addServer(page, { ip: "1.2.3.4", port: 1234 }); 33 | let req = page.waitForResponse(response => response.request().postData() ? response.request().postData().includes('1.1.1.1') : false) 34 | await addNewDevice(page, { 35 | ip: { v4: 2 }, 36 | name: "test-1", 37 | type: "Android", 38 | }); 39 | 40 | await req; 41 | 42 | await page.waitForSelector(".device[data-name='test-1']"); 43 | await page.reload(); 44 | await addServer(page, { ip: "1.2.3", port: 1234 }); 45 | const serverConfigOnPage = await getServerConfig(page); 46 | 47 | const deviceConfigPath = await downloadDeviceConfig(page, "test-1"); 48 | const deviceConfig = await readFile(`${deviceConfigPath}`, "utf-8"); 49 | 50 | assert.strictEqual(serverConfigOnPage.ip.v4, "1.2.3"); 51 | let valid = await page.$eval( 52 | "#server-widget input[name='ip-input']", 53 | (e) => { 54 | return e.validity.valid; 55 | } 56 | ); 57 | 58 | assert.strictEqual(valid, false); 59 | assert.match(deviceConfig, /.*Endpoint = 1.2.3.4/); 60 | 61 | // Test that DNS Ip wont update on incorrect input 62 | await setDNSIP(page, "1.1.1"); 63 | const dnsConfigOnpage = await getDNSConfig(page); 64 | let dnsConfigFromCore = await readFile( 65 | `${wirtBotFileDir}/Corefile`, 66 | "utf-8" 67 | ); 68 | 69 | assert.strictEqual(dnsConfigOnpage.ip.v4, "1.1.1"); 70 | valid = await page.$eval("#dns-widget input[name='ip-input']", (e) => { 71 | return e.validity.valid; 72 | }); 73 | assert.strictEqual(valid, false); 74 | assert.match(dnsConfigFromCore, /.*tls:\/\/1.1.1.1/); 75 | } catch (error) { 76 | console.error(error); 77 | throw error; 78 | } 79 | }; 80 | -------------------------------------------------------------------------------- /System-Tests/tests/test_backups_overwrite_old_config.spec.mjs: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import { promises as fsPromises } from "fs"; 3 | import process from "process"; 4 | import { importBackup } from "./widgets/settings.mjs"; 5 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 6 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 7 | import { addServer } from "./widgets/server.mjs"; 8 | import { addNewDevice } from "./widgets/devices.mjs"; 9 | 10 | const { readFile } = fsPromises; 11 | // This is where the Core writes its updates 12 | const wirtBotFileDir = "/tmp/WirtBotTests"; 13 | 14 | export default async (browser) => { 15 | try { 16 | let page = await browser.newPage(); 17 | await page.goto(process.env.URL); 18 | await skipInitialConfig(page); 19 | await page.close(); 20 | 21 | let backup = ["./backups/2.6.0.json"]; 22 | 23 | page = await browser.newPage(); 24 | await page.goto(process.env.URL); 25 | 26 | // create config that should be overwritten 27 | await setAPIHost(page, process.env.API); 28 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 29 | // Check the Build-Automation directory for more info 30 | await setDNSName(page, "test"), 31 | 32 | await addServer(page, { ip: "1.2.3.4", port: 1234 }); 33 | 34 | await addNewDevice(page, { 35 | ip: { v4: 255 }, 36 | name: "test-initial", 37 | type: "Android", 38 | }); 39 | 40 | await page.waitForResponse(response => response.request().postData().includes('10.10.0.255')) 41 | 42 | let serverConfigFromCore = await readFile( 43 | `${wirtBotFileDir}/server.conf`, 44 | "utf-8" 45 | ); 46 | assert.match(serverConfigFromCore, /.*AllowedIPs = 10.10.0.255.*/); 47 | 48 | // Overwrite config with backup 49 | 50 | await importBackup(page, backup); 51 | 52 | // Make sure initially setup device is removed 53 | serverConfigFromCore = await readFile( 54 | `${wirtBotFileDir}/server.conf`, 55 | "utf-8" 56 | ); 57 | assert.doesNotMatch(serverConfigFromCore, /.*AllowedIPs = 10.10.0.255.*/); 58 | } catch (error) { 59 | console.error(error); 60 | throw error; 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /System-Tests/tests/test_creating_simple_network.spec.mjs: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { promises as fsPromises } from "fs"; 3 | const { readFile } = fsPromises; 4 | import process from "process"; 5 | 6 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 7 | import { 8 | addServer, 9 | downloadConfig as downloadServerConfig, 10 | } from "./widgets/server.mjs"; 11 | import { 12 | addNewDevice, 13 | downloadConfig as downloadDeviceConfig, 14 | } from "./widgets/devices.mjs"; 15 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 16 | 17 | // This is where the Core writes its updates 18 | const wirtBotFileDir = "/tmp/WirtBotTests"; 19 | 20 | export default async (browser) => { 21 | try { 22 | const page = await browser.newPage(); 23 | await page.goto(process.env.URL); 24 | await skipInitialConfig(page); 25 | 26 | await setAPIHost(page, process.env.API); 27 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 28 | // Check the Build-Automation directory for more info 29 | await setDNSName(page, "test"); 30 | 31 | const dnsUpdateResponse = page.waitForResponse( 32 | /.*\/update-device-dns-entries/ 33 | ); 34 | let req = page.waitForResponse(response => response.request().postData() ? response.request().postData().includes('test-1') : false) 35 | await addServer(page, { ip: "1.2.3.4", port: 1234 }); 36 | await addNewDevice(page, { 37 | ip: { v4: 2 }, 38 | name: "test-1", 39 | type: "Android", 40 | }); 41 | 42 | await Promise.all([ 43 | 44 | await dnsUpdateResponse, 45 | await req 46 | ]) 47 | 48 | // Wait for alerts to pop up 49 | await page.waitForTimeout(200); 50 | 51 | const numberOfSuccessAlertsForDNSAndConfig = await page.$$eval( 52 | "#alerts .success", 53 | (items) => items.length 54 | ); 55 | assert.strictEqual(numberOfSuccessAlertsForDNSAndConfig, 2); 56 | 57 | const deviceConfigPath = await downloadDeviceConfig(page, "test-1"); 58 | const serverConfigPath = await downloadServerConfig(page); 59 | 60 | const deviceConfig = await readFile(`${deviceConfigPath}`, "utf-8"); 61 | const serverConfig = await readFile(`${serverConfigPath}`, "utf-8"); 62 | const serverConfigFromCore = await readFile( 63 | `${wirtBotFileDir}/server.conf`, 64 | "utf-8" 65 | ); 66 | 67 | assert.match(deviceConfig, /.*Endpoint = 1.2.3.4:1234/); 68 | assert.match(deviceConfig, /.*Address = 10.10.0.2.*/); 69 | 70 | assert.match(serverConfig, /.*ListenPort = 1234.*/); 71 | assert.match(serverConfig, /.*Address = 10.10.0.1.*/); 72 | assert.strictEqual(serverConfig, serverConfigFromCore); 73 | } catch (error) { 74 | console.error(error); 75 | throw error; 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /System-Tests/tests/test_deleting_device.spec.mjs: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { promises as fsPromises } from "fs"; 3 | const { readFile } = fsPromises; 4 | import process from "process"; 5 | 6 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 7 | import { 8 | addServer, 9 | downloadConfig as downloadServerConfig, 10 | } from "./widgets/server.mjs"; 11 | import { addNewDevice, deleteDevice } from "./widgets/devices.mjs"; 12 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 13 | 14 | // This is where the Core writes its updates 15 | const wirtBotFileDir = "/tmp/WirtBotTests"; 16 | 17 | export default async (browser) => { 18 | try { 19 | const page = await browser.newPage(); 20 | await page.goto(process.env.URL); 21 | await skipInitialConfig(page); 22 | 23 | await setAPIHost(page, process.env.API); 24 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 25 | // Check the Build-Automation directory for more info 26 | await setDNSName(page, "test"); 27 | 28 | await addServer(page, { ip: "1.2.3.4", port: 1234 }); 29 | 30 | 31 | let updateResponse = page.waitForResponse(/.*\/update/); 32 | let dnsUpdateResponse = page.waitForResponse( 33 | /.*\/update-device-dns-entries/ 34 | ); 35 | await addNewDevice(page, { 36 | ip: { v4: 2 }, 37 | name: "test-1", 38 | type: "Android", 39 | }); 40 | 41 | await updateResponse; 42 | await dnsUpdateResponse; 43 | 44 | // Wait for alerts to pop up 45 | await page.waitForTimeout(500); 46 | 47 | const numberOfSuccessAlertsForDNSAndConfig = await page.$$eval( 48 | "#alerts .success", 49 | (items) => items.length 50 | ); 51 | assert.strictEqual(numberOfSuccessAlertsForDNSAndConfig, 2); 52 | 53 | const serverConfigPath = await downloadServerConfig(page); 54 | 55 | let serverConfig = await readFile(`${serverConfigPath}`, "utf-8"); 56 | const serverConfigFromCore = await readFile( 57 | `${wirtBotFileDir}/server.conf`, 58 | "utf-8" 59 | ); 60 | 61 | assert.match(serverConfig, /.*ListenPort = 1234.*/); 62 | assert.match(serverConfig, /.*Address = 10.10.0.1.*/); 63 | assert.match(serverConfig, /.*AllowedIPs = 10.10.0.2.*/); 64 | assert.strictEqual(serverConfig, serverConfigFromCore); 65 | 66 | // Delete device and make sure it is removed from the server config 67 | updateResponse = page.waitForResponse(/.*\/update/); 68 | dnsUpdateResponse = page.waitForResponse(/.*\/update-device-dns-entries/); 69 | 70 | await deleteDevice(page, "test-1"); 71 | await updateResponse; 72 | await dnsUpdateResponse; 73 | 74 | serverConfig = await readFile(`${wirtBotFileDir}/server.conf`, "utf-8"); 75 | assert.doesNotMatch(serverConfig, /.*AllowedIPs = 10.10.0.2.*/); 76 | } catch (error) { 77 | console.error(error); 78 | throw error; 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /System-Tests/tests/test_device_input_validations.spec.mjs: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { promises as fsPromises } from "fs"; 3 | import process from "process"; 4 | 5 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 6 | import { addServer } from "./widgets/server.mjs"; 7 | import { 8 | addNewDevice, 9 | setIPv6, 10 | setIPv4, 11 | expandDevice, 12 | } from "./widgets/devices.mjs"; 13 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 14 | 15 | export default async (browser) => { 16 | try { 17 | const page = await browser.newPage(); 18 | await page.goto(process.env.URL); 19 | await skipInitialConfig(page); 20 | 21 | await setAPIHost(page, process.env.API); 22 | await setDNSName(page, "test"); 23 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 24 | // Check the Build-Automation directory for more info 25 | await addServer(page, { ip: "1.2.3.4", port: 1234 }); 26 | 27 | // devices cant have same ip 28 | await addNewDevice(page, { 29 | ip: { v4: 2 }, 30 | name: "test-1", 31 | type: "Android", 32 | }); 33 | await page.waitForSelector(".device[data-name='test-1']"); 34 | await addNewDevice(page, { 35 | ip: { v4: 3 }, 36 | name: "test-2", 37 | type: "Android", 38 | }); 39 | await page.waitForSelector(".device[data-name='test-2']"); 40 | 41 | let device = await page.$(".device[data-name='test-1']"); 42 | await setIPv4(device, 3); 43 | let valid = await page.$eval( 44 | ".device[data-name='test-1'] input[name='device-ipv4']", 45 | (e) => { 46 | return e.validity.valid; 47 | } 48 | ); 49 | assert.strictEqual(valid, false); 50 | 51 | // cant set to 1, where the WirtBot is 52 | await setIPv4(device, 1); 53 | valid = await page.$eval( 54 | ".device[data-name='test-1'] input[name='device-ipv4']", 55 | (e) => { 56 | return e.validity.valid; 57 | } 58 | ); 59 | assert.strictEqual(valid, false); 60 | 61 | // cant set above 255 62 | await setIPv4(device, 256); 63 | valid = await page.$eval( 64 | ".device[data-name='test-1'] input[name='device-ipv4']", 65 | (e) => { 66 | return e.validity.valid; 67 | } 68 | ); 69 | assert.strictEqual(valid, false); 70 | 71 | await expandDevice(device); 72 | // IPv6 cant be 0001, where the server is 73 | await setIPv6(device, 256); 74 | valid = await page.$eval( 75 | ".device[data-name='test-1'] input[name='device-ipv6']", 76 | (e) => { 77 | return e.validity.valid; 78 | } 79 | ); 80 | assert.strictEqual(valid, false); 81 | 82 | // IPv6 cant be above fffe, where the server is 83 | await setIPv6(device, "ffff"); 84 | valid = await page.$eval( 85 | ".device[data-name='test-1'] input[name='device-ipv6']", 86 | (e) => { 87 | return e.validity.valid; 88 | } 89 | ); 90 | assert.strictEqual(valid, false); 91 | 92 | // Pressing enter does not remove device. 93 | const firstDeviceNameBeforePressingEnter = await page.$eval( 94 | ".device:first-child input[name='device-name']", 95 | (e) => e.value 96 | ); 97 | await page.focus(".device[data-name='test-1'] input[name='device-ipv6']"); 98 | await page.keyboard.press("Enter"); 99 | const firstDeviceNameAfterPressingEnter = await page.$eval( 100 | ".device:first-child input[name='device-name']", 101 | (e) => e.value 102 | ); 103 | assert.strictEqual( 104 | firstDeviceNameAfterPressingEnter, 105 | firstDeviceNameBeforePressingEnter 106 | ); 107 | } catch (error) { 108 | console.error(error); 109 | throw error; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /System-Tests/tests/test_dns.spec.mjs: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { promises as fsPromises } from "fs"; 3 | import process from "process"; 4 | const { readFile } = fsPromises; 5 | import util from "util"; 6 | 7 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 8 | import { 9 | setDNSTlsName, 10 | enableDNSTLS, 11 | setDNSIP, 12 | disableDNSTLS, 13 | getValidity as getDNSValidity, 14 | } from "./widgets/dns.mjs"; 15 | import { addServer } from "./widgets/server.mjs"; 16 | import { addNewDevice } from "./widgets/devices.mjs"; 17 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 18 | 19 | // This is where the Core writes its updates 20 | const wirtBotFileDir = "/tmp/WirtBotTests"; 21 | 22 | export default async (browser) => { 23 | try { 24 | const page = await browser.newPage(); 25 | await page.goto(process.env.URL); 26 | await skipInitialConfig(page); 27 | 28 | await setAPIHost(page, process.env.API); 29 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 30 | // Check the Build-Automation directory for more info 31 | let dnsUpdateResponse = page.waitForResponse( 32 | /.*update-device-dns-entries.*/ 33 | ); 34 | await setDNSName(page, "test"); 35 | await dnsUpdateResponse; 36 | 37 | dnsUpdateResponse = page.waitForResponse(/.*\/update-device-dns-entries/); 38 | await enableDNSTLS(page); 39 | await setDNSIP(page, "1.0.3.4"); 40 | await dnsUpdateResponse; 41 | 42 | await setDNSTlsName(page, "https://testdns.test"); 43 | let dnsValid = await getDNSValidity(page); 44 | assert.strictEqual(dnsValid.name, false); 45 | 46 | dnsUpdateResponse = page.waitForResponse(/.*\/update-device-dns-entries/); 47 | await setDNSTlsName(page, "testdns.test"); 48 | dnsValid = await getDNSValidity(page); 49 | assert.strictEqual(dnsValid.name, true); 50 | await dnsUpdateResponse; 51 | 52 | dnsUpdateResponse = page.waitForResponse(/.*\/update-device-dns-entries/); 53 | let req = page.waitForResponse(response => response.request().postData() ? response.request().postData().includes('10.11.0.1') : false) 54 | await addServer(page, { 55 | ip: "1.2.3.4", 56 | port: 1234, 57 | subnet: {v4: "10.11.0"}, 58 | name: "test", 59 | }); 60 | await Promise.all( 61 | [ 62 | req, 63 | dnsUpdateResponse 64 | ] 65 | ) 66 | 67 | 68 | dnsUpdateResponse = page.waitForResponse(/.*\/update-device-dns-entries/); 69 | await addNewDevice(page, { 70 | ip: { v4: 2 }, 71 | name: "test-1", 72 | type: "Android", 73 | additionalDNSServers: "2.2.2.2", 74 | additionalNames: "test2", 75 | MTU: 1500, 76 | }); 77 | await dnsUpdateResponse; 78 | 79 | // wait for changes to be flushed to backend 80 | await page.waitForTimeout(100); 81 | let dnsConfigFromCore = await readFile( 82 | `${wirtBotFileDir}/Corefile`, 83 | "utf-8" 84 | ); 85 | 86 | assert.match(dnsConfigFromCore, /.*tls:\/\/1.0.3.4/); 87 | assert.match(dnsConfigFromCore, /.*tls_servername testdns.test/); 88 | assert.match(dnsConfigFromCore, /.*10.11.0.2 test2.test/); 89 | assert.match(dnsConfigFromCore, /.*10.11.0.2 test-1.test/); 90 | 91 | 92 | // Test with IP without TLS 93 | 94 | req = page.waitForResponse(response => response.request().postData() ? response.request().postData().includes('1.2.3.4') : false) 95 | await disableDNSTLS(page); 96 | await setDNSIP(page, "1.2.3.4"); 97 | await req; 98 | 99 | // wait for changes to be flushed to backend 100 | await page.waitForTimeout(100); 101 | dnsConfigFromCore = await readFile(`${wirtBotFileDir}/Corefile`, "utf-8"); 102 | 103 | assert.match(dnsConfigFromCore, /forward . 1.2.3.4/); 104 | assert.doesNotMatch(dnsConfigFromCore, /.*tls_servername/); 105 | 106 | 107 | } catch (error) { 108 | console.error(error); 109 | throw error; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /System-Tests/tests/test_dns_ignored_zones.spec.mjs: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { promises as fsPromises } from "fs"; 3 | import process from "process"; 4 | const { readFile } = fsPromises; 5 | 6 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 7 | import { setIgnoredZones } from "./widgets/dns.mjs"; 8 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 9 | 10 | // This is where the Core writes its updates 11 | const wirtBotFileDir = "/tmp/WirtBotTests"; 12 | 13 | export default async (browser) => { 14 | try { 15 | const page = await browser.newPage(); 16 | await page.goto(process.env.URL); 17 | await skipInitialConfig(page); 18 | 19 | await setAPIHost(page, process.env.API); 20 | let dnsUpdateResponse = page.waitForResponse( 21 | /.*update-device-dns-entries.*/ 22 | ); 23 | await setDNSName(page, "test"); 24 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 25 | // Check the Build-Automation directory for more info 26 | // wait for state to be ready 27 | await dnsUpdateResponse; 28 | 29 | let req = page.waitForResponse(response => response.request().postData() ? response.request().postData().includes('test2 what up.lan') : false) 30 | await setIgnoredZones(page, "test2,what,up.lan"); 31 | // wait to propagate changes to backend 32 | await req; 33 | 34 | 35 | const dnsConfigFromCore = await readFile( 36 | `${wirtBotFileDir}/Corefile`, 37 | "utf-8" 38 | ); 39 | 40 | assert.match(dnsConfigFromCore, /.* except test2 what up.lan/); 41 | } catch (error) { 42 | console.error(error); 43 | throw error; 44 | } 45 | }; 46 | -------------------------------------------------------------------------------- /System-Tests/tests/test_initial_setup.mjs: -------------------------------------------------------------------------------- 1 | 2 | 3 | import * as assert from "assert"; 4 | import process from "process"; 5 | 6 | 7 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 8 | 9 | 10 | export default async (browser) => { 11 | try { 12 | const page = await browser.newPage(); 13 | await page.goto(process.env.URL); 14 | await skipInitialConfig(page); 15 | const content = await page.textContent("#top-bar h1"); 16 | assert.strictEqual(content, "Dashboard"); 17 | 18 | } catch (error) { 19 | console.error(error); 20 | throw error; 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /System-Tests/tests/test_square_qr_codes.mjs: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | import { promises as fsPromises } from "fs"; 3 | const { readFile } = fsPromises; 4 | import process from "process"; 5 | 6 | import { setDNSName, setAPIHost } from "./widgets/network.mjs"; 7 | import { 8 | addServer, 9 | downloadConfig as downloadServerConfig, 10 | } from "./widgets/server.mjs"; 11 | import { 12 | addNewDevice, 13 | downloadConfig as downloadDeviceConfig, 14 | } from "./widgets/devices.mjs"; 15 | import { skipInitialConfig } from "./widgets/initial_setup.mjs"; 16 | 17 | // This is where the Core writes its updates 18 | const wirtBotFileDir = "/tmp/WirtBotTests"; 19 | 20 | export default async (browser) => { 21 | try { 22 | const page = await browser.newPage(); 23 | await page.goto(process.env.URL); 24 | await skipInitialConfig(page); 25 | 26 | await setAPIHost(page, process.env.API); 27 | // The DNS name has to set to .test to work in CI where the wirtbot is in the .test zone 28 | // Check the Build-Automation directory for more info 29 | await setDNSName(page, "test"); 30 | 31 | const updateResponse = page.waitForResponse(/.*\/update/); 32 | const dnsUpdateResponse = page.waitForResponse( 33 | /.*\/update-device-dns-entries/ 34 | ); 35 | await addServer(page, { ip: "1.2.3.4", port: 1234 }); 36 | await addNewDevice(page, { 37 | ip: { v4: 2 }, 38 | name: "test-1", 39 | type: "Android", 40 | }); 41 | 42 | await updateResponse; 43 | await dnsUpdateResponse; 44 | 45 | // Wait for alerts to pop up 46 | await page.waitForTimeout(200); 47 | 48 | const width = await page.$eval( 49 | ".device[data-name='test-1'] .qr-code", 50 | (el) => window.getComputedStyle(el).fontSize 51 | ); 52 | const height = await page.$eval( 53 | ".device[data-name='test-1'] .qr-code", 54 | (el) => window.getComputedStyle(el).fontSize 55 | ); 56 | assert.strictEqual(width, height); 57 | } catch (error) { 58 | console.error(error); 59 | throw error; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /System-Tests/tests/tests.mjs: -------------------------------------------------------------------------------- 1 | import process from "process"; 2 | import { firefox, chromium } from "playwright"; 3 | 4 | const testRunner = async (testPaths) => { 5 | const browsers = { 6 | firefox: await firefox.launch({ headless: true}), 7 | chromium: await chromium.launch({ 8 | headless: true, 9 | args: ["--disable-dev-shm-usage"], 10 | }), 11 | }; 12 | let testSuites = []; 13 | try { 14 | testSuites = [ 15 | // Promise.all resolves as an array with 1 element somehow 16 | await Promise.all( 17 | testPaths.map(async (test) => { 18 | return { name: test, module: await import(`./${test}`) }; 19 | }) 20 | ), 21 | ][0].map((suite) => { 22 | return { name: suite.name, module: suite.module.default }; 23 | }); 24 | } catch (error) { 25 | console.log(error); 26 | } 27 | for (const key of Object.keys(browsers)) { 28 | for (const suite of testSuites) { 29 | try { 30 | let browserContext = await browsers[key].newContext({ 31 | acceptDownloads: true, 32 | }); 33 | await suite.module(browserContext); 34 | 35 | console.log(`Test: ${suite.name} in browser ${key} ran successfully`); 36 | browsers[key].close(); 37 | } catch (error) { 38 | browsers[key].close(); 39 | console.log(`Test: ${suite.name} in browser ${key} failed`); 40 | throw "Tests failed"; 41 | } 42 | } 43 | } 44 | }; 45 | 46 | const main = async () => { 47 | try { 48 | if (!process.env.URL) { 49 | throw "Please define the Interface url with the URL environment variable"; 50 | } 51 | if (!process.env.API) { 52 | throw "Please defined the API Url with the URL environment variable"; 53 | } 54 | const testPaths = process.argv.slice(2); 55 | await testRunner(testPaths); 56 | } catch (error) { 57 | console.error(error); 58 | process.exit(1); 59 | } 60 | }; 61 | main(); 62 | -------------------------------------------------------------------------------- /System-Tests/tests/widgets/dns.mjs: -------------------------------------------------------------------------------- 1 | export const dnsWidget = async (page) => { 2 | return await page.locator("css=#dns-widget"); 3 | }; 4 | 5 | export const getValidity = async (page) => { 6 | const validName = await page.$eval( 7 | "#dns-widget input[name='tlsname']", 8 | (e) => e.validity.valid 9 | ); 10 | return { 11 | name: validName, 12 | }; 13 | }; 14 | 15 | export const getConfig = async (page) => { 16 | const dns = await dnsWidget(page); 17 | const dnsTLSNameInput = await dns.locator("input[name='tlsname']"); 18 | const dnsTLSCheckbox = await dns.locator("input[name='tls']"); 19 | const dnsIP = await dns.locator("input[name='ip-input']"); 20 | const dnsIgnoredZones = await dns.locator("textarea[name='ignoredZones']"); 21 | 22 | const tlsEnabled = await dnsTLSCheckbox.evaluate((e) => e.checked); 23 | 24 | return { 25 | tlsName: await dnsTLSNameInput.evaluate((e) => e.value), 26 | tls: tlsEnabled, 27 | ignoredZones: await dnsIgnoredZones.evaluate((e) => e.value), 28 | ip: { v4: await dnsIP.evaluate((e) => e.value) } 29 | }; 30 | 31 | }; 32 | 33 | export const setDNSTlsName = async (page, name) => { 34 | const dns = await dnsWidget(page); 35 | const dnsTLSNameInput = await dns.locator("input[name='tlsname']"); 36 | await dnsTLSNameInput.fill(name); 37 | }; 38 | 39 | export const enableDNSTLS = async (page) => { 40 | const dns = await dnsWidget(page); 41 | const checkbox = await dns.locator("input[name='tls']"); 42 | await checkbox.check(); 43 | }; 44 | 45 | export const disableDNSTLS = async (page) => { 46 | const dns = await dnsWidget(page); 47 | const checkbox = await dns.locator("input[name='tls']"); 48 | await checkbox.uncheck(); 49 | }; 50 | 51 | export const setDNSIP = async (page, ip) => { 52 | const dns = await dnsWidget(page); 53 | const input = await dns.locator("input[name='ip-input']"); 54 | await input.fill(ip); 55 | }; 56 | 57 | 58 | export const setIgnoredZones = async (page, zones) => { 59 | const dns = await dnsWidget(page); 60 | const input = await dns.locator("textarea[name='ignoredZones']"); 61 | await input.fill(zones); 62 | }; 63 | -------------------------------------------------------------------------------- /System-Tests/tests/widgets/initial_setup.mjs: -------------------------------------------------------------------------------- 1 | export const skipInitialConfig = async (page) => { 2 | // during development and testing the public and private key are set to 3 | // VUE_APP_PUBLIC_KEY=1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI= 4 | // VUE_APP_PRIVATE_KEY=5mulHuvASgsqAR282LC4nTKoALXpqJWfOTcpQseXRYg= 5 | // via Environment variables. Which will be used to pass the initial setup page here as well 6 | 7 | const simulatedPayloadFromCore = Buffer.from(JSON.stringify({ 8 | keys: { 9 | private: "5mulHuvASgsqAR282LC4nTKoALXpqJWfOTcpQseXRYg=", 10 | public: "1lLU3VhXsrSGMxESmqfY4m2oEVkpfEHyKlCQU6MMPsI=" 11 | } 12 | })).toString('base64'); 13 | const button = await page.locator("css=.submit"); 14 | const configInput = await page.locator("css=#input"); 15 | 16 | await configInput.fill(simulatedPayloadFromCore); 17 | await button.click(); 18 | }; 19 | -------------------------------------------------------------------------------- /System-Tests/tests/widgets/network.mjs: -------------------------------------------------------------------------------- 1 | export const networkWidget = async (page) => { 2 | return await page.locator("css=#network-widget"); 3 | }; 4 | 5 | export const getConfig = async (page) => { 6 | const widget = await networkWidget(page); 7 | const zoneNameInput = await widget.locator("input[name='dns-name']"); 8 | 9 | return { 10 | name: await zoneNameInput.evaluate(e => e.value) 11 | }; 12 | }; 13 | 14 | export const setDNSName = async (page, name) => { 15 | const network = await networkWidget(page); 16 | const zoneNameInput = await network.locator("input[name='dns-name']"); 17 | await zoneNameInput.fill(name); 18 | }; 19 | 20 | export const setAPIHost = async (page, host) => { 21 | const network = await networkWidget(page); 22 | const hostInput = await network.locator("input[name='api-host']"); 23 | await hostInput.fill(host); 24 | }; 25 | -------------------------------------------------------------------------------- /System-Tests/tests/widgets/server.mjs: -------------------------------------------------------------------------------- 1 | export const serverWidget = async (page) => { 2 | return await page.locator("css=#server-widget"); 3 | }; 4 | 5 | export const getConfig = async (page) => { 6 | const widget = await serverWidget(page); 7 | 8 | const portInput = await widget.locator("input[name='server-port']"); 9 | const port = await portInput.evaluate((e) => e.value); 10 | 11 | const ipInput = await widget.locator("input[name='ip-input']"); 12 | const ip = await ipInput.evaluate((e) => e.value); 13 | 14 | const nameInput = await widget.locator("input[name='server-name']"); 15 | const name = await nameInput.evaluate((e) => e.value); 16 | 17 | const hostnameInput = await widget.locator("input[name='server-hostname']"); 18 | const hostname = await hostnameInput.evaluate((e) => e.value); 19 | 20 | const subnetv4Input = await widget.locator("input[name='server-subnet-v4']"); 21 | const subnetv4 = await subnetv4Input.evaluate((e) => e.value); 22 | 23 | const subnetv6Input = await widget.locator("input[name='server-subnet-v6']"); 24 | const subnetv6 = await subnetv6Input.evaluate((e) => e.value); 25 | 26 | return { 27 | ip: { v4: ip, v6: "" }, 28 | port: parseInt(port), 29 | name, 30 | hostname, 31 | subnet: { v4: subnetv4, v6: subnetv6 }, 32 | }; 33 | }; 34 | 35 | const setIP = async (widget, ip) => { 36 | const input = await widget.locator("input[name='ip-input']"); 37 | await input.fill(ip); 38 | }; 39 | 40 | const setPort = async (widget, port) => { 41 | const input = await widget.locator("input[name='server-port']"); 42 | await input.type(port.toString()); 43 | }; 44 | const setName = async (widget, name) => { 45 | const input = await widget.locator("input[name='server-name']"); 46 | await input.fill(name); 47 | }; 48 | const setHostname = async (widget, hostname) => { 49 | const input = await widget.locator("input[name='server-hostname']"); 50 | await input.fill(hostname); 51 | }; 52 | const setSubnet = async (widget, { v4, v6 }) => { 53 | if (v4) { 54 | const inputv4 = await widget.locator("input[name='server-subnet-v4']"); 55 | await inputv4.fill(v4); 56 | } 57 | if (v6) { 58 | const inputv6 = await widget.locator("input[name='server-subnet-v6']"); 59 | await inputv6.fill(v6); 60 | } 61 | }; 62 | 63 | export const addServer = async (page, { ip, port, hostname, subnet, name }) => { 64 | if (ip) { 65 | const widget = await serverWidget(page); 66 | await setIP(widget, ip); 67 | } 68 | if (hostname) { 69 | const widget = await serverWidget(page); 70 | await setHostname(widget, hostname); 71 | } 72 | if (subnet) { 73 | const widget = await serverWidget(page); 74 | await setSubnet(widget, subnet); 75 | } 76 | if (name) { 77 | const widget = await serverWidget(page); 78 | await setName(widget, name); 79 | } 80 | if (port) { 81 | const widget = await serverWidget(page); 82 | await setPort(widget, port); 83 | } 84 | 85 | }; 86 | export const updateServer = async (page, server) => { 87 | await addServer(page, server); 88 | }; 89 | 90 | export const downloadConfig = async (page) => { 91 | const widget = await serverWidget(page); 92 | const downloadPath = new Promise((res) => { 93 | page.on("download", (dl) => { 94 | dl.path().then(res); 95 | }); 96 | }); 97 | const downloadButton = await widget.locator("#download"); 98 | await downloadButton.click(); 99 | return downloadPath; 100 | }; 101 | -------------------------------------------------------------------------------- /System-Tests/tests/widgets/settings.mjs: -------------------------------------------------------------------------------- 1 | export const settingsWidget = async (page) => { 2 | return await page.locator("css=#settings-widget"); 3 | }; 4 | 5 | export const downloadBackup = async (page) => { 6 | const widget = await settingsWidget(page); 7 | const downloadPath = new Promise((res) => { 8 | page.on("download", (dl) => { 9 | dl.path().then(res); 10 | }); 11 | }); 12 | const downloadButton = await widget.locator("#export"); 13 | await downloadButton.click(); 14 | return downloadPath; 15 | }; 16 | 17 | export const importBackup = async (page, backupPath) => { 18 | const widget = await settingsWidget(page); 19 | const input = await widget.locator("#import input"); 20 | const importButton = await widget.locator("#import button"); 21 | 22 | await input.setInputFiles(backupPath); 23 | await importButton.click(); 24 | // give the state some time to finish building 25 | await page.waitForTimeout(1500); 26 | }; 27 | -------------------------------------------------------------------------------- /Website/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | content/.vuepress/dist 3 | -------------------------------------------------------------------------------- /Website/LICENSE.md: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/ or send a letter to Creative Commons, PO Box 1866, Mountain View, CA 94042, USA. 2 | -------------------------------------------------------------------------------- /Website/README.md: -------------------------------------------------------------------------------- 1 | # WirtBot documentation 2 | 3 | This directory houses the [WirtBot website](https://wirtbot.com) 4 | 5 | Feel free to contribute to it if you think something is missing or you have a great tutorial! 6 | 7 | ## Contributing 8 | 9 | First of all download this repository. 10 | Now create a new [markdown](https://www.markdownguide.org/basic-syntax) file in the docs folder. 11 | When you are satisfied with your page add it to the sidebar in `documentation/.vuepress/config.js`. 12 | 13 | Everything is based on [vuepress](https://vuepress.vuejs.org/), which has great documentation and is beginner friendly. 14 | 15 | ### Test your changes 16 | 17 | You will need: 18 | - nodejs 19 | - npm 20 | 21 | After you have both installed run `npm install` in the root of this documentation repository. 22 | To start a local development server run `npm run dev:docs`. 23 | 24 | You local version will now be available at `http://localhost:9090` 25 | 26 | ### Submit your changes 27 | 28 | When you are done open a [Pull request](https://help.github.com/en/github/getting-started-with-github/github-glossary#pull-request) and target it to the development branch! Once it is reviewed and everything looks good it will be merged and soon appear on the website. 29 | 30 | If you need any help simply open an issue! 31 | -------------------------------------------------------------------------------- /Website/content/.vuepress/components/AccentedCard.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 25 | -------------------------------------------------------------------------------- /Website/content/.vuepress/components/Button.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | 22 | 60 | -------------------------------------------------------------------------------- /Website/content/.vuepress/components/Card.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 27 | -------------------------------------------------------------------------------- /Website/content/.vuepress/components/HomePageEnglish.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 34 | 35 | 97 | -------------------------------------------------------------------------------- /Website/content/.vuepress/components/Modal.vue: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/.vuepress/components/Modal.vue -------------------------------------------------------------------------------- /Website/content/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = { 4 | base: "/WirtBot/", 5 | port: 9090, 6 | title: "WirtBot, your LAN in the cloud", 7 | head: [ 8 | ], 9 | themeConfig: { 10 | logo: "/logo.svg", 11 | locales: { 12 | "/": { 13 | sidebar: { 14 | "/documentation/": [ 15 | ["", "What is it?"], 16 | ["what-can-i-do-with-wirt", "What can you do with Wirt?"], 17 | ["setup", "Setup"], 18 | ["join-a-network", "Join a network"], 19 | ["system-overview", "System Overview"], 20 | ["faq", "FAQ"], 21 | ["issues", "New features & Bugs"], 22 | ], 23 | "/announcements/": [ 24 | ["11-01-23", "Update 7: Time to say goodbye"], 25 | ["update-6", "Update 6: More focus, less bugs"], 26 | ["update-5", "Update 5: Time to maintain"], 27 | ["update-4", "Update 4: WirtBot weight loss and a nasty bug"], 28 | ["update-3", "Update 3: With confidence into the unknown"], 29 | [ 30 | "22-11-20", 31 | "22.11.20: Installer change and major Interface + System-Tests refactor", 32 | ], 33 | ["8-11-20", "8.11.20: New name for installer binary"], 34 | ["31-10-20", "31.10.20: All aboard the container"], 35 | ["21-10-20", "21.10.20: Please update your installer"], 36 | 37 | ], 38 | }, 39 | nav: [ 40 | { text: "Github", link: "https://github.com/b-m-f/WirtBot" }, 41 | { text: "Documentation", link: "/documentation/" }, 42 | { text: "Screenshots", link: "/screenshots" }, 43 | { text: "Announcements", link: "/announcements/" }, 44 | ], 45 | }, 46 | // "/de/": { 47 | // "/": { 48 | // nav: [{ text: "Back to the App", link: "/" }], 49 | // sidebar: [ 50 | // ["/", "Intro"], 51 | // ["/beginner", "Beginner Erklaerung"], 52 | // ["/expert", "Uebersicht fuer Experten"], 53 | // ["/server", "Wireguard Server"], 54 | // ["/faq", "FAQ"] 55 | // ] 56 | // } 57 | // } 58 | }, 59 | }, 60 | locales: { 61 | "/": { 62 | lang: "English", 63 | title: "WirtBot", 64 | description: "Stay connected", 65 | }, 66 | // "/de/": { 67 | // lang: "Deutsch", 68 | // title: "Wirt", 69 | // description: "Dein Netz im Netz" 70 | // } 71 | }, 72 | scss: { 73 | additionalData: `@import "styles/variables.scss";`, 74 | }, 75 | chainWebpack: (config, isServer) => { 76 | config.resolve.alias 77 | .set("styles", path.join(__dirname, "./styles")) 78 | .set("components", path.join(__dirname, "./components")) 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /Website/content/.vuepress/public/browser.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Website/content/.vuepress/public/glass.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Website/content/.vuepress/public/lightning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Website/content/.vuepress/public/logo.svg: -------------------------------------------------------------------------------- 1 | wirt-logo -------------------------------------------------------------------------------- /Website/content/.vuepress/public/wirt-bot.svg: -------------------------------------------------------------------------------- 1 | wirt-bot-logo -------------------------------------------------------------------------------- /Website/content/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | $accentColor = #e07a5e -------------------------------------------------------------------------------- /Website/content/.vuepress/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $background: #fbf8f3; 2 | $secondary: #e07a5f; 3 | $primary: #0f4b81; 4 | $accent: red; 5 | $grey-dark: #eff3f7; 6 | $grey-light: #eeeeee; 7 | 8 | $success: green; 9 | $error: red; 10 | $info: $background; 11 | $white: #ffffff; 12 | $black: #000000; 13 | 14 | $spacing-x-small: 2px; 15 | $spacing-small: 0.3rem; 16 | $spacing-medium: 1.3rem; 17 | $spacing-large: 2rem; 18 | $spacing-x-large: 4rem; 19 | 20 | $heading-large: 2.09rem; 21 | $heading-medium: 1.45rem; 22 | 23 | $font-medium: 1rem; 24 | $font-small: 0.73rem; 25 | 26 | $border-large: 1rem; 27 | $border-medium: 0.3rem; 28 | $border-small: 1px; 29 | 30 | @mixin box-shadow-light { 31 | box-shadow: $spacing-x-small $spacing-small $spacing-medium 0px 32 | rgba($black, 0.4); 33 | } 34 | 35 | $mobile-width: 600px; 36 | $tablet-width: 1000px; 37 | -------------------------------------------------------------------------------- /Website/content/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | --- 4 | -------------------------------------------------------------------------------- /Website/content/announcements/11-01-23.md: -------------------------------------------------------------------------------- 1 | # Archiving time 2 | 3 | WirtBot is feature complete and I do not plan to add any new features to it. 4 | 5 | As the stack should be resilient enough I will also start archiving the GitHub repository. 6 | 7 | ## Why? 8 | 9 | My focus for things going forward will be on tooling for automated setups instead of UI's. 10 | If you are interested you can check out my personal blog [ehlers.berlin](https://ehlers.berlin) (RSS: https://ehlers.berlin/blog/index.xml) to be updated. 11 | 12 | I hope that the WirtBot project helped you to get your Homelab connected to the Internet securely. 13 | 14 | Happy hacking, 15 | 16 | b-m-f 17 | -------------------------------------------------------------------------------- /Website/content/announcements/21-10-20.md: -------------------------------------------------------------------------------- 1 | # 21.10.20 2 | 3 | If you already installed the server please update your installer to at least version **1.0.8** and run the update function. 4 | 5 | The firewall was not configured correctly by ansible before that version. A simple typo disable it in the ansible script. 6 | -------------------------------------------------------------------------------- /Website/content/announcements/22-11-20.md: -------------------------------------------------------------------------------- 1 | # 22.11.20 WirtBot version 1.5 2 | 3 | 4 | With version 1.5 many things changed on the Interface codebase and the System-Tests. 5 | 6 | Instead of relying on [nightwatch](https://nightwatchjs.org/), the System-Tests are now written in NodeJS - leveraging the amazing [playwright](https://playwright.dev/) to interact with browsers. 7 | 8 | The reason for the **nightwatch** to **playwright** change were tests of input validity. 9 | While there seemed no straight forward way to achieve it in **nightwatch**, it became `page.$eval(element-selector, e => e.validity.valid)` with **playwright**. 10 | 11 | This major rewrite and the refactoring in the Interface lead to a few bugs being squashed and all use cases of the WirtBot Interface being tested from End to End. 12 | In addition all changes are now automatically saved. No more need for edit-toggles. Also the expert mode is gone, which means that all settings are now shown by default. -------------------------------------------------------------------------------- /Website/content/announcements/31-10-20.md: -------------------------------------------------------------------------------- 1 | # 31-10-20 2 | 3 | Happy Halloween! 4 | 5 | The WirtBot build-system has been updated and the whole system, including WireGuard, are running in a single Docker container. 6 | 7 | To achieve this the container needs to be run with `NET_ADMIN` capabilities. 8 | 9 | The Readme and setup documentation has been updated. 10 | 11 | If you are already running a WirtBot VPS and want to update do the following: 12 | 13 | - Save your dasboard backup from [http://wirtbot.wirt.internal](http://wirtbot.wirt.internal) 14 | - Rebuild a clean VPS with Debian 10 15 | 16 | 17 | - Use new installer `@wirtbot/installer@1.1.0` to install a new server 18 | - copy Backup file into the directory where you run the installer 19 | - make sure your previous `wirtbot-installer.config.json` is also in the directory 20 | - choose the function to install from backup 21 | - wait for server to be installed 22 | - In case the Server IP has not changed everything should be running the same as before the update. The dashboard is tied to your browser, so all settings are still present. In case something is out of date, edit and save a device to trigger an update from the Dashboard to the WirtBot -------------------------------------------------------------------------------- /Website/content/announcements/8-11-20.md: -------------------------------------------------------------------------------- 1 | # 8.11.20 2 | 3 | The `wirt-installer` is now `wirtbot-installer` on your computer and will look for the `wirtbot-installer.config.json` file to load previously input settings. 4 | 5 | If you come from a version before 1.4.4 please rename you old `wirt-installer.config.json` to `wirtbot-installer.config.json` before updating your server to avoid repeating all your info. -------------------------------------------------------------------------------- /Website/content/announcements/README.md: -------------------------------------------------------------------------------- 1 | # Stay updated on the WirtBot development by visiting this page from time to time 2 | 3 | [[toc]] 4 | -------------------------------------------------------------------------------- /Website/content/announcements/update-3.md: -------------------------------------------------------------------------------- 1 | # Update 3 2 | 3 | Already the third update and some things have changed on the surface, more on that at the [end of the page](#frontend-refactor). 4 | 5 | But underneath the hood, that is where things were really changed. 6 | So lets dive right in. 7 | 8 | ## Single Docker container 9 | First of all, the functionality that ensures the systems functionality is now encapsulated inside a single Docker container. 10 | The only exception is CoreDNS. It still resides in its own container and only the **Corefile** is shared via Docker volumes over the local filesystem. 11 | 12 | The single WirtBot Container uses [s6-overlay](https://github.com/just-containers/s6-overlay) for managing the different processes - Nginx for Interface, Core, File watcher to reload interface. All logic is encapsulated in this container, which on the other hand means that it has to be started with **NET_CAP** capabilities to control the WireGuard Interface on the host. 13 | On the other hand it simplifies setup. No more SystemD service configurations. 14 | 15 | Whether or not this is a good idea in the long term? Lets see. 16 | 17 | ### Long standing issue addressed 18 | More thans 6 months ago when I posted about WirtBot on [/r/homelab](https://reddit.com/r/homelab) a reddit user gave some [critique on how the interface is reloaded on every change](https://www.reddit.com/r/homelab/comments/ge0vdf/make_your_homelab_available_over_the_internet/fpmbw2c?utm_source=share&utm_medium=web2x&context=3). 19 | This issue is now addressed in the code, following this relevant section from the man pages of **wg-quick**: 20 | > The strip command is useful for reloading configuration files without 21 | > disrupting active sessions: 22 | 23 | > # wg syncconf wgnet0 <(wg-quick strip wgnet0) 24 | 25 | 26 | ## Repo Reoganization 27 | Some maintenance work on the repository: 28 | 29 | - All things build-automation and containerization are now in the `Build-Automation` directory 30 | - Modules now have a README.md 31 | 32 | ## Testing 33 | 34 | This is probabaly the biggest chunk of work. 35 | 36 | To support changes in the future and ensure that the whole system still works as intended a whole bunch of tests were added and rewritten. 37 | 38 | - The installer is now tested to supply correct values to ansible 39 | - [playwright](https://playwright.dev/) supersedes [nightwatch](https://nightwatchjs.org/) for End2End tests 40 | - Input validity now tested 41 | - Backup export and import tested 42 | - Correct IPv6 configuration tested 43 | - Test complex and simple network configurations 44 | - Tests run in Firefox AND Chromium in CI 45 | 46 | 47 | ## Frontend refactor 48 | Based on the improved testing I was able to refactor the frontend with confidence. 49 | So what changed? 50 | 51 | Things were removed! **Less complexity, more focus**. 52 | 53 | And here a list of the removed features: 54 | 55 | - Expert mode - All configuration options are shown by default 56 | - Edit mode - Changes, as long as valid, are saved and sent to the backend immediately 57 | - Update the UI button - That was a workaround for a bug in the reactivity system. The underlying issue is fixed and this button now unnecessary. 58 | 59 | Besides that some Bugs were squashed in the process of testing and refactoring. 60 | ## End 61 | Quite a lot of changes that hopefully ensure good maintainability and easier implementation of new features. 62 | 63 | Lets see in Update #4. Thanks for reading. -------------------------------------------------------------------------------- /Website/content/announcements/update-4.md: -------------------------------------------------------------------------------- 1 | # Update 4: V2 of the WirtBot lost some weight 2 | 3 | After using the WirtBot for a while I noticed the cruft I loaded onto myself. 4 | 5 | Maintaining the WirtBot + ansible scripts etc. while already having it containerized and the audience, despite my attempts to make it simple to use, being highly technical nonetheless just made no sense. 6 | 7 | If you use this tech you probably already have a pretty good grasp of Linux and networks. 8 | 9 | So **the installer is removed** and you can integrate the Docker container on your machines even easier. 10 | 11 | ## New setup flow 12 | 13 | When running the WirtBot container for the first time, meaning that you probably do not have a public key for your interface yet, it will automatically generate a keypair for you. 14 | 15 | This is printed into the logs and can be used to take control of the interface. 16 | Meaning that the setup is still as secure as possible, while staying straigt forward. 17 | 18 | Accompanying the changes on the server is a new page on the Interface to help importing the keys. 19 | If you happen to have a backup and started the WirtBot with a public key already in its environment you can of course simply skip this. 20 | 21 | ## Other changes 22 | 23 | ### updated dependencies 24 | 25 | With [tokio](https://tokio.rs/) hitting **1.0** and [warp](https://docs.rs/warp/0.3.0/warp/) integrating it both have been updated to the latest versions. 26 | 27 | ### Bug fixes 28 | 29 | There was a bug when importing backups. 30 | In order to understand it you must know how the backup process actually works: 31 | 32 | - read backup 33 | - do some automatic upgrading depending on backup version 34 | - remove all server configuration from the state 35 | - remove all device configs from the state 36 | - add the server from the backup via normal state dispatching 37 | - add all devices 1 by 1 using the normal state dispatching 38 | 39 | The bug was hiding in the device adding part. As new devices were assumed to not have keys when I originally wrote the function, this function would generate them every time. 40 | This in turn means that the server would not recognize the devices anymore as their keys seemed to have updated and you got locked out of the WirtBot. 41 | 42 | Making sure that keys are only regenerated when needed took care of this issue. 43 | 44 | ### Better feedback 45 | 46 | Alerts have been added to show up when the configuration on the WirtBot has been updated successfully or failed for some reason. 47 | 48 | ### Relative Routing 49 | 50 | To properly function behind any kind of proxy you might want to use for the interface the routing mode is now set to relative routing. 51 | 52 | This should make it possible to proxy to and from the Interface without problems. 53 | 54 | ### Custom API Endpoint 55 | 56 | Since the ansible script and automatic network creation on install are gone it is necessary to be able to specify where updates should be send. 57 | 58 | The interface now has an option to specify the hostname/IP of the WirtBot. 59 | 60 | ### Multiarch 61 | 62 | WirtBot can now run on both AMD64 and ARM64. 63 | 64 | Unfortunately this makes the build-pipeline quite slow, but hey atleast you can now run the WirtBot on ARM devices if you want. 65 | 66 | ## End 67 | 68 | Thats it. Well except for the updated setup documentation that is. 69 | 70 | And what is next, now that there is less to take care of? 71 | 72 | I am thinking of Metrics, but it might take another couple of months. 73 | 74 | Til then, happy hacking. 75 | -------------------------------------------------------------------------------- /Website/content/announcements/update-5.md: -------------------------------------------------------------------------------- 1 | # Update 5: Time to maintain 2 | 3 | A year is over, a project complete. 4 | 5 | As of today, the international womens day 2021, the WirtBot project is going into maintenance mode. 6 | 7 | All planned features have been integrated and the testing harness covers them. 8 | 9 | If you have never heard of the project you can check out the [repository](https://github.com/b-m-f/WirtBot) or [homepage](https://wirtbot.com). 10 | 11 | ## What changed since the last update? 12 | 13 | The last update was the preparation to get to this final stage. It threw away many unnecessary things and narrowed the focus. 14 | 15 | So, this time its feature galore: 16 | 17 | ### IPv6 subnets 18 | 19 | Want to take the plunge into IPv6? Go ahead, the DashBoard now fully supports IPv6 and IPv4 subnets. 20 | 21 | ### Configure ignored DNS zones 22 | 23 | Have some DNZ zones that you do not want to be handled by the WirtBot? 24 | 25 | You can now configure them on the DashBoard. 26 | 27 | ### AdBlocking 28 | 29 | Okay, this is my favorite. 30 | 31 | You have probably heard of the good old [Pi-Holes](https://pi-hole.net/). 32 | Amazing idea. 33 | So why not have the same thing running in the WirtBot? 34 | 35 | Yeah, why not? 36 | 37 | Compiling [coredns](https://github.com/coredns/coredns) with the [ads](https://github.com/c-mueller/ads/) plugin and making it configurable via the DashBoard, the WirtBot now supports blocking Ads for all connected devices. 38 | 39 | Use custom block lists, block specific hosts or disable Ad Blocking if you want to stay up to date with latest Malware. 40 | 41 | ### Metrics 42 | 43 | At some point you will want to know what is happening in the network. 44 | Exposing CoreDNS and WireGuard metrics as Endpoints scrapable by Prometheus you can now get all the info you need. 45 | 46 | Either expose them on the host or hook your monitoring solution into the network. 47 | 48 | ### Non-features 49 | 50 | Well, there are of course some internal things that changed to accomodate all the new features. 51 | Here are the most important ones: 52 | 53 | - Store refactoring for clean reactivity system throughout the Frontend 54 | - Completely responsive Frontend using only media-queries, no more JS based detection 55 | - Core is completely refactored and has Unit-Tests. Caught a few possible panics and should now run safely. Thanks Rust. 56 | 57 | ## What does the future bring? 58 | 59 | Who knows. 60 | 61 | But for me, I am going to explore different projects and use some of the things I learned to write some more on my [personal blog](https://ehlers.berlin). 62 | 63 | I will still have an eye on the [subreddit](https://reddit.com/r/WirtBot) and [Github repository](https://github.com/b-m-f/wirtbot) to make sure things keep running smoothly and squash bugs when they appear. 64 | 65 | But no more features will be added by me. If you feel like something is missing I will be happy to assist you in implementing it though. 66 | 67 | I hope that the WirtBot can make your life easier, just as it does for me and wish you happy hacking. 68 | -------------------------------------------------------------------------------- /Website/content/announcements/update-6.md: -------------------------------------------------------------------------------- 1 | # WirtBot v3 2 | 3 | Hello, 4 | 5 | long time no update. 6 | Over the past couple of months more people started to use WirtBot and gave Feedback. 7 | 8 | This has lead to bugs being squashed and a more stable system overall, including new tests for the fixes. 9 | 10 | But that alone would not be justifying an update post. 11 | 12 | So here is the real update: 13 | 14 | **AdBlocking has been removed** from WirtBot. 15 | 16 | The dependencies that made this possible were not mature enough and would need a lot of work to fit the WirtBot use-case. 17 | Deciding between helping the dependencies get their code fixed and removing AdBlocking from WirtBot was not easy. 18 | 19 | But in the end WirtBot is supposed to connect devices into a private network. While this private network needs a DNS (which is still included with request forwarding, DNSoverTLS etc.) the job of blocking specific name resolutions is much better suited to be handled by a project such as [pi-hole](https://pi-hole.net/). 20 | 21 | Simply forwarding DNS requests from the WirtBot network to your own **pi-hole** instance achieves the desired AdBlocking, WirtBot can retain its focus and a user can leverage the maturity of the **pi-hole** project. 22 | 23 | The change is rolled out in version **3.0.0**. 24 | 25 | I hope that all of you already using the WirtBot will be fine with this change. So long, happy coding. 26 | -------------------------------------------------------------------------------- /Website/content/announcements/update-7.md: -------------------------------------------------------------------------------- 1 | # WirtBot v3.1 2 | 3 | A small update is being released as I am writing this. 4 | 5 | Version 3.1 tries to remove some clutter on the Dashboard. 6 | 7 | From now on all advanced device options are hidden behind a `More` button. 8 | The QR code for mobile devices is now also a button click away instead of always being displayed. 9 | 10 | A small new feature was introduced as well. You can now add a Port to the device configuration settings. Whether or not all the clients you use need/support this feature should be checked first. It is mostly unnecessary unless you work with servers in a restricted network. 11 | 12 | So far, happy hacking. 13 | -------------------------------------------------------------------------------- /Website/content/announcements/update-8.md: -------------------------------------------------------------------------------- 1 | # WirtBot v3.8.17 2 | 3 | ## New restarter 4 | The WireGuard restarter script is again killing the interface and starting it again on the WirtBot. 5 | 6 | Using syncconf as it was previously described in the manpages of `wg` seemed to lead to instabilities when updating the configurations from the DashBoards. 7 | Restarts of the container were often necessary and are hard to debug for users. 8 | 9 | While this approach might not be the most elegant for some, it is the most reliable from my experience. 10 | Feel free to add another solution if you have one. 11 | 12 | ## SSL 13 | 14 | Both the core and Interface now support individual SSL encryption. 15 | The keys are expected in: 16 | 17 | ``` 18 | /core/public_key 19 | /core/private_key 20 | ``` 21 | for the core and 22 | ``` 23 | /interface/public_key 24 | /interface/private_key 25 | ``` 26 | for the interface. 27 | 28 | SSL will be activated when environment variables `"SSL_INTERFACE=true"` or `"SSL_CORE=true"` are set. 29 | 30 | Make sure to update your CORS allowed origin. 31 | 32 | ## New convenience script 33 | A python script to create `ed25519` keys for the connection between Interface and core has been added. 34 | 35 | You can use this to preseed a backup file to make the initial setup even easier by not having to change the Pubkey variable on the container. 36 | This makes it easier for automatically provisioned setups. 37 | 38 | 39 | ## Other updates 40 | - dependencies were updated 41 | - CoreDNS runs as root (this was happening before, but hidden behind a wrong configuration) 42 | 43 | 44 | So long, happy hacking. 45 | -------------------------------------------------------------------------------- /Website/content/announcements/update-9.md: -------------------------------------------------------------------------------- 1 | # WirtBot v3.9.0 2 | 3 | Wow, 2 updates in such a short time. 4 | Thanks to another project I currently work on WirtBot gets some new functionality. 5 | 6 | ## Additonal Names 7 | 8 | It is now possible to add additional names to a device which will then be used to create more entries in the DNS config for that device. 9 | This is useful if you would like to use subdomains for a reverse proxy for example. 10 | 11 | On top of that there is now a name validation in place to make sure that no 1 name points to multiple devices in the network. 12 | 13 | ## Tests 14 | 15 | The tests should be more robust due to a new way of waiting for correct requests to be fired from the browser before continuing a test execution. 16 | 17 | 18 | Hopefully this is it for quite a while! 19 | 20 | Maybe Ill see you on the new project, which I will announce here as well. -------------------------------------------------------------------------------- /Website/content/documentation/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebarDepth: 2 3 | --- 4 | 5 | # What is WirtBot®? 6 | 7 | The WirtBot is a system designed to take you into Cyberspace safely. 8 | Think of it as a core component that will allow you yo extend your LAN over the whole internet. 9 | 10 | It has a few properties that make this safe and secure: 11 | 12 | - Based on WireGuard® to have battle tested cryptography protecting your traffic. No eavesdropping 13 | - You choose who is part of the network. Its impossible to join without your knowledge 14 | - Stealthy as Sam Fisher. Its there, but no one will know. No open ports except for WireGuard, which can not be port-scanned. 15 | - High speed. By having WireGuard integrated directly into the Linux Kernel, which a WirtBot uses, your network will be blazing fast. 16 | 17 | Convinced and want a WirtBot? 18 | 19 | Check out [this page](/documentation/setup) to set one up. Its completely free (Except for the server/VPS you run it on)! 20 | -------------------------------------------------------------------------------- /Website/content/documentation/faq.md: -------------------------------------------------------------------------------- 1 | # Frequently asked questions 2 | 3 | ## Is my WirtBot secure when exposed? 4 | 5 | By design yes. The WirtBot only accepts payloads from the Interface that are signed with the trusted public key. 6 | 7 | But there might always be Bugs in the Source Code, so if you want to make sure that you are as secure as possible follow the setup guide for achieving extra security. 8 | 9 | ## Who is building this? 10 | 11 | My name is [Maximilian Ehlers](https://ehlers.berlin). A developer living in Berlin. 12 | 13 | The initial UI design from is [Tabitha Tan](https://tabithatxc.com), a UX researcher from Singapore. 14 | -------------------------------------------------------------------------------- /Website/content/documentation/images/android1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/android1.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/android2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/android2.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/android3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/android3.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/android4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/android4.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/android5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/android5.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/android6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/android6.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/android7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/android7.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/windows1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/windows1.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/windows2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/windows2.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/windows3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/windows3.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/windows4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/windows4.jpg -------------------------------------------------------------------------------- /Website/content/documentation/images/windows5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/documentation/images/windows5.jpg -------------------------------------------------------------------------------- /Website/content/documentation/issues.md: -------------------------------------------------------------------------------- 1 | # Reporting issues and requesting new fetures 2 | 3 | Please head over to [Github](https://github.com/b-m-f/WirtBot/issues) and open up an issue. 4 | We will make sure to look at it and fix any bugs in a timely manner! 5 | 6 | New features can also be discussed there easily. 7 | -------------------------------------------------------------------------------- /Website/content/documentation/join-a-network.md: -------------------------------------------------------------------------------- 1 | # Joining a network 2 | 3 | You have received a configuration file for a WirtBot network? 4 | 5 | Follow the guide for your the operating system you use to join it. 6 | 7 | - [Linux](#linux) 8 | - [Windows](#windows) 9 | - [Android](#android) 10 | - [iOS](#ios) 11 | 12 | ## Linux 13 | 14 | First install Wireguard: 15 | 16 | - Debian: `sudo apt-get install wireguard wireguard-tools` 17 | - Arch Linux: `sudo pacman -S wireguard-tools` 18 | 19 | 20 | Now copy the config that you downloaded to `/etc/wireguard`. You will need **sudo** for this operation, as the `/etc/wireguard` directory is protected. 21 | 22 | To test things out run `sudo wg-quick@DeviceName`. **wg-quick** will automatically check the `/etc/wireguard` directory for the correct config, in this cast `DeviceName.conf`. 23 | 24 | The connection should be established. You can check this with `sudo wg`. 25 | 26 | ### Making the connection permanent 27 | 28 | If you want to stay connected you can enable this via `systemctl`. 29 | 30 | Typing `sudo systemctl enabled --now wg-quick@DeviceName` will start a system service that will establish the connection on every reboot - if your system is using **SystemD**. 31 | 32 | ## Windows 33 | 34 | **First download the WireGuard® software from** [https://wireguard.com](https://wireguard.com) 35 | 36 | ![](./images/windows1.jpg) 37 | 38 | **Now click on this button to import your config** 39 | 40 | ![](./images/windows2.jpg) 41 | 42 | **Go to the folder where you downloaded your config file and select it** 43 | 44 | ![](./images/windows3.jpg) 45 | 46 | **The config is now imported. Click activate to join the network** 47 | 48 | ![](./images/windows4.jpg) 49 | 50 | **Congratulations, you are now connected!** 51 | 52 | ![](./images/windows5.jpg) 53 | 54 | ## Android 55 | 56 | **First install the WireGuard app** 57 | 58 | ![](./images/android1.jpg) 59 | 60 | **Now open it up and click the blue button to add a config** 61 | 62 | ![](./images/android2.jpg) 63 | 64 | **You can choose between different ways to add a config, the easiest way is to simply scan a QR code** 65 | 66 | ![](./images/android3.jpg) 67 | 68 | **Point your camera at the QR code** 69 | 70 | ![](./images/android4.jpg) 71 | 72 | **Give a name for the network** 73 | 74 | ![](./images/android5.jpg) 75 | 76 | **Your network is now set up, but not connected. Toggle the switch to get in** 77 | 78 | ![](./images/android6.jpg) 79 | 80 | **All done! You are now connected to the network** 81 | 82 | ![](./images/android7.jpg) 83 | 84 | ## iOS 85 | 86 | - Download Wireguard App from AppStore 87 | - Scan QR code with the app 88 | - Establish the connection 89 | 90 | I do not have an iOS device to test this. If you have got a few minutes please be so kind to add a few screenshots via [https://github.com/b-m-f/WirtBot](https://github.com/b-m-f/WirtBot) 91 | -------------------------------------------------------------------------------- /Website/content/documentation/system-overview.md: -------------------------------------------------------------------------------- 1 | # System Overview 2 | 3 | This page is supposed to give an overview of the different functionalities of the system. 4 | 5 | Want more details? Please ask over on [GitHub](https://github.com/b-m-f/WirtBot). 6 | 7 | ## Interaction with the system 8 | 9 | - Change in browser 10 | - generate configurations in browser 11 | - send changes + signature to core 12 | - Core checks signature against a runtime publickey read from ENV 13 | - core updates necessary files 14 | - DNS change -> CoreDNS updates automatically every 10 seconds 15 | - WireGuard -> file watcher in the Container -> starts custom script to flush the changes onto the interface 16 | - Configurations from the Interface are immediately activated AND disactivated -------------------------------------------------------------------------------- /Website/content/documentation/what-can-i-do-with-wirt.md: -------------------------------------------------------------------------------- 1 | ## What can I do with a WirtBot? 2 | 3 | Ever wanted to run your own cloud at home, where the data is on **your** harddrive but still available to you anywhere in the world? 4 | No problem. 5 | 6 | Want to collect data from your IOT project securely onto your server back home? 7 | No problem. 8 | 9 | Want to have secure **DNS** on all your devices? 10 | No problem. 11 | 12 | As you can see WirtBot allows you to realize projects that need secure communication over the internet. 13 | But only up to a certain scale. The focus is to stay to true to the people and help beginners and experts alike. 14 | 15 | In the future I hope to add a few tutorials on the amazing things that my WirtBot enables me to do. 16 | And if you have got a use case, I'd love to hear about it, and maybe we can add a blog/showcases to this website together? -------------------------------------------------------------------------------- /Website/content/images/dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/content/images/dashboard.jpg -------------------------------------------------------------------------------- /Website/content/screenshots.md: -------------------------------------------------------------------------------- 1 | # One simple Dashboard to manage your network 2 | 3 | ![screenshot of Dashboard](./images/dashboard.jpg) 4 | 5 | -------------------------------------------------------------------------------- /Website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@wirtbot/website", 3 | "version": "3.9.4", 4 | "author": "b-m-f ", 5 | "description": "Documentation for https://wirtbot.com", 6 | "main": "index.js", 7 | "directories": { 8 | "doc": "docs" 9 | }, 10 | "scripts": { 11 | "test": "echo \"No tests implemented yet\"", 12 | "dev": "vuepress dev content", 13 | "build": "vuepress build content" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/b-m-f/WirtBot.git" 18 | }, 19 | "license": "AGPL-3", 20 | "bugs": { 21 | "url": "https://github.com/b-m-f/WirtBot/issues" 22 | }, 23 | "homepage": "https://github.com/b-m-f/WirtBot#readme", 24 | "dependencies": { 25 | "node-sass": "^6.0.1", 26 | "sass-loader": "^10", 27 | "vuepress": "1.9.7", 28 | "webpack": "^4.42.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Website/system-overview.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/Website/system-overview.md -------------------------------------------------------------------------------- /convenience-scripts/generate_interface_keys.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | 3 | import base64 4 | from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey 5 | from cryptography.hazmat.primitives.serialization import NoEncryption 6 | from cryptography.hazmat.primitives.serialization import Encoding 7 | from cryptography.hazmat.primitives.serialization import PrivateFormat 8 | from cryptography.hazmat.primitives.serialization import PublicFormat 9 | 10 | 11 | private_key = Ed25519PrivateKey.generate() 12 | priv_key = base64.b64encode(bytes(private_key.private_bytes(Encoding.Raw, PrivateFormat.Raw, NoEncryption()))).decode('utf-8') 13 | 14 | public_key = private_key.public_key() 15 | pub_key = base64.b64encode(bytes(public_key.public_bytes(Encoding.Raw, PublicFormat.Raw))).decode('utf-8') 16 | 17 | 18 | print("Private:" + priv_key) 19 | 20 | print("Public:" + pub_key) 21 | -------------------------------------------------------------------------------- /convenience-scripts/update-dependencies.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | if ! command -v ncu &> /dev/null 4 | then 5 | echo "Please install npm-check-updates with 'npm i -g npm-check-updates'" 6 | exit 7 | fi 8 | # TODO: remove the reject once the sass-loader 11 is supported by vuepress, which means it needs to support webpack5 properly. 9 | # Wait for v2 10 | cd ./Website && ncu -u --reject sass-loader,webpack,node-sass && npm i && npm audit fix || cd - 11 | cd ./Interface && ncu -u && npm i && npm audit fix || cd - 12 | cd ./System-Tests && ncu -u && npm i && npm audit fix || cd - 13 | cd ./Interface/src/lib/crate && cargo update || cd - 14 | cd ./Core && cargo update || cd - 15 | 16 | -------------------------------------------------------------------------------- /convenience-scripts/update-version.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -eou pipefail 3 | 4 | version=$(cat .version) 5 | echo "Updating all packages to $version" 6 | find . -type f -name "package.json" -not -path "*/node_modules**" -exec sed -i -e "s|version\": \".*\"|version\": \"$version\"|" {} \; 7 | find . -type f -wholename "**store/index.js" -not -path "*/node_modules**" -exec sed -i -e "s|const version = \".*\"|const version = \"$version\"|" {} \; 8 | find . -type f -name "Cargo.toml" -not -path "*/node_modules**" -exec sed -i -e "s|^version = \".*\"|version = \"$version\"|" {} \; 9 | cd Core 10 | cargo build 11 | cd .. 12 | 13 | 14 | mv CHANGELOG.md CHANGELOG.md.bkp 15 | git log $(git describe --tags --abbrev=0)..HEAD --pretty=tformat:"%h %s%n" > CHANGELOG.md 16 | cat CHANGELOG.md.bkp >> CHANGELOG.md 17 | mv CHANGELOG.md CHANGELOG.md.bkp 18 | echo "## $version" > CHANGELOG.md 19 | cat CHANGELOG.md.bkp >> CHANGELOG.md 20 | rm CHANGELOG.md.bkp 21 | 22 | git add . 23 | git commit -m "Updates to $version" 24 | echo "Finished updating. Update commit was added. Now create a Tag and push to release." 25 | -------------------------------------------------------------------------------- /dashboard.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/b-m-f/WirtBot/0a1fc594f66b4d60114764cf6642e73358b2a3c8/dashboard.jpg -------------------------------------------------------------------------------- /testwirtbot.conf.example: -------------------------------------------------------------------------------- 1 | [Interface] 2 | Address = 10.10.4.2 3 | PrivateKey = uLpBxEZScI95kg8a2PxFk47CFTF2k7G8Fy3Aly57G0E= 4 | 5 | [Peer] 6 | Endpoint = development_wirtbot:10101 7 | AllowedIPs = 10.10.4.0/24 8 | PublicKey = abY3VwejvKPG7mMBVdXWkqXE2TYinjfJGJDPIchcJEw= 9 | --------------------------------------------------------------------------------