<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ChenXX</title>
    <description>The latest articles on DEV Community by ChenXX (@chenxxpro).</description>
    <link>https://dev.to/chenxxpro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4015826%2Feed354b9-0b47-40bd-bdfe-981dde802581.png</url>
      <title>DEV Community: ChenXX</title>
      <link>https://dev.to/chenxxpro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chenxxpro"/>
    <language>en</language>
    <item>
      <title>"Managing a CLI Subprocess from Rust: Lifecycle, Logs, and Graceful Shutdown (Tauri)"</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Mon, 20 Jul 2026 08:02:46 +0000</pubDate>
      <link>https://dev.to/chenxxpro/managing-a-cli-subprocess-from-rust-lifecycle-logs-and-graceful-shutdown-tauri-3g66</link>
      <guid>https://dev.to/chenxxpro/managing-a-cli-subprocess-from-rust-lifecycle-logs-and-graceful-shutdown-tauri-3g66</guid>
      <description>&lt;p&gt;Most "wrapper" apps do the same thing: launch a CLI binary in the background, pipe its logs into the UI, and tear it down when the user quits. It sounds trivial — until you actually build it.&lt;/p&gt;

&lt;p&gt;I recently worked through this while building &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt;, a GUI desktop client that wraps the &lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;&lt;code&gt;frp&lt;/code&gt;&lt;/a&gt; CLI (&lt;code&gt;frpc&lt;/code&gt;). The Rust side has to own the full lifecycle of that child process: spawn it on demand, surface a ring buffer of its stdout/stderr to the frontend, detect "are we actually connected?" from log heuristics, stop it cleanly on window close, and restart it atomically when the binary updates itself on disk.&lt;/p&gt;

&lt;p&gt;This post is the pattern that emerged. It's framework-aware (Tauri v2) but the core ideas apply to any Rust app that needs to babysit a long-running CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive version (and why it bites you)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frpc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"frpc.toml"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nf"&gt;.stdout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Stdio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;piped&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;.stderr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Stdio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;piped&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This compiles, runs, and is wrong in four ways you won't notice until production:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You never read the pipes.&lt;/strong&gt; When the OS pipe buffer fills (~64 KB on Linux), the child process &lt;em&gt;blocks on its next write&lt;/em&gt; and silently stalls. The app shows "starting…" forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No way to ask "is it healthy?"&lt;/strong&gt; A spawned PID tells you the process exists, not that it's connected to the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On window close you call &lt;code&gt;child.kill()&lt;/code&gt;.&lt;/strong&gt; That sends SIGKILL (or &lt;code&gt;TerminateProcess&lt;/code&gt; on Windows). The server side never sees a clean disconnect — its connection table leaks until a keepalive timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't swap the binary while it's running.&lt;/strong&gt; File is locked on Windows; replacing it in-place on macOS is a race.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's fix each.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Drain the pipes into a ring buffer
&lt;/h2&gt;

&lt;p&gt;A dedicated reader thread per stream, writing into a fixed-size ring buffer that the frontend can poll:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;collections&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;VecDeque&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;LogRing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VecDeque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;LogRing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.buf&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.cap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="nf"&gt;.pop_front&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.buf&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.iter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.cloned&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.collect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;spawn_reader&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Read&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Send&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="k"&gt;'static&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;LogRing&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;BufReader&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="nf"&gt;.lines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.flatten&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="nf"&gt;.push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="c1"&gt;// stream closed = child exited&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key point: &lt;strong&gt;the ring is unbounded-consumer, bounded-memory&lt;/strong&gt;. Logs from a chatty CLI never OOM the app; the oldest line just rolls off. The frontend gets a &lt;code&gt;get_logs()&lt;/code&gt; command that snapshots the current contents — cheap to call on a timer.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Health = evidence, not just "process exists"
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;child.try_wait()&lt;/code&gt; returning &lt;code&gt;None&lt;/code&gt; means "still running." It does &lt;em&gt;not&lt;/em&gt; mean "doing useful work." For an &lt;code&gt;frpc&lt;/code&gt;-style client the interesting state lives in its own log output: it prints something like &lt;code&gt;[I] [proxy.go:xxx] start proxy success&lt;/code&gt; when a tunnel is actually up.&lt;/p&gt;

