Skip to content

Getting Started Guide

lfr-tunnel is a client-server utility. The client CLI (lfr-tunnel) runs on your local machine and establishes a secure tunnel to the gateway server (lfr-tunneld) running on a public VPS.

Info

The client CLI binary is of no use on its own. It cannot establish a tunnel without connecting to a running gateway server, and it requires a valid Personal Access Token (PAT) to authenticate.

This guide walks you through installing the client, registering for access, claiming your token, and running your first tunnel.


Overview of the Flow

[ Developer Laptop ]                                         [ Gateway Server ]
   (lfr-tunnel CLI)                                            (lfr-tunneld)
         │                                                           │
         │ 1. Submit Registration ───────────────────────────────────►
         │    (Provides email & requested subdomain)                 │
         │                                                           │
         │                        2. Admin Approves                  │
         │                           (Validates request)             │
         │                                                           │
         │ 3. Claim PAT Token ◄──────────────────────────────────────┘
         │    (Via approval email link)
         │
         │ 4. Store Token
         │    (Saved in ~/.lfr-tunnel/token)
         │
         │ 5. Connect Tunnel ────────────────────────────────────────► Exposes local ports

Step 1: Install the Client

Before registering, install the lfr-tunnel client for your operating system.

Using a package manager ensures you get automated integrity validation (SHA-256 checks) and clean path management.

macOS / Linux (Homebrew)

brew tap peterrichards-lr/tap
brew trust peterrichards-lr/tap
brew install lfr-tunnel

Windows (Scoop)

scoop bucket add peterrichards-lr https://github.com/peterrichards-lr/scoop-bucket
scoop install lfr-tunnel

Direct Script Fallback

If package managers are not available on your system, use the direct installation scripts.

macOS / Linux

curl -sSfL https://raw.githubusercontent.com/peterrichards-lr/lfr-tunnel/master/pkg/server/static/install.sh | sh

Windows (PowerShell)

iwr https://raw.githubusercontent.com/peterrichards-lr/lfr-tunnel/master/pkg/server/static/install.ps1 | iex

Verify your installation:

lfr-tunnel -version


Step 2: Register for Access

Access to the gateway server is authenticated via a Personal Access Token (PAT) associated with your user account.

1. Submit a Registration Request

To request access, send a registration request to the gateway server.

  • For Liferay Sales Engineering Team (connecting to the hosted SE gateway): Submit a request using the hosted server:

    curl -s -X POST \
      -H "Content-Type: application/json" \
      -d '{"email": "your.name@liferay.com", "requested_subdomain": "your-name-se"}' \
      https://tunnel.lfr-demo.se/api/register-request
    
    (Replace your.name@liferay.com with your official email, and your-name-se with your desired default subdomain).

  • For Self-Hosted Gateways: Replace https://tunnel.lfr-demo.se with your own gateway's URL:

    curl -s -X POST \
      -H "Content-Type: application/json" \
      -d '{"email": "admin@example.com", "requested_subdomain": "my-subdomain"}' \
      https://tunnel.yourdomain.com/api/register-request
    

You will receive a terminal output confirming your request has been successfully submitted and is pending admin approval.

2. Verify Your Email & Wait for Approval

  1. Check your inbox for a Verification Email from the gateway. Click the link inside to verify that you own the email address.
  2. Once verified, the gateway administrator receives a notification.
  3. Once the administrator approves your request, you will receive an Approval Email containing a link to claim your token.

3. Claim Your Token

Click the link in your approval email, or run the following curl command using the claim token found in the email:

curl -s "https://tunnel.lfr-demo.se/api/claim?token=<claim-token-from-email>"

The gateway will respond with your Personal Access Token (PAT) (e.g., lfr_pat_abc123...).

Warning

This token is shown only once. Copy it immediately and store it securely.


Step 3: Authenticate and Store Your Token

To make using lfr-tunnel seamless, the client CLI looks for a stored PAT in your home directory (~/.lfr-tunnel/token or %USERPROFILE%\.lfr-tunnel\token). Once saved, the client will automatically load it on every run without needing any -token flags.

There are two ways to generate, claim, and save your token:

The client includes an interactive Magic Handoff flow that automatically completes token generation and saves it to your configuration directory with zero manual copying:

  1. In your terminal, run the login command:
    lfr-tunnel login
    
  2. Your default web browser will open to the gateway's User Portal.
  3. Authenticate on the portal (using your approved email and magic link).
  4. Upon logging in, the portal will securely hand off a newly generated token back to your local client terminal session.
  5. The CLI saves the token automatically:
    ✅ Successfully authenticated! Your token has been saved securely to ~/.lfr-tunnel/token
    

