Skip to content

Conversation

@asophila
Copy link

Add USB Installation Support for Low-Memory Routers

Problem

Many GL.iNet routers, particularly older models like the GL-MT300N-V2 (Mango) (I had 3 of these at home, plus an old GL-AR150), have extremely limited internal flash storage. The current installation method requires at least 15MB of free space, which is often unavailable on these devices even after removing unnecessary packages.

This makes it impossible for users with low-memory routers to:

  • Install or update Tailscale using this updater
  • Use Tailscale without manual workarounds

Solution Overview

This PR adds a new --use-usb flag that enables USB-based installation for routers with insufficient internal storage. The implementation:

  1. Installs Tailscale binaries to USB storage instead of internal flash
  2. Creates persistent symlinks from /usr/sbin/ to USB-mounted binaries
  3. Auto-mounts USB on boot and hotplug using a robust hotplug script
  4. Survives firmware upgrades when configured with sysupgrade.conf
  5. Maintains full backward compatibility with existing installation methods

Key Changes

New Features

  • --use-usb flag: Enables USB installation mode
  • Automatic USB detection: Searches for USB devices on common paths (/dev/sda1, /dev/sdb1)
  • USB preparation: Formats USB as ext4 with label "tailscale" for easy identification
  • Auto-mount hotplug script: /etc/hotplug.d/block/10-mount-tailscale-usb
  • Custom init.d service: Ensures USB is mounted before starting Tailscale
  • Separate installation functions: install_tiny_tailscale_usb() and install_tailscale_usb()
  • USB-specific outro: invoke_outro_usb() with USB-relevant information

Modified Functions

  • preflight_check(): Now suggests --use-usb when low storage is detected
  • collect_user_preferences(): Works with both installation methods
  • upgrade_persistance(): New upgrade_persistance_usb() for USB-specific persistence
  • Main execution flow: Branches to USB path when --use-usb is set

Directory Structure on USB

/mnt/usb_tailscale/
├── bin/
│   ├── tailscale -> tailscaled
│   └── tailscaled
├── data/
└── backups/

User Experience

Before (low-memory router):

$ ./update-tailscale.sh
❌ Not enough space available. Please free up some space and try again.
❌ The script needs at least 15 MB of free space. Available space: 3 MB

After (with USB):

$ ./update-tailscale.sh --use-usb
✅ USB found: /dev/sda1
✅ USB formatted successfully
✅ Tailscale installed to USB
✅ Installation will survive firmware upgrades

Testing

Successfully tested on:

  • GL-MT300N-V2 (Mango) - MIPS architecture, 16MB flash, 128MB RAM
  • Firmware: GL.iNet 4.x
  • USB drive: Various sizes (4GB+), formatted as ext4
  • Tested on clean (factory reset) devices, as well as for devices that already are using USB for memory expansion (tailscale update case)

Verified:

  • ✅ Installation completes successfully with <3MB internal storage
  • ✅ Tailscale daemon starts and runs from USB
  • ✅ Auto-mounts on boot via fstab
  • ✅ Survives router reboots
  • ✅ Configuration persists across firmware upgrades (with sysupgrade.conf)
  • ✅ Backward compatibility: Standard installation still works unchanged
  • --force, --ssh, and other flags work with USB mode

Backward Compatibility

Fully backward compatible:

  • No changes to existing installation paths
  • Standard installation (--no-tiny or default) works exactly as before
  • New USB path only activates with explicit --use-usb flag
  • All existing flags and features continue to work

Documentation Updated on readme.md

Help text updated:

--use-usb            Install to USB storage for low-memory routers

README additions:

  • Usage examples for USB installation
  • Requirements: USB drive (4GB+ recommended), formatted as ext4
  • Warning: USB must remain connected for Tailscale to function

Important Notes

⚠️ User must keep USB connected: This is clearly communicated in the outro message

⚠️ USB formatting warning: Users must explicitly type "yes" to confirm formatting (unless --force is used)

⚠️ Requires additional packages: block-mount, e2fsprogs, kmod-usb-storage, kmod-fs-ext4 (auto-installed)

Files Modified

  • update-tailscale.sh: Added ~450 lines for USB support

Files Created (by script)

  • /etc/hotplug.d/block/10-mount-tailscale-usb: Auto-mount script
  • /etc/init.d/tailscale: Custom init service for USB installation
  • /etc/fstab: Entry added for persistent mounting

AI Notice

These changes were created with assistance from Claude Code over many iterations with clear focus on extending current functionality. Testing was done manually.

Thank you for creating and maintaining this excellent tool! This feature would help many users with older GL.iNet routers continue using Tailscale.