&lt;p&gt;So model the connection as a small state machine driven by log lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[derive(Clone,&lt;/span&gt; &lt;span class="nd"&gt;Copy,&lt;/span&gt; &lt;span class="nd"&gt;PartialEq)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;FrpcState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Stopped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connecting&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FrpcState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;FrpcState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"login to server failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"token is incorrect"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;FrpcState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="nf"&gt;.contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"start proxy success"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;current&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nn"&gt;FrpcState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Connecting&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nn"&gt;FrpcState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Connected&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;current&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This matters because a &lt;em&gt;running&lt;/em&gt; process with the wrong token will happily sit there retrying forever. You want the UI to show "Error: token rejected," not a fake green "Connected."&lt;/p&gt;

&lt;p&gt;A 30-second fallback timer that flips &lt;code&gt;Connecting → Error&lt;/code&gt; covers the case where the log never emits a decisive line (firewalled, DNS wedged, etc.).&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Graceful shutdown: ask, then escalate
&lt;/h2&gt;

&lt;p&gt;Don't go straight to &lt;code&gt;kill()&lt;/code&gt;. On Unix, send SIGTERM and give it a beat to flush; on Windows, the equivalent is harder, so we lean on the CLI's own shutdown path if it has one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;stop_gracefully&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;Child&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Prefer a clean path&lt;/span&gt;
    &lt;span class="nd"&gt;#[cfg(unix)]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;nix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;kill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nn"&gt;nix&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;unistd&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Pid&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="nf"&gt;.id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;i32&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="nn"&gt;Signal&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;SIGTERM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nd"&gt;#[cfg(windows)]&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Best-effort: on Windows we drop directly; for a well-behaved CLI&lt;/span&gt;
        &lt;span class="c1"&gt;// that traps the console close event this still lets it clean up.&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Wait up to `timeout` for it to exit on its own&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="nf"&gt;.elapsed&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="nf"&gt;.try_wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nn"&gt;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_millis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// Escalate&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="nf"&gt;.kill&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="nf"&gt;.wait&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a Tauri app you also want to hook &lt;code&gt;ExitRequested&lt;/code&gt; so that closing the window while a tunnel is up pops a confirm dialog (or, for "minimize to tray" mode, just hides the window and keeps &lt;code&gt;frpc&lt;/code&gt; alive in the background — the whole reason the user installed a GUI client).&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Atomic binary replacement
&lt;/h2&gt;

&lt;p&gt;The problem: you want to update &lt;code&gt;frpc&lt;/code&gt; from v0.69 to v0.70 without making the user reinstall the app, and without leaving a half-written file behind.&lt;/p&gt;

&lt;p&gt;Pattern: &lt;strong&gt;download to a temp path → SHA256-verify → rename into place&lt;/strong&gt;. Rename is atomic on the same filesystem.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Pseudocode for the happy path&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bin_dir&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frpc.new"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;download_and_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;release_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;expected_sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Stop the running child FIRST (see §3), then:&lt;/span&gt;
&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;final_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// atomic on same fs&lt;/span&gt;
&lt;span class="c1"&gt;// Restart on next "start" click, or auto-restart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The two things that bite you here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows holds an exclusive lock on a running .exe.&lt;/strong&gt; You cannot rename over it while the child is alive. The order is non-negotiable: stop → replace → start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SHA256 before rename, not after.&lt;/strong&gt; A truncated or MITM'd binary that "looks like" frpc but isn't would otherwise get swapped in silently. Verify the bytes you downloaded against a digest fetched over a trusted channel (the GitHub Release API + a pinned algorithm), &lt;em&gt;then&lt;/em&gt; promote.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A &lt;code&gt;frpc_update.json&lt;/code&gt; next to the binary tracks &lt;code&gt;current&lt;/code&gt; / &lt;code&gt;pending&lt;/code&gt; versions, so a crash mid-update is recoverable on next launch rather than leaving the user with a broken install.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The part Tauri adds on top
&lt;/h2&gt;

&lt;p&gt;All of the above is plain Rust. Tauri's job is to (a) ship the binary inside the app bundle via the &lt;strong&gt;sidecar&lt;/strong&gt; mechanism — &lt;code&gt;tauri.conf.json&lt;/code&gt; declares it and the build renames the per-target binary to the sidecar convention (&lt;code&gt;frpc-x86_64-pc-windows-msvc.exe&lt;/code&gt;, etc.) — and (b) expose &lt;code&gt;invoke_handler&lt;/code&gt; commands so the Vue frontend can call &lt;code&gt;start_frpc&lt;/code&gt;, &lt;code&gt;stop_frpc&lt;/code&gt;, &lt;code&gt;get_state&lt;/code&gt;, &lt;code&gt;get_logs&lt;/code&gt;, &lt;code&gt;update_frpc&lt;/code&gt; over the IPC bridge.&lt;/p&gt;

&lt;p&gt;The tempting mistake is to put lifecycle logic in the frontend and just have Rust be a thin &lt;code&gt;Command::spawn&lt;/code&gt; wrapper. Don't. The frontend can be reloaded, the window can be closed, the process tree cares about neither — &lt;strong&gt;the Rust side is the single source of truth for "what is the child doing right now."&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Drain stdout/stderr into a bounded ring buffer, always.&lt;/li&gt;
&lt;li&gt;Treat "running" and "healthy" as different questions; derive health from the child's own output.&lt;/li&gt;
&lt;li&gt;Shutdown is SIGTERM-then-SIGKILL with a timeout, never the other way around.&lt;/li&gt;
&lt;li&gt;Binary updates are atomic &lt;em&gt;rename after verify&lt;/em&gt;, and Windows forces a strict stop-before-replace order.&lt;/li&gt;
&lt;li&gt;In Tauri, the Rust core owns the state machine; the frontend is a view onto it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full implementation of this lives in &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/tree/main/src-tauri/src" rel="noopener noreferrer"&gt;&lt;code&gt;src-tauri/src/&lt;/code&gt;&lt;/a&gt; — &lt;code&gt;process.rs&lt;/code&gt; for lifecycle, &lt;code&gt;frpc_state.rs&lt;/code&gt; for the log classifier, &lt;code&gt;proxy_relay.rs&lt;/code&gt; for the traffic-counting TCP middle layer, and &lt;code&gt;frpc_update.rs&lt;/code&gt; for the verified-then-atomic update path. Comments and corrections welcome.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;MoonProxy is MIT-licensed and on &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest" rel="noopener noreferrer"&gt;macOS + Windows&lt;/a&gt; — if you're shipping a Tauri app that wraps a CLI, the patterns above are field-tested.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>tutorial</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How to Host a Minecraft Server Friends Can Join from Anywhere (No Port Forwarding)</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Sat, 18 Jul 2026 08:04:48 +0000</pubDate>
      <link>https://dev.to/chenxxpro/how-to-host-a-minecraft-server-friends-can-join-from-anywhere-no-port-forwarding-4b30</link>
      <guid>https://dev.to/chenxxpro/how-to-host-a-minecraft-server-friends-can-join-from-anywhere-no-port-forwarding-4b30</guid>
      <description>

&lt;h1&gt;
  
  
  The Problem: "Connection Refused"
&lt;/h1&gt;

&lt;p&gt;You set up a Minecraft server. Your friends type in your IP. Nothing. The dreaded &lt;strong&gt;"Connection refused"&lt;/strong&gt; or endless &lt;strong&gt;"Connecting to server..."&lt;/strong&gt; timeout.&lt;/p&gt;

&lt;p&gt;The culprit is almost always the same: &lt;strong&gt;NAT&lt;/strong&gt;. Your home router sits behind your ISP's network address translation, so your server's local port (25565) is invisible to the public internet.&lt;/p&gt;

&lt;p&gt;The classic answer is &lt;strong&gt;port forwarding&lt;/strong&gt; — log into your router, open port 25565, point it at your server's local IP. But that approach has real problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't work&lt;/strong&gt; behind carrier-grade NAT (CGNAT), which most mobile and many home ISPs now use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It exposes your home IP&lt;/strong&gt; directly to the internet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every router UI is different&lt;/strong&gt;, making it a frustrating guessing game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: A Reverse Tunnel with FRP
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;FRP (Fast Reverse Proxy)&lt;/a&gt; is an open-source tool that creates a &lt;strong&gt;reverse tunnel&lt;/strong&gt;: your server connects &lt;em&gt;outward&lt;/em&gt; to a public relay, and players connect to the relay instead of your home network. No port forwarding, no exposed home IP, and it works behind CGNAT.&lt;/p&gt;

&lt;p&gt;The catch: FRP's official client (&lt;code&gt;frpc&lt;/code&gt;) is a &lt;strong&gt;command-line binary&lt;/strong&gt;. Running it means editing a &lt;code&gt;.toml&lt;/code&gt; config, opening a terminal, and babysitting a process. For a lot of people who just want to play Minecraft with friends, that's friction.&lt;/p&gt;

&lt;p&gt;That's exactly the problem &lt;strong&gt;&lt;a href="https://moonproxy.app/en/" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt;&lt;/strong&gt; was built to solve.&lt;/p&gt;

&lt;h2&gt;
  
  
  MoonProxy: A GUI for FRP
&lt;/h2&gt;

&lt;p&gt;MoonProxy is a free, open-source desktop app that wraps &lt;code&gt;frpc&lt;/code&gt; in a graphical interface. You fill in your FRP server details (address, port, token), add a proxy rule for port 25565, and click &lt;strong&gt;Start&lt;/strong&gt;. That's it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt; (Apple Silicon + Intel) and &lt;strong&gt;Windows x64&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Built with &lt;strong&gt;Tauri v2 + Rust&lt;/strong&gt; — small binary, low memory, no Electron bloat&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System tray&lt;/strong&gt; so the tunnel keeps running in the background&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-connect on boot&lt;/strong&gt; so your server is reachable 24/7 without you thinking about it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled connect / disconnect&lt;/strong&gt; if you only want it up on weekends&lt;/li&gt;
&lt;li&gt;The FRP engine ships &lt;strong&gt;bundled&lt;/strong&gt; — you never install &lt;code&gt;frpc&lt;/code&gt; separately&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step-by-Step: Minecraft Server Over FRP with MoonProxy
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Get an FRP server
&lt;/h3&gt;

&lt;p&gt;You need a machine with a &lt;strong&gt;public IP&lt;/strong&gt; running &lt;code&gt;frps&lt;/code&gt; (the FRP server). Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A cheap VPS ($3-5/month) from any provider&lt;/li&gt;
&lt;li&gt;A free-tier cloud instance (Oracle Cloud, Google Cloud, AWS)&lt;/li&gt;
&lt;li&gt;A friend's server that already has a public IP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On that machine, &lt;a href="https://github.com/fatedier/frp/releases" rel="noopener noreferrer"&gt;download FRP&lt;/a&gt; and run &lt;code&gt;frps&lt;/code&gt; with a minimal config exposing a port range for tunnels. Note down the server's &lt;strong&gt;public IP&lt;/strong&gt;, the &lt;strong&gt;bind port&lt;/strong&gt; (usually 7000), and the &lt;strong&gt;token&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Download MoonProxy
&lt;/h3&gt;

&lt;p&gt;Grab the latest release for your platform from the &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest" rel="noopener noreferrer"&gt;releases page&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;macOS Apple Silicon: the &lt;code&gt;aarch64.dmg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;macOS Intel: the &lt;code&gt;x64.dmg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Windows: the &lt;code&gt;x64-setup.exe&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Configure your FRP server
&lt;/h3&gt;

&lt;p&gt;Open MoonProxy, go to &lt;strong&gt;Settings &amp;gt; Server&lt;/strong&gt;, and enter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server address:&lt;/strong&gt; your VPS public IP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; 7000 (or whatever &lt;code&gt;frps&lt;/code&gt; binds)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token:&lt;/strong&gt; your FRP auth token&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Username:&lt;/strong&gt; any unique name for this client&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Add a proxy rule for Minecraft
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Settings &amp;gt; Proxies&lt;/strong&gt; and add a rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Type:&lt;/strong&gt; TCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local IP:&lt;/strong&gt; 127.0.0.1&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local port:&lt;/strong&gt; 25565 (your Minecraft server port)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remote port:&lt;/strong&gt; 25565 (the port players will connect to on your VPS)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Start the tunnel
&lt;/h3&gt;

&lt;p&gt;Back on the home screen, click &lt;strong&gt;Start&lt;/strong&gt;. The status indicator turns green when &lt;code&gt;frpc&lt;/code&gt; has connected to your FRP server.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Share the address
&lt;/h3&gt;

&lt;p&gt;Give your friends the address &lt;code&gt;&amp;lt;your-vps-public-ip&amp;gt;:25565&lt;/code&gt;. They add it as a server in Minecraft and they're in.&lt;/p&gt;

&lt;p&gt;That's the whole flow — no terminal, no config files, no router gymnastics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping It Up 24/7
&lt;/h2&gt;

&lt;p&gt;If you want your server reachable around the clock:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enable &lt;strong&gt;Launch at login&lt;/strong&gt; in MoonProxy's settings.&lt;/li&gt;
&lt;li&gt;Enable &lt;strong&gt;Auto-connect&lt;/strong&gt; — the tunnel starts automatically whenever the app launches.&lt;/li&gt;
&lt;li&gt;Minimize to the &lt;strong&gt;system tray&lt;/strong&gt; so it runs quietly in the background.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now your Minecraft world is reachable whenever your computer is on, with zero manual steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Not Just Use a Hosting Service?
&lt;/h2&gt;

&lt;p&gt;You absolutely can pay for managed Minecraft hosting ($5-15/month typically). But:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Running it yourself on your own hardware is &lt;strong&gt;free&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;You keep &lt;strong&gt;full control&lt;/strong&gt; over mods, worlds, and backups.&lt;/li&gt;
&lt;li&gt;You learn networking along the way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;MoonProxy + a $3 VPS gives you a self-hosted server with the same reachability as a paid plan, for a fraction of the cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-Up
&lt;/h2&gt;

&lt;p&gt;The combination of &lt;strong&gt;FRP&lt;/strong&gt; (the protocol) and &lt;strong&gt;MoonProxy&lt;/strong&gt; (the GUI) removes the last piece of friction in self-hosting a Minecraft server: the networking. No port forwarding, no command line, no exposed home IP.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MoonProxy website:&lt;/strong&gt; &lt;a href="https://moonproxy.app/en/" rel="noopener noreferrer"&gt;https://moonproxy.app/en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source code (MIT):&lt;/strong&gt; &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;https://github.com/MoonProxyHQ/moonproxy-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest" rel="noopener noreferrer"&gt;https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy building.&lt;/p&gt;

</description>
      <category>minecraft</category>
      <category>selfhosting</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Put a Local Service on the Public Internet with FRP (Without Losing Your Mind Over Config Files)</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Fri, 17 Jul 2026 08:04:39 +0000</pubDate>
      <link>https://dev.to/chenxxpro/moonproxy-a-cross-platform-desktop-gui-for-frp-nat-traversal-without-the-cli-496o</link>
      <guid>https://dev.to/chenxxpro/moonproxy-a-cross-platform-desktop-gui-for-frp-nat-traversal-without-the-cli-496o</guid>
      <description>&lt;h1&gt;
  
  
  How to Put a Local Service on the Public Internet with FRP (Without Losing Your Mind Over Config Files)
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;A practical walkthrough of FRP — what it is, how it works, and why you might want a GUI on top of it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The problem every self-hoster hits
&lt;/h2&gt;

&lt;p&gt;You built something. A local API. A Minecraft world for your friends. A self-hosted dashboard. An ERP running on the office machine. It works great — on your LAN.&lt;/p&gt;

&lt;p&gt;The moment you want someone &lt;em&gt;outside&lt;/em&gt; to reach it, the fun begins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port forwarding?&lt;/strong&gt; Good luck if you're behind CGNAT, a corporate firewall, or an ISP that doesn't give you a public IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPN?&lt;/strong&gt; Now every person who wants access has to install a client, join a network, and stay connected. Overkill for "let me show you this one page."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud deploy?&lt;/strong&gt; Now you're maintaining two environments, paying for a VPS you didn't need, and shipping data somewhere it doesn't have to live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What most people actually want is simpler: &lt;em&gt;take this one local port, give it a public address, done.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's exactly what &lt;strong&gt;FRP&lt;/strong&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What FRP is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;FRP (Fast Reverse Proxy)&lt;/a&gt; is an open-source tool by &lt;code&gt;fatedier&lt;/code&gt; that exposes a local service behind a NAT or firewall to the public internet. It's battle-tested, written in Go, and has been the go-to answer in self-hosting communities for years.&lt;/p&gt;

&lt;p&gt;The model is clean — two pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;frps&lt;/code&gt; (the server)&lt;/strong&gt; — runs on a machine with a public IP (a $5 VPS is plenty).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;frpc&lt;/code&gt; (the client)&lt;/strong&gt; — runs on your local machine, the one with the service you want to expose.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client opens an outbound tunnel to the server. The server listens on a public port and forwards traffic back through the tunnel. NAT and firewalls don't matter because the connection is initiated &lt;em&gt;from inside&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Visitor] → [frps on public VPS:7000] ⇄ tunnel ⇄ [frpc on your laptop] → [localhost:8080]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole idea. It works for TCP, UDP, HTTP, HTTPS. People run Minecraft servers, remote desktops, internal dashboards, and dev previews through it every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: config files
&lt;/h2&gt;

&lt;p&gt;FRP works great. The friction isn't the protocol — it's the &lt;strong&gt;workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To run &lt;code&gt;frpc&lt;/code&gt;, you write a TOML/INI config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;serverAddr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"203.0.113.10"&lt;/span&gt;
&lt;span class="py"&gt;serverPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt;
&lt;span class="py"&gt;auth.token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-key"&lt;/span&gt;

&lt;span class="nn"&gt;[[proxies]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-web"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tcp"&lt;/span&gt;
&lt;span class="py"&gt;localIP&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;localPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;remotePort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you run it from a terminal. If it crashes, you find out when someone texts you "is the server down?" If you want it to start on boot, you write a systemd unit. If you want to change one port, you edit the file and restart. If you're on Windows, you're juggling a console window or a scheduled task.&lt;/p&gt;

&lt;p&gt;None of this is &lt;em&gt;hard&lt;/em&gt;. But it adds up. And for users who aren't comfortable in a terminal — a small business owner running an ERP, a parent hosting Minecraft for their kid, a designer previewing a static site — it's a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The piece I actually wanted to share
&lt;/h2&gt;

&lt;p&gt;This is where I mention a project I think is worth knowing about, because it directly addresses that friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt;&lt;/strong&gt; is an open-source desktop GUI for FRP. MIT licensed, built with Tauri v2 + Vue 3 + Rust, runs on macOS and Windows. The &lt;code&gt;frpc&lt;/code&gt; binary is bundled — you don't install FRP separately.&lt;/p&gt;

&lt;p&gt;What it does is take the config-file workflow above and turn it into a form:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a tunnel&lt;/strong&gt; by filling in fields (local port, remote port, type, key) — no text editor, no syntax to get wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start/stop with one button&lt;/strong&gt; — a circular toggle with four visual states (stopped, connecting, connected, error). "Connected" is derived from real &lt;code&gt;frpc&lt;/code&gt; output, not an optimistic flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health polling&lt;/strong&gt; — it checks whether your local port is actually reachable every 3 seconds, so you find out &lt;em&gt;your service is down&lt;/em&gt; before your users do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System tray resident&lt;/strong&gt; — close the window, the tunnel keeps running. Launch-at-login means it comes back after a reboot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled connect&lt;/strong&gt; — set weekdays and a time window; tunnels only stay open during working hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine self-update&lt;/strong&gt; — pulls new &lt;code&gt;frpc&lt;/code&gt; releases from the upstream FRP GitHub Releases, SHA256-verifies them, and atomically swaps the binary without reinstalling the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still bring your own &lt;code&gt;frps&lt;/code&gt; (self-hosted or a community-public one you trust) — MoonProxy is strictly the client side. It doesn't relay your traffic through any third-party infrastructure. The auth key stays with you.&lt;/p&gt;

&lt;p&gt;It's independent of the upstream FRP project — FRP is &lt;code&gt;fatedier&lt;/code&gt;'s work, licensed and maintained by its original authors. MoonProxy is just a desktop client for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick walkthrough
&lt;/h2&gt;

&lt;p&gt;Say you have a local dev server on &lt;code&gt;localhost:5173&lt;/code&gt; and an &lt;code&gt;frps&lt;/code&gt; running on your VPS at &lt;code&gt;203.0.113.10:7000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With plain FRP&lt;/strong&gt;, you'd write the config, run &lt;code&gt;./frpc -c frpc.toml&lt;/code&gt;, and leave a terminal open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With MoonProxy&lt;/strong&gt;, you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the app.&lt;/li&gt;
&lt;li&gt;Click "Add tunnel."&lt;/li&gt;
&lt;li&gt;Fill in: name &lt;code&gt;vite-dev&lt;/code&gt;, type &lt;code&gt;tcp&lt;/code&gt;, local &lt;code&gt;127.0.0.1:5173&lt;/code&gt;, remote port &lt;code&gt;5173&lt;/code&gt;, server address + your token.&lt;/li&gt;
&lt;li&gt;Click the toggle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The button goes &lt;em&gt;connecting → connected&lt;/em&gt;. Your dev server is now reachable at &lt;code&gt;203.0.113.10:5173&lt;/code&gt;. Close the laptop lid, open it again — the tunnel comes back. You didn't write a config file or touch a terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain &lt;code&gt;frp&lt;/code&gt; CLI&lt;/strong&gt; if you live in the terminal, want to script everything, or run headless servers. FRP is excellent. This isn't a replacement — it's a layer on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GUI like MoonProxy&lt;/strong&gt; if you want set-and-forget tunnels, you're sharing the machine with non-technical users, or you're tired of debugging a YAML file at midnight because a colleague can't reach the staging URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed relays (Ngrok, Cloudflare Tunnel)&lt;/strong&gt; if you want zero infrastructure and don't mind routing through a vendor's network. Different tradeoff — convenience for dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;FRP remains one of the cleanest ways to put a local service on the public internet. The protocol is solid; the friction has always been in the &lt;em&gt;operating&lt;/em&gt; of it.&lt;/p&gt;

&lt;p&gt;If you've been meaning to try FRP but bounced off the config-file workflow — or you're already running it and want a tray icon instead of a terminal window — MoonProxy is worth a look.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;moonproxy.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT)&lt;/strong&gt;: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;github.com/MoonProxyHQ/moonproxy-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⬇️ &lt;strong&gt;Releases&lt;/strong&gt;: macOS (Apple Silicon + Intel) and Windows x64&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it, I'd love to hear what you're tunneling. 👋&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>5 Real-World Scenarios Where a GUI FRP Client Beats the Command Line</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Thu, 16 Jul 2026 08:15:10 +0000</pubDate>
      <link>https://dev.to/chenxxpro/5-real-world-scenarios-where-a-gui-frp-client-beats-the-command-line-2911</link>
      <guid>https://dev.to/chenxxpro/5-real-world-scenarios-where-a-gui-frp-client-beats-the-command-line-2911</guid>
      <description>&lt;p&gt;If you've ever set up &lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;frp&lt;/a&gt; — the go-to open-source reverse proxy for NAT traversal — you know the drill: SSH into a VPS, install &lt;code&gt;frps&lt;/code&gt;, write a &lt;code&gt;frpc.toml&lt;/code&gt; on your local machine, wrestle with ports and tokens, run it as a background service, and hope nothing drifts.&lt;/p&gt;

&lt;p&gt;It works. But for a lot of people — self-hosters, remote workers, small teams, anyone who isn't a sysadmin — the CLI workflow is friction they'd rather avoid. That's exactly why I built &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt;: an open-source desktop GUI for frp (macOS + Windows, MIT-licensed, built on Tauri v2).&lt;/p&gt;

&lt;p&gt;Rather than pitch features, here are &lt;strong&gt;five concrete scenarios&lt;/strong&gt; where reaching for a GUI client saves real time and headaches.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. The "I Just Want to SSH Into My Home Server" User
&lt;/h2&gt;

&lt;p&gt;You're at a coffee shop. You need to fix something on the mini-PC running under your desk at home. No public IP, carrier-grade NAT, no VPN.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CLI path&lt;/strong&gt;: write a &lt;code&gt;frpc.toml&lt;/code&gt; with a &lt;code&gt;tcp&lt;/code&gt; proxy stanza, make sure the remote port on your VPS matches, start &lt;code&gt;frpc&lt;/code&gt;, keep it alive, SSH to &lt;code&gt;vps-ip:remote-port&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GUI path&lt;/strong&gt;: open MoonProxy, type your frps server address + token, add one TCP proxy rule (local port 22 → remote port 6000), click Start. The public address is shown right on the home screen. Done.&lt;/p&gt;

&lt;p&gt;The win isn't speed for an expert — it's that a non-Linux-savvy family member can do it after a one-time setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Debugging Webhooks and OAuth Callbacks Locally
&lt;/h2&gt;

&lt;p&gt;Third-party services (Stripe, GitHub, Slack) need to POST to a publicly reachable URL. Your dev server runs on &lt;code&gt;localhost:3000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With MoonProxy you spin up an &lt;strong&gt;HTTP proxy&lt;/strong&gt; rule pointing at &lt;code&gt;localhost:3000&lt;/code&gt;, and you get a public domain on your frps server that forwards straight to your laptop. No ngrok account, no tunnel-as-a-service limits — just your own infra.&lt;/p&gt;

&lt;p&gt;The bonus: MoonProxy's home tab shows a &lt;strong&gt;live traffic chart&lt;/strong&gt; for the tunnel, so you can &lt;em&gt;see&lt;/em&gt; the webhook hitting your machine in real time. That's surprisingly useful when debugging "did the callback actually fire?"&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Sharing a Local Service With a Non-Technical Colleague
&lt;/h2&gt;

&lt;p&gt;You've got a demo running locally and want a teammate to take a look — but they're on a different network and you don't want to deploy to a cloud VM just for a 10-minute review.&lt;/p&gt;

&lt;p&gt;Start a tunnel in MoonProxy, hand them the public URL. When they're done, click Stop. The URL dies. No lingering exposure, no cloud bill, no &lt;code&gt;kubectl&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Keeping a Tunnel Up 24/7 Without Babying It
&lt;/h2&gt;

&lt;p&gt;Long-running tunnels (home NAS access, a personal blog hosted on a Raspberry Pi, a Minecraft server for friends) need to survive reboots, sleep/wake cycles, and network hiccups.&lt;/p&gt;

&lt;p&gt;MoonProxy handles this with three built-in features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System tray resident&lt;/strong&gt; — close the window and the tunnel keeps running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch at login + silent start&lt;/strong&gt; — boots hidden to the tray.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled connect&lt;/strong&gt; — pick weekdays and start/stop times; the scheduler hot-reloads every minute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a tunnel can run Monday–Friday 9–18 and turn itself off otherwise — without a cron job or a systemd timer you'll forget about.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Knowing &lt;em&gt;Immediately&lt;/em&gt; When a Tunnel Breaks
&lt;/h2&gt;

&lt;p&gt;The silent failure mode of a backgrounded &lt;code&gt;frpc&lt;/code&gt; process is brutal: everything looks fine until you try to connect and realize the tunnel died three hours ago.&lt;/p&gt;

&lt;p&gt;MoonProxy polls &lt;strong&gt;local endpoint reachability&lt;/strong&gt; at adaptive intervals (exponential backoff from 3s up to 24s) and surfaces the result as colored health dots on the home screen. A broken tunnel shows red within seconds, not after you've already left the house relying on it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Broader Point
&lt;/h2&gt;

&lt;p&gt;frp the protocol is excellent. The &lt;em&gt;workflow&lt;/em&gt; around it — hand-edited TOML, process supervision, port juggling — is where most non-experts give up. A GUI doesn't make frp more powerful; it makes it &lt;strong&gt;accessible&lt;/strong&gt; to the people who need NAT traversal but don't want a side quest in systems administration.&lt;/p&gt;

&lt;p&gt;If any of those five scenarios sound familiar, MoonProxy is free and open source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;MoonProxyHQ/moonproxy-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://moonproxy.app/en/" rel="noopener noreferrer"&gt;moonproxy.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platforms&lt;/strong&gt;: macOS (Apple Silicon + Intel), Windows x64&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You bring your own frps server (self-hosted or any community endpoint you trust) — MoonProxy handles configuration, lifecycle, monitoring, and updates. The bundled frpc engine even self-updates from upstream releases, so you never have to &lt;code&gt;wget&lt;/code&gt; a binary again.&lt;/p&gt;

&lt;p&gt;Questions, feature ideas, or war stories? The &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/discussions" rel="noopener noreferrer"&gt;Discussions tab&lt;/a&gt; is open. 🌙&lt;/p&gt;

</description>
      <category>frp</category>
      <category>selfhosting</category>
      <category>networking</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Bundling a CLI Binary as a Tauri v2 Sidecar: Architecture and Lessons from Building MoonProxy</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Tue, 14 Jul 2026 08:07:44 +0000</pubDate>
      <link>https://dev.to/chenxxpro/bundling-a-cli-binary-as-a-tauri-v2-sidecar-architecture-and-lessons-from-building-moonproxy-21n3</link>
      <guid>https://dev.to/chenxxpro/bundling-a-cli-binary-as-a-tauri-v2-sidecar-architecture-and-lessons-from-building-moonproxy-21n3</guid>
      <description>&lt;p&gt;When you build a desktop app that needs to ship a CLI binary alongside it, the Tauri v2 &lt;strong&gt;sidecar&lt;/strong&gt; mechanism is the cleanest solution. This post is a deep-dive into how &lt;strong&gt;MoonProxy&lt;/strong&gt; (an open-source FRP GUI client for macOS &amp;amp; Windows) uses it to bundle the &lt;code&gt;frpc&lt;/code&gt; binary so users never have to install it separately.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;MoonProxy v1.2.0+&lt;/strong&gt; — Tauri v2 + Vue 3 + Rust, MIT licensed, &lt;a href="https://moonproxy.app/" rel="noopener noreferrer"&gt;moonproxy.app&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why sidecar?
&lt;/h2&gt;

&lt;p&gt;A FRP client is intrinsically a &lt;strong&gt;client-server tool&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The user runs &lt;code&gt;frpc&lt;/code&gt; (client) on their machine&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frpc&lt;/code&gt; connects to &lt;code&gt;frps&lt;/code&gt; (server) running on a public cloud&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Options for distributing &lt;code&gt;frpc&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Tell users to install frp separately&lt;/strong&gt; — too many support tickets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spawn &lt;code&gt;frpc&lt;/code&gt; via system PATH lookup&lt;/strong&gt; — fragile, version drift&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bundle &lt;code&gt;frpc&lt;/code&gt; binary inside the app&lt;/strong&gt; — ✅ best UX&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Tauri v2's sidecar feature lets you ship a binary that the app spawns as a child process, with the same lifecycle management as the app itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1 — Naming convention
&lt;/h2&gt;

&lt;p&gt;Tauri identifies sidecar binaries by &lt;strong&gt;file-name suffix&lt;/strong&gt; per target platform:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;File name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS Apple Silicon&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frpc-aarch64-apple-darwin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS Intel&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frpc-x86_64-apple-darwin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows x64&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frpc-x86_64-pc-windows-msvc.exe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows ARM64&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frpc-aarch64-pc-windows-msvc.exe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;strong&gt;base name&lt;/strong&gt; (&lt;code&gt;frpc&lt;/code&gt;) is what you reference from &lt;code&gt;tauri.conf.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2 — Place binaries in &lt;code&gt;src-tauri/binaries/&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Tauri v2 looks for sidecar binaries in &lt;code&gt;src-tauri/binaries/&lt;/code&gt; at compile time and embeds them into the app bundle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src-tauri/
  binaries/
    frpc-aarch64-apple-darwin
    frpc-x86_64-apple-darwin
    frpc-x86_64-pc-windows-msvc.exe
    frpc-aarch64-pc-windows-msvc.exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;sync-frpc.sh&lt;/code&gt; script downloads the matching &lt;code&gt;frpc&lt;/code&gt; release and renames it to the above pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Declare in &lt;code&gt;tauri.conf.json&lt;/code&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"externalBin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"binaries/frpc"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Tauri strips the platform suffix automatically; you only write the base name (&lt;code&gt;frpc&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Spawn the sidecar from Rust
&lt;/h2&gt;

&lt;p&gt;Tauri exposes the &lt;code&gt;tauri-plugin-shell&lt;/code&gt; for command-based sidecar execution. In our case we use a Rust wrapper that gives us &lt;strong&gt;stdout streaming + log buffering + restart on failure&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tauri_plugin_shell&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ShellExt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sidecar_command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.shell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;.sidecar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frpc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to create sidecar"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/path/to/frpc.toml"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sidecar_command&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"failed to spawn"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Stream stdout → in-memory ring buffer + log UI&lt;/span&gt;
&lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;async_runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tauri_plugin_shell&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;process&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nn"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Stdout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* push to log ring buffer */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* push to log ring buffer */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="nn"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Terminated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* mark exit */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5 — Log ring buffer (for the UI)
&lt;/h2&gt;

&lt;p&gt;The Vue UI shows a real-time log feed. We don't want unbounded memory growth, so we use a &lt;code&gt;RingBuffer&amp;lt;String&amp;gt;&lt;/code&gt; with a 2000-line capacity in &lt;code&gt;frpc_state.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;LogRing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Arc&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VecDeque&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;impl&lt;/span&gt; &lt;span class="n"&gt;LogRing&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.inner&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.capacity&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="nf"&gt;.pop_front&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6 — Auto-restart with backoff
&lt;/h2&gt;

&lt;p&gt;If the frpc process dies (network glitch, server reboot), we want to retry with exponential backoff. State machine in &lt;code&gt;frpc_state.rs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;ConnState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Stopped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Connecting&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Connected&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;next_retry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Instant&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;schedule_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2u64&lt;/span&gt;&lt;span class="nf"&gt;.saturating_pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.attempt&lt;/span&gt;&lt;span class="nf"&gt;.min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// 1, 2, 4, 8, 16, 32, 64s&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ConnState&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Connecting&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="py"&gt;.timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Instant&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;backoff&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7 — Self-update of the sidecar binary
&lt;/h2&gt;

&lt;p&gt;When upstream &lt;code&gt;frpc&lt;/code&gt; releases a new version, we want users to get it &lt;strong&gt;without reinstalling the app&lt;/strong&gt;. We ship an updater that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Downloads the new frpc release tarball&lt;/li&gt;
&lt;li&gt;Verifies SHA256&lt;/li&gt;
&lt;li&gt;Atomically replaces the binary&lt;/li&gt;
&lt;li&gt;Restarts frpc
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;update_frpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://github.com/fatedier/frp/releases/download/v{}/frp_{}_darwin_amd64.tar.gz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new_version&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.app_config_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frpc.new"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;download_and_verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;expected_sha&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// atomic rename&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;final_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="nf"&gt;.path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.app_config_dir&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frpc"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;final_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;app itself&lt;/strong&gt; has a separate updater path (Tauri's built-in &lt;code&gt;tauri-plugin-updater&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Gotchas we hit
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. macOS code signing &amp;amp; Gatekeeper
&lt;/h3&gt;

&lt;p&gt;Unsigned binaries downloaded at runtime trigger Gatekeeper on first launch. Solution: ship the binary inside the .app bundle (already code-signed with the app), not downloaded at runtime &lt;strong&gt;for the initial version&lt;/strong&gt;. Runtime updates still work because the new binary inherits the parent app's permissions (mostly).&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Windows Defender false positive
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;frpc&lt;/code&gt; is sometimes flagged because of its network behavior. We:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Document the SHA256 + source URL in the update flow&lt;/li&gt;
&lt;li&gt;Provide a "report false positive" link in the UI&lt;/li&gt;
&lt;li&gt;Sign the binary (planned for v1.3.0)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Architecture mismatch on Apple Silicon
&lt;/h3&gt;

&lt;p&gt;A common bug: user is on M1 Mac but the app shipped the Intel &lt;code&gt;frpc&lt;/code&gt;. With Tauri v2's platform-suffix naming, this is solved at bundle time, but you need to make sure your build matrix covers all 4 platform suffixes (we use GitHub Actions matrix builds).&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pin frpc version in &lt;code&gt;Cargo.toml&lt;/code&gt;&lt;/strong&gt; rather than downloading at build time — simpler CI&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;tauri-plugin-shell&lt;/code&gt;'s structured sidecar API&lt;/strong&gt; (newer versions have it) — saves the manual stdout stream loop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expose the sidecar process to the JS frontend via IPC&lt;/strong&gt; — we do this for status, but logs are still polled; streaming via Tauri events would be smoother&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Code links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;MoonProxy repository: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;https://github.com/MoonProxyHQ/moonproxy-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sidecar wrapper: &lt;code&gt;src-tauri/src/process.rs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Log ring buffer: &lt;code&gt;src-tauri/src/frpc_state.rs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Auto-restart: &lt;code&gt;src-tauri/src/frpc_state.rs::schedule_retry&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Self-update: &lt;code&gt;src-tauri/src/frpc_update.rs&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sync script: &lt;code&gt;scripts/sync-frpc.sh&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;📥 &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest" rel="noopener noreferrer"&gt;Download MoonProxy v1.2.0+&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🌐 &lt;a href="https://moonproxy.app/" rel="noopener noreferrer"&gt;Project site&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/discussions" rel="noopener noreferrer"&gt;Discussions / Q&amp;amp;A&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building a desktop app that needs to bundle a CLI tool, I hope this deep-dive saves you some time. Tauri sidecar is a powerful pattern — once you have the boilerplate right, shipping a "runs offline, talks to my server" app becomes tractable for a small team.&lt;/p&gt;

</description>
      <category>tauri</category>
      <category>rust</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Comparing FRP GUI Clients in 2026: MoonProxy vs frpc-desktop vs frpmgr vs frp-panel</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Sat, 11 Jul 2026 08:11:00 +0000</pubDate>
      <link>https://dev.to/chenxxpro/comparing-frp-gui-clients-in-2026-moonproxy-vs-frpc-desktop-vs-frpmgr-vs-frp-panel-51bh</link>
      <guid>https://dev.to/chenxxpro/comparing-frp-gui-clients-in-2026-moonproxy-vs-frpc-desktop-vs-frpmgr-vs-frp-panel-51bh</guid>
      <description>&lt;p&gt;FRP (Fast Reverse Proxy) is one of the most popular open-source tools for exposing local services to the public internet. But FRP is a CLI tool — and for many users, editing TOML config files and running terminal commands feels intimidating.&lt;/p&gt;

&lt;p&gt;That's where GUI clients come in. Let's compare the top FRP desktop clients available today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use a GUI for FRP?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;No command line knowledge needed&lt;/li&gt;
&lt;li&gt;Visual traffic monitoring and connection status&lt;/li&gt;
&lt;li&gt;One-click start/stop instead of managing background processes&lt;/li&gt;
&lt;li&gt;System tray integration for always-on operation&lt;/li&gt;
&lt;li&gt;Easier configuration management across multiple servers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The contenders
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. MoonProxy (cross-platform)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | macOS + Windows | MIT License&lt;/p&gt;

&lt;p&gt;The newest entry, built with Tauri v2 + Vue 3 + Rust. Key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cross-platform: macOS (Apple Silicon + Intel) and Windows x64&lt;/li&gt;
&lt;li&gt;Real-time traffic monitoring with live charts&lt;/li&gt;
&lt;li&gt;System tray with silent start and auto-connect on boot&lt;/li&gt;
&lt;li&gt;Scheduled connections (weekday + time range)&lt;/li&gt;
&lt;li&gt;Built-in frpc auto-update (no manual binary management)&lt;/li&gt;
&lt;li&gt;App self-update via Tauri updater&lt;/li&gt;
&lt;li&gt;Built with Tauri — lightweight (~10 MB), not Electron&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Best for: Non-technical users who want a polished, modern experience on both Mac and Windows.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. frpc-desktop
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/luckjiawei/frpc-desktop" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | Windows + macOS + Linux&lt;/p&gt;

&lt;p&gt;A mature Electron-based FRP client with a large community. Supports proxy management and basic monitoring.&lt;/p&gt;

&lt;p&gt;Best for: Users who need Linux support and don't mind Electron's resource footprint.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. frpmgr
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/koho/frpmgr" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | Windows only&lt;/p&gt;

&lt;p&gt;A Windows-native FRP manager with a clean Win32 UI. Lightweight and focused.&lt;/p&gt;

&lt;p&gt;Best for: Windows-only users who want a native, no-frills experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. frp-panel
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/VaalaCat/frp-panel" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | Web-based&lt;/p&gt;

&lt;p&gt;A web dashboard for managing FRP servers and clients. Great for multi-machine setups.&lt;/p&gt;

&lt;p&gt;Best for: Server administrators managing multiple FRP instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;MoonProxy&lt;/th&gt;
&lt;th&gt;frpc-desktop&lt;/th&gt;
&lt;th&gt;frpmgr&lt;/th&gt;
&lt;th&gt;frp-panel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;Planned&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traffic monitor&lt;/td&gt;
&lt;td&gt;Live charts&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;System tray&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-connect&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;N/A&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scheduled&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-update&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Framework&lt;/td&gt;
&lt;td&gt;Tauri v2&lt;/td&gt;
&lt;td&gt;Electron&lt;/td&gt;
&lt;td&gt;Win32&lt;/td&gt;
&lt;td&gt;Go web&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;~10 MB&lt;/td&gt;
&lt;td&gt;~80 MB&lt;/td&gt;
&lt;td&gt;~5 MB&lt;/td&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Which one should you choose?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Want the most polished Mac + Windows experience?&lt;/strong&gt; Try MoonProxy&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Need Linux support today?&lt;/strong&gt; Go with frpc-desktop&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Windows only and want minimal overhead?&lt;/strong&gt; frpmgr&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managing multiple servers?&lt;/strong&gt; frp-panel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of these are open-source and free. The FRP ecosystem is better when these projects support each other — many of them maintain mutual cross-references in their READMEs.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is an independent comparison. I am a contributor to MoonProxy, but I respect all projects in the FRP ecosystem. Star any project that helps you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>networking</category>
      <category>showdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to Put a Local Service on the Public Internet with FRP (Without Losing Your Mind Over Config Files)</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:55:11 +0000</pubDate>
      <link>https://dev.to/chenxxpro/how-to-put-a-local-service-on-the-public-internet-with-frp-without-losing-your-mind-over-config-21li</link>
      <guid>https://dev.to/chenxxpro/how-to-put-a-local-service-on-the-public-internet-with-frp-without-losing-your-mind-over-config-21li</guid>
      <description>&lt;h2&gt;
  
  
  The problem every self-hoster hits
&lt;/h2&gt;

&lt;p&gt;You built something. A local API. A Minecraft world for your friends. A self-hosted dashboard. An ERP running on the office machine. It works great — on your LAN.&lt;/p&gt;

&lt;p&gt;The moment you want someone &lt;em&gt;outside&lt;/em&gt; to reach it, the fun begins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Port forwarding?&lt;/strong&gt; Good luck if you're behind CGNAT, a corporate firewall, or an ISP that doesn't give you a public IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPN?&lt;/strong&gt; Now every person who wants access has to install a client, join a network, and stay connected. Overkill for "let me show you this one page."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud deploy?&lt;/strong&gt; Now you're maintaining two environments, paying for a VPS you didn't need, and shipping data somewhere it doesn't have to live.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What most people actually want is simpler: &lt;em&gt;take this one local port, give it a public address, done.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's exactly what &lt;strong&gt;FRP&lt;/strong&gt; does.&lt;/p&gt;

&lt;h2&gt;
  
  
  What FRP is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;FRP (Fast Reverse Proxy)&lt;/a&gt; is an open-source tool by &lt;code&gt;fatedier&lt;/code&gt; that exposes a local service behind a NAT or firewall to the public internet. It's battle-tested, written in Go, and has been the go-to answer in self-hosting communities for years.&lt;/p&gt;

&lt;p&gt;The model is clean — two pieces:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;frps&lt;/code&gt; (the server)&lt;/strong&gt; — runs on a machine with a public IP (a $5 VPS is plenty).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;frpc&lt;/code&gt; (the client)&lt;/strong&gt; — runs on your local machine, the one with the service you want to expose.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The client opens an outbound tunnel to the server. The server listens on a public port and forwards traffic back through the tunnel. NAT and firewalls don't matter because the connection is initiated &lt;em&gt;from inside&lt;/em&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Visitor] → [frps on public VPS:7000] ⇄ tunnel ⇄ [frpc on your laptop] → [localhost:8080]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole idea. It works for TCP, UDP, HTTP, HTTPS. People run Minecraft servers, remote desktops, internal dashboards, and dev previews through it every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: config files
&lt;/h2&gt;

&lt;p&gt;FRP works great. The friction isn't the protocol — it's the &lt;strong&gt;workflow&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;To run &lt;code&gt;frpc&lt;/code&gt;, you write a TOML/INI config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;serverAddr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"203.0.113.10"&lt;/span&gt;
&lt;span class="py"&gt;serverPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt;
&lt;span class="py"&gt;auth.token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-key"&lt;/span&gt;

&lt;span class="nn"&gt;[[proxies]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-web"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tcp"&lt;/span&gt;
&lt;span class="py"&gt;localIP&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;localPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;remotePort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you run it from a terminal. If it crashes, you find out when someone texts you "is the server down?" If you want it to start on boot, you write a systemd unit. If you want to change one port, you edit the file and restart. If you're on Windows, you're juggling a console window or a scheduled task.&lt;/p&gt;

&lt;p&gt;None of this is &lt;em&gt;hard&lt;/em&gt;. But it adds up. And for users who aren't comfortable in a terminal — a small business owner running an ERP, a parent hosting Minecraft for their kid, a designer previewing a static site — it's a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The piece I actually wanted to share
&lt;/h2&gt;

&lt;p&gt;This is where I mention a project I think is worth knowing about, because it directly addresses that friction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt;&lt;/strong&gt; is an open-source desktop GUI for FRP. MIT licensed, built with Tauri v2 + Vue 3 + Rust, runs on macOS and Windows. The &lt;code&gt;frpc&lt;/code&gt; binary is bundled — you don't install FRP separately.&lt;/p&gt;

&lt;p&gt;What it does is take the config-file workflow above and turn it into a form:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a tunnel&lt;/strong&gt; by filling in fields (local port, remote port, type, key) — no text editor, no syntax to get wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start/stop with one button&lt;/strong&gt; — a circular toggle with four visual states (stopped, connecting, connected, error). "Connected" is derived from real &lt;code&gt;frpc&lt;/code&gt; output, not an optimistic flag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health polling&lt;/strong&gt; — it checks whether your local port is actually reachable every 3 seconds, so you find out &lt;em&gt;your service is down&lt;/em&gt; before your users do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System tray resident&lt;/strong&gt; — close the window, the tunnel keeps running. Launch-at-login means it comes back after a reboot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled connect&lt;/strong&gt; — set weekdays and a time window; tunnels only stay open during working hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine self-update&lt;/strong&gt; — pulls new &lt;code&gt;frpc&lt;/code&gt; releases from the upstream FRP GitHub Releases, SHA256-verifies them, and atomically swaps the binary without reinstalling the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You still bring your own &lt;code&gt;frps&lt;/code&gt; (self-hosted or a community-public one you trust) — MoonProxy is strictly the client side. It doesn't relay your traffic through any third-party infrastructure. The auth key stays with you.&lt;/p&gt;

&lt;p&gt;It's independent of the upstream FRP project — FRP is &lt;code&gt;fatedier&lt;/code&gt;'s work, licensed and maintained by its original authors. MoonProxy is just a desktop client for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick walkthrough
&lt;/h2&gt;

&lt;p&gt;Say you have a local dev server on &lt;code&gt;localhost:5173&lt;/code&gt; and an &lt;code&gt;frps&lt;/code&gt; running on your VPS at &lt;code&gt;203.0.113.10:7000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With plain FRP&lt;/strong&gt;, you'd write the config, run &lt;code&gt;./frpc -c frpc.toml&lt;/code&gt;, and leave a terminal open.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;With MoonProxy&lt;/strong&gt;, you:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open the app.&lt;/li&gt;
&lt;li&gt;Click "Add tunnel."&lt;/li&gt;
&lt;li&gt;Fill in: name &lt;code&gt;vite-dev&lt;/code&gt;, type &lt;code&gt;tcp&lt;/code&gt;, local &lt;code&gt;127.0.0.1:5173&lt;/code&gt;, remote port &lt;code&gt;5173&lt;/code&gt;, server address + your token.&lt;/li&gt;
&lt;li&gt;Click the toggle.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The button goes &lt;em&gt;connecting → connected&lt;/em&gt;. Your dev server is now reachable at &lt;code&gt;203.0.113.10:5173&lt;/code&gt;. Close the laptop lid, open it again — the tunnel comes back. You didn't write a config file or touch a terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plain &lt;code&gt;frp&lt;/code&gt; CLI&lt;/strong&gt; if you live in the terminal, want to script everything, or run headless servers. FRP is excellent. This isn't a replacement — it's a layer on top.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A GUI like MoonProxy&lt;/strong&gt; if you want set-and-forget tunnels, you're sharing the machine with non-technical users, or you're tired of debugging a YAML file at midnight because a colleague can't reach the staging URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed relays (Ngrok, Cloudflare Tunnel)&lt;/strong&gt; if you want zero infrastructure and don't mind routing through a vendor's network. Different tradeoff — convenience for dependency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;FRP remains one of the cleanest ways to put a local service on the public internet. The protocol is solid; the friction has always been in the &lt;em&gt;operating&lt;/em&gt; of it.&lt;/p&gt;

&lt;p&gt;If you've been meaning to try FRP but bounced off the config-file workflow — or you're already running it and want a tray icon instead of a terminal window — MoonProxy is worth a look.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🌐 &lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;moonproxy.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;Source (MIT)&lt;/strong&gt;: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;github.com/MoonProxyHQ/moonproxy-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;⬇️ &lt;strong&gt;Releases&lt;/strong&gt;: macOS (Apple Silicon + Intel) and Windows x64&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build something with it, I'd love to hear what you're tunneling. 👋&lt;/p&gt;

</description>
      <category>frp</category>
      <category>selfhosting</category>
      <category>tutorial</category>
      <category>opensource</category>
    </item>
    <item>
      <title>MoonProxy: A Desktop GUI Client for FRP (Fast Reverse Proxy)</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Wed, 08 Jul 2026 14:00:37 +0000</pubDate>
      <link>https://dev.to/chenxxpro/moonproxy-a-desktop-gui-client-for-frp-1cdm</link>
      <guid>https://dev.to/chenxxpro/moonproxy-a-desktop-gui-client-for-frp-1cdm</guid>
      <description>&lt;h1&gt;
  
  
  MoonProxy: The Complete Guide to FRP Intranet Penetration
&lt;/h1&gt;

&lt;blockquote&gt;
&lt;p&gt;From setup to real-world use cases. For developers and self-hosters.&lt;br&gt;
Compatible with MoonProxy v1.2.0+&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why FRP + MoonProxy?
&lt;/h2&gt;

&lt;p&gt;FRP (Fast Reverse Proxy) is the most popular open-source tunneling tool (80k+ GitHub stars). But its CLI-only workflow keeps non-technical users away. &lt;strong&gt;MoonProxy&lt;/strong&gt; wraps FRP's &lt;code&gt;frpc&lt;/code&gt; into a native desktop app — visual configuration, one-click start/stop, real-time traffic monitoring.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not ngrok / Cloudflare Tunnel / Tailscale?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ngrok&lt;/strong&gt;: Free tier is limited, paid plans are expensive&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Tunnel&lt;/strong&gt;: Closed source, only handles HTTP(S), vendor lock-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tailscale&lt;/strong&gt;: Great for mesh networking, but not traditional port forwarding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;FRP + MoonProxy gives you full control with your own server, supports any TCP/UDP/HTTP protocol, and costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Your Computer]                     [Public Server]            [Visitor]

  Local Service (NAS:5000)                                      
       ↓                                                       
  MoonProxy (frpc GUI) ←── tunnel ──→  frps (server) ←── public access
       ↓                                                       
  127.0.0.1:5000                                     your-server:6000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 1: Set Up frps Server (5 min)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Download FRP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;wget https://github.com/fatedier/frp/releases/download/v0.69.1/frp_0.69.1_linux_amd64.tar.gz
&lt;span class="nb"&gt;tar &lt;/span&gt;xzf frp_0.69.1_linux_amd64.tar.gz
&lt;span class="nb"&gt;cd &lt;/span&gt;frp_0.69.1_linux_amd64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure frps.toml
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;bindPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt;
&lt;span class="py"&gt;auth.token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-token-change-me"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Run as systemd service
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo cp &lt;/span&gt;frps /usr/local/bin/
&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /etc/frp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo cp &lt;/span&gt;frps.toml /etc/frp/
&lt;span class="nb"&gt;sudo tee&lt;/span&gt; /etc/systemd/system/frps.service &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
[Unit]
Description=frps service
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/frps -c /etc/frp/frps.toml
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;frps &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start frps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Open firewall ports
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 7000/tcp   &lt;span class="c"&gt;# FRP communication&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 6000/tcp   &lt;span class="c"&gt;# Your tunnel port&lt;/span&gt;
&lt;span class="c"&gt;# Also check your cloud provider's security group&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 2: Install MoonProxy
&lt;/h2&gt;

&lt;p&gt;Download from &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Apple Silicon)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MoonProxy_1.2.0_aarch64.dmg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Intel)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MoonProxy_1.2.0_x64.dmg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows (x64)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;MoonProxy_1.2.0_x64-setup.exe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;📸 Main panel: &lt;code&gt;https://moonproxy.app/screenshots/main-full-en.webp&lt;/code&gt;&lt;br&gt;
📸 Provider config: &lt;code&gt;https://moonproxy.app/screenshots/server-provider-en.webp&lt;/code&gt;&lt;br&gt;
📸 Proxy rules: &lt;code&gt;https://moonproxy.app/screenshots/server-proxy-en.webp&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Part 3: Connect in 3 Steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Enter Server Info
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Services&lt;/strong&gt; → &lt;strong&gt;Provider&lt;/strong&gt; tab:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Server Address&lt;/td&gt;
&lt;td&gt;Your server's public IP (e.g., &lt;code&gt;203.0.113.50&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bind Port&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;7000&lt;/code&gt; (matches frps bindPort)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token&lt;/td&gt;
&lt;td&gt;Same as frps auth.token&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: Add Proxy Rule
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Proxy Rules&lt;/strong&gt; tab → &lt;strong&gt;Add Rule&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expose a Web service (HTTP):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protocol: &lt;code&gt;http&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Local Port: &lt;code&gt;8080&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Custom Domain: &lt;code&gt;myapp.example.com&lt;/code&gt; (point DNS to your server)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Expose SSH (TCP):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protocol: &lt;code&gt;tcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Local Port: &lt;code&gt;22&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remote Port: &lt;code&gt;6000&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then connect with: &lt;code&gt;ssh -p 6000 user@your-server-ip&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expose NAS (TCP):&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Protocol: &lt;code&gt;tcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Local Port: &lt;code&gt;5000&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Remote Port: &lt;code&gt;5000&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Access at: &lt;code&gt;http://your-server-ip:5000&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Click Start
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;📸 Home panel: &lt;code&gt;https://moonproxy.app/screenshots/main-full-en.webp&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Return to the home screen. The large button changes color:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Gray&lt;/strong&gt; = Stopped&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blue&lt;/strong&gt; = Connecting&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Green&lt;/strong&gt; = Connected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The endpoint list shows green dots (reachable) or red dots (unreachable). Click &lt;strong&gt;Start&lt;/strong&gt; — when the button turns green, you're live!&lt;/p&gt;

