Skip to content

Build smaller Tailscale binary #1253

Build smaller Tailscale binary

Build smaller Tailscale binary #1253

name: Build smaller Tailscale binary
on:
workflow_dispatch:
inputs:
version:
description: 'Specific Tailscale version tag to build (e.g. v1.68.0). Leave empty to use latest.'
required: false
type: string
force_build:
description: 'Force build even if versions match'
required: false
default: false
type: boolean
schedule:
- cron: '0 3 * * *'
env:
SOFTWARE_NAME: "Tailscale"
FILE_NAME: "tailscaled"
REPO: "tailscale/tailscale"
REPO_SMALL: "Admonstrator/glinet-tailscale-updater"
GIT_AUTHOR_NAME: "Admonstrator"
jobs:
check-versions:
runs-on: ubuntu-latest
permissions:
contents: read
outputs:
TAG: ${{ steps.tag.outputs.TAG }}
TAG_SMALL: ${{ steps.tag_small.outputs.TAG_SMALL }}
SHOULD_BUILD: ${{ steps.compare.outputs.SHOULD_BUILD }}
steps:
- name: Get latest ${{ env.SOFTWARE_NAME }} tag
id: tag
run: |
if [ -n "${{ inputs.version }}" ]; then
latest_tag="${{ inputs.version }}"
echo "Using manually specified version: $latest_tag"
else
latest_tag=$(curl -s "https://api.github.com/repos/${{ env.REPO }}/releases/latest" | jq -r '.tag_name // empty')
echo "Latest ${{ env.SOFTWARE_NAME }} Tag: $latest_tag"
fi
echo "TAG=$latest_tag" >> "$GITHUB_OUTPUT"
- name: Get latest ${{ env.SOFTWARE_NAME }} Small tag
id: tag_small
run: |
latest_tag_small=$(
curl -s "https://api.github.com/repos/${{ env.REPO_SMALL }}/releases/latest" \
| grep -oP '"tag_name": "\K(.*)(?=")' || echo ""
)
echo "TAG_SMALL=$latest_tag_small" >> "$GITHUB_OUTPUT"
echo "Latest ${{ env.SOFTWARE_NAME }} Small Tag: $latest_tag_small"
- name: Compare upstream and local tags
id: compare
run: |
upstream_tag="${{ steps.tag.outputs.TAG }}"
local_tag="${{ steps.tag_small.outputs.TAG_SMALL }}"
should_build="false"
if [ -z "$upstream_tag" ]; then
echo "No upstream tag detected. Skipping build."
elif [ -z "$local_tag" ]; then
echo "No local release tag found. Build is required."
should_build="true"
elif [ "$upstream_tag" = "$local_tag" ]; then
echo "Tags are equal ($upstream_tag). Skipping build."
else
newest_tag=$(printf '%s\n%s\n' "$local_tag" "$upstream_tag" | sort -V | tail -n 1)
if [ "$newest_tag" = "$upstream_tag" ]; then
echo "Upstream tag ($upstream_tag) is newer than local tag ($local_tag)."
should_build="true"
else
echo "Local tag ($local_tag) is newer than upstream tag ($upstream_tag). Skipping build."
fi
fi
echo "SHOULD_BUILD=$should_build" >> "$GITHUB_OUTPUT"
build:
runs-on: ubuntu-latest
permissions:
contents: write
needs: check-versions
if: needs.check-versions.outputs.SHOULD_BUILD == 'true' || (github.event_name == 'workflow_dispatch' && (inputs.force_build || inputs.version != ''))
env:
TAG: ${{ needs.check-versions.outputs.TAG }}
strategy:
matrix:
go-version: [stable]
os: [linux]
platform:
- amd64
- arm
- arm64
- mips
- mipsle
- mips64
- mips64le
steps:
- name: Checkout ${{ env.SOFTWARE_NAME }} repository
uses: actions/checkout@v4
with:
repository: ${{ env.REPO }}
ref: ${{ env.TAG }}
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Download Go modules
run: go mod download
- name: Cross-compile
run: |
# Get version information
eval `CGO_ENABLED=0 go run ./cmd/mkversion`
# Generate custom build number based on GitHub run number, or use timestamp if unavailable
if [ -n "${GITHUB_RUN_NUMBER}" ]; then
CUSTOM_BUILD_NUMBER="${GITHUB_RUN_NUMBER}"
else
CUSTOM_BUILD_NUMBER="$(date +%s)"
fi
CUSTOM_VERSION_SUFFIX="-tiny.by.admon.${CUSTOM_BUILD_NUMBER}"
# Append custom version suffix to Tailscale version
VERSION_SHORT_CUSTOM="${VERSION_SHORT}${CUSTOM_VERSION_SUFFIX}"
VERSION_LONG_CUSTOM="${VERSION_LONG}${CUSTOM_VERSION_SUFFIX}"
echo "Original Tailscale version: ${VERSION_SHORT}"
echo "Custom build version: ${VERSION_SHORT_CUSTOM}"
# Prepare feature set for smaller binary
# 1. CORE & NETWORKING
KEEP="dns,osrouter,iptables,netstack,health,logtail,portmapper,usermetrics,useroutes,cli"
# 2. ROUTING & NODES
KEEP="${KEEP},advertiseexitnode,useexitnode,advertiseroutes"
KEEP="${KEEP},clientmetrics,cliconndiag,doctor"
# 4. SECURITY & AUTH
KEEP="${KEEP},acme,bakedroots,tailnetlock,unixsocketidentity"
# 5. EXTRA
KEEP="${KEEP},appconnectors,cachenetmap,colorable,conn25,debug,gro"
KEEP="${KEEP},hujsonconf,lazywg,listenrawdisco,portlist,serve,ssh,useproxy,relayserver"
# Finalize build tags
BUILD_TAGS=$(go run ./cmd/featuretags --min --add="${KEEP}")
# Set ldflags with custom version info and stripping
LDFLAGS="-w -s -X tailscale.com/version.longStamp=${VERSION_LONG_CUSTOM} -X tailscale.com/version.shortStamp=${VERSION_SHORT_CUSTOM}"
# Build the binary
if [[ "${{ matrix.platform }}" == "mipsle" ]]; then
CGO_ENABLED=0 GOOS=${{ matrix.os }} GOARCH=${{ matrix.platform }} GOMIPS=softfloat \
go build -tags="${BUILD_TAGS}" -trimpath -ldflags "${LDFLAGS}" \
-o "${{ env.FILE_NAME }}-${{ matrix.os }}-${{ matrix.platform }}" tailscale.com/cmd/${{ env.FILE_NAME }}
else
CGO_ENABLED=0 GOOS=${{ matrix.os }} GOARCH=${{ matrix.platform }} \
go build -tags="${BUILD_TAGS}" -trimpath -ldflags "${LDFLAGS}" \
-o "${{ env.FILE_NAME }}-${{ matrix.os }}-${{ matrix.platform }}" tailscale.com/cmd/${{ env.FILE_NAME }}
fi
echo "${VERSION_SHORT_CUSTOM}" > "version.txt"
- name: Upload built binary
uses: actions/upload-artifact@v4
with:
name: ${{ env.FILE_NAME }}-${{ matrix.os }}-${{ matrix.platform }}
path: ./${{ env.FILE_NAME }}-${{ matrix.os }}-${{ matrix.platform }}
- name: Upload version file
if: matrix.platform == 'amd64'
uses: actions/upload-artifact@v4
with:
name: version-file
path: ./version.txt
publish:
runs-on: ubuntu-latest
permissions:
contents: write
needs:
- build
- check-versions
if: needs.check-versions.outputs.SHOULD_BUILD == 'true' || (github.event_name == 'workflow_dispatch' && (inputs.force_build || inputs.version != ''))
env:
TAG: ${{ needs.check-versions.outputs.TAG }}
steps:
- name: Get UPX latest version
id: get-upx-version
run: |
upx_version=$(
curl -s https://api.github.com/repos/upx/upx/releases/latest \
| jq -r '.tag_name' \
| cut -c 2-
)
echo "UPX_VERSION=${upx_version}" >> "$GITHUB_ENV"
echo "version=${upx_version}" >> "$GITHUB_OUTPUT"
- name: Download UPX
env:
UPX_VERSION: ${{ steps.get-upx-version.outputs.version }}
run: |
wget -q "https://github.com/upx/upx/releases/latest/download/upx-${UPX_VERSION}-amd64_linux.tar.xz"
tar --to-stdout -xf "upx-${UPX_VERSION}-amd64_linux.tar.xz" \
"upx-${UPX_VERSION}-amd64_linux/upx" > "${PWD}/upx"
chmod -v +x "${PWD}/upx"
- name: Download built binaries
uses: actions/download-artifact@v4
with:
pattern: ${{ env.FILE_NAME }}-*
- name: Download version file
uses: actions/download-artifact@v4
with:
name: version-file
path: .
- name: Moving files
run: |
for dir in "${{ env.FILE_NAME }}-"*; do
mv -v "${dir}" "${dir}.d"
mv -v "${dir}.d/${{ env.FILE_NAME }}-"* .
rmdir -v "${dir}.d"
done
chmod -v +x "${{ env.FILE_NAME }}-"*
- name: Compress Binary with UPX
run: |
for file in "${{ env.FILE_NAME }}-"*; do
if [[ "$file" == *"mips64"* || "$file" == *"mips64le"* ]]; then
echo "Skipping UPX compression for $file due to unsupported format"
else
"${PWD}/upx" --lzma --best --no-progress "$file"
fi
done
- name: Create checksums
run: |
sha256sum "${{ env.FILE_NAME }}-"* > "checksums.txt"
- name: Checkout ${{ env.SOFTWARE_NAME }} Small repository
uses: actions/checkout@v4
with:
path: tools
repository: ${{ env.REPO_SMALL }}
fetch-tags: true
- name: Create tag in ${{ env.SOFTWARE_NAME }} Small repository
if: github.event_name == 'schedule'
run: |
cd tools
if git rev-parse --quiet --verify "refs/tags/${{ env.TAG }}"; then
echo "Tag already exists"
exit 0
else
echo "Tag does not exist, creating"
git tag "${{ env.TAG }}"
git push --tags
fi
- name: Create Release
uses: ncipollo/release-action@v1
with:
name: Small ${{ env.SOFTWARE_NAME }} ${{ env.TAG }}
tag: ${{ env.TAG }}
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
prerelease: false
removeArtifacts: true
replacesArtifacts: true
allowUpdates: true
artifacts: |
${{ env.FILE_NAME }}-*
checksums.txt
version.txt
body: |
Small ${{ env.SOFTWARE_NAME }} build ${{ env.TAG }}
For a complete changelog go to https://github.com/${{ env.REPO }}/releases/tag/${{ env.TAG }}
This release was created by:
* Building a combined binary of `tailscale` and `tailscaled` with the `ts_include_cli` build tag
* Optimized for router/embedded use - removes desktop/enterprise features while keeping essential router functionality
* Keeps critical features: DNS, routes, exit nodes, health checks, peer API, NAT traversal, and more
* Including `unixsocketidentity` feature for proper LocalAPI access control on Unix/OpenWrt systems
* Including `ssh` feature for Tailscale SSH support
* Compressing the binary with UPX
To use both programs, rename `tailscaled-OS-ARCH` to `tailscaled` and create a symbolic link (`ln -sv tailscaled tailscale`). When invoked as `tailscale`, it acts as the CLI; when invoked as `tailscaled`, it acts as the daemon.