claude and others added 9 commits November 10, 2025 15:14
This commit adds comprehensive USB installation functionality to support
routers with very limited internal storage (e.g., GL-MT300N-V2 Mango).

New features:
- New --use-usb parameter for USB-based installation
- Automatic USB detection and formatting (with user confirmation)
- Symlink-based installation to keep binaries on USB
- Auto-mount scripts (hotplug and init.d) for boot persistence
- USB-specific persistence configuration
- Helpful error messages suggesting USB mode for low-memory devices

Key components:
- detect_usb_device(): Finds USB storage devices
- setup_usb_storage(): Formats and mounts USB drive
- create_usb_hotplug_script(): Auto-mount on USB insertion
- create_usb_init_script(): Boot-time service for USB-based Tailscale
- install_*_tailscale_usb(): USB installation variants
- upgrade_persistance_usb(): Firmware upgrade persistence
- invoke_outro_usb(): USB-specific completion message

Documentation:
- Updated README with --use-usb parameter documentation
- Added usage examples for USB installation
- Updated features list and requirements table

The implementation maintains full backward compatibility with existing
functionality. All existing parameters and workflows remain unchanged.

This contribution expands the script's compatibility to very low-memory
routers that previously couldn't run updated Tailscale versions.
Add USB installation support for low-memory routers
Changes:
- Show verbose output from opkg install to see package installation status
- Add verification step after e2fsprogs installation
- Display device information before formatting for debugging
- Improve unmount handling with process killing (fuser) and forced unmount
- Show actual mke2fs error output instead of suppressing it
- Add better unmount checks and retry logic with forced/lazy unmount
- Increase sleep time after unmounting to ensure device is ready
- Add more informative error messages for USB formatting failures

This should help diagnose and resolve USB formatting issues.
This commit significantly improves USB device handling to resolve
issues with devices that are already mounted or in use.

Key improvements:

1. Enhanced unmount logic:
   - Kill processes using the device with fuser before unmounting
   - Retry unmount up to 5 times with increasing delays
   - Use both forced (-f) and lazy (-l) unmount as fallbacks
   - Clear error messages if unmount fails after all attempts

2. Filesystem signature clearing:
   - Use wipefs to clear existing filesystem signatures
   - Fallback to dd if wipefs is not available
   - Prevents "device in use" errors from old filesystem metadata

3. Force formatting:
   - Try normal mke2fs first with single -F flag
   - If that fails, use double -F flag (mke2fs -F -F) to force
     formatting even if device appears to be in use or mounted
   - Show detailed diagnostics if formatting still fails

4. Better diagnostics:
   - Use lsof and fuser to show which processes are using device
   - Provide helpful manual recovery commands if automated fix fails

This resolves the "device is apparently in use" error that occurs
when trying to format a USB drive that was previously used or is
still mounted.
…el cache

This commit resolves persistent "device is busy" errors when formatting USB
drives on OpenWrt/GL.iNet routers, even after unmounting.

Root cause: The kernel maintains cached metadata about the device, and the
block-mount/automount services can remount devices automatically after
partition table changes.

Key fixes:

1. Stop automount services before formatting:
   - Temporarily stop /etc/init.d/fstab and block-mount
   - Prevents automatic remounting during the format process
   - Services are restarted after successful mount

2. Comprehensive kernel cache clearing:
   - sync to flush all pending writes
   - blockdev --flushbufs to clear device buffers
   - wipefs to remove filesystem signatures
   - partprobe/blockdev --rereadpt on parent device to reload partition table
   - Multiple sync points with appropriate delays

3. Fix partition table reload:
   - Extract parent device (e.g., /dev/sda from /dev/sda1)
   - Run partprobe and blockdev --rereadpt on parent, not partition
   - This properly refreshes the kernel's partition table cache

4. Enhanced fallback strategy:
   - First try: mke2fs -F (normal force)
   - Second try: mke2fs -F -F (double force, ignores mount check)
   - Third try: dd 100MB of zeros + retry mke2fs
   - This handles even the most stubborn device busy states

5. Better diagnostics:
   - Check for processes holding the device before formatting
   - Verify device isn't remounted by checking mount output
   - Show lsof and fuser output if formatting still fails
   - Helpful error messages for manual recovery

6. Longer delays at critical points:
   - 3 seconds after wipefs/partprobe before formatting
   - 2 seconds after dd before retry
   - Ensures kernel has time to process changes

This robust approach handles OpenWrt's aggressive automounting and kernel
caching, making USB formatting reliable even on devices that were previously
used or formatted.