&lt;h2&gt;
  
  
  Real-World Use Cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Remote NAS Access
&lt;/h3&gt;

&lt;p&gt;Expose your Synology NAS (port 5000) to access files from anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dev Preview Sharing
&lt;/h3&gt;

&lt;p&gt;Share your &lt;code&gt;localhost:5173&lt;/code&gt; Vue/React project with clients via a public URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minecraft Server
&lt;/h3&gt;

&lt;p&gt;Expose your local Minecraft server (port 25565) so friends can join directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Home Automation
&lt;/h3&gt;

&lt;p&gt;Access Home Assistant while traveling — no VPN needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Auto-Start + Silent Mode
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;📸 Launch settings: &lt;code&gt;https://moonproxy.app/screenshots/settings-launch-en.webp&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Auto-start&lt;/strong&gt;: Launches on system boot&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Silent start&lt;/strong&gt;: Hides to system tray, no window popup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-connect&lt;/strong&gt;: Automatically starts frpc on boot&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scheduled Connection
&lt;/h3&gt;

&lt;p&gt;Set working hours (e.g., Mon-Fri 9:00-18:00). MoonProxy auto-connects at start time and disconnects at end time. &lt;strong&gt;No exposed ports outside working hours.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-Time Traffic Monitor
&lt;/h3&gt;

