How to Add Advanced Tailscale Options to GL.iNet Routers via UCI
Background
GL.iNet firmware includes a convenient Tailscale interface, but the Web Admin Panel does not expose every option supported by the Tailscale CLI. This can be limiting when you need options such as a custom coordination server, Tailscale SSH, a custom hostname, ACL tags, or features added by a future Tailscale release.
On supported GL.iNet firmware, /usr/bin/gl_tailscale reads the router configuration and builds the final tailscale up command. A setting applied manually with tailscale up or tailscale set can therefore be reset the next time Tailscale is restarted from the Web Admin Panel.
This tutorial makes a small modification to the GL.iNet wrapper so that it reads two additional ordered UCI lists:
tailscale.settings.extra_up_args: arguments appended to GL.iNet's existingtailscale upcommand.tailscale.settings.extra_set_args: arguments applied withtailscale setimmediately aftertailscale up.
The existing GL.iNet route, exit-node, DNS, firewall, and MTU logic remains in place. Each UCI list entry is passed as one literal command-line argument.
Important Note / Disclaimer
This tutorial manually modifies a GL.iNet system file via SSH.
- Advanced users only: An invalid Tailscale option can prevent Tailscale from starting correctly.
- Use a LAN connection: Restarting Tailscale can disconnect an SSH session established through Tailscale.
- Structure validation: The installer checks the original wrapper's command and insertion point before changing it. It stops without modifying the router if the expected structure is not found exactly once.
- Firmware upgrades: A firmware or GL.iNet Tailscale component update may replace
/usr/bin/gl_tailscale. Re-run the installer after upgrading; it will apply the modification only if the updated wrapper still has the expected structure.- Recovery: The firmware's original file remains available at
/rom/usr/bin/gl_tailscaleand can be restored with one command.- Support scope: This modification is not an officially supported GL.iNet feature. GL.iNet support may ask you to restore the original file before troubleshooting.
Verified Test Environment
This method has been tested with:
- Flint 2 (
GL-MT6000) - GL.iNet firmware
v4.9.0 - Tailscale
1.92.5-1 (OpenWrt)
The installer will checks the actual /rom/usr/bin/gl_tailscale structure and exits without modifying the router if the expected command or insertion point is not found exactly once.
Prerequisites
Before proceeding, make sure that:
- You can connect to the router over SSH through its LAN address.
- Tailscale is pre-installed under Applications -> Tailscale in the GL.iNet Web Admin Panel, meaning the model you're using supports Tailscale
- You are comfortable restoring the router file from
/romif necessary. - For the Headscale example, your Headscale server is reachable over HTTPS and you can create a one-time pre-authentication key.
Part 1: Install the UCI Extension
Connect to the router over SSH. Copy the entire block below, paste it into the SSH terminal once, and press Enter.
sh <<'GL_TAILSCALE_UCI_PATCH'
set -eu
target='/usr/bin/gl_tailscale'
rom_file='/rom/usr/bin/gl_tailscale'
temporary="/tmp/gl_tailscale.uci.$$"
original_line=' timeout 10 /usr/sbin/tailscale up --reset --accept-routes $param --timeout 3s --accept-dns=false > /dev/null'
[ -r "$rom_file" ] || {
echo "Error: $rom_file is not available. Nothing was changed." >&2
exit 1
}
cp -p "$rom_file" "$temporary"
trap 'rm -f "$temporary"' 0 1 2 15
call_count="$(grep -Fc "$original_line" "$temporary" || true)"
anchor_count="$(grep -c '^add_policy_route()$' "$temporary" || true)"
[ "$call_count" = '1' ] || {
echo 'Error: the expected Tailscale command was not found exactly once.' >&2
echo 'The wrapper structure is not supported by this patch. Nothing was changed.' >&2
exit 1
}
[ "$anchor_count" = '1' ] || {
echo 'Error: the insertion point was not found exactly once.' >&2
echo 'The wrapper structure is not supported by this patch. Nothing was changed.' >&2
exit 1
}
sed -i '/^add_policy_route()$/i\
run_tailscale_command()\
{\
local arg arg_count arg_index=1 list_name="$1" required="$2"\
\
shift 2\
config_get arg_count settings "${list_name}_LENGTH" 0\
[ "$required" = "1" ] || [ "$arg_count" -gt 0 ] || return 0\
\
while [ "$arg_index" -le "$arg_count" ]; do\
config_get arg settings "${list_name}_ITEM$arg_index"\
[ -n "$arg" ] && set -- "$@" "$arg"\
arg_index=$((arg_index + 1))\
done\
\
timeout 10 "$@" > /dev/null\
}\
\
run_tailscale_up()\
{\
config_load tailscale\
run_tailscale_command extra_up_args 1 /usr/sbin/tailscale up --reset --accept-routes "$@" --timeout 3s --accept-dns=false\
run_tailscale_command extra_set_args 0 /usr/sbin/tailscale set\
}\
' "$temporary"
sed -i 's# timeout 10 /usr/sbin/tailscale up --reset --accept-routes $param --timeout 3s --accept-dns=false > /dev/null# run_tailscale_up $param#' "$temporary"
sh -n "$temporary"
[ "$(grep -c '^run_tailscale_command()$' "$temporary")" = '1' ]
[ "$(grep -Fc 'run_tailscale_up $param' "$temporary")" = '1' ]
cp -p "$temporary" "$target"
sh -n "$target"
echo 'The advanced Tailscale UCI extension was installed successfully.'
echo 'Restarting the GL.iNet Tailscale component...'
/usr/bin/gl_tailscale restart
sleep 5
tailscale status
GL_TAILSCALE_UCI_PATCH
The installer always builds the modified file from the firmware-matched copy under /rom. This makes the operation repeatable and avoids stacking the patch on top of an already modified file. It will, however, replace any other manual changes previously made to /usr/bin/gl_tailscale.
It is normal for the GL.iNet restart command to report that its three-second Tailscale startup wait expired on some connections. Wait several seconds and verify the final state as shown below.
Part 2: Verify the Installation
Run:
sh -n /usr/bin/gl_tailscale
grep -n -E '^run_tailscale_(command|up)\(\)|run_tailscale_up \$param' /usr/bin/gl_tailscale
tailscale status --json | jsonfilter -e '@.BackendState' -e '@.Health'
Expected results:
sh -nproduces no output.- The
grepcommand findsrun_tailscale_command(),run_tailscale_up(), and onerun_tailscale_up $paramcall. BackendStatebecomesRunningafter startup.- The health array is empty when the node is healthy.
Part 3: Add Advanced Tailscale Options with UCI
Use extra_up_args for flags shown by:
tailscale up --help
Use extra_set_args for flags shown by:
tailscale set --help
Each uci add_list command adds exactly one command-line argument. Using the --flag=value form is recommended because it keeps the flag and its value in one list entry.
For example, the following configuration assigns a custom hostname, requests an ACL tag, enables Tailscale SSH, and enables the Tailscale web client:
uci add_list tailscale.settings.extra_up_args='--hostname=GL-MT6000'
uci add_list tailscale.settings.extra_up_args='--advertise-tags=tag:router'
uci add_list tailscale.settings.extra_set_args='--ssh=true'
uci add_list tailscale.settings.extra_set_args='--webclient=true'
uci commit tailscale
/usr/bin/gl_tailscale restart
sleep 5
tailscale status
Only use tags permitted by your Tailscale or Headscale access policy. An unauthorized tag can prevent registration.
To inspect the additional arguments currently stored in UCI, run:
uci show tailscale | grep -E 'extra_(up|set)_args'
The order of the UCI list entries is preserved. The extra up arguments are placed after GL.iNet's generated arguments, so a later duplicate flag can override the corresponding GL.iNet default where the Tailscale CLI permits repeated flags.
Part 4: Example — Connect the Router to a Self-hosted Headscale Server
Headscale supports both interactive registration and pre-authentication keys. This example uses a one-time pre-authentication key so that the router can register non-interactively.
Step 1: Prepare Headscale
On the Headscale server, create a user if required and list its ID:
headscale users create router
headscale users list
Create a one-time pre-authentication key. Replace <USER_ID> with the appropriate user ID:
headscale preauthkeys create --user <USER_ID>
By default, a Headscale pre-authentication key is single-use and has a limited lifetime. Copy the returned key for the next step.
Step 2: Configure and Register the Router
The following block replaces any existing extra_up_args list. Edit the first two values, then copy and paste the entire block into the router's SSH terminal.
Only the Headscale URL and hostname are stored in UCI. The pre-authentication key is used directly for the one-time registration command and is never added to the UCI configuration.
HEADSCALE_URL='https://headscale.example.com'
HEADSCALE_AUTH_KEY='REPLACE_WITH_YOUR_ONE_TIME_PREAUTH_KEY'
uci -q delete tailscale.settings.extra_up_args
uci add_list tailscale.settings.extra_up_args="--login-server=${HEADSCALE_URL}"
uci add_list tailscale.settings.extra_up_args='--hostname=GL-MT6000'
uci commit tailscale
# Perform registration once without storing the pre-authentication key in UCI.
/usr/sbin/tailscale up \
--reset \
--accept-routes \
--accept-dns=false \
--login-server="${HEADSCALE_URL}" \
--hostname=GL-MT6000 \
--auth-key="${HEADSCALE_AUTH_KEY}" \
--force-reauth \
--timeout=30s
unset HEADSCALE_AUTH_KEY
# Hand control back to the GL.iNet wrapper. It will reapply its normal routes,
# DNS choice, firewall integration, and the persistent Headscale URL from UCI.
/usr/bin/gl_tailscale restart
sleep 5
tailscale status
tailscale status --json | jsonfilter -e '@.BackendState' -e '@.Self.HostName' -e '@.Self.Online'
The persistent UCI list now contains only:
--login-server=https://headscale.example.com
--hostname=GL-MT6000
The one-time key and --force-reauth are not stored in UCI, so they are not reused whenever GL.iNet restarts Tailscale. The key may still be visible in terminal scrollback while you enter the variable assignment. Use a short-lived, single-use key and close the SSH session when finished.
Step 3: Verify the Node on Headscale
On the Headscale server, run:
headscale nodes list
The GL-MT6000 node should appear online.
Part 5: Remove or Replace Individual Options
Remove an exact list entry with uci del_list:
uci del_list tailscale.settings.extra_set_args='--ssh=true'
uci commit tailscale
/usr/bin/gl_tailscale restart
Clear all custom arguments while keeping the wrapper modification installed:
uci -q delete tailscale.settings.extra_up_args
uci -q delete tailscale.settings.extra_set_args
uci commit tailscale
/usr/bin/gl_tailscale restart
If you are moving the router from Headscale back to the official Tailscale control server, clear the custom arguments, log out, and restart Tailscale. You will need to authenticate the router again:
uci -q delete tailscale.settings.extra_up_args
uci -q delete tailscale.settings.extra_set_args
uci commit tailscale
tailscale logout
/usr/bin/gl_tailscale restart
Part 6: Firmware Upgrades and Persistence
The two additional lists are stored in the standard UCI package /etc/config/tailscale. They are normally retained when Keep Settings is selected during a firmware upgrade.
The modified /usr/bin/gl_tailscale file may be replaced by a firmware upgrade. To include the current modified wrapper in the firmware backup list, copy and paste the following block into SSH:
grep -qxF '/usr/bin/gl_tailscale' /etc/sysupgrade.conf 2>/dev/null || \
echo '/usr/bin/gl_tailscale' >> /etc/sysupgrade.conf
grep -nxF '/usr/bin/gl_tailscale' /etc/sysupgrade.conf
The command is idempotent: it adds the path only if the exact entry is not already present. The second command displays the saved entry for verification.
Important: Preserving this file can carry an older GL.iNet wrapper into a newer firmware and replace fixes or behavioral changes supplied by that firmware. After every upgrade, verify Tailscale operation and the flags supported by the installed Tailscale version. If necessary, restore the new firmware's original wrapper from
/rom/usr/bin/gl_tailscaleand reapply Part 1.
After an upgrade:
- Check whether the extension is still present.
- Run
sh -n /usr/bin/gl_tailscale. - Verify the supported flags with
tailscale up --helpandtailscale set --help. - Restart Tailscale and confirm that it reaches the
Runningstate.
Use this command to check whether the extension is installed:
grep -q '^run_tailscale_command()$' /usr/bin/gl_tailscale \
&& echo 'UCI extension installed' \
|| echo 'UCI extension not installed'
Part 7: Restore the Original GL.iNet File
To completely remove the modification and all custom arguments, copy the entire block below, paste it into SSH once, and press Enter:
sh <<'GL_TAILSCALE_UCI_RESTORE'
set -eu
cp -p /rom/usr/bin/gl_tailscale /usr/bin/gl_tailscale
uci -q delete tailscale.settings.extra_up_args
uci -q delete tailscale.settings.extra_set_args
uci commit tailscale
sh -n /usr/bin/gl_tailscale
echo 'The original GL.iNet Tailscale wrapper has been restored.'
/usr/bin/gl_tailscale restart
sleep 5
tailscale status
GL_TAILSCALE_UCI_RESTORE
This restores the original wrapper that belongs to the currently installed firmware. It does not delete the Tailscale identity or state file.
Troubleshooting
Tailscale temporarily reports Starting
The GL.iNet wrapper currently gives tailscale up a three-second internal startup timeout. On some connections, the daemon needs several more seconds. Wait and check again:
sleep 10
tailscale status
tailscale status --json | jsonfilter -e '@.BackendState' -e '@.Health'
Tailscale reports an unknown flag
Check which subcommand supports the flag:
tailscale up --help
tailscale set --help
Move the entry to extra_up_args or extra_set_args as appropriate. A future Tailscale release may add, rename, or remove flags.
Headscale registration fails
Verify that:
- The Headscale URL includes
https://and is reachable from the router. - The certificate is valid for the Headscale hostname.
- The pre-authentication key has not expired or already been used.
- The router can reach the Headscale health endpoint.
- Any requested tag is permitted by the Headscale policy.
For production deployments, Headscale recommends HTTPS on TCP port 443.
Restore before further troubleshooting
If Tailscale no longer starts and the cause is unclear, restore the original file:
cp -p /rom/usr/bin/gl_tailscale /usr/bin/gl_tailscale
/usr/bin/gl_tailscale restart