Tested on GL.iNet routers with existing Tailscale installations on USB.
Changed approach from complex automatic handling to simple detection
with helpful manual instructions when formatting fails.

Key changes:

1. Detect existing Tailscale USB installations:
   - Check for LABEL="tailscale" on the device
   - If found, attempt to stop services before formatting

2. Simplified formatting logic:
   - Try basic unmount and format
   - No complex retry loops or multiple fallback strategies
   - Cleaner and more maintainable code

3. Clear manual instructions on failure:
   - Detects WHY formatting failed (existing installation vs other)
   - Provides exact copy-paste commands for the user
   - Different instructions for existing vs new installations
   - User runs commands, then re-runs script with --use-usb

4. Two scenarios handled:
   - New installation: Simple format usually works
   - Existing installation: If format fails, show how to:
     * Stop and disable Tailscale service
     * Remove old scripts
     * Unmount USB
     * Format USB
     * Re-run the updater

This approach is more reliable and user-friendly than trying to
automatically handle every edge case with kernel cache clearing,
forced unmounts, and multiple retry strategies.

Benefits:
- Simpler code (140 lines -> 60 lines)
- More reliable (fewer failure modes)
- Better user experience (clear instructions when help is needed)
- Easier to maintain and debug
Updated the manual fallback instructions to handle stubborn device-in-use
errors more effectively.

Changes:
- Use lazy unmount (umount -l) instead of regular unmount
- Add dd command to write zeros and clear kernel cache
- Add sync commands before and after dd
- Remove symlinks as part of cleanup
- Number steps clearly (1-5)
- Add fallback suggestion to reboot if still failing

The lazy unmount releases the device from the mount point even if
processes are using it, and dd writing zeros forces the kernel to
release its cache of the device, allowing mke2fs to work.
Reverted to the clearer, simpler manual instructions that are easier
to follow. The original version works well when users follow all steps
carefully.
Tailscale manual instructions for updates already using usb
@Admonstrator Admonstrator self-assigned this Nov 12, 2025
@Admonstrator Admonstrator added the enhancement New feature or request label Nov 12, 2025
@Admonstrator Admonstrator marked this pull request as draft November 12, 2025 13:28
@Admonstrator Admonstrator marked this pull request as ready for review November 12, 2025 13:29
Copilot finished reviewing on behalf of Admonstrator November 12, 2025 13:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds USB installation support for GL.iNet routers with extremely limited internal storage (< 15MB free). The --use-usb flag enables installation of Tailscale binaries to external USB storage, allowing users with very low-memory routers like the GL-MT300N-V2 Mango to use Tailscale.

Key changes:

  • New USB installation mode that formats and uses external USB storage for binaries
  • Auto-mount infrastructure with hotplug scripts and fstab entries for persistent USB mounting
  • Custom init.d service that ensures USB is mounted before starting Tailscale daemon
  • Enhanced preflight checks that suggest USB installation when low storage is detected

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 19 comments.

File Description
update-tailscale.sh Adds ~450 lines implementing USB detection, storage setup, hotplug/init scripts, USB-specific installation functions, and persistence configuration for USB-based installations
readme.md Documents the new --use-usb flag with usage examples, requirements, warnings about USB formatting, and important notes about keeping the USB connected

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +929 to +932
mount -t ext4 "$USB_DEV" "$USB_MOUNT_POINT"

if ! mount | grep -q "$USB_MOUNT_POINT"; then
log "ERROR" "Failed to mount USB"
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mount command lacks error handling. If the mount fails, the subsequent check at line 931 will catch it, but the mount command should explicitly check its return status to provide a more specific error message about why the mount failed.

Consider:

if ! mount -t ext4 "$USB_DEV" "$USB_MOUNT_POINT"; then
    log "ERROR" "Failed to mount USB device $USB_DEV to $USB_MOUNT_POINT"
    log "ERROR" "Check if the device is properly formatted and not in use"
    exit 1
fi
Suggested change
mount -t ext4 "$USB_DEV" "$USB_MOUNT_POINT"
if ! mount | grep -q "$USB_MOUNT_POINT"; then
log "ERROR" "Failed to mount USB"
if ! mount -t ext4 "$USB_DEV" "$USB_MOUNT_POINT"; then
log "ERROR" "Failed to mount USB device $USB_DEV to $USB_MOUNT_POINT"
log "ERROR" "Check if the device is properly formatted and not in use"
exit 1
fi
if ! mount | grep -q "$USB_MOUNT_POINT"; then
log "ERROR" "Failed to verify USB mount at $USB_MOUNT_POINT"