&lt;p&gt;The home panel shows live upload/download speed curves, active connections, and cumulative traffic — powered by a built-in TCP relay counting layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Self-Updating Engine
&lt;/h3&gt;

&lt;p&gt;FRP engine updates automatically: checks GitHub releases → downloads → SHA256 verify → atomic replace. No reinstall needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can't connect?&lt;/strong&gt; Check: (1) frps running? &lt;code&gt;systemctl status frps&lt;/code&gt; (2) Port 7000 open? (3) Token matches? (4) Remote port open in firewall?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS "unidentified developer"?&lt;/strong&gt; System Settings → Privacy &amp;amp; Security → "Open Anyway"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many proxy rules?&lt;/strong&gt; Up to 50 (v1.2.0+).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data privacy?&lt;/strong&gt; MoonProxy runs 100% locally. Your data flows: your computer → your server → visitor. No third party involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Vue 3 + TypeScript + Vite 6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Rust + Tauri v2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;frp (frpc) v0.69.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bundle size&lt;/td&gt;
&lt;td&gt;~5MB (far smaller than Electron)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;https://github.com/MoonProxyHQ/moonproxy-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;https://moonproxy.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Releases&lt;/strong&gt;: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest" rel="noopener noreferrer"&gt;https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Discussions&lt;/strong&gt;: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/discussions" rel="noopener noreferrer"&gt;https://github.com/MoonProxyHQ/moonproxy-desktop/discussions&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Found this helpful? Give us a Star ⭐ on GitHub!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>showdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How to Put a Local Service on the Public Internet with FRP (Without Losing Your Mind Over Config Files)</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Wed, 08 Jul 2026 12:10:52 +0000</pubDate>
      <link>https://dev.to/moonproxyhq/how-to-put-a-local-service-on-the-public-internet-with-frp-without-losing-your-mind-over-config-1o4m</link>
      <guid>https://dev.to/moonproxyhq/how-to-put-a-local-service-on-the-public-internet-with-frp-without-losing-your-mind-over-config-1o4m</guid>
      <description>&lt;p&gt;The problem every self-hoster hits&lt;/p&gt;