Option B: Manual Clipboard Configuration

If you claimed your token manually via curl or generated one in the User Portal web interface, you can save it to the default path yourself:

macOS / Linux

mkdir -p ~/.lfr-tunnel
echo "lfr_pat_your-token-here" > ~/.lfr-tunnel/token
chmod 600 ~/.lfr-tunnel/token

Windows (PowerShell)

New-Item -ItemType Directory -Force -Path "$Home\.lfr-tunnel"
Set-Content -Path "$Home\.lfr-tunnel\token" -Value "lfr_pat_your-token-here"

Danger

Never commit your PAT to source control. Storing the token in ~/.lfr-tunnel/token ensures it is kept completely outside your development workspace.

Storing it somewhere else

~/.lfr-tunnel/token is found automatically, but it is the only path that is. If you keep credentials elsewhere — a password-manager mount, a per-project directory, ~/.config — name that file with token_file: in ~/.lfr-tunnel/config.yaml instead:

token_file: "~/.config/lfr-tunnel/pat"

The point is the same as Option B's: the token stays out of the config file, which is the file that gets pasted into support threads and copied between machines. See Keeping the token out of this file.


Option C: Restricted Secrets File (Advanced & Secure)

This matches the security practices taken in LDM. Instead of storing the token raw in ~/.lfr-tunnel/token, you store it in a restricted variables file which you source in your shell profile.

On macOS / Linux (Bash or Zsh)

  1. Create the restricted folder and secrets file:
    mkdir -p ~/.config/lfr
    touch ~/.config/lfr/secrets
    chmod 600 ~/.config/lfr/secrets
    
  2. Add your token variable to the file:
    echo 'export LFT_CLIENT_TOKEN="your_actual_token_here"' >> ~/.config/lfr/secrets
    
  3. Source the file in your profile by adding this to the bottom of your ~/.zshrc or ~/.bashrc:
    [ -f ~/.config/lfr/secrets ] && source ~/.config/lfr/secrets
    

On Windows (PowerShell)

  1. Run these commands in PowerShell to create the secrets folder/file and restrict permissions to only your explicit user account:
    New-Item -ItemType Directory -Path "$HOME\.config\lfr" -Force
    $SecretFile = New-Item -ItemType File -Path "$HOME\.config\lfr\secrets.ps1" -Force
    
    # Restrict permissions so ONLY you can access it
    $Acl = Get-Acl $SecretFile.FullName
    $Acl.SetAccessRuleProtection($true, $false)
    $User = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
    $Rule = New-Object System.Security.AccessControl.FileSystemAccessRule($User, "FullControl", "Allow")
    $Acl.AddAccessRule($Rule)
    Set-Acl $SecretFile.FullName $Acl
    
  2. Add the token to the file:
    Set-Content -Path "$HOME\.config\lfr\secrets.ps1" -Value '$env:LFT_CLIENT_TOKEN="your_actual_token_here"'
    
  3. Load it automatically on shell startup. Open your PowerShell profile (notepad $PROFILE) and add:
    if (Test-Path "$HOME\.config\lfr\secrets.ps1") { . "$HOME\.config\lfr\secrets.ps1" }
    

The client CLI (lfr-tunnel) will automatically load your token from these files if it is not configured via other mechanisms.


Step 4: Run Your First Tunnel

Once your token is saved, you can run the client. If you do not tell it which ports to publish, it works them out — by scanning the Liferay Workspace you are standing in, or, if you are not in one, by looking for a running Liferay or LDM instance. Port 8080 is what you get when neither turns anything up.

Naming ports yourself — -ports, ports: in your config file, or LFT_CLIENT_PORTS — switches all of that off and publishes exactly what you asked for.

Zero-Config Workspace Mode (LDM/Workspaces)

Navigate to your Liferay Workspace root directory and run:

lfr-tunnel -subdomain your-name-se

The client will: 1. Scan for active client extensions and detect their development ports automatically. 2. Authenticate with the stored PAT. 3. Print the live public HTTPS URLs where your local server and assets are now accessible.

The scan walks the current directory for client-extension.yaml files and takes the port from each. Port 8080 is always the first thing published, on your plain subdomain; each client extension gets its own subdomain suffixed with the extension's key, so an extension named my-remote-app on port 4001 is served at your-name-se-my-remote-app.

