How to Add Advanced Tailscale Options to GL.iNet Routers via UCI

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 existing tailscale up command.
  • tailscale.settings.extra_set_args: arguments applied with tailscale set immediately after tailscale 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_tailscale and 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:

  1. You can connect to the router over SSH through its LAN address.
  2. Tailscale is pre-installed under Applications -> Tailscale in the GL.iNet Web Admin Panel, meaning the model you're using supports Tailscale
  3. You are comfortable restoring the router file from /rom if necessary.
  4. 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 -n produces no output.
  • The grep command finds run_tailscale_command(), run_tailscale_up(), and one run_tailscale_up $param call.
  • BackendState becomes Running after 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_tailscale and reapply Part 1.

After an upgrade:

  1. Check whether the extension is still present.
  2. Run sh -n /usr/bin/gl_tailscale.
  3. Verify the supported flags with tailscale up --help and tailscale set --help.
  4. Restart Tailscale and confirm that it reaches the Running state.

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

References

3 Likes