&lt;p&gt;You built something. A local API. A Minecraft world for your friends. A self-hosted dashboard. An ERP running on the office machine. It works great — on your LAN.&lt;/p&gt;

&lt;p&gt;The moment you want someone outside to reach it, the fun begins:&lt;/p&gt;

&lt;p&gt;Port forwarding? Good luck if you're behind CGNAT, a corporate firewall, or an ISP that doesn't give you a public IP.&lt;br&gt;
VPN? Now every person who wants access has to install a client, join a network, and stay connected. Overkill for "let me show you this one page."&lt;br&gt;
Cloud deploy? Now you're maintaining two environments, paying for a VPS you didn't need, and shipping data somewhere it doesn't have to live.&lt;/p&gt;

&lt;p&gt;What most people actually want is simpler: take this one local port, give it a public address, done.&lt;/p&gt;

&lt;p&gt;That's exactly what FRP does.&lt;/p&gt;

&lt;p&gt;What FRP is&lt;/p&gt;

&lt;p&gt;FRP (Fast Reverse Proxy) is an open-source tool by fatedier that exposes a local service behind a NAT or firewall to the public internet. It's battle-tested, written in Go, and has been the go-to answer in self-hosting communities for years.&lt;/p&gt;

&lt;p&gt;The model is clean — two pieces:&lt;/p&gt;