This did not work until #1710. The client shipped with ports pre-set to [8080], which it could not tell apart from your having asked for 8080, so the scan above never ran and only 8080 was ever published. If you had worked around it with an explicit ports: list, that list still wins — delete it to get the scan.

Port-Specific Standalone Mode (Tomcat/Docker)

If you are running a standalone Tomcat bundle on port 8080 without a Liferay Workspace:

lfr-tunnel -subdomain your-name-se -ports 8080

Outside a workspace, leaving -ports off instead asks the client to find the instance itself: it reads docker ps for a container whose name or image mentions liferay, dxp or ldm and takes the ports it publishes, and failing that probes 8080, 13000 and 3000 on 127.0.0.1. Naming the port explicitly, as above, is still the more predictable option when you already know it.

That same pass also decides where to forward, not only what to publish: the host it found the instance on becomes target_host, unless you supplied one with -target-host, LFT_TARGET_HOST or target_host: — all three of which beat it. A discovered localhost is normalised to 127.0.0.1 first, because localhost often resolves to ::1 ahead of the IPv4 address, and where ::1 is dropped rather than refused that costs you a hang mid-demo instead of a fast fallback. With nothing set and nothing discovered you still get 127.0.0.1. The full order is in Leaving target_host unset.

Choosing Which Gateway to Use

Most people need none of this: the client ships knowing its gateway, fetches the list of available ones, and picks whichever answers fastest.

If you do need to name one, the flag you choose changes the behaviour:

How you supply it Picks the closest Fails over
-bootstrap <url>
server_url: in your client config file
-pin <url> ❌ pinned to that gateway
LFT_SERVER_URL / LFT_CLIENT_SERVER / LFT_SERVER ❌ pinned
# Start from this gateway, but still pick the closest and fail over:
lfr-tunnel -bootstrap https://your-gateway.example.com -subdomain your-name-se

# Or persist it, with the same effect:
#   server_url: "https://your-gateway.example.com"

-pin pins deliberately -- use it when you want one specific gateway and nothing else. But if you pin a gateway on another continent you stay there however close an edge is, and your tunnel drops for the whole window if that gateway is scheduled to stop.

To prefer a region while keeping failover, use -prefer-region <name>. To re-run the latency probe after a gateway has come back, add -refresh-region once -- the election is otherwise cached for 24 hours.

Note

These three flags were renamed. -pin, -bootstrap and -prefer-region used to be -server, -gateway and -region -- names that said where the value came from rather than what it did to routing, which is how -server came to pin a US user to a European gateway for the life of a tunnel.

old new
-server <url> -pin <url>
-gateway <url> -bootstrap <url>
-region <name> -prefer-region <name>

The old spellings still work. They set the same thing and print a deprecation warning, so existing scripts and aliases keep running -- but they will be removed, and giving both spellings of one setting with different values is an error rather than a silent winner.

Tip

server_url: above goes in the client config file, ~/.lfr-tunnel/config.yaml. It is worth knowing that file exists: it holds every setting you would otherwise retype -- subdomain, ports, target host, access controls -- and it is the only place a gateway can be named without pinning. See the Client Configuration File reference for every key, its default, and how it interacts with flags and environment variables.


Running in the Background & Start on Login

If you want the tunnel client to run silently or initialize automatically when you log into your machine, you can configure background execution and autostart configurations.

1. Headless Background Execution (CLI)

You can launch and manage the client in the background directly from your terminal using process control flags:

  • Start in Background: Runs the tunnel as a detached daemon process:
    lfr-tunnel -background -subdomain your-name-se
    
  • Check Status: Verifies if the daemon is active and prints the running PID and leased public URLs:
    lfr-tunnel -status
    
  • Stop Daemon: Terminates the background process cleanly:
    lfr-tunnel -stop
    
  • Status as JSON: The same information as -status, machine-readable, for a script or a monitoring check rather than a human:
    lfr-tunnel -status-json
    

Other flags worth knowing