Copilot uses AI. Check for mistakes.
fi
# Recreate symlinks to ensure they point to USB
if [ -f /mnt/usb_tailscale/bin/tailscaled ]; then
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the hotplug script, symlinks should be created after explicitly removing any existing files. If /usr/sbin/tailscale or /usr/sbin/tailscaled exist as regular files, ln -sf may not behave as expected. Add rm -f before creating symlinks:

rm -f /usr/sbin/tailscale /usr/sbin/tailscaled
ln -sf /mnt/usb_tailscale/bin/tailscale /usr/sbin/tailscale
ln -sf /mnt/usb_tailscale/bin/tailscaled /usr/sbin/tailscaled
Suggested change
if [ -f /mnt/usb_tailscale/bin/tailscaled ]; then
if [ -f /mnt/usb_tailscale/bin/tailscaled ]; then
rm -f /usr/sbin/tailscale /usr/sbin/tailscaled

Copilot uses AI. Check for mistakes.
# Create symlink on USB (tailscale -> tailscaled)
ln -sf "$USB_MOUNT_POINT/bin/tailscaled" "$USB_MOUNT_POINT/bin/tailscale"

# Create symlinks in /usr/sbin to USB binaries
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar symlink issue: remove existing files before creating symlinks to avoid potential conflicts:

rm -f /usr/sbin/tailscale /usr/sbin/tailscaled
ln -sf "$USB_MOUNT_POINT/bin/tailscale" /usr/sbin/tailscale
ln -sf "$USB_MOUNT_POINT/bin/tailscaled" /usr/sbin/tailscaled
Suggested change
# Create symlinks in /usr/sbin to USB binaries
# Create symlinks in /usr/sbin to USB binaries
rm -f /usr/sbin/tailscale /usr/sbin/tailscaled

Copilot uses AI. Check for mistakes.
Comment on lines +997 to +1002
sleep 2
# Fallback to common device paths
elif [ -b /dev/sda1 ]; then
mkdir -p /mnt/usb_tailscale
mount -t ext4 /dev/sda1 /mnt/usb_tailscale 2>/dev/null
sleep 2
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The init script checks if USB is mounted but doesn't verify if the mount was successful after attempting to mount. If the mount operation fails silently, the script will continue and fail later when trying to access the binaries. Add a verification step after the mount attempt:

if ! mount | grep -q "/mnt/usb_tailscale"; then
    # Try to mount by label first
    if blkid | grep -q 'LABEL="tailscale"'; then
        mkdir -p /mnt/usb_tailscale
        mount LABEL=tailscale /mnt/usb_tailscale 2>/dev/null
        sleep 2
        # Verify mount succeeded
        if ! mount | grep -q "/mnt/usb_tailscale"; then
            logger -t tailscale "ERROR: Failed to mount USB by label"
            return 1
        fi
    # Fallback to common device paths
    elif [ -b /dev/sda1 ]; then
        mkdir -p /mnt/usb_tailscale
        mount -t ext4 /dev/sda1 /mnt/usb_tailscale 2>/dev/null
        sleep 2
        # Verify mount succeeded
        if ! mount | grep -q "/mnt/usb_tailscale"; then
            logger -t tailscale "ERROR: Failed to mount USB from /dev/sda1"
            return 1
        fi
    fi
fi
Suggested change
sleep 2
# Fallback to common device paths
elif [ -b /dev/sda1 ]; then
mkdir -p /mnt/usb_tailscale
mount -t ext4 /dev/sda1 /mnt/usb_tailscale 2>/dev/null
sleep 2
sleep 2
# Verify mount succeeded
if ! mount | grep -q "/mnt/usb_tailscale"; then
logger -t tailscale "ERROR: Failed to mount USB by label"
return 1
fi
# Fallback to common device paths
elif [ -b /dev/sda1 ]; then
mkdir -p /mnt/usb_tailscale
mount -t ext4 /dev/sda1 /mnt/usb_tailscale 2>/dev/null
sleep 2
# Verify mount succeeded
if ! mount | grep -q "/mnt/usb_tailscale"; then
logger -t tailscale "ERROR: Failed to mount USB from /dev/sda1"
return 1
fi

Copilot uses AI. Check for mistakes.
Comment on lines +961 to +962
sleep 2
LABEL=$(blkid -s LABEL -o value /dev/$DEVNAME 2>/dev/null)
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2-second sleep may not be sufficient to ensure the device is ready for operations. Consider increasing this to 3-5 seconds or implementing a retry loop to check if the device is truly ready before attempting to access it. On some systems, USB devices can take longer to initialize, especially slower/older USB drives.

