Skip to content

Commit dba1c1c

Browse files
authored
Merge pull request #11130 from chewitt/tailscale
tailscale: add initial service add-on
2 parents 3dbbd49 + e208f91 commit dba1c1c

17 files changed

Lines changed: 526 additions & 42 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
initial release
16.9 KB
Loading
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
# Copyright (C) 2026-present Team LibreELEC (https://libreelec.tv)
3+
4+
PKG_NAME="tailscale"
5+
PKG_VERSION="1.96.4"
6+
PKG_REV="0"
7+
PKG_ARCH="any"
8+
PKG_LICENSE="BSD-3-Clause"
9+
PKG_SITE="https://tailscale.com"
10+
PKG_DEPENDS_TARGET="toolchain"
11+
PKG_SECTION="service"
12+
PKG_SHORTDESC="Tailscale: private WireGuard made easy"
13+
PKG_LONGDESC="Tailscale (${PKG_VERSION}) creates a secure network between your devices using WireGuard. Connect to your tailnet, use exit nodes, and access local subnets."
14+
PKG_TOOLCHAIN="manual"
15+
16+
PKG_IS_ADDON="yes"
17+
PKG_ADDON_NAME="Tailscale"
18+
PKG_ADDON_TYPE="xbmc.service"
19+
PKG_MAINTAINER="LibreELEC"
20+
21+
case "${ARCH}" in
22+
x86_64)
23+
TAILSCALE_ARCH="amd64"
24+
PKG_SHA256="a1cba18826b1f91cb25ef7f5b8259b5258339b42db7867af9269e21829ea78cc"
25+
;;
26+
arm)
27+
TAILSCALE_ARCH="arm"
28+
PKG_SHA256="6c8731f096147aaf9e187b39f892692fb2bd56dc2eb1fb8fd06982164f339869"
29+
;;
30+
aarch64)
31+
TAILSCALE_ARCH="arm64"
32+
PKG_SHA256="a27249bc70d7b37a68f8be7f5c4507ea5f354e592dce43cb5d4f3e742b313c3c"
33+
;;
34+
esac
35+
36+
PKG_URL="https://pkgs.tailscale.com/stable/tailscale_${PKG_VERSION}_${TAILSCALE_ARCH}.tgz"
37+
38+
unpack() {
39+
mkdir -p ${PKG_BUILD}
40+
tar xzf ${SOURCES}/${PKG_NAME}/${PKG_NAME}-${PKG_VERSION}.tgz -C ${PKG_BUILD} --strip-components=1
41+
}
42+
43+
make_target() {
44+
:
45+
}
46+
47+
addon() {
48+
mkdir -p ${ADDON_BUILD}/${PKG_ADDON_ID}/bin
49+
cp ${PKG_BUILD}/tailscale ${ADDON_BUILD}/${PKG_ADDON_ID}/bin
50+
cp ${PKG_BUILD}/tailscaled ${ADDON_BUILD}/${PKG_ADDON_ID}/bin
51+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
#!/bin/sh
2+
3+
# SPDX-License-Identifier: GPL-2.0
4+
# Copyright (C) 2026-present Team LibreELEC (https://libreelec.tv)
5+
6+
. /etc/profile
7+
oe_setup_addon service.tailscale
8+
9+
TAILSCALED="$ADDON_DIR/bin/tailscaled"
10+
TAILSCALE="$ADDON_DIR/bin/tailscale"
11+
STATE_DIR="/storage/.cache/tailscale"
12+
SOCKET="/run/tailscale/tailscaled.sock"
13+
14+
mkdir -p "$STATE_DIR" "$(dirname $SOCKET)"
15+
16+
# Enable IP forwarding (required for exit node and subnet routing)
17+
if [ "$ts_exit_node" = "true" ] || [ "$ts_subnet_routes" = "true" ]; then
18+
echo 1 > /proc/sys/net/ipv4/ip_forward
19+
echo 1 > /proc/sys/net/ipv6/conf/all/forwarding
20+
fi
21+
22+
# Ensure LAN traffic is not misrouted through tailscale's routing table.
23+
# tailscaled adds "from all lookup 52" ip rules that catch all traffic.
24+
# With --netfilter-mode=off no fwmark rules exist to bypass table 52,
25+
# so add high-priority rules to keep private subnet traffic in the main
26+
# routing table before tailscaled starts.
27+
for subnet in 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12; do
28+
ip rule add to $subnet priority 100 lookup main 2>/dev/null
29+
done
30+
31+
# Start tailscaled daemon in the background
32+
$TAILSCALED -state "$STATE_DIR/tailscaled.state" -socket "$SOCKET" &
33+
TAILSCALED_PID=$!
34+
35+
# Wait for the socket to become available
36+
for i in $(seq 1 30); do
37+
[ -S "$SOCKET" ] && break
38+
sleep 1
39+
done
40+
41+
if [ ! -S "$SOCKET" ]; then
42+
echo "Error: tailscaled socket not available after 30s"
43+
kill $TAILSCALED_PID 2>/dev/null
44+
exit 1
45+
fi
46+
47+
# Build tailscale up arguments - always specify all flags via --reset
48+
# to avoid "re-run with --reset" errors when changing settings.
49+
# Use --netfilter-mode=off to preserve LAN access (SSH, Kodi, etc.)
50+
TS_ARGS="--reset --netfilter-mode=off"
51+
52+
# Set node hostname
53+
if [ "$ts_auto_hostname" != "true" ] && [ -n "$ts_hostname" ]; then
54+
TS_ARGS="$TS_ARGS --hostname=$ts_hostname"
55+
fi
56+
57+
# Accept subnet routes from other nodes
58+
if [ "$ts_accept_routes" = "true" ]; then
59+
TS_ARGS="$TS_ARGS --accept-routes"
60+
fi
61+
62+
# Advertise as exit node and use exit node are mutually exclusive
63+
if [ "$ts_exit_node" = "true" ] && [ "$ts_use_exit_node" != "true" ]; then
64+
TS_ARGS="$TS_ARGS --advertise-exit-node"
65+
elif [ "$ts_use_exit_node" = "true" ] && [ "$ts_exit_node" != "true" ] && [ -n "$ts_exit_node_host" ]; then
66+
TS_ARGS="$TS_ARGS --exit-node=$ts_exit_node_host"
67+
if [ "$ts_exit_node_allow_lan" = "true" ]; then
68+
TS_ARGS="$TS_ARGS --exit-node-allow-lan-access"
69+
fi
70+
fi
71+
72+
# Advertise local subnet routes (independent of exit node)
73+
if [ "$ts_subnet_routes" = "true" ] && [ -n "$ts_subnets" ]; then
74+
TS_ARGS="$TS_ARGS --advertise-routes=$ts_subnets"
75+
fi
76+
77+
# Connect or disconnect based on settings
78+
if [ "$ts_connect" = "true" ]; then
79+
$TAILSCALE --socket="$SOCKET" up $TS_ARGS
80+
81+
# With --netfilter-mode=off tailscale creates no iptables rules.
82+
# Exit node and subnet routing need manual NAT and forwarding rules.
83+
if [ "$ts_exit_node" = "true" ] || [ "$ts_subnet_routes" = "true" ]; then
84+
iptables -A FORWARD -i tailscale0 -j ACCEPT 2>/dev/null
85+
iptables -A FORWARD -o tailscale0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null
86+
iptables -t nat -A POSTROUTING -s 100.64.0.0/10 ! -o tailscale0 -j MASQUERADE 2>/dev/null
87+
fi
88+
else
89+
$TAILSCALE --socket="$SOCKET" down 2>/dev/null
90+
fi
91+
92+
# Wait for tailscaled to exit
93+
wait $TAILSCALED_PID
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/bin/sh
2+
3+
# SPDX-License-Identifier: GPL-2.0
4+
# Copyright (C) 2026-present Team LibreELEC (https://libreelec.tv)
5+
6+
SOCKET="/run/tailscale/tailscaled.sock"
7+
TAILSCALE="/storage/.kodi/addons/service.tailscale/bin/tailscale"
8+
9+
if [ -S "$SOCKET" ]; then
10+
$TAILSCALE --socket="$SOCKET" down 2>/dev/null
11+
fi
12+
13+
# Clean up manual NAT and forwarding rules
14+
iptables -D FORWARD -i tailscale0 -j ACCEPT 2>/dev/null
15+
iptables -D FORWARD -o tailscale0 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null
16+
iptables -t nat -D POSTROUTING -s 100.64.0.0/10 ! -o tailscale0 -j MASQUERADE 2>/dev/null
17+
18+
# Clean up LAN routing rules
19+
for subnet in 192.168.0.0/16 10.0.0.0/8 172.16.0.0/12; do
20+
ip rule del to $subnet priority 100 lookup main 2>/dev/null
21+
done
22+
23+
# tailscaled will be stopped by systemd SIGTERM
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# SPDX-License-Identifier: GPL-2.0
2+
# Copyright (C) 2026-present Team LibreELEC (https://libreelec.tv)
3+
4+
import json
5+
import subprocess
6+
import sys
7+
import xbmc
8+
import xbmcaddon
9+
import xbmcgui
10+
11+
SOCKET = '/run/tailscale/tailscaled.sock'
12+
13+
14+
def select_exit_node():
15+
addon = xbmcaddon.Addon()
16+
tailscale = addon.getAddonInfo('path') + '/bin/tailscale'
17+
strings = addon.getLocalizedString
18+
19+
try:
20+
result = subprocess.check_output(
21+
[tailscale, '--socket', SOCKET, 'status', '--json'],
22+
stderr=subprocess.STDOUT
23+
)
24+
data = json.loads(result)
25+
except Exception:
26+
xbmcgui.Dialog().ok('Tailscale', strings(30023))
27+
return
28+
29+
peers = []
30+
for peer in data.get('Peer', {}).values():
31+
if peer.get('ExitNodeOption') and peer.get('Online'):
32+
dns_name = peer.get('DNSName', '').rstrip('.')
33+
machine_name = dns_name.split('.')[0] if dns_name else ''
34+
ips = peer.get('TailscaleIPs', [])
35+
ip = ips[0] if ips else ''
36+
if machine_name and ip:
37+
peers.append((machine_name, ip))
38+
39+
if not peers:
40+
xbmcgui.Dialog().ok('Tailscale', strings(30024))
41+
return
42+
43+
labels = [name for name, ip in peers]
44+
sel = xbmcgui.Dialog().select(strings(30025), labels)
45+
if sel >= 0:
46+
addon.setSetting('ts_exit_node_host', peers[sel][1])
47+
48+
49+
class Monitor(xbmc.Monitor):
50+
51+
def __init__(self, *args, **kwargs):
52+
xbmc.Monitor.__init__(self)
53+
self.addon = xbmcaddon.Addon()
54+
self.id = self.addon.getAddonInfo('id')
55+
56+
def onSettingsChanged(self):
57+
strings = self.addon.getLocalizedString
58+
if xbmcgui.Dialog().yesno('Tailscale', strings(30026)):
59+
subprocess.call(['systemctl', 'restart', self.id])
60+
61+
62+
if __name__ == '__main__':
63+
if len(sys.argv) > 1 and sys.argv[1] == 'select_exit_node':
64+
select_exit_node()
65+
else:
66+
Monitor().waitForAbort()
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# Kodi Media Center language file
2+
# Addon Name: Tailscale
3+
# Addon id: service.tailscale
4+
# Addon Provider: Team LibreELEC
5+
msgid ""
6+
msgstr ""
7+
8+
msgctxt "#30000"
9+
msgid "Tailscale"
10+
msgstr ""
11+
12+
msgctxt "#30001"
13+
msgid "Enable"
14+
msgstr ""
15+
16+
msgctxt "#30002"
17+
msgid "Connect this node to your tailnet. Initial authentication must be completed via SSH using: tailscale up"
18+
msgstr ""
19+
20+
msgctxt "#30003"
21+
msgid "Auto-generate name from OS hostname"
22+
msgstr ""
23+
24+
msgctxt "#30004"
25+
msgid "Use the system hostname as the node name on your tailnet."
26+
msgstr ""
27+
28+
msgctxt "#30005"
29+
msgid "Node name"
30+
msgstr ""
31+
32+
msgctxt "#30006"
33+
msgid "Custom name for this node on your tailnet."
34+
msgstr ""
35+
36+
msgctxt "#30007"
37+
msgid "Accept routes"
38+
msgstr ""
39+
40+
msgctxt "#30008"
41+
msgid "Accept subnet routes advertised by other nodes on your tailnet."
42+
msgstr ""
43+
44+
msgctxt "#30009"
45+
msgid "Advertise exit node"
46+
msgstr ""
47+
48+
msgctxt "#30010"
49+
msgid "Offer this device as an exit node, allowing other tailnet devices to route their internet traffic through it. Requires approval in the admin console."
50+
msgstr ""
51+
52+
msgctxt "#30011"
53+
msgid "Advertise local subnets"
54+
msgstr ""
55+
56+
msgctxt "#30012"
57+
msgid "Share local network subnets with your tailnet, allowing other devices to access them through this node. Requires approval in the admin console."
58+
msgstr ""
59+
60+
msgctxt "#30013"
61+
msgid "Subnets"
62+
msgstr ""
63+
64+
msgctxt "#30014"
65+
msgid "Comma-separated CIDR ranges to advertise (e.g. 192.168.1.0/24,10.0.0.0/8)."
66+
msgstr ""
67+
68+
msgctxt "#30015"
69+
msgid "Use exit node"
70+
msgstr ""
71+
72+
msgctxt "#30016"
73+
msgid "Route all internet traffic from this device through another tailnet node that is offering exit node access."
74+
msgstr ""
75+
76+
msgctxt "#30017"
77+
msgid "Select exit node"
78+
msgstr ""
79+
80+
msgctxt "#30018"
81+
msgid "Choose an exit node from active tailnet peers offering exit node access."
82+
msgstr ""
83+
84+
msgctxt "#30019"
85+
msgid "Allow LAN access"
86+
msgstr ""
87+
88+
msgctxt "#30020"
89+
msgid "Retain direct access to the local network while routing internet traffic through the exit node."
90+
msgstr ""
91+
92+
msgctxt "#30021"
93+
msgid "Exit node address"
94+
msgstr ""
95+
96+
msgctxt "#30022"
97+
msgid "Enter the IP address of the exit node, e.g. 100.x.y.z."
98+
msgstr ""
99+
100+
msgctxt "#30023"
101+
msgid "Unable to query tailscale. Is the service running?"
102+
msgstr ""
103+
104+
msgctxt "#30024"
105+
msgid "No exit nodes available on your tailnet."
106+
msgstr ""
107+
108+
msgctxt "#30025"
109+
msgid "Select exit node"
110+
msgstr ""
111+
112+
msgctxt "#30026"
113+
msgid "Settings have changed. Restart the Tailscale service now?"
114+
msgstr ""

0 commit comments

Comments
 (0)