These are defined by the client and were, until now, documented nowhere -- found by rendering the flag parser rather than reading the source (#2081):

Flag Default What it does
-inspector-port <port> 4040 Local port for the Inspector web UI. Change it when something else already holds 4040 -- the Inspector is how you read request bodies, so losing the port loses the feature.
-no-tui off Disables the interactive terminal dashboard and prints plain log lines instead. What you want when the client runs under a CI job, a wrapper script, or anything that is not a terminal.
-status-json off -status as JSON.
-check-version off Asks the gateway what client version it requires and prints the answer as JSON, without starting a tunnel. Use it to find out whether an upgrade is due before one is forced.
-theme <name> unset Local UI theme: light, dark, system or time.
-log-dir <path> ~/.lfr-tunnel/logs Where the persistent traffic and error logs are written.

2. Autostart on Login

You can install startup items that launch the client automatically when your user logs in.

Headless CLI Client

To configure the headless background tunnel daemon to launch on login: * macOS / Linux / Windows: Run the subcommand:

lfr-tunnel install-service

  • System Details:
  • macOS: Creates a LaunchAgent plist at ~/Library/LaunchAgents/com.liferay.tunnel.plist.
  • Windows: Installs a hidden script at ~\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\lfr-tunnel.vbs.
  • Linux: Creates and registers a systemd user service at ~/.config/systemd/user/lfr-tunnel.service. You can monitor it using:
    systemctl --user status lfr-tunnel.service
    

System Tray GUI Client

Start the tray with:

lfr-tunnel -gui

The tray connects on startup. It brings the tunnel up with exactly the arguments its Connect menu item would use -- so every flag you give the GUI applies to the tunnel it starts, and lfr-tunnel -gui -prefer-region apac lands in apac without anyone clicking anything. If a client is already running, the tray attaches to that one instead of starting a second.

To start the tray as a control surface only, leaving the tunnel down until you ask for it:

lfr-tunnel -gui -no-autoconnect

This is worth knowing before you enable the autostart below: with it on, the default means a tunnel is up as soon as you log in.

To configure the System Tray / Menu Bar utility to launch on login: * Tray Toggle: Simply open the system tray menu and click Launch on Login (displays a checkmark when enabled). * CLI Command: Alternatively, register the autostart items using subcommands: * Enable GUI autostart: lfr-tunnel install-gui-service * Disable GUI autostart: lfr-tunnel uninstall-gui-service

  • System Details:
  • macOS: Registers ~/Library/LaunchAgents/com.liferay.tunnel.gui.plist.
  • Windows: Installs ~\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\lfr-tunnel-gui.vbs.
  • Linux: Creates a desktop autostart entry at ~/.config/autostart/lfr-tunnel-gui.desktop.

Client Lifecycle Hooks & Failover Automation

lfr-tunnel runs user-configured shell commands when the tunnel moves between gateways -- when a gateway announces it is stopping, and around the failover or failback that follows. They exist so the things that care about the tunnel's public URL (a Liferay virtual host, a webhook registration, a DNS record) can be updated without watching the logs.

Configuration (~/.lfr-tunnel/config.yaml)

Add script paths or commands under the hooks section. Anything you leave out is simply not run. Every other key in this file is described in the Client Configuration File reference:

hooks:
  warning_received: "/usr/local/bin/on-gateway-warning.sh" # The gateway has announced it is stopping
  stopping: "/usr/local/bin/on-gateway-stopping.sh"        # Right before the old tunnel is torn down
  stopped: "/usr/local/bin/on-gateway-stopped.sh"          # The old tunnel has closed
  starting: "/usr/local/bin/on-gateway-starting.sh"        # Before the client re-registers elsewhere
  started: "/usr/local/bin/on-gateway-started.sh"          # A session is live again on the new gateway

Each value is a command line, not a file path with special handling -- it is passed to /bin/sh -c (cmd.exe /c on Windows), so pipes, arguments and shell syntax all work.

When each hook fires

The four session hooks fire around a move, not around the process. A move is a failover after a fault, a failback to the primary region, or a planned migration ahead of an announced gateway stop.

Event Fires
warning_received The connected gateway announces it is going down. Once per announcement, not once per heartbeat -- the warning repeats for the whole countdown.
stopping The session is ending and the client is about to move, while the old tunnel is still standing.
stopped The old tunnel has closed and its session has been cancelled.
starting Before the client begins re-registering. The destination region is not chosen yet.
started A session has been re-established and the new endpoint recorded. Fires for both a failback and a failover.

In a planned migration the full sequence is warning_receivedstoppingstoppedstartingstarted. An unannounced failure produces the same sequence without warning_received. If every candidate region is exhausted, started does not fire -- there is no new session to report.

A client that has nowhere to fail over to -- pinned with -pin, or offered no region list -- re-registers with the gateway it is already on when its session ends, which is what carries it across a gateway restart (#1946). That fires stoppedstartingstarted, with no stopping: nothing announced the stop, so there was no moment before the tunnel came down at which to run it.

Three cases where nothing fires, deliberately:

  • The first connection and a normal shutdown. starting/started are about moving to a different gateway; the initial connect prints its URLs and stopping/stopped on Ctrl+C would delay the exit for a hook whose work is already done.
  • A client pinned with -pin, for warning_received and stopping. A pinned client never fails over (#1275), so it is not warned off a gateway and never moves ahead of one. It does now reconnect to its own gateway after a restart (#1946), and that fires stoppedstartingstarted like any other re-established session.
  • A hook you have not configured. No shell is spawned.

Contextual environment variables

Every hook receives all five, always, plus the environment the client itself was started with. Values that are not knowable for that event are empty (or 0):

Variable Value
LFT_EVENT The event name: warning_received, stopping, stopped, starting, started.
LFT_NODE_ID The gateway that announced a stop. Empty when the move was not triggered by an announcement. It keeps naming the gateway being left for the rest of the sequence.
LFT_SECONDS_REMAINING Seconds until the announced stop, counting down as the move proceeds. 0 when no stop has been announced.
LFT_FAILOVER_REGION The region now serving the tunnel. Set on started; empty on the others, because the destination is not elected until the move is underway.
LFT_SUBDOMAIN The leased subdomain prefix.

What a hook can and cannot do

  • A hook cannot veto a transition. Its exit status is logged and discarded. The move has already been decided by the time a hook runs, and letting a script refuse one would leave the tunnel down with no way to recover it. A failing hook does not stop the next one.
  • A hook is bounded at 15 seconds. After that it is killed and the client carries on. The bound covers a hook that backgrounds a child as well, so a stray & cannot wedge the client.
  • The four session hooks run in order, one at a time, so stopped has finished before starting begins. The cost of that guarantee is that a slow hook delays the move by up to its 15-second bound. warning_received is the exception: it runs concurrently, because the point of the warning is to move sooner, and blocking the heartbeat to run a script would work against that.
  • Failures are visible. Every run is logged, with the hook's combined output on failure.

Client Logs & Diagnostics

The client keeps persistent logs under ~/.lfr-tunnel/logs/, for both foreground and background runs. They are the first place to look when a tunnel misbehaved and the terminal output is gone.

File Contents
traffic-<subdomain>.log One JSON object per proxied HTTP request: timestamp, method, path, status, duration, target port and the region serving it.
error-<subdomain>.log Structured diagnostic events — failover, failback, lease eviction, exhausted regions — with the fields explaining each.
client-<subdomain>.log Console output from a -background run.

All three are JSON Lines and are rotated rather than overwritten: the previous run is kept as .1, up to three generations, each capped at 8 MiB. A client that exits unexpectedly leaves its log behind instead of erasing it on the next start.

Because they are JSON Lines, they can be filtered directly:

# Every request that failed
jq 'select(.status >= 400)' ~/.lfr-tunnel/logs/traffic-your-name-se.log

# What happened during the last region switch
jq 'select(.event | startswith("failover"))' ~/.lfr-tunnel/logs/error-your-name-se.log

# Slowest requests first
jq -s 'sort_by(-.dur_ms) | .[0:10]' ~/.lfr-tunnel/logs/traffic-your-name-se.log

Recording request and response bodies

Bodies are not written by default. They routinely carry OAuth tokens, session cookies and customer data, and these files persist on disk. When you need them — debugging an incoming webhook payload, for instance — enable them explicitly:

lfr-tunnel -subdomain your-name-se -log-bodies

Bodies are capped at 10 KB each. Prefer the Inspector at http://localhost:4040 for casual payload inspection: it holds the last 100 requests in memory only, so nothing reaches disk.


Need Help?

  • Common Errors:
  • [Error] Unauthorized: Your token may be invalid or revoked. Check ~/.lfr-tunnel/token and verify your token is copied correctly.
  • [Error] Subdomain already registered: Another active user is currently using the requested subdomain prefix. Try a different -subdomain flag.
  • Detailed Guides:
  • For advanced setup and Liferay virtual host configurations, see the Liferay SE Guide.
  • For details on self-hosting your own gateway, see the Server Setup Guide.

Last Updated: 2026-09-20 | Last Reviewed: 2026-09-20