&lt;p&gt;frps (the server) — runs on a machine with a public IP (a $5 VPS is plenty).&lt;br&gt;
frpc (the client) — runs on your local machine, the one with the service you want to expose.&lt;/p&gt;

&lt;p&gt;The client opens an outbound tunnel to the server. The server listens on a public port and forwards traffic back through the tunnel. NAT and firewalls don't matter because the connection is initiated from inside.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Visitor] → [frps on public VPS:7000] ⇄ tunnel ⇄ [frpc on your laptop] → [localhost:8080]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole idea. It works for TCP, UDP, HTTP, HTTPS. People run Minecraft servers, remote desktops, internal dashboards, and dev previews through it every day.&lt;/p&gt;

&lt;p&gt;The catch: config files&lt;/p&gt;

&lt;p&gt;FRP works great. The friction isn't the protocol — it's the workflow.&lt;/p&gt;

&lt;p&gt;To run frpc, you write a TOML config file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;serverAddr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"203.0.113.10"&lt;/span&gt;
&lt;span class="py"&gt;serverPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;7000&lt;/span&gt;
&lt;span class="py"&gt;auth.token&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"your-secret-key"&lt;/span&gt;

&lt;span class="nn"&gt;[[proxies]]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"my-web"&lt;/span&gt;
&lt;span class="py"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tcp"&lt;/span&gt;
&lt;span class="py"&gt;localIP&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&lt;/span&gt;
&lt;span class="py"&gt;localPort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;remotePort&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you launch it from a terminal. Change a port? Edit the file, restart the process. Add a second tunnel? Edit again, restart again. Want to know if it's actually working? Check the terminal output — or don't, because you closed that tab two hours ago.&lt;/p&gt;

