Skip to content

Commit

Permalink
Merge pull request #121 from cradlepoint/dapplegate/tailscale
Browse files Browse the repository at this point in the history
tailscale app
  • Loading branch information
phate999 authored Apr 24, 2024
2 parents 770aa7b + d8a588f commit 00a3722
Show file tree
Hide file tree
Showing 9 changed files with 339 additions and 2 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/check_for_readme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand Down
40 changes: 40 additions & 0 deletions tailscale/README.md
Original file line number Diff line number Diff line change
@@ -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`.
10 changes: 10 additions & 0 deletions tailscale/cs_get.py
Original file line number Diff line number Diff line change
@@ -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)
123 changes: 123 additions & 0 deletions tailscale/download.py
Original file line number Diff line number Diff line change
@@ -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()
27 changes: 27 additions & 0 deletions tailscale/get_tskey.py
Original file line number Diff line number Diff line change
@@ -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))
14 changes: 14 additions & 0 deletions tailscale/package.ini
Original file line number Diff line number Diff line change
@@ -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

7 changes: 7 additions & 0 deletions tailscale/setup.py
Original file line number Diff line number Diff line change
@@ -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()
114 changes: 114 additions & 0 deletions tailscale/start.sh
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 00a3722

Please sign in to comment.