├── .gitignore ├── LICENSE ├── README.md ├── balena.yml ├── captive-portal ├── Dockerfile.template ├── captive-portal.py ├── redirect.py └── start.sh ├── docker-compose.yml ├── ipfs ├── Dockerfile └── config_local_access ├── logo.png └── wifi-repeater ├── Dockerfile.template ├── package-lock.json ├── package.json ├── src ├── dbus.ts ├── index.ts ├── nm.ts └── types │ └── index.ts ├── tsconfig.json └── typings └── dbus-native.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux 5 | 6 | ### Balena ### 7 | .balena 8 | balena-router/build 9 | 10 | ### Linux ### 11 | *~ 12 | 13 | # temporary files which can be created if a process still has a handle open of a deleted file 14 | .fuse_hidden* 15 | 16 | # KDE directory preferences 17 | .directory 18 | 19 | # Linux trash folder which might appear on any partition or disk 20 | .Trash-* 21 | 22 | # .nfs files are created when an open file is removed but is still being accessed 23 | .nfs* 24 | 25 | ### VisualStudioCode ### 26 | .vscode/* 27 | !.vscode/settings.json 28 | !.vscode/tasks.json 29 | !.vscode/launch.json 30 | !.vscode/extensions.json 31 | !.vscode/*.code-snippets 32 | 33 | # Local History for Visual Studio Code 34 | .history/ 35 | 36 | # Built Visual Studio Code Extensions 37 | *.vsix 38 | 39 | ### VisualStudioCode Patch ### 40 | # Ignore all local history of files 41 | .history 42 | .ionide 43 | 44 | # Support for Project snippet scope 45 | 46 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux 47 | 48 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 49 | 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 coolabnet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # P2P Hotspot 2 | 3 | [![balena deploy button](https://www.balena.io/deploy.svg)](https://dashboard.balena-cloud.com/deploy?repoUrl=https://github.com/coolabnet/hotspot) 4 | 5 | 6 | P2P Hotspot is a low-energy device that creates an access-point serving p2p applications, bridges and content. Software and content are automatically updated while online. 7 | 8 | A team can easily monitor and give direct support to all devices (with granted consent) while online. 9 | 10 | ## Getting started 11 | 12 | We'll need Internet connection for downloading latest software and content. But after initial setup, device can exist and be updated offline. 13 | 14 | *Needed hardware:* 15 | 16 | - Computer: Raspberry Pi 3 or 4 17 | - Storage: class 10 micro sd with min 16Gb or SSD connected thru usb 18 | - (if using SSD && acess-point) Wifi Dongle: a Wifi dongle compatible with the device 19 | - Internet connection through ethernet cable 20 | 21 | *Setup* 22 | 23 | 1. Click "[Deploy with Balena](https://www.balena.io/docs/learn/deploy/deploy-with-balena-button/)" button 24 | 1. Create free Balena account 25 | 1. Set community environmental variables (wifi acess point name, wifi password, device name on network, community name, anguage, content repository address, map-tiles links) 26 | 1. Download project image 27 | 1. Burn image to micro sd or usb SSD 28 | 1. Plug storage into device 29 | 1. Plug device to power supply 30 | 1. Connect to the Internet thru an Ethernet cable 31 | 1. Check Balena dashboard if device is online 32 | 1. Wait for device to update it's software on Balena dashboard 33 | 1. Set environment variables (or use defaults) 34 | 35 | | Env var | Description | Default | 36 | | ------------- | ------------- | ------------- | 37 | | AP_SSID | Access point network name. | `WiFi Repeater` | 38 | | AP_PASSWORD | Access point network password. | `none` | 39 | | WIFI_SSID | WiFi network name | | 40 | | WIFI_PASSWORD | WiFi network password | | 41 | | PUB_NAME | The SSB's pub name | `P2P Hotspot` | 42 | | ROOM | Room address where the pub will connnect | `room.coolab.org:8008~shs:foHx1HuvaN++iCrQS+Zi916V6iNYEOtj9ZOAo+a0E+Q=` | 43 | | PUB | Pub address where the pub will connect | `pub.freesocial.co:8008~shs:ofYKOy2p9wsaxV73GqgOyh6C6nRGFM5FyciQyxwBd6A=` | 44 | 45 | 46 | ## Usage 47 | 48 | 1. On a phone or computer search for WiFi access point and connect 49 | 1. Captive-portal with portal should pop-up, if not go to [10.42.0.1:8081](http://10.42.0.1:8081) 50 | 1. Follow guides and install tools on user devices 51 | 52 | ## Connectivity 53 | ![WiFi Repeater modes of operation](https://github.com/balenalabs/wifi-repeater/raw/master/img/modes.png) 54 | 55 | *Uses [wifi-repeater](https://github.com/balenalabs/wifi-repeater)* 56 | 57 | Device will automatically scan your network and check each device's capabilities. It will then attempt to configure the devices to work in `Access Point` mode and if that is not possible it will switch to `Repeater` mode. You *do not* need to pre configure the device to work in either mode. 58 | 59 | For `Access Point` mode it needs: 60 | - Internet connectivity via Ethernet 61 | - Wireless device with AP capabilities (such as the onboard WiFi chip in Raspberry Pi devices) 62 | 63 | For `Repeater` mode it needs: 64 | - Wireless device with AP capabilities (such as the onboard WiFi chip in Raspberry Pi devices) 65 | - Secondary wireless device and valid credentials for a wireless network. 66 | 67 | In case you have no access to an ethernet cable connecting to the routing device, WiFi connection can be set directly inside the SD card by following [this guide](https://www.balena.io/docs/reference/OS/network/#wifi-setup). 68 | ### LED patterns 69 | 70 | In case something goes wrong WiFi repeater will produce a series of blinking patterns with the ACT LED (next to PWR LED) to help troubleshoot the issue. 71 | Valid patterns are the following: 72 | 73 | | LED pattern | Problem | Description | Solution (AP mode) | Solution (Repeater mode) | 74 | | ------------- | ------------- | ------------- | ------------- | ------------- | 75 | | 2 blinks | Could not find a wireless device with Access Point capabilities | Wireless devices detected don't support Access Point mode. | Use a WiFi chipset that supports AP mode or a WiFi dongle. | Use a WiFi chipset that supports AP mode or a WiFi dongle. | 76 | | 3 blinks | Could not find a secondary wireless device | Ethernet is disconnected or has no internet access. Switched to repeater mode but could not find a secondary wireless device. | Provide internet access via Ethernet cable. | Provide a secondary wireless device by using a WiFi dongle. | 77 | | 4 blinks | WiFi credentials for secondary wireless device not provided. | Ethernet is disconnected or has no internet access. Switched to repeater mode but could not find WiFi credentials. | Provide internet access via Ethernet cable. | Provide valid WiFi credentials | 78 | | 5 blinks | No internet access | Ethernet is disconnected or has no internet access. Switched to repeater mode, connected to WiFi but still have no internet access. | Provide internet access via Ethernet cable. | Ensure the target WiFi has internet access. | 79 | ## Software stack 80 | 81 | ### Operating System 82 | 83 | Meant to run on [Balena](https://balena.io), which enables easily composing service blocks on a performant container-based operating system. Advantages: 84 | 85 | - [Balena Hub](https://hub.balena.io/) has a growing number of out-of-the-box apps 86 | - [Balena Cloud](https://balena-cloud.com/) is free as long as project is published and used from Balena Hub 87 | - simple migration from existing docker/docker-compose stack 88 | - lightweight container-based operating system 89 | - support for various types of single-board-computers and architectures 90 | - over-the-air-updates 91 | - small self-updating images 92 | - per-device release pinning 93 | - bulk monitoring of all devices and their services 94 | - remote access to all devices and services via web terminal 95 | - ssh tunnel and remote support 96 | 97 | ### Service Stack 98 | 99 | - **Wifi**: creates a WiFi Access-Point with a Captive-portal redirecting user to portal application 100 | - **Portal**: application which onboards new users and links to content and services 101 | - **SSB-Pub**: [Secure Scuttlebut's](https://scuttlebutt.nz/) distributed database that connects automatically to a room and pub outside the local network (on the Internet) 102 | - **Distributed File System**: automatically updates content if online through p2p sync using [IPFS](https://ipfs.io/) 103 | ## Extending 104 | - Support more simultaneous users and extend wifi range with a router 105 | - Extend router's wifi range by setting up a mesh network with your neighbors 106 | 107 | ## TODO 108 | 109 | - Make it work offline 110 | - Portal work on port 80 111 | - Check for portal and installer updates before attempting to download them 112 | - Have Internet block work 113 | - Optionally password protect Internet access 114 | - Add p2p installer and browser applications ([Dat Installer](https://github.com/staltz/dat-installer) and [Agregore Browser](https://agregore.mauve.moe/) for example) 115 | - Run an [F-Droid repository](https://gitlab.com/fdroid/wiki/-/wikis/List-of-F-Droid-repositories) 116 | - Add service that scrapes latest apks and installers and serves them as F-droid and Dat-Installer repositories 117 | - Have the pub publish an about photo 118 | - Integrate [Peach Cloud](http://peachcloud.org/) 119 | - Create an ARM32 version (NodeJS ssb pub) 120 | - Add dnsmasq to WiFi and configure proxy for custom domains 121 | -------------------------------------------------------------------------------- /balena.yml: -------------------------------------------------------------------------------- 1 | name: P2P Hotpost 2 | type: sw.application 3 | description: >- 4 | P2P Hotspot is a low-energy device that creates an access-point serving p2p applications, bridges and content. 5 | Software stack and content are automatically updated while online. 6 | assets: 7 | repository: 8 | type: blob.asset 9 | data: 10 | url: 'https://github.com/coolabnet/hotspot' 11 | logo: 12 | type: blob.asset 13 | data: 14 | url: >- 15 | https://raw.githubusercontent.com/coolabnet/hotspot/main/logo.png 16 | data: 17 | applicationEnvironmentVariables: 18 | - AP_SSID: "P2P Hotspot" 19 | - AP_PASSWORD: "" 20 | - TZ: America/Brasil 21 | - SET_HOSTNAME: p2p 22 | - PUB_NAME: "P2P Hotspot" 23 | - ROOM: "net:room.coolab.org:8008~shs:foHx1HuvaN++iCrQS+Zi916V6iNYEOtj9ZOAo+a0E+Q=" 24 | - PUB: "net:pub.freesocial.co:8008~shs:ofYKOy2p9wsaxV73GqgOyh6C6nRGFM5FyciQyxwBd6A=" 25 | defaultDeviceType: raspberry-pi 26 | supportedDeviceTypes: 27 | - raspberrypi4-64 28 | - raspberrypi3-64 29 | version: 1.0.2 30 | 31 | 32 | -------------------------------------------------------------------------------- /captive-portal/Dockerfile.template: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM balenalib/%%BALENA_MACHINE_NAME%%-debian 3 | 4 | RUN install_packages python dbus jq python wget iptables unzip 5 | 6 | ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket 7 | 8 | RUN update-alternatives --set iptables /usr/sbin/iptables-legacy && \ 9 | update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy 10 | 11 | WORKDIR /usr/src 12 | COPY . . 13 | 14 | CMD ["bash", "start.sh"] 15 | -------------------------------------------------------------------------------- /captive-portal/captive-portal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #adapted from https://github.com/nikosft/captive-portal/ 3 | import subprocess 4 | from SimpleHTTPServer import SimpleHTTPRequestHandler 5 | from SocketServer import TCPServer 6 | import cgi 7 | import os 8 | 9 | # These variables are used as settings 10 | RE_PORT = 9090 11 | PORT = 8081 # the port in which the captive portal web server listens 12 | IFACE = "wlan0" # the interface that captive portal protects 13 | FDROID_IP_1 = "148.251.140.42" 14 | FDROID_IP_2 = "149.202.95.241" 15 | 16 | ip_pipe = subprocess.Popen(['ip', 'r'], stdout=subprocess.PIPE) 17 | interface_pipe = subprocess.Popen (['grep', IFACE], stdin=ip_pipe.stdout, stdout=subprocess.PIPE) 18 | IP_ADDRESS = subprocess.check_output (['cut', '-d', ' ', '-f', '9'], stdin=interface_pipe.stdout).split()[0] 19 | 20 | 21 | ''' 22 | This it the http server used by the the captive portal 23 | ''' 24 | class CaptivePortal(SimpleHTTPRequestHandler): 25 | ''' 26 | https://stackoverflow.com/questions/16583827/cors-with-python-basehttpserver-501-unsupported-method-options-in-chrome 27 | ''' 28 | def do_OPTIONS(self): 29 | self.send_response(200, "ok") 30 | self.send_header('Access-Control-Allow-Origin', '*') 31 | self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 32 | self.send_header("Access-Control-Allow-Headers", "X-Requested-With") 33 | self.send_header("Access-Control-Allow-Headers", "Content-Type") 34 | self.end_headers() 35 | ''' 36 | this is called when the user submits the login form 37 | ''' 38 | def do_POST(self): 39 | self.send_response(200) 40 | self.send_header("Content-Type", "application/json") 41 | self.send_header('Access-Control-Allow-Origin', '*') 42 | self.send_header('Access-Control-Allow-Headers', 'Authorization, Content-Type') 43 | self.send_header('Access-Control-Allow-Methods', 'POST') 44 | self.end_headers() 45 | form = cgi.FieldStorage( 46 | fp=self.rfile, 47 | headers=self.headers, 48 | environ={'REQUEST_METHOD':'POST', 49 | 'CONTENT_TYPE':self.headers['Content-Type'], 50 | }) 51 | # username = form.getvalue("username") 52 | # password = form.getvalue("password") 53 | #dummy security check 54 | # if username == 'nikos' and password == 'fotiou': 55 | #authorized user 56 | remote_IP = self.client_address[0] 57 | print ('New authorization from '+ remote_IP) 58 | print ('Updating IP tables') 59 | subprocess.call(["iptables","-t", "nat", "-I", "PREROUTING","1", "-s", remote_IP, "-j" ,"ACCEPT"]) 60 | subprocess.call(["iptables", "-I", "FORWARD", "-s", remote_IP, "-j" ,"ACCEPT"]) 61 | self.wfile.write("{success: true, ip:" + remote_IP + "}") 62 | # else: 63 | # #show the login form 64 | # self.wfile.write(self.html_login) 65 | 66 | #the following function makes server produce no output 67 | #comment it out if you want to print diagnostic messages 68 | #def log_message(self, format, *args): 69 | # return 70 | 71 | print "*********************************************" 72 | print "* Note, if there are already iptables rules *" 73 | print "* this script may not work. Flush iptables *" 74 | print "* at your own risk using iptables -F *" 75 | print "*********************************************" 76 | print "Updating iptables" 77 | print ".. Allow TCP DNS" 78 | subprocess.call(["iptables", "-A", "FORWARD", "-i", IFACE, "-p", "tcp", "--dport", "53", "-j" ,"ACCEPT"]) 79 | print ".. Allow UDP DNS" 80 | subprocess.call(["iptables", "-A", "FORWARD", "-i", IFACE, "-p", "udp", "--dport", "53", "-j" ,"ACCEPT"]) 81 | print ".. Allow traffic to captive portal" 82 | subprocess.call(["iptables", "-A", "FORWARD", "-i", IFACE, "-p", "tcp", "--dport", str(PORT),"-d", IP_ADDRESS, "-j" ,"ACCEPT"]) 83 | subprocess.call(["iptables", "-A", "FORWARD", "-i", IFACE, "-p", "tcp", "--dport", str(RE_PORT),"-d", IP_ADDRESS, "-j" ,"ACCEPT"]) 84 | print ".. Allow traffic to F-Droid repositories" 85 | subprocess.call(["iptables", "-A", "FORWARD", "-i", IFACE, "-p", "tcp", "--dport", "80","-d", FDROID_IP_1, "-j" ,"ACCEPT"]) 86 | subprocess.call(["iptables", "-A", "FORWARD", "-i", IFACE, "-p", "tcp", "--dport", "80","-d", FDROID_IP_2, "-j" ,"ACCEPT"]) 87 | print ".. Block all other traffic" 88 | subprocess.call(["iptables", "-A", "FORWARD", "-i", IFACE, "-j" ,"DROP"]) 89 | print "Starting web server" 90 | httpd = TCPServer(('', PORT), CaptivePortal) 91 | print 'started httpserver on'+str(PORT) 92 | print "Redirecting HTTP traffic to captive portal" 93 | subprocess.call(["iptables", "-t", "nat", "-A", "PREROUTING", "-i", IFACE, "-p", "tcp", "--dport", "80", "-j" ,"DNAT", "--to-destination", IP_ADDRESS+":"+str(RE_PORT)]) 94 | 95 | 96 | try: 97 | httpd.serve_forever() 98 | except KeyboardInterrupt: 99 | pass 100 | httpd.server_close() 101 | -------------------------------------------------------------------------------- /captive-portal/redirect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 4 | import subprocess 5 | 6 | RE_PORT = 9090 7 | PORT = 8081 8 | IFACE = "wlan0" # the interface that captive portal protects 9 | 10 | ip_pipe = subprocess.Popen(['ip', 'r'], stdout=subprocess.PIPE) 11 | interface_pipe = subprocess.Popen (['grep', IFACE], stdin=ip_pipe.stdout, stdout=subprocess.PIPE) 12 | IP_ADDRESS = subprocess.check_output (['cut', '-d', ' ', '-f', '9'], stdin=interface_pipe.stdout).split()[0] 13 | ''' 14 | Redirect 15 | ''' 16 | class Redirect(BaseHTTPRequestHandler): 17 | #this is the index of the captive portal 18 | #it simply redirects the user to the to login page 19 | html_redirect = """ 20 | 21 | 22 | 23 | 24 | 25 | Redirecting to portal 26 | 27 | 28 | """%(IP_ADDRESS, PORT) 29 | 30 | ''' 31 | if the user requests the login page show it, else 32 | use the redirect page 33 | ''' 34 | def do_GET(self): 35 | path = self.path 36 | self.send_response(200) 37 | self.send_header("Content-type", "text/html") 38 | self.end_headers() 39 | self.wfile.write(self.html_redirect) 40 | 41 | httpdRe = HTTPServer(('', RE_PORT), Redirect) 42 | print 'started httpserver on'+str(RE_PORT) 43 | 44 | try: 45 | httpdRe.serve_forever() 46 | except KeyboardInterrupt: 47 | pass 48 | httpdRe.server_close() 49 | -------------------------------------------------------------------------------- /captive-portal/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # portal 4 | echo 'Downloading Portal' 5 | curl -s https://api.github.com/repos/coolabnet/hotspot/releases/latest \ 6 | | grep "browser_download_url.*zip" \ 7 | | cut -d : -f 2,3 \ 8 | | tr -d \" \ 9 | | wget -qi - 10 | rm -rf /usr/src/dist 11 | unzip portal.zip 12 | mv /usr/src/dist/* /usr/src/ 13 | # Apks 14 | mkdir -p /usr/src/apks 15 | wget https://f-droid.org/F-Droid.apk -O /usr/src/apks/fdroid.apk 16 | wget https://manyver.se/apk -O /usr/src/apks/manyverse.apk 17 | # 18 | # Python 19 | echo 'Starting Python server' 20 | python /usr/src/captive-portal.py & 21 | python /usr/src/redirect.py -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | volumes: 4 | media: 5 | sync-download: 6 | sync-staging: 7 | ssb-data: 8 | services: 9 | # https://github.com/balenablocks/wifi-connect 10 | wifi: 11 | build: wifi-repeater 12 | restart: always 13 | network_mode: host 14 | privileged: true 15 | labels: 16 | io.balena.features.dbus: '1' 17 | io.balena.features.firmware: '1' 18 | captive-portal: 19 | build: captive-portal 20 | restart: always 21 | network_mode: host 22 | privileged: true 23 | labels: 24 | io.balena.features.dbus: '1' 25 | depends_on: 26 | - wifi 27 | 28 | # https://github.com/balenablocks/hostname 29 | hostname: 30 | image: balenablocks/hostname 31 | restart: "no" 32 | labels: 33 | io.balena.features.supervisor-api: 1 34 | environment: 35 | SET_HOSTNAME: p2p 36 | # https://hub.docker.com/repository/docker/communityfirst/ssb-go-pub 37 | ssb: 38 | image: communityfirst/ssb-go-pub:latest 39 | network_mode: host 40 | environment: 41 | ROOM: "net:room.coolab.org:8008~shs:foHx1HuvaN++iCrQS+Zi916V6iNYEOtj9ZOAo+a0E+Q=" 42 | PUB: "net:pub.freesocial.co:8008~shs:ofYKOy2p9wsaxV73GqgOyh6C6nRGFM5FyciQyxwBd6A=" 43 | volumes: 44 | - 'ssb-data:/root/.ssb-go' 45 | restart: unless-stopped 46 | ipfs: 47 | build: ipfs 48 | network_mode: host 49 | volumes: 50 | - 'sync-download:/data/ipfs' 51 | - 'sync-staging:/export' 52 | - 'media:/data' 53 | labels: 54 | io.balena.features.supervisor-api: '1' 55 | depends_on: 56 | - wifi 57 | 58 | -------------------------------------------------------------------------------- /ipfs/Dockerfile: -------------------------------------------------------------------------------- 1 | # Note: when updating the go minor version here, also update the go-channel in snap/snapcraft.yml 2 | FROM golang:1.17-buster 3 | LABEL maintainer="Steven Allen " 4 | 5 | # Install deps 6 | RUN apt-get update && apt-get install -y \ 7 | git \ 8 | libssl-dev \ 9 | ca-certificates \ 10 | fuse \ 11 | jq cron 12 | 13 | ENV SRC_DIR /go-ipfs 14 | 15 | 16 | RUN cd /tmp && git clone https://github.com/ipfs/go-ipfs.git 17 | # Download packages first so they can be cached. 18 | RUN ls /tmp/ 19 | RUN mkdir $SRC_DIR 20 | RUN cp /tmp/go-ipfs/go.mod $SRC_DIR/ 21 | RUN cp /tmp/go-ipfs/go.sum $SRC_DIR/ 22 | RUN cd $SRC_DIR \ 23 | && go mod download 24 | 25 | RUN cp -r /tmp/go-ipfs/* $SRC_DIR/ 26 | 27 | # Preload an in-tree but disabled-by-default plugin by adding it to the IPFS_PLUGINS variable 28 | # e.g. docker build --build-arg IPFS_PLUGINS="foo bar baz" 29 | ARG IPFS_PLUGINS 30 | 31 | # Build the thing. 32 | # Also: fix getting HEAD commit hash via git rev-parse. 33 | RUN cd $SRC_DIR \ 34 | && mkdir -p .git/objects \ 35 | && make build GOTAGS=openssl IPFS_PLUGINS=$IPFS_PLUGINS 36 | 37 | # Get su-exec, a very minimal tool for dropping privileges, 38 | # and tini, a very minimal init daemon for containers 39 | ENV SUEXEC_VERSION v0.2 40 | ENV TINI_VERSION v0.19.0 41 | RUN set -eux; \ 42 | dpkgArch="$(dpkg --print-architecture)"; \ 43 | case "${dpkgArch##*-}" in \ 44 | "amd64" | "armhf" | "arm64") tiniArch="tini-static-$dpkgArch" ;;\ 45 | *) echo >&2 "unsupported architecture: ${dpkgArch}"; exit 1 ;; \ 46 | esac; \ 47 | cd /tmp \ 48 | && git clone https://github.com/ncopa/su-exec.git \ 49 | && cd su-exec \ 50 | && git checkout -q $SUEXEC_VERSION \ 51 | && make su-exec-static \ 52 | && cd /tmp \ 53 | && wget -q -O tini https://github.com/krallin/tini/releases/download/$TINI_VERSION/$tiniArch \ 54 | && chmod +x tini 55 | 56 | #RUN cat $SRC_DIR/start_ipfs >> $SRC_DIR/bin/container_daemon 57 | COPY config_local_access $SRC_DIR 58 | RUN head -n -1 $SRC_DIR/bin/container_daemon > $SRC_DIR/start && cat $SRC_DIR/start $SRC_DIR/config_local_access > $SRC_DIR/bin/container_daemon 59 | 60 | # Now comes the actual target image, which aims to be as small as possible. 61 | FROM busybox:1.31.1-glibc 62 | LABEL maintainer="Steven Allen " 63 | 64 | # Get the ipfs binary, entrypoint script, and TLS CAs from the build container. 65 | ENV SRC_DIR /go-ipfs 66 | COPY --from=0 $SRC_DIR/cmd/ipfs/ipfs /usr/local/bin/ipfs 67 | COPY --from=0 $SRC_DIR/bin/container_daemon /usr/local/bin/start_ipfs 68 | COPY --from=0 /tmp/su-exec/su-exec-static /sbin/su-exec 69 | COPY --from=0 /tmp/tini /sbin/tini 70 | COPY --from=0 /bin/fusermount /usr/local/bin/fusermount 71 | COPY --from=0 /etc/ssl/certs /etc/ssl/certs 72 | COPY --from=0 /usr/bin/jq /usr/local/bin/jq 73 | 74 | # Add suid bit on fusermount so it will run properly 75 | RUN chmod 4755 /usr/local/bin/fusermount 76 | 77 | # Fix permissions on start_ipfs (ignore the build machine's permissions) 78 | RUN chmod 0755 /usr/local/bin/start_ipfs 79 | 80 | # This shared lib (part of glibc) doesn't seem to be included with busybox. 81 | COPY --from=0 /lib/*-linux-gnu*/libdl.so.2 /lib/ 82 | 83 | # Copy over SSL libraries. 84 | COPY --from=0 /usr/lib/*-linux-gnu*/libssl.so* /usr/lib/ 85 | COPY --from=0 /usr/lib/*-linux-gnu*/libcrypto.so* /usr/lib/ 86 | 87 | # Copy jq library 88 | COPY --from=0 /usr/lib/*-linux-gnu*/libjq.so* /usr/lib/ 89 | COPY --from=0 /usr/lib/*-linux-gnu*/libonig.so* /usr/lib/ 90 | COPY --from=0 /usr/lib/*-linux-gnu*/libc.so* /usr/lib/ 91 | COPY --from=0 /usr/lib/*-linux-gnu*/libm.so* /usr/lib/ 92 | 93 | 94 | # Swarm TCP; should be exposed to the public 95 | EXPOSE 4001 96 | # Swarm UDP; should be exposed to the public 97 | EXPOSE 4001/udp 98 | # Daemon API; must not be exposed publicly but to client services under you control 99 | EXPOSE 5001 100 | # Web Gateway; can be exposed publicly with a proxy, e.g. as https://ipfs.example.org 101 | EXPOSE 8080 102 | # Swarm Websockets; must be exposed publicly when the node is listening using the websocket transport (/ipX/.../tcp/8081/ws). 103 | EXPOSE 8081 104 | 105 | # Create the fs-repo directory and switch to a non-privileged user. 106 | ENV IPFS_PATH /data/ipfs 107 | RUN mkdir -p $IPFS_PATH \ 108 | && adduser -D -h $IPFS_PATH -u 1000 -G users ipfs \ 109 | && chown ipfs:users $IPFS_PATH 110 | 111 | # Create mount points for `ipfs mount` command 112 | RUN mkdir /ipfs /ipns \ 113 | && chown ipfs:users /ipfs /ipns 114 | 115 | # Expose the fs-repo as a volume. 116 | # start_ipfs initializes an fs-repo if none is mounted. 117 | # Important this happens after the USER directive so permissions are correct. 118 | VOLUME $IPFS_PATH 119 | 120 | # The default logging level 121 | ENV IPFS_LOGGING "" 122 | 123 | # This just makes sure that: 124 | # 1. There's an fs-repo, and initializes one if there isn't. 125 | # 2. The API and Gateway are accessible from outside the container. 126 | ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/start_ipfs"] 127 | 128 | # Heathcheck for the container 129 | # QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn is the CID of empty folder 130 | HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ 131 | CMD ipfs dag stat /ipfs/QmUNLLsPACCz1vLxQVkXqqLX5R1X345qqfHbsf67hvA3Nn || exit 1 132 | 133 | 134 | # Execute the daemon subcommand by default 135 | CMD ["daemon", "--migrate=true", "--agent-version-suffix=docker"] 136 | -------------------------------------------------------------------------------- /ipfs/config_local_access: -------------------------------------------------------------------------------- 1 | #balena ---- 2 | #now let's configure it to accept local network connections 3 | IP=$(wget -qO- "$BALENA_SUPERVISOR_ADDRESS/v1/device?apikey=$BALENA_SUPERVISOR_API_KEY"| jq -r .ip_address) 4 | NAME=$(wget -qO- "$BALENA_SUPERVISOR_ADDRESS/v1/device/host-config?apikey=$BALENA_SUPERVISOR_API_KEY"| jq -r .network.hostname) 5 | 6 | J='[' 7 | J_FORMAT='"http://%s:5001", ' 8 | 9 | for one_ip in $IP ; do 10 | J=$J$(printf "$J_FORMAT" "$one_ip") 11 | done 12 | 13 | J_FORMAT='"http://%s.local:5001", "http://localhost:3000", "http://127.0.0.1:5001", "https://webui.ipfs.io"]' 14 | J=$J$(printf "$J_FORMAT" "$NAME") 15 | 16 | 17 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "$J" 18 | ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods '["PUT", "POST"]' 19 | 20 | exec ipfs "$@" 21 | 22 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/coolabnet/hotspot/b23bff4b79a9e215cb62a6a773590aaab5750798/logo.png -------------------------------------------------------------------------------- /wifi-repeater/Dockerfile.template: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM balenalib/%%BALENA_MACHINE_NAME%%-node:12-build as builder 3 | 4 | WORKDIR /usr/src 5 | 6 | RUN npm install blinking 7 | 8 | # Run stage 9 | FROM balenalib/%%BALENA_MACHINE_NAME%%-node:12-run 10 | ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/host/run/dbus/system_bus_socket 11 | 12 | RUN install_packages dbus 13 | 14 | WORKDIR /usr/src 15 | COPY . . 16 | COPY --from=builder /usr/src . 17 | RUN npm install && npm run build 18 | 19 | CMD [ "node", "/usr/src/build/index.js" ] 20 | -------------------------------------------------------------------------------- /wifi-repeater/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "balenarepeater", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "13.9.0", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.0.tgz", 10 | "integrity": "sha512-0ARSQootUG1RljH2HncpsY2TJBfGQIKOOi7kxzUY6z54ePu/ZD+wJA8zI2Q6v8rol2qpG/rvqsReco8zNMPvhQ==", 11 | "dev": true 12 | }, 13 | "abstract-socket": { 14 | "version": "2.1.1", 15 | "resolved": "https://registry.npmjs.org/abstract-socket/-/abstract-socket-2.1.1.tgz", 16 | "integrity": "sha512-YZJizsvS1aBua5Gd01woe4zuyYBGgSMeqDOB6/ChwdTI904KP6QGtJswXl4hcqWxbz86hQBe++HWV0hF1aGUtA==", 17 | "optional": true, 18 | "requires": { 19 | "bindings": "^1.2.1", 20 | "nan": "^2.12.1" 21 | } 22 | }, 23 | "bindings": { 24 | "version": "1.5.0", 25 | "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", 26 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 27 | "optional": true, 28 | "requires": { 29 | "file-uri-to-path": "1.0.0" 30 | } 31 | }, 32 | "blinking": { 33 | "version": "0.0.3", 34 | "resolved": "https://registry.npmjs.org/blinking/-/blinking-0.0.3.tgz", 35 | "integrity": "sha1-c6LX+J2z2lSzYFxJiqXYYGv8Hnc=", 36 | "requires": { 37 | "bluebird": "^3.0.0" 38 | } 39 | }, 40 | "bluebird": { 41 | "version": "3.7.2", 42 | "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", 43 | "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" 44 | }, 45 | "dbus-native": { 46 | "version": "0.4.0", 47 | "resolved": "https://registry.npmjs.org/dbus-native/-/dbus-native-0.4.0.tgz", 48 | "integrity": "sha512-i3zvY3tdPEOaMgmK4riwupjDYRJ53rcE1Kj8rAgnLOFmBd0DekUih59qv8v+Oyils/U9p+s4sSsaBzHWLztI+Q==", 49 | "requires": { 50 | "abstract-socket": "^2.0.0", 51 | "event-stream": "^4.0.0", 52 | "hexy": "^0.2.10", 53 | "long": "^4.0.0", 54 | "optimist": "^0.6.1", 55 | "put": "0.0.6", 56 | "safe-buffer": "^5.1.1", 57 | "xml2js": "^0.4.17" 58 | } 59 | }, 60 | "duplexer": { 61 | "version": "0.1.1", 62 | "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", 63 | "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" 64 | }, 65 | "event-stream": { 66 | "version": "4.0.1", 67 | "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", 68 | "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", 69 | "requires": { 70 | "duplexer": "^0.1.1", 71 | "from": "^0.1.7", 72 | "map-stream": "0.0.7", 73 | "pause-stream": "^0.0.11", 74 | "split": "^1.0.1", 75 | "stream-combiner": "^0.2.2", 76 | "through": "^2.3.8" 77 | } 78 | }, 79 | "file-uri-to-path": { 80 | "version": "1.0.0", 81 | "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 82 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 83 | "optional": true 84 | }, 85 | "from": { 86 | "version": "0.1.7", 87 | "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", 88 | "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=" 89 | }, 90 | "hexy": { 91 | "version": "0.2.11", 92 | "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", 93 | "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==" 94 | }, 95 | "long": { 96 | "version": "4.0.0", 97 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", 98 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" 99 | }, 100 | "map-stream": { 101 | "version": "0.0.7", 102 | "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", 103 | "integrity": "sha1-ih8HiW2CsQkmvTdEokIACfiJdKg=" 104 | }, 105 | "minimist": { 106 | "version": "0.0.10", 107 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", 108 | "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" 109 | }, 110 | "nan": { 111 | "version": "2.14.0", 112 | "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", 113 | "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==", 114 | "optional": true 115 | }, 116 | "optimist": { 117 | "version": "0.6.1", 118 | "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", 119 | "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", 120 | "requires": { 121 | "minimist": "~0.0.1", 122 | "wordwrap": "~0.0.2" 123 | } 124 | }, 125 | "pause-stream": { 126 | "version": "0.0.11", 127 | "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", 128 | "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", 129 | "requires": { 130 | "through": "~2.3" 131 | } 132 | }, 133 | "put": { 134 | "version": "0.0.6", 135 | "resolved": "https://registry.npmjs.org/put/-/put-0.0.6.tgz", 136 | "integrity": "sha1-MPX2C9bkOJvTKeFqJThsuy5KAKM=" 137 | }, 138 | "safe-buffer": { 139 | "version": "5.2.0", 140 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 141 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==" 142 | }, 143 | "sax": { 144 | "version": "1.2.4", 145 | "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", 146 | "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" 147 | }, 148 | "split": { 149 | "version": "1.0.1", 150 | "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", 151 | "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", 152 | "requires": { 153 | "through": "2" 154 | } 155 | }, 156 | "stream-combiner": { 157 | "version": "0.2.2", 158 | "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", 159 | "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", 160 | "requires": { 161 | "duplexer": "~0.1.1", 162 | "through": "~2.3.4" 163 | } 164 | }, 165 | "through": { 166 | "version": "2.3.8", 167 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 168 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" 169 | }, 170 | "typescript": { 171 | "version": "3.8.3", 172 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 173 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==" 174 | }, 175 | "wordwrap": { 176 | "version": "0.0.3", 177 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", 178 | "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" 179 | }, 180 | "xml2js": { 181 | "version": "0.4.23", 182 | "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", 183 | "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", 184 | "requires": { 185 | "sax": ">=0.6.0", 186 | "xmlbuilder": "~11.0.0" 187 | } 188 | }, 189 | "xmlbuilder": { 190 | "version": "11.0.1", 191 | "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", 192 | "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" 193 | } 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /wifi-repeater/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "balenarepeater", 3 | "version": "0.1.0", 4 | "description": "A WiFi/Ethernet network repeater", 5 | "main": "build/index.js", 6 | "scripts": { 7 | "build": "tsc --project tsconfig.json" 8 | }, 9 | "author": "Tomás Migone ", 10 | "license": "MIT", 11 | "dependencies": { 12 | "blinking": "0.0.3", 13 | "bluebird": "^3.7.2", 14 | "dbus-native": "^0.4.0", 15 | "typescript": "^3.8.3" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^13.9.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /wifi-repeater/src/dbus.ts: -------------------------------------------------------------------------------- 1 | import * as Bluebird from "bluebird"; 2 | import { systemBus, Message, Bus } from 'dbus-native'; 3 | 4 | export const dbus: Bus = systemBus(); 5 | 6 | export const dbusInvoker = (message: Message): PromiseLike => { 7 | return Bluebird.fromCallback(cb => { 8 | return dbus.invoke(message, cb); 9 | }); 10 | }; 11 | 12 | export const getProperty = async (service: string, objectPath: string, objectInterface: string, property: string): Promise => { 13 | let message: Message = { 14 | destination: service, 15 | path: objectPath, 16 | interface: 'org.freedesktop.DBus.Properties', 17 | member: 'Get', 18 | signature: 'ss', 19 | body: [objectInterface, property] 20 | } 21 | const [[], [value]] = await dbusInvoker(message); 22 | return value; 23 | }; 24 | 25 | -------------------------------------------------------------------------------- /wifi-repeater/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as blinking from 'blinking'; 2 | import { 3 | createAccessPoint, 4 | connectToWifi, 5 | checkNMConnectivity, 6 | getWiFiDevices, 7 | getWiredDevices 8 | } from "./nm"; 9 | 10 | // defaults 11 | const AP_SSID = process.env.AP_SSID || `P2PHotspot`; 12 | const AP_PASSWORD = process.env.AP_PASSWORD ? (process.env.AP_PASSWORD.length < 8 ? null : process.env.AP_PASSWORD) : null; 13 | const WIFI_SSID = process.env.WIFI_SSID; 14 | const WIFI_PASSWORD = process.env.WIFI_PASSWORD; 15 | 16 | // LED notifications 17 | const ledFile = '/sys/class/leds/led0/brightness'; 18 | const led = blinking(ledFile); 19 | const LED_ERROR_PATTERNS = { 20 | NO_AP_CAPABLE_DEVICE: 2, 21 | NO_SECONDARY_WIRELESS: 3, 22 | NO_WIFI_CREDENTIALS: 4, 23 | NO_INTERNET: 5 24 | }; 25 | 26 | (async () => { 27 | console.log('-- WiFi repeater: starting...'); 28 | 29 | // Get available devices 30 | const wifiDevices = await getWiFiDevices(); 31 | const wiredDevices = await getWiredDevices(); 32 | console.log(`Wireless interfaces found: ${wifiDevices.map(d => d.iface).join(', ')}`); 33 | console.log(`Wired interfaces found: ${wiredDevices.map(d => d.iface).join(', ')}`); 34 | 35 | // Get available devices and find out which are useful. Only interested in: 36 | // - accessPoint: Any wireless device capable of creating an AP 37 | // - bridge: Any wireless device excluding accessPoint device 38 | // - ethernet: Any wired device that has internet connectivity 39 | const accessPoint = wifiDevices.find(device => device.apCapable); 40 | const bridge = wifiDevices.find(device => device.iface !== accessPoint?.iface); 41 | const ethernet = wiredDevices.find(device => device.connected); 42 | 43 | // Create Access Point, required for both modes of operation 44 | if (!accessPoint) { 45 | console.log(`Could not find a wireless device with AP capabilities. Exiting...`); 46 | led.pattern.start({ blinks: LED_ERROR_PATTERNS.NO_AP_CAPABLE_DEVICE, pause: 1000 }); 47 | return; 48 | } 49 | 50 | console.log(`Creating WiFi AP on ${accessPoint.iface} with SSID "${AP_SSID}" and password "${AP_PASSWORD}"...`); 51 | await createAccessPoint({ iface: accessPoint.iface, ssid: AP_SSID, password: AP_PASSWORD }); 52 | 53 | // Use secondary wireless device for internet if ethernet doesn't do the job. 54 | if (!ethernet) { 55 | console.log(`Ethernet device has no internet. Attempting to use secondary wireless device to connect to WiFi...`); 56 | 57 | if (!bridge) { 58 | console.log(`Could not find a secondary wireless device. Exiting...`); 59 | led.pattern.start({ blinks: LED_ERROR_PATTERNS.NO_SECONDARY_WIRELESS, pause: 1000 }); 60 | return; 61 | } 62 | 63 | if (!WIFI_SSID || !WIFI_PASSWORD) { 64 | console.log(`WiFi credentials for secondary wireless device not provided. Exiting...`); 65 | led.pattern.start({ blinks: LED_ERROR_PATTERNS.NO_WIFI_CREDENTIALS, pause: 1000 }); 66 | return; 67 | } 68 | 69 | // Connect secondary wireless interface to WiFi 70 | console.log(`Connecting ${bridge.iface} to WiFi with SSID "${WIFI_SSID}" and password "${WIFI_PASSWORD}"`); 71 | await connectToWifi({ iface: bridge.iface, ssid: WIFI_SSID, password: WIFI_PASSWORD }); 72 | 73 | // Check if we are now connected to the internet 74 | let nmConnected = await checkNMConnectivity(); 75 | if (!nmConnected) { 76 | console.log(`Warning: Could not detect internet access. Bad WiFi credentials provided or WiFi network has no internet access...`); 77 | led.pattern.start({ blinks: LED_ERROR_PATTERNS.NO_INTERNET, pause: 1000 }); 78 | return; 79 | } 80 | 81 | console.log(`WiFi repeater started in REPEATER mode.`); 82 | } else { 83 | console.log(`WiFi repeater started in AP mode.`); 84 | } 85 | 86 | })(); -------------------------------------------------------------------------------- /wifi-repeater/src/nm.ts: -------------------------------------------------------------------------------- 1 | import { NetworkManagerTypes } from './types'; 2 | import { BodyEntry } from 'dbus-native'; 3 | import { 4 | dbusInvoker, 5 | getProperty 6 | } from './dbus'; 7 | 8 | export interface NetworkDevice { 9 | iface: string; // IP interface name 10 | path: string; // DBus object path 11 | type: string; 12 | driver: string; 13 | connected: boolean; 14 | } 15 | 16 | export interface WirelessDevice extends NetworkDevice { 17 | apCapable: boolean; 18 | } 19 | 20 | export interface WiredDevice extends NetworkDevice { 21 | } 22 | 23 | export interface WirelessNetwork { 24 | iface: string; 25 | ssid: string; 26 | password?: string | null | undefined; 27 | } 28 | 29 | const nm: string = 'org.freedesktop.NetworkManager'; 30 | 31 | // Wireless 32 | export const createAccessPoint = async (device: WirelessNetwork): Promise => { 33 | try { 34 | // Error out if the interface does not exist 35 | const wifiDevices: NetworkDevice[] = await getWiFiDevices() 36 | 37 | if (!wifiDevices.some(d => d.iface === device.iface)) { 38 | console.log(`Selected interface ${device.iface} does not exist. Hotspot creation aborted...`); 39 | return 40 | } 41 | 42 | let connectionParams: BodyEntry[] = [ 43 | [ 44 | "connection", 45 | [ 46 | ["id", ["s", device.ssid]], 47 | ["type", ["s", "802-11-wireless"]], 48 | ], 49 | ], 50 | [ 51 | "802-11-wireless", 52 | [ 53 | ["ssid", ["ay", stringToArrayOfBytes(device.ssid)]], 54 | ["mode", ["s", "ap"]], 55 | ], 56 | ], 57 | ["ipv4", [["method", ["s", "shared"]]]], 58 | ["ipv6", [["method", ["s", "ignore"]]]], 59 | ]; 60 | if (device.password) { 61 | connectionParams.push([ 62 | "802-11-wireless-security", 63 | [ 64 | ["key-mgmt", ["s", "wpa-psk"]], 65 | ["psk", ["s", device.password]], 66 | ], 67 | ]) 68 | } 69 | 70 | const dbusPath = await getPathByIface(device.iface); 71 | const connection = await addConnection(connectionParams); 72 | const result = await activateConnection(connection, dbusPath); 73 | return result 74 | } catch (error) { 75 | console.log(`Error creating Hotspot: ${error}`); 76 | } 77 | }; 78 | 79 | export const connectToWifi = async (network: WirelessNetwork): Promise => { 80 | try { 81 | const connectionParams = [ 82 | ['connection', [ 83 | ['id', ['s', network.ssid]], 84 | ['type', ['s', '802-11-wireless']], 85 | ]], 86 | ['802-11-wireless', [ 87 | ['ssid', ['ay', stringToArrayOfBytes(network.ssid)]], 88 | ['mode', ['s', 'infrastructure']], 89 | ]], 90 | ['802-11-wireless-security', [ 91 | ['key-mgmt', ['s', 'wpa-psk']], 92 | ['psk', ['s', network.password]], 93 | ]], 94 | ['ipv4', [ 95 | ['method', ['s', 'auto']], 96 | ]], 97 | ['ipv6', [ 98 | ['method', ['s', 'auto']], 99 | ]], 100 | ]; 101 | 102 | let device = await getPathByIface(network.iface); 103 | let connection = await addConnection(connectionParams); 104 | let result = await activateConnection(connection, device); 105 | return result 106 | } catch (error) { 107 | console.log(`Error connecting to WiFi: ${error}`); 108 | } 109 | }; 110 | 111 | // NetworkManager 112 | export const getWiFiDevices = async (): Promise => { 113 | const devices: NetworkDevice[] = await getDevicesByType(NetworkManagerTypes.DEVICE_TYPE.WIFI) 114 | const wifiDevices: WirelessDevice[] = [] 115 | 116 | for await (const device of devices) { 117 | const apCapable: boolean = !!(await getProperty(nm, device.path, 'org.freedesktop.NetworkManager.Device.Wireless', 'WirelessCapabilities') & NetworkManagerTypes.WIFI_DEVICE_CAP.AP); 118 | wifiDevices.push({ ...device, apCapable }); 119 | } 120 | 121 | return wifiDevices; 122 | }; 123 | 124 | export const getWiredDevices = async (): Promise => { 125 | return await getDevicesByType(NetworkManagerTypes.DEVICE_TYPE.ETHERNET); 126 | }; 127 | 128 | export const getDevicesByType = async (type: number): Promise => { 129 | const paths: string[] = await getDevicesPath(); 130 | const devices: NetworkDevice[] = []; 131 | 132 | for await (const path of paths) { 133 | const deviceType: number = await getProperty(nm, path, 'org.freedesktop.NetworkManager.Device', 'DeviceType'); 134 | 135 | if (deviceType === type) { 136 | const iface: string = await getProperty(nm, path, 'org.freedesktop.NetworkManager.Device', 'Interface'); 137 | const connected: boolean = await getProperty(nm, path, 'org.freedesktop.NetworkManager.Device', 'Ip4Connectivity') === NetworkManagerTypes.CONNECTIVITY.FULL; 138 | const driver: string = await getProperty(nm, path, 'org.freedesktop.NetworkManager.Device', 'Driver'); 139 | const typeName: string = Object.keys(NetworkManagerTypes.DEVICE_TYPE).find(key => NetworkManagerTypes.DEVICE_TYPE[key] === type) || "UNKNOWN"; 140 | devices.push({ path, iface, connected, driver, type: typeName }); 141 | } 142 | } 143 | 144 | return devices 145 | }; 146 | 147 | export const getDevicesPath = async (): Promise => { 148 | return await dbusInvoker({ 149 | destination: nm, 150 | path: '/org/freedesktop/NetworkManager', 151 | interface: 'org.freedesktop.NetworkManager', 152 | member: 'GetDevices' 153 | }); 154 | }; 155 | 156 | export const getPathByIface = async (iface: string): Promise => { 157 | return await dbusInvoker({ 158 | destination: nm, 159 | path: '/org/freedesktop/NetworkManager', 160 | interface: 'org.freedesktop.NetworkManager', 161 | member: 'GetDeviceByIpIface', 162 | signature: 's', 163 | body: [iface] 164 | }); 165 | }; 166 | 167 | export const checkDeviceConnectivity = async (iface: string): Promise => { 168 | const path: string = await getPathByIface(iface) 169 | return await getProperty(nm, path, 'org.freedesktop.NetworkManager.Device', 'Ip4Connectivity') 170 | }; 171 | 172 | export const checkNMConnectivity = async (): Promise => { 173 | let nmConnectivityState = await dbusInvoker({ 174 | destination: nm, 175 | path: '/org/freedesktop/NetworkManager', 176 | interface: 'org.freedesktop.NetworkManager', 177 | member: 'CheckConnectivity' 178 | }); 179 | 180 | return nmConnectivityState === NetworkManagerTypes.CONNECTIVITY.FULL 181 | }; 182 | 183 | export const addConnection = async (params: BodyEntry[]): Promise => { 184 | return await dbusInvoker({ 185 | destination: nm, 186 | path: '/org/freedesktop/NetworkManager/Settings', 187 | interface: 'org.freedesktop.NetworkManager.Settings', 188 | member: 'AddConnection', 189 | signature: 'a{sa{sv}}', 190 | body: [params] 191 | }); 192 | }; 193 | 194 | export const activateConnection = async (connection: string, path: string) => { 195 | return await dbusInvoker({ 196 | destination: nm, 197 | path: '/org/freedesktop/NetworkManager', 198 | interface: 'org.freedesktop.NetworkManager', 199 | member: 'ActivateConnection', 200 | signature: 'ooo', 201 | body: [connection, path, '/'] 202 | }); 203 | }; 204 | 205 | function stringToArrayOfBytes(str) { 206 | const bytes: number[] = []; 207 | for (let i = 0; i < str.length; ++i) { 208 | bytes.push(str.charCodeAt(i)); 209 | } 210 | 211 | return bytes; 212 | } -------------------------------------------------------------------------------- /wifi-repeater/src/types/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 balena.io 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export class NetworkManagerTypes { 18 | /** 19 | * NMCapability 20 | * NOTE: The range 0x7000 - 0x7FFF of capabilities is guaranteed 21 | * not to be used by upstream NetworkManager. 22 | * It could thus be used for downstream extensions. 23 | * @enum {Number} 24 | */ 25 | static CAPABILITY = { 26 | // Teams can be managed 27 | TEAM: 1, 28 | }; 29 | 30 | /** 31 | * NMState 32 | * @enum {Number} 33 | */ 34 | static STATE = { 35 | /** 36 | * Networking state is unknown; 37 | * Indicates a daemon error 38 | */ 39 | UNKNOWN: 0, 40 | /** 41 | * Networking is not enabled; 42 | * System is being suspended or resumed 43 | */ 44 | ASLEEP: 10, 45 | /** 46 | * No active network connection 47 | */ 48 | DISCONNECTED: 20, 49 | /** 50 | * Network connections are being cleaned up; 51 | * Applications should tear down their network sessions 52 | */ 53 | DISCONNECTING: 30, 54 | /** 55 | * Network connection is being started 56 | */ 57 | CONNECTING: 40, 58 | /** 59 | * Only local IPv4 and/or IPv6 connectivity, 60 | * but no default route to access the Internet 61 | */ 62 | CONNECTED_LOCAL: 50, 63 | /** 64 | * Only site-wide IPv4 and/or IPv6 connectivity; 65 | * Means a default route is available, 66 | * but the Internet connectivity check did not succeed 67 | */ 68 | CONNECTED_SITE: 60, 69 | /** 70 | * Global IPv4 and/or IPv6 Internet connectivity 71 | */ 72 | CONNECTED_GLOBAL: 70, 73 | }; 74 | 75 | /** 76 | * NMConnectivityState 77 | * @enum {Number} 78 | */ 79 | static CONNECTIVITY = { 80 | /** 81 | * Network connectivity is unknown; 82 | * Connectivity checks disabled, or not run yet 83 | */ 84 | UNKNOWN: 0, 85 | /** 86 | * Host is not connected to any network 87 | */ 88 | NONE: 1, 89 | /** 90 | * Internet connection is hijacked by a captive portal gateway 91 | */ 92 | PORTAL: 2, 93 | /** 94 | * Host is connected to a network, 95 | * not able to reach the full Internet, 96 | * but a captive portal has not been detected 97 | */ 98 | LIMITED: 3, 99 | /** 100 | * Host is connected to a network, 101 | * and able to reach the full Internet 102 | */ 103 | FULL: 4, 104 | }; 105 | 106 | /** 107 | * NMDeviceType 108 | * @enum {Number} 109 | */ 110 | static DEVICE_TYPE = { 111 | /** Unknown device */ 112 | UNKNOWN: 0, 113 | /** Generic support for unrecognized device types */ 114 | GENERIC: 14, 115 | /** A wired ethernet device */ 116 | ETHERNET: 1, 117 | /** An 802.11 WiFi device */ 118 | WIFI: 2, 119 | /** Not used */ 120 | UNUSED1: 3, 121 | /** Not used */ 122 | UNUSED2: 4, 123 | /** A Bluetooth device supporting PAN or DUN access protocols */ 124 | BT: 5, 125 | /** An OLPC XO mesh networking device */ 126 | OLPC_MESH: 6, 127 | /** An 802.16e Mobile WiMAX broadband device */ 128 | WIMAX: 7, 129 | /** 130 | * A modem supporting analog telephone, 131 | * CDMA/EVDO, GSM/UMTS, or LTE network access protocols 132 | */ 133 | MODEM: 8, 134 | /** An IP-over-InfiniBand device */ 135 | INFINIBAND: 9, 136 | /** A bond master interface */ 137 | BOND: 10, 138 | /** An 802.1Q VLAN interface */ 139 | VLAN: 11, 140 | /** ADSL modem */ 141 | ADSL: 12, 142 | /** A bridge master interface */ 143 | BRIDGE: 13, 144 | /** A team master interface */ 145 | TEAM: 15, 146 | /** A TUN or TAP interface */ 147 | TUN: 16, 148 | /** A IP tunnel interface */ 149 | IP_TUNNEL: 17, 150 | /** A MACVLAN interface */ 151 | MACVLAN: 18, 152 | /** A VXLAN interface */ 153 | VXLAN: 19, 154 | /** A VETH interface */ 155 | VETH: 20, 156 | /** A MACsec interface */ 157 | MACSEC: 21, 158 | /** A dummy interface */ 159 | DUMMY: 22, 160 | /** A PPP interface */ 161 | PPP: 23, 162 | /** A OpenVSwitch interface */ 163 | OVS_INTERFACE: 24, 164 | /** A OpenVSwitch port */ 165 | OVS_PORT: 25, 166 | /** A OpenVSwitch bridge */ 167 | OVS_BRIDGE: 26, 168 | }; 169 | 170 | /** 171 | * NMDeviceCapabilities 172 | * @enum {Number} 173 | */ 174 | static DEVICE_CAP = { 175 | /** Device has no special capabilities */ 176 | NONE: 0x00000000, 177 | /** NetworkManager supports this device */ 178 | NM_SUPPORTED: 0x00000001, 179 | /** This device can indicate carrier status */ 180 | CARRIER_DETECT: 0x00000002, 181 | /** This device is a software device */ 182 | IS_SOFTWARE: 0x00000003, 183 | /** This device supports single-root I/O virtualization */ 184 | SRIOV: 0x00000004, 185 | }; 186 | 187 | /** 188 | * NMDeviceWifiCapabilities 189 | * @enum {Number} 190 | */ 191 | static WIFI_DEVICE_CAP = { 192 | /** Device has no encryption/authentication capabilities */ 193 | NONE: 0x00000000, 194 | /** Device supports 40/64-bit WEP encryption */ 195 | CIPHER_WEP40: 0x00000001, 196 | /** Device supports 104/128-bit WEP encryption */ 197 | CIPHER_WEP104: 0x00000002, 198 | /** Device supports TKIP encryption */ 199 | CIPHER_TKIP: 0x00000004, 200 | /** Device supports AES/CCMP encryption */ 201 | CIPHER_CCMP: 0x00000008, 202 | /** Device supports WPA1 authentication */ 203 | WPA: 0x00000010, 204 | /** Device supports WPA2/RSN authentication */ 205 | RSN: 0x00000020, 206 | /** Device supports Access Point mode */ 207 | AP: 0x00000040, 208 | /** Device supports Ad-Hoc mode */ 209 | ADHOC: 0x00000080, 210 | /** Device reports frequency capabilities */ 211 | FREQ_VALID: 0x00000100, 212 | /** Device supports 2.4GHz frequencies */ 213 | FREQ_2GHZ: 0x00000200, 214 | /** Device supports 5GHz frequencies */ 215 | FREQ_5GHZ: 0x00000400, 216 | }; 217 | 218 | /** 219 | * NM80211ApFlags 220 | * @enum {Number} 221 | */ 222 | static AP_802_11S = { 223 | /** Access point has no special capabilities */ 224 | NONE: 0x00000000, 225 | /** Access point requires authentication and encryption (usually means WEP) */ 226 | PRIVACY: 0x00000001, 227 | /** Access point supports some WPS method */ 228 | WPS: 0x00000002, 229 | /** Access point supports push-button WPS */ 230 | WPS_PBC: 0x00000004, 231 | /** Access point supports PIN-based WPS */ 232 | WPS_PIN: 0x00000008, 233 | }; 234 | 235 | /** 236 | * NM80211ApSecurityFlags 237 | * @enum {Number} 238 | * @description current security requirements of an 239 | * access point as determined from the access point's beacon 240 | */ 241 | static AP_802_11_SEC = { 242 | /** The access point has no special security requirements */ 243 | NONE: 0x00000000, 244 | /** 40/64-bit WEP is supported for pairwise/unicast encryption */ 245 | PAIR_WEP40: 0x00000001, 246 | /** 104/128-bit WEP is supported for pairwise/unicast encryption */ 247 | PAIR_WEP104: 0x00000002, 248 | /** TKIP is supported for pairwise/unicast encryption */ 249 | PAIR_TKIP: 0x00000004, 250 | /** AES/CCMP is supported for pairwise/unicast encryption */ 251 | PAIR_CCMP: 0x00000008, 252 | /** 40/64-bit WEP is supported for group/broadcast encryption */ 253 | GROUP_WEP40: 0x00000010, 254 | /** 104/128-bit WEP is supported for group/broadcast encryption */ 255 | GROUP_WEP104: 0x00000020, 256 | /** TKIP is supported for group/broadcast encryption */ 257 | GROUP_TKIP: 0x00000040, 258 | /** AES/CCMP is supported for group/broadcast encryption */ 259 | GROUP_CCMP: 0x00000080, 260 | /** WPA/RSN Pre-Shared Key encryption is supported */ 261 | KEY_MGMT_PSK: 0x00000100, 262 | /** 802.1x authentication and key management is supported */ 263 | KEY_MGMT_802_1X: 0x00000200, 264 | }; 265 | 266 | /** 267 | * NM80211Mode 268 | * @enum {Number} 269 | * @description 802.11 mode an access point or device is currently in 270 | */ 271 | static MODE_802_11 = { 272 | /** The device or access point mode is unknown */ 273 | UNKNOWN: 0, 274 | /** 275 | * For both devices and access point objects, 276 | * indicates the object is part of an Ad-Hoc 802.11 network 277 | * without a central coordinating access point. 278 | */ 279 | ADHOC: 1, 280 | /** 281 | * The device or access point is in infrastructure mode. 282 | * For devices, this indicates the device is an 802.11 client/station. 283 | * For access point objects, this indicates the object is an access point 284 | * that provides connectivity to clients. 285 | */ 286 | INFRA: 2, 287 | /** 288 | * The device is an access point/hotspot. 289 | * Not valid for access point objects; 290 | * used only for hotspot mode on the local machine. 291 | */ 292 | AP: 3, 293 | }; 294 | 295 | /** 296 | * NMBluetoothCapabilities 297 | * @enum {Number} 298 | * @description Usable capabilities of a Bluetooth device 299 | */ 300 | static BT_CAPABILITY = { 301 | /** Device has no usable capabilities */ 302 | NONE: 0x00000000, 303 | /** Device provides Dial-Up Networking capability */ 304 | DUN: 0x00000001, 305 | /** Device provides Network Access Point capability */ 306 | NAP: 0x00000002, 307 | }; 308 | 309 | /** 310 | * NMDeviceModemCapabilities 311 | * @enum {Number} 312 | * @description Radio access technology families a modem device supports. 313 | * For more information on the specific access technologies 314 | * the device supports use the ModemManager D-Bus API 315 | */ 316 | static DEVICE_MODEM_CAPABILITY = { 317 | /** Modem has no usable capabilities */ 318 | NONE: 0x00000000, 319 | /** Modem uses the analog wired telephone network and is not a wireless/cellular device */ 320 | POTS: 0x00000001, 321 | /** Modem supports at least one of CDMA 1xRTT, EVDO revision 0, EVDO revision A, or EVDO revision B */ 322 | CDMA_EVDO: 0x00000002, 323 | /** Modem supports at least one of GSM, GPRS, EDGE, UMTS, HSDPA, HSUPA, or HSPA+ packet switched data capability */ 324 | GSM_UMTS: 0x00000004, 325 | /** Modem has LTE data capability */ 326 | LTE: 0x00000008, 327 | }; 328 | 329 | /** 330 | * NMWimaxNspNetworkType 331 | * @enum {Number} 332 | */ 333 | static WIMAX_NSP_NETWORK_TYPE = { 334 | /** Unknown network type */ 335 | UNKNOWN: 0, 336 | /** Home network */ 337 | HOME: 1, 338 | /** Partner network */ 339 | PARTNER: 2, 340 | /** Roaming partner network */ 341 | ROAMING_PARTNER: 3, 342 | }; 343 | 344 | /** 345 | * NMDeviceState 346 | * @enum {Number} 347 | */ 348 | static DEVICE_STATE = { 349 | /** 350 | * The device's state is unknown 351 | * @type {Number} 352 | */ 353 | UNKNOWN: 0, 354 | /** 355 | * The device is recognized, but not managed by NetworkManager 356 | * @type {Number} 357 | */ 358 | UNMANAGED: 10, 359 | /** 360 | * The device is managed by NetworkManager, but is not available for use. 361 | * Reasons may include the wireless switched off, missing firmware, 362 | * no ethernet carrier, missing supplicant or modem manager, etc. 363 | * @type {Number} 364 | */ 365 | UNAVAILABLE: 20, 366 | /** 367 | * The device can be activated, but is currently idle and not connected to a network. 368 | * @type {Number} 369 | */ 370 | DISCONNECTED: 30, 371 | /** 372 | * The device is preparing the connection to the network. 373 | * This may include operations like changing the MAC address, 374 | * setting physical link properties, 375 | * and anything else required to connect to the requested network. 376 | * @type {Number} 377 | */ 378 | PREPARE: 40, 379 | /** 380 | * The device is connecting to the requested network. 381 | * This may include operations like associating with the WiFi AP, 382 | * dialing the modem, connecting to the remote Bluetooth device, etc. 383 | * @type {Number} 384 | */ 385 | CONFIG: 50, 386 | /** 387 | * The device requires more information to continue connecting to the requested network. 388 | * This includes secrets like WiFi passphrases, login passwords, PIN codes, etc. 389 | * @type {Number} 390 | */ 391 | NEED_AUTH: 60, 392 | /** 393 | * The device is requesting IPv4 and/or IPv6 addresses 394 | * and routing information from the network. 395 | * @type {Number} 396 | */ 397 | IP_CONFIG: 70, 398 | /** 399 | * The device is checking whether further action is required for the requested network connection. 400 | * This may include checking whether only local network access is available, 401 | * whether a captive portal is blocking access to the Internet, etc. 402 | * @type {Number} 403 | */ 404 | IP_CHECK: 80, 405 | /** 406 | * The device is waiting for a secondary connection (like a VPN) 407 | * which must activated before the device can be activated 408 | * @type {Number} 409 | */ 410 | SECONDARIES: 90, 411 | /** 412 | * The device has a network connection, either local or global. 413 | * @type {Number} 414 | */ 415 | ACTIVATED: 100, 416 | /** 417 | * A disconnection from the current network connection was requested, 418 | * and the device is cleaning up resources used for that connection. 419 | * The network connection may still be valid. 420 | * @type {Number} 421 | */ 422 | DEACTIVATING: 110, 423 | /** 424 | * The device failed to connect to the requested network and 425 | * is cleaning up the connection request 426 | * @type {Number} 427 | */ 428 | FAILED: 120, 429 | }; 430 | 431 | /** 432 | * NMDeviceStateReason 433 | * @enum {Number} 434 | */ 435 | static DEVICE_STATE_REASON = { 436 | /** No reason given */ 437 | NONE: 0, 438 | /** Unknown error */ 439 | UNKNOWN: 1, 440 | /** Device is now managed */ 441 | NOW_MANAGED: 2, 442 | /** Device is now unmanaged */ 443 | NOW_UNMANAGED: 3, 444 | /** The device could not be readied for configuration */ 445 | CONFIG_FAILED: 4, 446 | /** IP configuration could not be reserved (no available address, timeout, etc) */ 447 | IP_CONFIG_UNAVAILABLE: 5, 448 | /** The IP config is no longer valid */ 449 | IP_CONFIG_EXPIRED: 6, 450 | /** Secrets were required, but not provided */ 451 | NO_SECRETS: 7, 452 | /** 802.1x supplicant disconnected */ 453 | SUPPLICANT_DISCONNECT: 8, 454 | /** 802.1x supplicant configuration failed */ 455 | SUPPLICANT_CONFIG_FAILED: 9, 456 | /** 802.1x supplicant failed */ 457 | SUPPLICANT_FAILED: 10, 458 | /** 802.1x supplicant took too long to authenticate */ 459 | SUPPLICANT_TIMEOUT: 11, 460 | /** PPP service failed to start */ 461 | PPP_START_FAILED: 12, 462 | /** PPP service disconnected */ 463 | PPP_DISCONNECT: 13, 464 | /** PPP failed */ 465 | PPP_FAILED: 14, 466 | /** DHCP client failed to start */ 467 | DHCP_START_FAILED: 15, 468 | /** DHCP client error */ 469 | DHCP_ERROR: 16, 470 | /** DHCP client failed */ 471 | DHCP_FAILED: 17, 472 | /** Shared connection service failed to start */ 473 | SHARED_START_FAILED: 18, 474 | /** Shared connection service failed */ 475 | SHARED_FAILED: 19, 476 | /** AutoIP service failed to start */ 477 | AUTOIP_START_FAILED: 20, 478 | /** AutoIP service error */ 479 | AUTOIP_ERROR: 21, 480 | /** AutoIP service failed */ 481 | AUTOIP_FAILED: 22, 482 | /** The line is busy */ 483 | MODEM_BUSY: 23, 484 | /** No dial tone */ 485 | MODEM_NO_DIAL_TONE: 24, 486 | /** No carrier could be established */ 487 | MODEM_NO_CARRIER: 25, 488 | /** The dialing request timed out */ 489 | MODEM_DIAL_TIMEOUT: 26, 490 | /** The dialing attempt failed */ 491 | MODEM_DIAL_FAILED: 27, 492 | /** Modem initialization failed */ 493 | MODEM_INIT_FAILED: 28, 494 | /** Failed to select the specified APN */ 495 | GSM_APN_FAILED: 29, 496 | /** Not searching for networks */ 497 | GSM_REGISTRATION_NOT_SEARCHING: 30, 498 | /** Network registration denied */ 499 | GSM_REGISTRATION_DENIED: 31, 500 | /** Network registration timed out */ 501 | GSM_REGISTRATION_TIMEOUT: 32, 502 | /** Failed to register with the requested network */ 503 | GSM_REGISTRATION_FAILED: 33, 504 | /** PIN check failed */ 505 | GSM_PIN_CHECK_FAILED: 34, 506 | /** Necessary firmware for the device may be missing */ 507 | FIRMWARE_MISSING: 35, 508 | /** The device was removed */ 509 | REMOVED: 36, 510 | /** NetworkManager went to sleep */ 511 | SLEEPING: 37, 512 | /** The device's active connection disappeared */ 513 | CONNECTION_REMOVED: 38, 514 | /** Device disconnected by user or client */ 515 | USER_REQUESTED: 39, 516 | /** Carrier/link changed */ 517 | CARRIER: 40, 518 | /** The device's existing connection was assumed */ 519 | CONNECTION_ASSUMED: 41, 520 | /** The supplicant is now available */ 521 | SUPPLICANT_AVAILABLE: 42, 522 | /** The modem could not be found */ 523 | MODEM_NOT_FOUND: 43, 524 | /** The Bluetooth connection failed or timed out */ 525 | BT_FAILED: 44, 526 | /** GSM Modem's SIM Card not inserted */ 527 | GSM_SIM_NOT_INSERTED: 45, 528 | /** GSM Modem's SIM Pin required */ 529 | GSM_SIM_PIN_REQUIRED: 46, 530 | /** GSM Modem's SIM Puk required */ 531 | GSM_SIM_PUK_REQUIRED: 47, 532 | /** GSM Modem's SIM wrong */ 533 | GSM_SIM_WRONG: 48, 534 | /** InfiniBand device does not support connected mode */ 535 | INFINIBAND_MODE: 49, 536 | /** A dependency of the connection failed */ 537 | DEPENDENCY_FAILED: 50, 538 | /** Problem with the RFC 2684 Ethernet over ADSL bridge */ 539 | BR2684_FAILED: 51, 540 | /** ModemManager not running */ 541 | MODEM_MANAGER_UNAVAILABLE: 52, 542 | /** The WiFi network could not be found */ 543 | SSID_NOT_FOUND: 53, 544 | /** A secondary connection of the base connection failed */ 545 | SECONDARY_CONNECTION_FAILED: 54, 546 | /** DCB or FCoE setup failed */ 547 | DCB_FCOE_FAILED: 55, 548 | /** Teamd control failed */ 549 | TEAMD_CONTROL_FAILED: 56, 550 | /** Modem failed or no longer available */ 551 | MODEM_FAILED: 57, 552 | /** Modem now ready and available */ 553 | MODEM_AVAILABLE: 58, 554 | /** SIM PIN was incorrect */ 555 | SIM_PIN_INCORRECT: 59, 556 | /** New connection activation was enqueued */ 557 | NEW_ACTIVATION: 60, 558 | /** The device's parent changed */ 559 | PARENT_CHANGED: 61, 560 | /** The device parent's management changed */ 561 | PARENT_MANAGED_CHANGED: 62, 562 | /** Problem communicating with OpenVSwitch database */ 563 | OVSDB_FAILED: 63, 564 | /** A duplicate IP address was detected */ 565 | IP_ADDRESS_DUPLICATE: 64, 566 | /** The selected IP method is not supported */ 567 | IP_METHOD_UNSUPPORTED: 65, 568 | }; 569 | 570 | /** 571 | * NMMetered 572 | * Since: v1.2 573 | * @enum {Number} 574 | */ 575 | static METERED = { 576 | /** The metered status is unknown */ 577 | UNKNOWN: 0, 578 | /** Metered, the value was statically set */ 579 | YES: 1, 580 | /** Not metered, the value was statically set */ 581 | NO: 2, 582 | /** Metered, the value was guessed */ 583 | GUESS_YES: 3, 584 | /** Not metered, the value was guessed */ 585 | GUESS_NO: 4, 586 | }; 587 | 588 | /** 589 | * NMActiveConnectionState 590 | * NOTE: NMActiveConnectionState values indicate the state of a 591 | * connection to a specific network while it is starting, 592 | * connected, or disconnecting from that network. 593 | * @enum {Number} 594 | */ 595 | static ACTIVE_CONNECTION_STATE = { 596 | /** The state of the connection is unknown */ 597 | UNKNOWN: 0, 598 | /** A network connection is being prepared */ 599 | ACTIVATING: 1, 600 | /** There is a connection to the network */ 601 | ACTIVATED: 2, 602 | /** The network connection is being torn down and cleaned up */ 603 | DEACTIVATING: 3, 604 | /** The network connection is disconnected and will be removed */ 605 | DEACTIVATED: 4, 606 | }; 607 | 608 | /** 609 | * NMActiveConnectionStateReason 610 | * Since: v1.8 611 | * @enum {Number} 612 | */ 613 | static ACTIVE_CONNECTION_STATE_REASON = { 614 | /** 615 | * The reason for the active connection state change is unknown. 616 | * @type {Number} 617 | */ 618 | UNKNOWN: 0, 619 | /** 620 | * No reason was given for the active connection state change. 621 | * @type {Number} 622 | */ 623 | NONE: 1, 624 | /** 625 | * The active connection changed state because the user disconnected it. 626 | * @type {Number} 627 | */ 628 | USER_DISCONNECTED: 2, 629 | /** 630 | * The active connection changed state because the device it was using was disconnected. 631 | * @type {Number} 632 | */ 633 | DEVICE_DISCONNECTED: 3, 634 | /** 635 | * The service providing the VPN connection was stopped. 636 | * @type {Number} 637 | */ 638 | SERVICE_STOPPED: 4, 639 | /** 640 | * The IP config of the active connection was invalid. 641 | * @type {Number} 642 | */ 643 | IP_CONFIG_INVALID: 5, 644 | /** 645 | * The connection attempt to the VPN service timed out. 646 | * @type {Number} 647 | */ 648 | CONNECT_TIMEOUT: 6, 649 | /** 650 | * A timeout occurred while starting the service providing the VPN connection. 651 | * @type {Number} 652 | */ 653 | SERVICE_START_TIMEOUT: 7, 654 | /** 655 | * Starting the service providing the VPN connection failed. 656 | * @type {Number} 657 | */ 658 | SERVICE_START_FAILED: 8, 659 | /** 660 | * Necessary secrets for the connection were not provided. 661 | * @type {Number} 662 | */ 663 | NO_SECRETS: 9, 664 | /** 665 | * Authentication to the server failed. 666 | * @type {Number} 667 | */ 668 | LOGIN_FAILED: 10, 669 | /** 670 | * The connection was deleted from settings. 671 | * @type {Number} 672 | */ 673 | CONNECTION_REMOVED: 11, 674 | /** 675 | * Master connection of this connection failed to activate. 676 | * @type {Number} 677 | */ 678 | DEPENDENCY_FAILED: 12, 679 | /** 680 | * Could not create the software device link. 681 | * @type {Number} 682 | */ 683 | DEVICE_REALIZE_FAILED: 13, 684 | /** 685 | * The device this connection depended on disappeared. 686 | * @type {Number} 687 | */ 688 | DEVICE_REMOVED: 14, 689 | }; 690 | 691 | /** 692 | * NMSecretAgentGetSecretsFlags 693 | * NOTE: NMSecretAgentGetSecretsFlags values modify the behavior of a GetSecrets request 694 | * @enum {Number} 695 | */ 696 | static SECRET_AGENT_GET_SECRETS = { 697 | /** 698 | * No special behavior; by default no user interaction is 699 | * allowed and requests for secrets are fulfilled from persistent storage, 700 | * or if no secrets are available an error is returned. 701 | * @type {Number} 702 | */ 703 | NONE: 0x0, 704 | /** 705 | * Allows the request to interact with the user, 706 | * possibly prompting via UI for secrets if any are required, 707 | * or if none are found in persistent storage. 708 | * @type {Number} 709 | */ 710 | ALLOW_INTERACTION: 0x1, 711 | /** 712 | * Explicitly prompt for new secrets from the user. 713 | * This flag signals that NetworkManager thinks any existing secrets are invalid or wrong. 714 | * This flag implies that interaction is allowed. 715 | * @type {Number} 716 | */ 717 | REQUEST_NEW: 0x2, 718 | /** 719 | * Set if the request was initiated by user-requested action via the D-Bus interface, 720 | * as opposed to automatically initiated by NetworkManager in response to scan results or carrier changes. 721 | * @type {Number} 722 | */ 723 | USER_REQUESTED: 0x4, 724 | /** 725 | * Indicates that WPS enrollment is active with PBC method. 726 | * The agent may suggest that the user pushes a button on the router instead of supplying a PSK. 727 | * @type {Number} 728 | */ 729 | WPS_PBC_ACTIVE: 0x8, 730 | /** 731 | * Internal flag, not part of the D-Bus API. 732 | * @type {Number} 733 | */ 734 | ONLY_SYSTEM: 0x80000000, 735 | /** 736 | * Internal flag, not part of the D-Bus API. 737 | * @type {Number} 738 | */ 739 | NO_ERRORS: 0x40000000, 740 | }; 741 | 742 | /** 743 | * NMSecretAgentCapabilities 744 | * NOTE: NMSecretAgentCapabilities indicate various capabilities of the agent 745 | * @enum {Number} 746 | */ 747 | static SECRET_AGENT_CAPABILITY = { 748 | /** 749 | * The agent supports no special capabilities 750 | * @type {Number} 751 | */ 752 | NONE: 0x0, 753 | /** 754 | * The agent supports passing hints to VPN plugin authentication dialogs. 755 | * @type {Number} 756 | */ 757 | VPN_HINTS: 0x1, 758 | }; 759 | 760 | /** 761 | * NMIPTunnelMode 762 | * Since: v1.2 763 | * @enum {Number} 764 | */ 765 | static IP_TUNNEL_MODE = { 766 | /** Unknown/unset tunnel mode */ 767 | UNKNOWN: 0, 768 | /** IP in IP tunnel */ 769 | IPIP: 1, 770 | /** GRE tunnel */ 771 | GRE: 2, 772 | /** SIT tunnel */ 773 | SIT: 3, 774 | /** ISATAP tunnel */ 775 | ISATAP: 4, 776 | /** VTI tunnel */ 777 | VTI: 5, 778 | /** IPv6 in IPv6 tunnel */ 779 | IP6IP6: 6, 780 | /** IPv4 in IPv6 tunnel */ 781 | IPIP6: 7, 782 | /** IPv6 GRE tunnel */ 783 | IP6GRE: 8, 784 | /** IPv6 VTI tunnel */ 785 | VTI6: 9, 786 | }; 787 | 788 | /** 789 | * NMCheckpointCreateFlags 790 | * Since: v1.4 791 | * @enum {Number} 792 | */ 793 | static CHECKPOINT_CREATE = { 794 | /** 795 | * No flags 796 | * @type {Number} 797 | */ 798 | NONE: 0, 799 | /** 800 | * When creating a new checkpoint, 801 | * destroy all existing ones. 802 | * @type {Number} 803 | */ 804 | DESTROY_ALL: 0x01, 805 | /** 806 | * Upon rollback, delete any new connection added after the checkpoint 807 | * Since: v1.6 808 | * @type {Number} 809 | */ 810 | DELETE_NEW_CONNECTIONS: 0x02, 811 | /** 812 | * Upon rollback, disconnect any new device appeared after the checkpoint 813 | * Since: v1.6 814 | * @type {Number} 815 | */ 816 | DISCONNECT_NEW_DEVICES: 0x04, 817 | }; 818 | 819 | /** 820 | * NMRollbackResult 821 | * Since: v1.4 822 | * @enum {Number} 823 | */ 824 | static ROLLBACK_RESULT = { 825 | /** The rollback succeeded. */ 826 | OK: 0, 827 | /** The device no longer exists. */ 828 | ERR_NO_DEVICE: 1, 829 | /** The device is now unmanaged. */ 830 | ERR_DEVICE_UNMANAGED: 2, 831 | /** Other errors during rollback. */ 832 | ERR_FAILED: 3, 833 | }; 834 | 835 | /** 836 | * NMActivationStateFlags 837 | * Since: v1.10 838 | * @enum {Number} 839 | */ 840 | static ACTIVATION_STATE = { 841 | /** 842 | * An alias for numeric zero, no flags set. 843 | * @type {Number} 844 | */ 845 | NONE: 0, 846 | /** 847 | * The device is a master. 848 | * @type {Number} 849 | */ 850 | IS_MASTER: null, // (1LL 851 | /** 852 | * The device is a slave. 853 | * @type {Number} 854 | */ 855 | IS_SLAVE: null, // (1LL 856 | /** 857 | * Layer2 is activated and ready. 858 | * @type {Number} 859 | */ 860 | LAYER2_READY: null, // (1LL 861 | /** 862 | * IPv4 setting is completed. 863 | * @type {Number} 864 | */ 865 | IP4_READY: null, // (1LL 866 | /** 867 | * IPv6 setting is completed. 868 | * @type {Number} 869 | */ 870 | IP6_READY: null, // (1LL 871 | /** 872 | * The master has any slave devices attached. 873 | * This only makes sense if the device is a master. 874 | * @type {Number} 875 | */ 876 | MASTER_HAS_SLAVES: null, // (1LL 877 | }; 878 | 879 | /** 880 | * NMSettingsUpdate2Flags 881 | * Since: v1.10.2 882 | * @enum {Number} 883 | */ 884 | static SETTINGS_UPDATE2 = { 885 | /** 886 | * An alias for numeric zero, no flags set. 887 | * @type {Number} 888 | */ 889 | NONE: 0, 890 | /** 891 | * To persist the connection to disk. 892 | * @type {Number} 893 | */ 894 | TO_DISK: null, // (1LL 895 | /** 896 | * To make the connection in-memory only. 897 | * If the connection was previously persistent, 898 | * the corresponding file on disk is not deleted but merely the 899 | * connection is decoupled from the file on disk. 900 | * If you later delete an in-memory connection, 901 | * the connection on disk will be deleted as well. 902 | * @type {Number} 903 | */ 904 | IN_MEMORY: null, // (1LL 905 | /** 906 | * This is like @NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY, 907 | * but if the connection has a corresponding file on disk, 908 | * the association between the connection and the file is 909 | * forgotten but the file is not modified. 910 | * The difference to %NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY is if you later 911 | * save the connection again to disk, a new file name will be chosen without 912 | * overwriting the remaining file on disk. 913 | * Also, if you delete the connection later, 914 | * the file on disk will not be deleted. 915 | * @type {Number} 916 | */ 917 | IN_MEMORY_DETACHED: null, // (1LL 918 | /** 919 | * This is like @NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY, 920 | * but if the connection has a corresponding file on disk, 921 | * the file on disk will be deleted. 922 | * @type {Number} 923 | */ 924 | IN_MEMORY_ONLY: null, // (1LL 925 | /** 926 | * This can be specified with either %NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY_DETACHED or 927 | * %NM_SETTINGS_UPDATE2_FLAG_IN_MEMORY_ONLY. After making the connection in-memory only, 928 | * the connection is marked as volatile. 929 | * That means, if the connection is currently not active it will be deleted right away. 930 | * Otherwise, it is marked to for deletion once the connection deactivates. 931 | * A volatile connection cannot autoactivate again (because it's about to be deleted), 932 | * but a manual activation will clear the volatile flag. 933 | * @type {Number} 934 | */ 935 | VOLATILE: null, // (1LL 936 | /** 937 | * Usually, when the connection has autoconnect enabled and is modified, 938 | * it becomes elegible to autoconnect right away. 939 | * Setting this flag, disables autoconnect until the connection is manually activated. 940 | * @type {Number} 941 | */ 942 | BLOCK_AUTOCONNECT: null, // (1LL 943 | }; 944 | } -------------------------------------------------------------------------------- /wifi-repeater/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "forceConsistentCasingInFileNames": true, 5 | "module": "commonjs", 6 | "noErrorTruncation": true, 7 | "noFallthroughCasesInSwitch": true, 8 | "noImplicitAny": false, 9 | "noUnusedParameters": true, 10 | "noUnusedLocals": true, 11 | "outDir": "build/", 12 | "preserveConstEnums": true, 13 | "removeComments": true, 14 | "resolveJsonModule": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "strictNullChecks": true, 18 | "target": "es2018" 19 | }, 20 | "include": [ 21 | "./src/**.ts", 22 | "./**.ts", 23 | "./typings/**.d.ts" 24 | ], 25 | "exclude": [ 26 | "node_modules" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /wifi-repeater/typings/dbus-native.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'dbus-native' { 2 | 3 | export type BodyEntry = string | number | array | null; 4 | 5 | export interface Message { 6 | destination: string; 7 | path: string; 8 | interface: string; 9 | member: string; 10 | signature?: string; 11 | body?: BodyEntry[]; 12 | } 13 | 14 | export interface Bus { 15 | invoke: ( 16 | message: Message, 17 | callback: (error: Error, response: any) => void, 18 | ) => void; 19 | } 20 | 21 | export function systemBus(): Bus; 22 | 23 | } --------------------------------------------------------------------------------