&lt;p&gt;This is fine if you live in the terminal. It's a wall for everyone else — a small-business owner running an ERP, a parent hosting Minecraft for their kid, a designer sharing a local mockup.&lt;/p&gt;

&lt;p&gt;MoonProxy: a GUI for FRP&lt;/p&gt;

&lt;p&gt;&lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt; is an MIT-licensed desktop client for FRP. It wraps frpc in a GUI so you don't have to touch config files or terminals.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it adds over plain frpc:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Form-based tunnel config&lt;/strong&gt; — no toml/ini files. Add a tunnel like filling out a form.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click start/stop&lt;/strong&gt; — a toggle button with real connection state, derived from frpc output (not an optimistic flag).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoint health polling every 3s&lt;/strong&gt; — catches a dead local service before your users do.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System tray resident&lt;/strong&gt; — close the window, the tunnel keeps running. Launch at login, silent start.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled tunnels&lt;/strong&gt; — set weekday + time window. Hot-reloaded, no restart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engine self-update&lt;/strong&gt; — pulls new frpc from upstream FRP releases, SHA256-verifies, atomically swaps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with &lt;strong&gt;Tauri v2 + Vue 3 + Rust + TypeScript&lt;/strong&gt;. The frpc binary is bundled via Tauri sidecar — no separate FRP install. macOS (Apple Silicon + Intel) and Windows x64.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You bring your own frps&lt;/strong&gt; (self-hosted VPS or a community node you trust). MoonProxy only manages the client side — it doesn't relay your traffic through any third-party infrastructure. Auth key stays with you.&lt;/p&gt;

