frpc-ruby
The frp client as a Ruby gem: the upstream
frpc binary for your platform, plus a small wrapper that runs it and drives
its admin HTTP API from Ruby.
Packaged the way tailwindcss-ruby
is — one platform gem per target, each carrying the matching binary from the
official frp release, and a plain ruby gem that carries none.
Install
gem "frpc-ruby"
Bundler only fetches platform gems for platforms in your lockfile, so add the ones you deploy to:
bundle lock --add-platform x86_64-linux aarch64-linux arm64-darwin
BUNDLE_FORCE_RUBY_PLATFORM=true selects the binary-less gem — don't set it
unless you're supplying frpc yourself.
Use
The wrapper talks to frpc over its admin API, so the config must enable it:
serverAddr = "example.com"
serverPort = 7000
webServer.addr = "127.0.0.1"
webServer.port = 7400
webServer.user = "admin"
webServer.password = "hunter2"
[[proxies]]
name = "ssh"
type = "tcp"
localPort = 22
remotePort = 6000
Client reads webServer.* out of that file, so it needs nothing but a path:
require "frpc"
client = Frpc::Client.new("frpc.toml")
client.status # => {"tcp" => [{"name" => "ssh", "status" => "running", ...}]}
client.proxy("ssh") # => that one proxy's hash, or nil
client.config # => the config file as frpc currently sees it
client.reload(new_toml) # PUT /api/config, then GET /api/reload — no restart
client.stop # POST /api/stop, then reap; TERM/KILL if it hangs
Client.run guarantees the child is stopped, even on an exception:
Frpc::Client.run("frpc.toml") { |client| pp client.status }
To talk to an frpc you didn't spawn — a systemd unit, another container — skip
Client and use Admin directly:
admin = Frpc::Admin.new(host: "127.0.0.1", port: 7400, user: "admin", password: "hunter2")
admin.wait_until_healthy
admin.status
admin.reload
Reloading takes a moment to settle: frpc diffs the new config against the running proxies and creates, updates, or removes them asynchronously.
Just the binary
Frpc::Ruby.executable # => "/…/gems/frpc-ruby-0.70.1-x86_64-linux/exe/x86_64-linux/frpc"
The gem also installs an frpc shim on your PATH, so bundle exec frpc -c frpc.toml
works, as does frps-style usage of any subcommand — the shim execs the real
binary with your arguments untouched.
Resolution order:
Frpc::Ruby.executable(exe_path: "…")FRPC_INSTALL_DIR(a directory) orFRPC_PATH(a file)- the binary vendored in this gem
frpconPATH
That's the escape hatch for unsupported architectures: install frpc yourself,
set FRPC_INSTALL_DIR, and the ruby-platform gem works fine.
Platforms
| gem platform | frp release target |
|---|---|
x86_64-linux |
linux_amd64 |
aarch64-linux |
linux_arm64 |
arm-linux |
linux_arm |
arm64-darwin |
darwin_arm64 |
x86_64-darwin |
darwin_amd64 |
x64-mingw-ucrt |
windows_amd64 |
aarch64-mingw-ucrt |
windows_arm64 |
Upstream builds every non-darwin target with CGO_ENABLED=0, so one Linux
binary per arch covers glibc and musl alike — no separate -gnu/-musl gems.
Why a subprocess and not cgo
You can wrap client.Service in a -buildmode=c-shared object and load it with
FFI, but a process that has loaded the Go runtime can't fork and keep using Go
in the child (golang/go#15538),
which is a live hazard under Puma cluster mode, Unicorn, Resque and Spring. It
also can't be dlclosed (golang/go#11100),
and it re-breaks whenever frp changes its internal API.
A subprocess has a better failure mode: frpc crashing kills frpc, not your VM.
The cgo route only pays off if you need the tunnel's data path inside the Ruby
process — a custom HandleWorkConnCb or ConnectorCreator — which has no HTTP
equivalent.
Development
direnv allow # or: nix develop
bin/test # unit tests — a fake frpc, no network
bin/smoke # real frps + frpc, real bytes through a real tunnel
rake vendor:all # download + verify + unpack every platform's frpc
rake gem:all # build pkg/*.gem for every platform, plus the ruby gem
Releasing
Versions mirror upstream: gem 0.70.1 vendors frp 0.70.1. A wrapper-only fix
appends a fourth segment (0.70.1.1), which RubyGems orders after 0.70.1 and
before 0.70.2.
bin/sync-upstream # latest frp release: bumps VERSION, pins checksums
bin/sync-upstream 0.71.0 # or a specific one
bin/increment-version # wrapper-only bump instead: 0.70.1 -> 0.70.1.1
bin/smoke # prove the new binary tunnels before shipping it
git commit -am "frp 0.71.0" && git push # CI builds the platform gems
bin/release-gem # push them to RubyGems, then tag
bin/release-gem builds the ruby-platform gem locally, downloads the platform
gems from the latest successful build-gems.yml run, refuses to continue if any
platform is missing or if CI built a different version, and tolerates gems that
are already published so a half-finished release can be re-run.
CI is what builds the shipped binaries, on purpose: it fetches them from GitHub
Releases and verifies each against the committed checksums/v<version>.txt, so
what ships never depends on the state of somebody's vendor/ directory. Without
that pin the Rakefile warns and falls back to the checksum file published
alongside the release, which only catches transport corruption.
License
MIT for this wrapper. The vendored frpc binary is
Apache-2.0, © the frp authors.