Suggested change
sleep 2
LABEL=$(blkid -s LABEL -o value /dev/$DEVNAME 2>/dev/null)
# Wait up to 5 seconds for the device to be ready
for i in $(seq 1 5); do
LABEL=$(blkid -s LABEL -o value /dev/$DEVNAME 2>/dev/null)
[ -n "$LABEL" ] && break
sleep 1
done

Copilot uses AI. Check for mistakes.
chmod +x "$USB_MOUNT_POINT/bin/tailscale"
chmod +x "$USB_MOUNT_POINT/bin/tailscaled"

# Create symlinks in /usr/sbin to USB binaries
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same symlink issue: explicitly remove existing files first:

rm -f /usr/sbin/tailscale /usr/sbin/tailscaled
ln -sf "$USB_MOUNT_POINT/bin/tailscale" /usr/sbin/tailscale
ln -sf "$USB_MOUNT_POINT/bin/tailscaled" /usr/sbin/tailscaled
Suggested change
# Create symlinks in /usr/sbin to USB binaries
# Create symlinks in /usr/sbin to USB binaries
rm -f /usr/sbin/tailscale /usr/sbin/tailscaled

Copilot uses AI. Check for mistakes.
# USB Installation Functions
detect_usb_device() {
log "INFO" "Searching for USB device..."
sleep 2
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The 2-second sleep before checking for USB devices may be unnecessary on most systems. Consider either reducing this delay or making it conditional based on whether devices are already detected. This would speed up the installation process when the USB is already ready.

Suggested change
sleep 2

Copilot uses AI. Check for mistakes.
exit 1
fi
if [ ! -f "/tmp/tailscale/$TAILSCALE_SUBDIR_IN_TAR/tailscaled" ]; then
log "ERROR" "Tailscaled binary not found"
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, this error message should include the full path for consistency:

log "ERROR" "Tailscaled binary not found at /tmp/tailscale/$TAILSCALE_SUBDIR_IN_TAR/tailscaled"
Suggested change
log "ERROR" "Tailscaled binary not found"
log "ERROR" "Tailscaled binary not found at /tmp/tailscale/$TAILSCALE_SUBDIR_IN_TAR/tailscaled"

Copilot uses AI. Check for mistakes.
if [ "$FORCE" -eq 0 ]; then
printf "\033[31m┌────────────────────────────────────────────────────────────────────────┐\033[0m\n"
printf "\033[31m│ WARNING: USB FORMATTING │\033[0m\n"
printf "\033[31m│ This script will FORMAT: $USB_DEV │\033[0m\n"
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The device name in the warning message may not align properly in the box due to variable length. Consider using a fixed-width format or padding the device name to ensure the box borders align correctly regardless of device name length (e.g., /dev/sda1 vs /dev/sdb1).

Suggested change
printf "\033[31m│ This script will FORMAT: $USB_DEV │\033[0m\n"
printf "\033[31m│ This script will FORMAT: %-16s%-36s│\033[0m\n" "$USB_DEV" " "

Copilot uses AI. Check for mistakes.
Comment on lines +851 to +852
opkg update
opkg install block-mount e2fsprogs kmod-usb-storage kmod-fs-ext4
Copy link

Copilot AI Nov 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The opkg update and opkg install commands lack error handling. If the update fails (e.g., due to network issues or repository problems), the installation will continue and potentially fail later. Consider checking the return status and exiting if the package installation fails.

Example:

if ! opkg update; then
    log "ERROR" "Failed to update package lists"
    exit 1
fi
if ! opkg install block-mount e2fsprogs kmod-usb-storage kmod-fs-ext4; then
    log "ERROR" "Failed to install required packages"
    exit 1
fi
Suggested change
opkg update
opkg install block-mount e2fsprogs kmod-usb-storage kmod-fs-ext4
if ! opkg update; then
log "ERROR" "Failed to update package lists"
exit 1
fi
if ! opkg install block-mount e2fsprogs kmod-usb-storage kmod-fs-ext4; then
log "ERROR" "Failed to install required packages"
exit 1
fi

Copilot uses AI. Check for mistakes.
@Admonstrator Admonstrator added this to the Version 2 milestone Nov 13, 2025
@Admonstrator
Copy link
Owner

Thank you for taking the time to add this feature.
I'm planning to integrate it into version 2 of the script, since I need to change a few things anyway.

It may take a few more days, but it won't be forgotten!

@asophila
Copy link
Author

Thank you for taking the time to add this feature. I'm planning to integrate it into version 2 of the script, since I need to change a few things anyway.

It may take a few more days, but it won't be forgotten!

Happy to help. I love to extend the lifespan of old-but-working devices.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants