From d13895a82fdd696e970bc6d78a806f12bf7b4f93 Mon Sep 17 00:00:00 2001 From: Doug Applegate Date: Tue, 23 Apr 2024 16:35:26 -0600 Subject: [PATCH 1/2] allow for readme.md as well --- .github/workflows/check_for_readme.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_for_readme.sh b/.github/workflows/check_for_readme.sh index bab0c2a3..e3ca8041 100755 --- a/.github/workflows/check_for_readme.sh +++ b/.github/workflows/check_for_readme.sh @@ -3,9 +3,9 @@ shopt -s extglob for folder in ./!(built_apps|tools)/ do - if [ ! -f $folder/readme.txt ] + if ! find $folder -iname 'readme.txt' -o -iname 'readme.md' | grep -q .; then - echo "The app "$folder" is missing a readme.txt. Please ensure it's named "readme.txt"." + echo "The app "$folder" is missing a readme. Please ensure it's named "readme.txt" or "readme.md"." exit 1 fi done From d8a588fd7af5e1f693ad559a0dd87c741b1abfae Mon Sep 17 00:00:00 2001 From: Doug Applegate Date: Tue, 23 Apr 2024 16:48:55 -0600 Subject: [PATCH 2/2] tailscale app --- README.md | 2 + tailscale/README.md | 40 ++++++++++++++ tailscale/cs_get.py | 10 ++++ tailscale/download.py | 123 +++++++++++++++++++++++++++++++++++++++++ tailscale/get_tskey.py | 27 +++++++++ tailscale/package.ini | 14 +++++ tailscale/setup.py | 7 +++ tailscale/start.sh | 114 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 337 insertions(+) create mode 100644 tailscale/README.md create mode 100644 tailscale/cs_get.py create mode 100755 tailscale/download.py create mode 100644 tailscale/get_tskey.py create mode 100755 tailscale/package.ini create mode 100644 tailscale/setup.py create mode 100755 tailscale/start.sh diff --git a/README.md b/README.md index 4c170c42..deadee0e 100755 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ The Application Developers Guide is the best document to read first. - A simple web server to receive messages. Note that any 'server function' requires the router firewall to be correctly changed to allow client access to the router. - **system_monitor** - Get various system diagnostics, alert on thresholds, and put current status in asset_id field. +- **tailscale** + - A 3rd party mesh VPN called [Tailscale](https://tailscale.com) that makes it easy to connect your devices, wherever they are. This application provides a way to proxy traffic from the LAN to the Tailscale network. See the README.md for more information. - **tornado_sample** - A webserver using Tornado with NCM-themed example to set WiFi SSIDs. - **throttle_cellular_datacap** diff --git a/tailscale/README.md b/tailscale/README.md new file mode 100644 index 00000000..2603a97f --- /dev/null +++ b/tailscale/README.md @@ -0,0 +1,40 @@ +# tailscale + +## NCOS Devices Supported +ALL + +## Application Purpose +[Tailscale](https://tailscale.com) is a mesh VPN that makes it easy to connect your devices, wherever they are. This application provides a way to proxy traffic from the LAN to the Tailscale network. + +## Notes +Tailscale can function as a fully L3 routed VPN, but as a Cradlepoint app, it can only run as a proxy. In other words. This app uses the `userspace-networking`. It also exposes a SOCKS5 proxy on port 1055, which can be used to proxy traffic directed to it to another device on the tailscale network. Also incoming traffic from tailscale to the LAN is possible. This sdk app automatically adds the lan networks as routes to the tailscale network. + +## Security Notice +Depending on the tailscale network configuration. This app can expose the router to any device on the tailscale network. It is recommended to use the Access Controls feature in tailscale to limit access to the router. + +## Usage +It is assumed that a tailscale account is already created. Log into the account and navitage to the admin console. Create a new key under Settings->Keys and then "Generate auth key". Generate a 90 day reusable key, and confgiure the tags as desired. Click "Generate key" and be sure to copy the tskey-auth code as shown. + +![generate_auth_key](https://github.com/cradlepoint/sdk-samples/assets/59579399/67c243b4-78da-482c-a5e5-ee01d33c2228) + +Next, add the tailscale app as an SDK app in your Cradlepoint ncm account. Add the SDK to a new group (see https://docs.cradlepoint.com/r/NetCloudOS-SDK-Sample-Apps-Quick-Start-Guide for more details) + +Configure the group, navigate to System->SDK Appdata, and add a new key value pair with tskey as the key and the tskey-auth code as the value. + +![app data](https://github.com/cradlepoint/sdk-samples/assets/59579399/4d785b56-ede7-43bf-9462-f76a7ba4d6ac) + +The router will automatically download tailscale and use the key to authenticate. The router's hostname should show up in the list of tailscale machines. + +![machines](https://github.com/cradlepoint/sdk-samples/assets/59579399/d47d8bcb-e8ab-45ce-858d-9f32c6011a18) + +## Other Settings +You can configure the tailscale version, add additional routes if you would like. +For example: + +| Name | Value | Notes | +| ---- | ----- | ----- | +| tsroutes | 172.16.0.0/12 | Manually add a tailscale routes, comma separated +| tsversion | 1.60.1 | Use this version of tailscale explicitly + +## Overlapping subnets +You can use tailscales 4via6 feature if you would like to get to devices behind a Cradlepoint routers that might share the same subnet. First come up with a site id you would like to use (0-65535). Then from a computer with tailscale installed execute: `tailscale debug via [site-id] [subnet]`. For example: `tailscale debug via 1 172.16.0.0/12` should generate a 4via6 subnet of `fd7a:115c:a1e0:b1a:0:1:ac10:0/108`. Add this as a tsroute above and you can access the network via ipv6 or by the domain name following the format `Q-R-S-T-via-X` where Q-R-S-T is the ipv4 address and X is the site id, e.g.: `172-16-0-1-via-1`. diff --git a/tailscale/cs_get.py b/tailscale/cs_get.py new file mode 100644 index 00000000..965373bd --- /dev/null +++ b/tailscale/cs_get.py @@ -0,0 +1,10 @@ +import json +import subprocess + +def cs_get(path): + p = subprocess.run(["csclient", "-m", "get", "-b", path], capture_output=True, check=True, text=True) + return json.loads(p.stdout.strip()) + +def get_appdata(key): + appdata = cs_get("/config/system/sdk/appdata") + return next((j['value'] for j in appdata if j['name'] == key), None) \ No newline at end of file diff --git a/tailscale/download.py b/tailscale/download.py new file mode 100755 index 00000000..4eaa96d6 --- /dev/null +++ b/tailscale/download.py @@ -0,0 +1,123 @@ +import requests +import tarfile +import os +import shutil + +TSVERSION = "1.60.1" + +def download(url, target_folder): + try: + # Create the target folder if it doesn't exist + if not os.path.exists(target_folder): + os.makedirs(target_folder) + + # Download the tar.gz file + print(f"Downloading {url}...") + response = requests.get(url) + if response.status_code != 200: + print(f"Failed to download the file from {url}. Status code: {response.status_code}") + return + + # Save the file in the target folder + filename = os.path.join(target_folder, list(filter(None, url.split("/")))[-1]) + with open(filename, "wb") as file: + file.write(response.content) + return filename + except Exception as e: + print(f"An error occurred: {e}") + +def extract_tar_gz(filename, target_folder): + try: + # Create the target folder if it doesn't exist + if not os.path.exists(target_folder): + os.makedirs(target_folder) + + # Extract the contents of the tar.gz file + with tarfile.open(filename, "r:gz") as tar: + tar.extractall(target_folder) + + print(f"Extracting {filename} completed successfully.") + return filename + except Exception as e: + print(f"An error occurred: {e}") + +def download_and_extract_tar_gz(url, target_folder): + filename = download(url, target_folder) # expects a tar.gz file + filename = extract_tar_gz(filename, target_folder) + # Delete the tar.gz file after extraction + os.remove(filename) + +def move_files(source_folder, target_folder): + try: + # Create the target folder if it doesn't exist + if not os.path.exists(target_folder): + os.makedirs(target_folder) + + # Move the files from the source folder to the target folder + for file in os.listdir(source_folder): + os.rename(os.path.join(source_folder, file), os.path.join(target_folder, file)) + + print("Files moved successfully.") + except Exception as e: + print(f"An error occurred: {e}") + +def rename_file(source, target): + try: + os.rename(source, target) + print(f"Files renamed from {source} to {target} successfully.") + except Exception as e: + print(f"An error occurred renaming {source} to {target}: {e}") + +def add_executable_perm(filename): + try: + # Add executable permissions to the file + os.chmod(filename, 0o755) + + print(f"Executable permissions for {filename} added successfully.") + except Exception as e: + print(f"An error occurred adding permissions for {filename}: {e}") + +def check_version(version): + # check version.txt file, if it is missing or the version doesn't match, return false + try: + with open('version.txt', 'r') as file: + if file.read() != version: + return False + except FileNotFoundError: + return False + return True + +def main(): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('archs', nargs='*') + parser.add_argument("-v", "--version", help="Tailscale version", default=TSVERSION) + args = parser.parse_args() + if not args.archs: + archs = ( + 'arm', + 'arm64', + # 'amd64', + # 'mipsle' + ) + else: + archs = args.archs + tsversion = args.version + + if not check_version(tsversion): + + for arch in archs: + download_and_extract_tar_gz(f'https://pkgs.tailscale.com/stable/tailscale_{tsversion}_{arch}.tgz', './') + move_files(f'./tailscale_{tsversion}_{arch}/', './') + shutil.rmtree(f'./tailscale_{tsversion}_{arch}') + shutil.rmtree('./systemd') + rename_file('./tailscale', f'./tailscale_{arch}') + rename_file('./tailscaled', f'./tailscaled_{arch}') + add_executable_perm(f'./tailscale_{arch}') + add_executable_perm(f'./tailscaled_{arch}') + + with open('version.txt', 'w') as file: + file.write(tsversion) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tailscale/get_tskey.py b/tailscale/get_tskey.py new file mode 100644 index 00000000..6a3aa34e --- /dev/null +++ b/tailscale/get_tskey.py @@ -0,0 +1,27 @@ +from cs_get import cs_get, get_appdata +import ipaddress +import sys + + +if __name__ == "__main__": + command = sys.argv[1] + + if command in ["tskey", "tsversion"]: + try: + value = get_appdata(command) + if value: + print(value) + except Exception as e: + print(f"An error occurred: {e}", file=sys.stderr) + exit(1) + + elif command == "tsroutes": + lans = cs_get("/config/lan") + networks = [] + for lan in lans: + network = str(ipaddress.ip_network(f"{lan['ip_address']}/{lan['netmask']}", strict=False)) + networks.append(network) + tsroutes = get_appdata('tsroutes') + if tsroutes: + networks.extend(list(map(str.strip, tsroutes.split(',')))) + print(",".join(networks)) \ No newline at end of file diff --git a/tailscale/package.ini b/tailscale/package.ini new file mode 100755 index 00000000..bcb69136 --- /dev/null +++ b/tailscale/package.ini @@ -0,0 +1,14 @@ +[tailscale] +uuid = d4c47aa5-4409-4edf-bf1a-550182ad70a1 +vendor = Cradlepoint +notes = tailscale +version_major = 0 +version_minor = 0 +version_patch = 32 +auto_start = true +restart = true +reboot = true +firmware_major = 7 +firmware_minor = 23 +fimrware_patch = 50 + diff --git a/tailscale/setup.py b/tailscale/setup.py new file mode 100644 index 00000000..d8bf7688 --- /dev/null +++ b/tailscale/setup.py @@ -0,0 +1,7 @@ +from download import main + +PREDOWNLOAD = False # Switch to true and build to include binaries in sdk package, otherwise they will be downloaded at runtime + +if __name__ == "__main__": + if PREDOWNLOAD: + main() \ No newline at end of file diff --git a/tailscale/start.sh b/tailscale/start.sh new file mode 100755 index 00000000..72ef05cf --- /dev/null +++ b/tailscale/start.sh @@ -0,0 +1,114 @@ +#!/bin/bash +set -o pipefail +set -o errexit + +logger -s -t tailscale -p 6 "tailscale istarting up..." + +logerr() { + if [ "$#" -gt 0 ]; then + logger -s -t tailscale -p 3 "$*" + else + cat | logger -s -t tailscale -p 3 + fi +} + +check_tskey() { + tskey="$(cppython ./get_tskey.py tskey)" + tskey_ec=$? +} + +get_tsroutes() { + tsroutes="$(cppython ./get_tskey.py tsroutes)" +} + +get_tsarch() { + arch="$(uname -m)" + if [ "$arch" = "armv7l" ]; then + tsarch="arm" + elif [ "$arch" = "x86_64" ]; then + tsarch="amd64" + elif [ "$arch" = "aarch64" ]; then + tsarch="arm64" + fi +} + +download() { + cmd="cppython ./download.py $tsarch" + tsversion="$(cppython ./get_tskey.py tsversion)" + if [ -n "$tsversion" ]; then + cmd+=" -v $tsversion" + fi + eval $cmd | logerr + if [ $? -ne 0 ]; then + logerr "Failed to download tailscale binary" + exit 1 + fi +} + +tskey="" +tskey_ec=0 +tsroutes="" +tsarch="arm64" + +check_tskey +get_tsroutes +get_tsarch +download + +tsdbinary="tailscaled_$tsarch" +tsbinary="tailscale_$tsarch" + +if [ $tskey_ec -ne 0 ] || [ -z "$tskey" ]; then + sleep 10 + logerr "Couldn't get tskey. Exiting..." + exit 1 +fi + +prev_tskey="$tskey" + +exit_safely() { + ./${tsbinary} --socket ./tailscaled.sock logout 2>&1 | logerr + killall ${tsdbinary} + exit 1 +} + +check_tskey_change() { + prev_tskey=$tskey + check_tskey + prev_tsroutes=$tsroutes + get_tsroutes + + if [ $tskey_ec -ne 0 ] || [ -z "$tskey" ]; then + logerr "Couldn't get tskey. Exiting..." + exit_safely + fi + + if [ "$tskey" != "$prev_tskey" ]; then + logerr "tskey has changed. Exiting..." + exit_safely + fi + + if [ "$tsroutes" != "$prev_tsroutes" ]; then + logerr "tsroutes has changed. Exiting..." + exit_safely + fi +} + +trap exit_safely SIGINT SIGTERM EXIT + +HOME=$(pwd) ./${tsdbinary} --socket=./tailscaled.sock --tun=userspace-networking --socks5-server=localhost:1055 2>&1 | logerr & +sleep 2 +HOME=$(pwd) ./${tsbinary} --socket ./tailscaled.sock up --auth-key="$tskey" --advertise-routes="$tsroutes" 2>&1 | logerr + +tsretcode=$? +if [ $tsretcode -ne 0 ]; then + logerr "tailscale failed to run: exit code $tsretcode" + exit_safely +fi + +logger -s -t tailscale -p 6 "tailscale should be up and running now" + +while true; do + sleep 10 + check_tskey_change +done \ No newline at end of file