-
Notifications
You must be signed in to change notification settings - Fork 50
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #121 from cradlepoint/dapplegate/tailscale
tailscale app
- Loading branch information
Showing
9 changed files
with
339 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |