demo/ provides a Docker Compose environment for trying the main lssh connection patterns locally in one place.
From the client container, you can use lssh / lscp / lsftp / lssync / lsshfs / lsshell / lsmon to verify the following:
- Using the demo client itself as an SSH bastion that launches
lssh - Password-based SSH authentication
- Private key-based SSH authentication
- Multi-hop connections through an SSH proxy
- Nested SSH proxy chains through an intermediate private host
- HTTP and SOCKS5 proxy hops that sit behind an SSH-only intermediate host
- Connections through an HTTP proxy
- Connections through a SOCKS5 proxy
- Loading local settings with
local_rc - Wrapping remote
vim/tmuxwith local config generated byupdate_lvim/update_ltmux - One-way and bidirectional file sync with build-time demo fixtures
This Compose setup uses four Docker networks.
flowchart LR
client["client
frontend
172.31.0.10"]
password_ssh["password_ssh
frontend
172.31.0.21"]
key_ssh["key_ssh
frontend
172.31.0.22"]
over_proxy_ssh["over_proxy_ssh
backend
172.31.1.41"]
deep_proxy_ssh["deep_proxy_ssh
deepback
172.31.2.51"]
deep_http_proxy["deep_http_proxy
deepback: 172.31.2.61
finalback: 172.31.3.61"]
deep_socks_proxy["deep_socks_proxy
deepback: 172.31.2.62
finalback: 172.31.3.62"]
over_deep_http_ssh["over_deep_http_ssh
finalback
172.31.3.71"]
ssh_proxy["ssh_proxy
frontend: 172.31.0.31
backend: 172.31.1.31"]
http_proxy["http_proxy
frontend: 172.31.0.32
backend: 172.31.1.32"]
socks_proxy["socks_proxy
frontend: 172.31.0.33
backend: 172.31.1.33"]
client --> password_ssh
client --> key_ssh
client --> ssh_proxy
client -. via proxy .-> http_proxy
client -. via proxy .-> socks_proxy
ssh_proxy --> over_proxy_ssh
http_proxy -. via proxy .-> over_proxy_ssh
socks_proxy -. via proxy .-> over_proxy_ssh
over_proxy_ssh --> deep_proxy_ssh
over_proxy_ssh -. via proxy .-> deep_http_proxy
over_proxy_ssh -. via proxy .-> deep_socks_proxy
deep_http_proxy -. via proxy .-> over_deep_http_ssh
deep_socks_proxy -. via proxy .-> over_deep_http_ssh
frontendclientpassword_sshkey_sshssh_proxyhttp_proxysocks_proxy
backendssh_proxyhttp_proxysocks_proxyover_proxy_ssh
deepbackover_proxy_sshdeep_proxy_sshdeep_http_proxydeep_socks_proxy
finalbackdeep_http_proxydeep_socks_proxyover_deep_http_ssh
over_proxy_ssh belongs only to backend, so it is not directly reachable from client.
To connect to it, you must go through ssh_proxy, http_proxy, or socks_proxy.
deep_proxy_ssh belongs only to deepback, so it is reachable only through over_proxy_ssh.
over_deep_http_ssh belongs only to finalback, so it is reachable only after OverSshProxy and then either deep_http_proxy or deep_socks_proxy.
cd demo
docker compose up --build -d
docker compose exec --user demo client bashThe client container also opens SSH on host port 2222.
Its SSH daemon uses ForceCommand /usr/local/bin/demo-lssh-bastion.sh, and the same bastion wrapper can also be invoked directly inside the container for quick checks.
From the host, you can try:
# open the interactive lssh bastion session
ssh -t -p 2222 -i demo/client/home/.ssh/demo_lssh_ed25519 demo@127.0.0.1
# run a non-interactive check through the same forced command
ssh -p 2222 -i demo/client/home/.ssh/demo_lssh_ed25519 demo@127.0.0.1 -- --list
# or check the same bastion wrapper directly inside the client container
docker compose exec --user demo client /usr/local/bin/demo-lssh-bastion.sh --listAfter entering the client container, the demo configuration is available at /home/demo/.lssh.conf.
This configuration also serves as an example of the include feature, and the actual settings are split across ~/.lssh.d/*.toml.
For the OpenSSH import demo, the client container also includes
/home/demo/.ssh/config_match_demo.
That file contains Match originalhost, Match user, and Match localuser
examples that can be imported into lssh.
[includes]
path = [
"~/.lssh.d/servers_proxy.toml",
"~/.lssh.d/servers_direct.toml",
"~/.lssh.d/servers_match.toml"
]The split files are:
~/.lssh.d/servers_direct.tomlPasswordAuthKeyAuthLocalRcKeyAuth
~/.lssh.d/servers_proxy.tomlssh_proxyOverSshProxyOverHttpProxyOverSocksProxyOverNestedSshProxyOverNestedHttpProxyOverNestedSocksProxy
~/.lssh.d/servers_match.tomlConditionalOverProxyConditionalNestedProxy
~/.lssh.conf also defines shared settings in [common] and the proxy entries http_proxy, socks_proxy, deep_http_proxy, and deep_socks_proxy.
~/.lssh.conf defines the following targets:
PasswordAuth- Password-authenticated server
KeyAuth- Private key-authenticated server
OverSshProxy- Private server reached through an SSH proxy
OverHttpProxy- Private server reached through an HTTP proxy
OverSocksProxy- Private server reached through a SOCKS5 proxy
OverNestedSshProxy- Private server reached through
OverSshProxyas a second SSH hop
- Private server reached through
OverNestedHttpProxy- Private server reached through
OverSshProxyand then an HTTP proxy
- Private server reached through
OverNestedSocksProxy- Private server reached through
OverSshProxyand then a SOCKS5 proxy
- Private server reached through
LocalRcKeyAuth- Private key-authenticated server with
local_rc = "yes"enabled
- Private key-authenticated server with
ConditionalOverProxy- Demo target that switches between direct access and
ssh_proxybased on the local IP
- Demo target that switches between direct access and
ConditionalNestedProxy- Demo target that switches between deep HTTP and SOCKS5 proxy routes based on the local IP
The demo client is attached to frontend as 172.31.0.10, so the conditional match examples are written to match that network first.
They show how server.<name>.match.<branch> can override only the fields that need to change.
[server.ConditionalOverProxy]
addr = "172.31.1.41"
key = "~/.ssh/demo_lssh_ed25519"
note = "direct on backend, ssh_proxy on frontend"
[server.ConditionalOverProxy.match.frontend_via_ssh_proxy]
priority = 1
when.local_ip_in = ["172.31.0.0/24"]
proxy = "ssh_proxy"
note = "frontend clients use ssh_proxy"
[server.ConditionalOverProxy.match.outside_demo]
priority = 90
when.local_ip_not_in = ["172.31.0.0/24", "172.31.1.0/24", "172.31.2.0/24", "172.31.3.0/24"]
ignore = trueInside the client container, frontend_via_ssh_proxy is selected because the local IP is 172.31.0.10.
That makes ConditionalOverProxy behave like a normal host entry that transparently goes through ssh_proxy.
Inside the client container, you can try commands like these:
# List configured targets
lssh --list
# Password authentication
lssh --host PasswordAuth
# Private key authentication
lssh --host KeyAuth
# Connect to the private server through an SSH proxy
lssh --host OverSshProxy
# Connect to the private server through an HTTP proxy
lssh --host OverHttpProxy
# Connect to the private server through a SOCKS5 proxy
lssh --host OverSocksProxy
# Connect to the deeper private server through OverSshProxy
lssh --host OverNestedSshProxy
# Connect to the final private server through OverSshProxy and a deep HTTP proxy
lssh --host OverNestedHttpProxy
# Connect to the final private server through OverSshProxy and a deep SOCKS5 proxy
lssh --host OverNestedSocksProxy
# Connect with local_rc applied
lssh --host LocalRcKeyAuth
# Conditional routing example: on the demo client this uses ssh_proxy
lssh --host ConditionalOverProxy
# Conditional nested route example: on the demo client this uses deep_http_proxy
lssh --host ConditionalNestedProxyInside the client container, you can also test the OpenSSH import path with a dedicated sample file:
# inspect the Match-based OpenSSH config sample
sed -n '1,200p' ~/.ssh/config_match_demo
# generate lssh config from the sample OpenSSH config
lssh --generate-lssh-conf=~/.ssh/config_match_demo
# save it and inspect the imported hosts
lssh --generate-lssh-conf=~/.ssh/config_match_demo > /tmp/lssh-from-match.conf
lssh --file /tmp/lssh-from-match.conf --listThe sample file defines these import targets:
password-auth-match- Basic host with
Match originalhost
- Basic host with
key-auth-match- Host with
Match user demo
- Host with
over-proxy-match- Host with
Match localuser demo
- Host with
From the host, you can also treat client as a jump entrypoint that always launches lssh:
# choose from the configured hosts over SSH
ssh -t -p 2222 -i demo/client/home/.ssh/demo_lssh_ed25519 demo@127.0.0.1
# or forward arguments to the forced lssh command
ssh -p 2222 -i demo/client/home/.ssh/demo_lssh_ed25519 demo@127.0.0.1 -- --host OverNestedSshProxy hostname
# or reach the final host through OverSshProxy and deep_http_proxy
ssh -p 2222 -i demo/client/home/.ssh/demo_lssh_ed25519 demo@127.0.0.1 -- --host OverNestedHttpProxy hostnameThe nested SSH and nested HTTP examples are defined like this:
[server.OverSshProxy]
addr = "172.31.1.41"
key = "~/.ssh/demo_lssh_ed25519"
proxy = "ssh_proxy"
[server.OverNestedSshProxy]
addr = "172.31.2.51"
key = "~/.ssh/demo_lssh_ed25519"
proxy = "OverSshProxy"
[proxy.deep_http_proxy]
addr = "172.31.2.61"
port = "8888"
proxy = "OverSshProxy"
[server.OverNestedHttpProxy]
addr = "172.31.3.71"
key = "~/.ssh/demo_lssh_ed25519"
proxy = "deep_http_proxy"
proxy_type = "http"LocalRcKeyAuth is configured to transfer the following local files:
~/.demo_localrc/bash_prompt~/.demo_localrc/sh_alias~/.demo_localrc/sh_export~/.demo_localrc/sh_function~/.demo_localrc/generated/lvim.sh~/.demo_localrc/generated/ltmux.sh
The generated wrappers come from these editable local files:
~/.demo_localrc/vimrc~/.demo_localrc/tmux.conf~/.demo_localrc/bin/update_lvim~/.demo_localrc/bin/update_ltmux
After connecting, run echo $LSSH_LOCAL_RC, demo_whoami, or demo_localrc_status to confirm that local_rc has been applied.
LocalRcKeyAuth demonstrates the same pattern used in blacknon/dotfiles: keep the shell pieces in local_rc_file, then generate small wrapper functions that decode local vimrc / tmux.conf on demand.
Inside the client container:
# inspect the editable local files
ls -1 ~/.demo_localrc
# regenerate the shipped wrapper functions after editing vimrc / tmux.conf
update_lvim
update_ltmux
# or run both together
demo-refresh-localrcAfter connecting with lssh --host LocalRcKeyAuth, you can confirm that the wrappers are active:
# local_rc flag and generated functions
demo_localrc_status
# lvim wrapper should be loaded in the remote shell
declare -f lvim
# tmux should read ~/.demo_localrc/tmux.conf through ltmux
tmux start-server \; show -gv status-left \; show-environment -g LSSH_DEMO_TMUX_CONF \; kill-serverExpected results:
demo_localrc_statusreportslvim: readyandltmux: readydeclare -f lvimprints a function body that runsvim -u <(printf ...)tmuxprints[demo-localrc]andLSSH_DEMO_TMUX_CONF=enabled
If you edit ~/.demo_localrc/vimrc or ~/.demo_localrc/tmux.conf, run update_lvim and update_ltmux again before reconnecting so the generated wrapper files are refreshed.
client is expected to be unable to reach over_proxy_ssh directly.
You can confirm this by running the following inside the client container:
nc -zv 172.31.1.41 22
nc -zv 172.31.2.51 22
nc -zv 172.31.3.71 22Direct access should fail, while proxy-based connections such as lssh --host OverSshProxy, lssh --host OverNestedSshProxy, lssh --host OverNestedHttpProxy, and lssh --host OverNestedSocksProxy should succeed.
- Demo keys and passwords are included under
demo/with fixed values. Do not use them in production. - The client container also includes the OpenSSH client, so you can compare behavior with the
sshcommand. - Stop the demo environment with
docker compose down -v.