&lt;p&gt;It's independent of fatedier/frp — that project deserves all the credit for the protocol. This is just a GUI layer.&lt;/p&gt;

&lt;p&gt;Getting started&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download from &lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;moonproxy.app&lt;/a&gt; or &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt; (DMG for macOS, EXE for Windows).&lt;/li&gt;
&lt;li&gt;On macOS first launch, right-click → Open, or run &lt;code&gt;xattr -cr "/Applications/MoonProxy Desktop.app"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Set up your frps on a VPS (plenty of community tutorials exist).&lt;/li&gt;
&lt;li&gt;Open MoonProxy, add a tunnel, click start.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. Your local service now has a public address.&lt;/p&gt;

&lt;p&gt;Project links&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;moonproxy.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Source&lt;/strong&gt;: &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;github.com/MoonProxyHQ/moonproxy-desktop&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT, permanently free&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's not trying to replace the CLI — if you're happy scripting frp, keep doing that. This is for folks who want a tray icon and a toggle instead of a terminal window.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments!&lt;/p&gt;

</description>
      <category>frp</category>
      <category>selfhosting</category>
      <category>opensource</category>
      <category>web3</category>
    </item>
    <item>
      <title>Bundling a CLI Binary as a Tauri v2 Sidecar: Lessons from Building a Desktop App</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Sun, 05 Jul 2026 06:25:43 +0000</pubDate>
      <link>https://dev.to/chenxxpro/bundling-a-cli-binary-as-a-tauri-v2-sidecar-lessons-from-building-a-desktop-app-5po</link>
      <guid>https://dev.to/chenxxpro/bundling-a-cli-binary-as-a-tauri-v2-sidecar-lessons-from-building-a-desktop-app-5po</guid>
      <description>&lt;p&gt;When you build a desktop app with &lt;a href="https://tauri.app" rel="noopener noreferrer"&gt;Tauri v2&lt;/a&gt;, sooner or later you'll hit a question: &lt;em&gt;how do I bundle and manage an external CLI binary inside my app?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Maybe it's &lt;code&gt;ffmpeg&lt;/code&gt; for video processing. Maybe it's a database engine. Maybe — as in my case — it's &lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;&lt;code&gt;frpc&lt;/code&gt;&lt;/a&gt;, the reverse-proxy client from the popular &lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;frp&lt;/a&gt; project.&lt;/p&gt;

&lt;p&gt;This post walks through the full lifecycle: bundling, spawning, lifecycle management, and even &lt;strong&gt;self-updating&lt;/strong&gt; the binary at runtime — all from Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Declaring the Sidecar
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;tauri.conf.json&lt;/code&gt;, declare the binary under &lt;code&gt;bundle.externalBin&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bundle"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"externalBin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"binaries/frpc"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tauri identifies the target platform by a &lt;strong&gt;filename suffix convention&lt;/strong&gt;. You need to place the correctly-named binary in your project:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Filename&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Apple Silicon)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frpc-aarch64-apple-darwin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Intel)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frpc-x86_64-apple-darwin&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows (x64)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frpc-x86_64-pc-windows-msvc.exe&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tauri automatically strips the suffix at runtime and loads the right binary for the current platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Spawning the Process
&lt;/h2&gt;

&lt;p&gt;Use &lt;code&gt;tauri_plugin_shell&lt;/code&gt; to spawn the sidecar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;tauri_plugin_shell&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;ShellExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;process&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="nd"&gt;#[tauri::command]&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;start_frpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AppHandle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;sidecar&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;
        &lt;span class="nf"&gt;.shell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.sidecar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frpc"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sidecar&lt;/span&gt;
        &lt;span class="nf"&gt;.args&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s"&gt;"-c"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"frpc.toml"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.map_err&lt;/span&gt;&lt;span class="p"&gt;(|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="nf"&gt;.to_string&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Store the child handle so we can kill it later&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="py"&gt;.state&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Mutex&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nb"&gt;Option&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nn"&gt;tauri_plugin_shell&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;process&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;CommandChild&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;.replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Listen to stdout/stderr in a background task&lt;/span&gt;
    &lt;span class="nn"&gt;tauri&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;async_runtime&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;spawn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;move&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rx&lt;/span&gt;&lt;span class="nf"&gt;.recv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nn"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Stdout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Parse log line, update UI state...&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nn"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Stderr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ... */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="nn"&gt;CommandEvent&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;Terminated&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="c1"&gt;// Process exited — update state machine&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;always store the &lt;code&gt;CommandChild&lt;/code&gt; handle&lt;/strong&gt;. You'll need it to kill the process cleanly when the user clicks "Stop".&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Lifecycle: Don't Trust Optimistic Flags
&lt;/h2&gt;

&lt;p&gt;A subtle trap: &lt;code&gt;spawn()&lt;/code&gt; succeeding does &lt;strong&gt;not&lt;/strong&gt; mean the process is actually working. It just means the OS started it. The binary might immediately crash due to a bad config, a missing port, or a network error.&lt;/p&gt;

&lt;p&gt;The fix is to derive the "connected" state from &lt;strong&gt;real evidence&lt;/strong&gt;. In my app, after spawning &lt;code&gt;frpc&lt;/code&gt;, I poll its admin API endpoint (&lt;code&gt;/api/status&lt;/code&gt;) with an exponential backoff: 3s → 6s → 12s → 24s. Only when I get a healthy response do I flip the UI to "Connected".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="cd"&gt;/// Adaptive health polling: 3 → 6 → 12 → 24 seconds&lt;/span&gt;
&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;next_interval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Duration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;from_secs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If no healthy response arrives within 30 seconds, I fall back to an "Error" state — much better than showing a fake "Connected" to the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Self-Updating the Binary at Runtime
&lt;/h2&gt;

&lt;p&gt;This is the fun part. Users shouldn't have to reinstall the entire app just because &lt;code&gt;frpc&lt;/code&gt; shipped a new version. Here's the update flow I implemented:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fetch&lt;/strong&gt; the latest release info from GitHub's API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt; the binary to a temp path&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; the SHA256 checksum&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomically swap&lt;/strong&gt;: rename old → &lt;code&gt;.old&lt;/code&gt;, rename new → target&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restart&lt;/strong&gt; the sidecar process
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;update_frpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Result&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;UpdateError&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// 1. Download to temp&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;app_config_dir&lt;/span&gt;&lt;span class="nf"&gt;.join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;".frpc.downloading"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;download_binary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 2. SHA256 verify&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sha256_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expected_hash&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Err&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;UpdateError&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;ChecksumMismatch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// 3. Kill current process first&lt;/span&gt;
    &lt;span class="nf"&gt;kill_current_frpc&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 4. Atomic swap&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;final_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sidecar_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;app_config_dir&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;final_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}.old"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;final_path&lt;/span&gt;&lt;span class="nf"&gt;.display&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
        &lt;span class="nf"&gt;.ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// best-effort cleanup&lt;/span&gt;
    &lt;span class="nn"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;rename&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;final_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// 5. Restart with new binary&lt;/span&gt;
    &lt;span class="nf"&gt;start_frpc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(())&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.old&lt;/code&gt; suffix trick lets you roll back if the new binary fails to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Platform Gotchas
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;macOS code signing&lt;/strong&gt;: The sidecar binary must be signed, or Gatekeeper will block it. If you're cross-compiling, you need to sign the binary &lt;em&gt;after&lt;/em&gt; downloading it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;codesign &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nt"&gt;--sign&lt;/span&gt; &lt;span class="s2"&gt;"Developer ID Application: Your Name"&lt;/span&gt; frpc-aarch64-apple-darwin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows&lt;/strong&gt;: Watch out for antivirus false positives. CLI tools that manage network connections tend to trigger heuristics. Digitally signing with an EV certificate helps a lot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions&lt;/strong&gt;: On macOS, you may need to request network entitlements in your &lt;code&gt;Info.plist&lt;/code&gt; or capabilities. Tauri v2's capability system handles most of this, but double-check your &lt;code&gt;default.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Store the &lt;code&gt;CommandChild&lt;/code&gt;&lt;/strong&gt; — you'll need it for clean shutdown.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust spawn success&lt;/strong&gt; — verify the process is actually doing its job.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff&lt;/strong&gt; for health checks keeps your UI responsive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomic swap&lt;/strong&gt; for updates means no half-written binaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform signing&lt;/strong&gt; is not optional for distribution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building a desktop app that manages an external CLI taught me a lot about Rust's process management, async lifecycle, and the care needed for cross-platform distribution. It's more work than shelling out via &lt;code&gt;std::process::Command&lt;/code&gt;, but the control you get is worth it.&lt;/p&gt;

&lt;p&gt;If you're interested in seeing these patterns in a real app, I'm building &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt; — a desktop GUI for frp that uses exactly this sidecar architecture. It's open-source (MIT) and the code is all there.&lt;/p&gt;

&lt;p&gt;Happy shipping! 🦀&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
