<?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>Measuring Throughput When the Subprocess Owns the Socket: A TCP Relay Pattern for Tauri Apps</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Sun, 25 Oct 2026 08:26:37 +0000</pubDate>
      <link>https://dev.to/chenxxpro/measuring-throughput-when-the-subprocess-owns-the-socket-a-tcp-relay-pattern-for-tauri-apps-1j1n</link>
      <guid>https://dev.to/chenxxpro/measuring-throughput-when-the-subprocess-owns-the-socket-a-tcp-relay-pattern-for-tauri-apps-1j1n</guid>
      <description>&lt;p&gt;If you have ever wanted a traffic graph for a tunnel client, you know the awkward part: the client usually owns the socket. You cannot wrap it, you cannot ask it, and its status endpoint is often a step-function of "connected" or not.&lt;/p&gt;

&lt;p&gt;When I was building &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;MoonProxy&lt;/a&gt; — a cross-platform GUI desktop client for &lt;a href="https://github.com/fatedier/frp" rel="noopener noreferrer"&gt;FRP&lt;/a&gt; — I hit exactly this. &lt;code&gt;frpc&lt;/code&gt; exposes an &lt;code&gt;/api/status&lt;/code&gt; endpoint, and it is genuinely useful: it tells you whether the tunnel is up. What it does &lt;strong&gt;not&lt;/strong&gt; expose is a single byte counter. Yet the whole point of a tunnel dashboard is answering "how much data is moving right now?"&lt;/p&gt;

&lt;p&gt;This is a writeup of the pattern I landed on, because it generalizes well beyond FRP.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint
&lt;/h2&gt;

&lt;p&gt;The naive approach is to watch the process's network usage from the outside. On Linux that is &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/net/dev&lt;/code&gt;; on macOS and Windows it is a platform-specific call. All three have the same two problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They report traffic for &lt;strong&gt;the whole process&lt;/strong&gt;, with no way to attribute bytes to a specific tunnel.&lt;/li&gt;
&lt;li&gt;They report &lt;strong&gt;host-level interface counters&lt;/strong&gt;, which include every other program on the machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a desktop app whose entire purpose is to show you per-tunnel traffic, neither is acceptable. The user has five tunnels and wants to see them individually.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trick: be the middleman
&lt;/h2&gt;

&lt;p&gt;frpc's local proxy config points at a &lt;code&gt;localIP:localPort&lt;/code&gt; where your service is listening. That address is just a string in a TOML file. If you own the process that writes that file, you can point it somewhere else.&lt;/p&gt;

&lt;p&gt;So instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frpc  ──────────────▶  127.0.0.1:8080   (your service)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frpc  ──────▶  127.0.0.1:53912  ──────▶  127.0.0.1:8080   (your service)
             (MoonProxy relay)          (your service)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MoonProxy opens a listener on an ephemeral port, rewrites &lt;code&gt;localIP&lt;/code&gt;/&lt;code&gt;localPort&lt;/code&gt; in the proxy config to point at it, and copies bytes in both directions while counting them.&lt;/p&gt;

&lt;p&gt;The relay is a plain TCP proxy. It is not a protocol-aware proxy, it does not parse HTTP, and it does not care what is on the other end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Counting bytes without &lt;code&gt;copy_bidirectional&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Tokio has &lt;code&gt;tokio::io::copy_bidirectional&lt;/code&gt;, which is exactly the wrong tool here. It is efficient, it is correct, and it returns the total bytes moved in each direction at the end — but only &lt;em&gt;at the end&lt;/em&gt;. For a live dashboard you want running totals you can sample at any instant.&lt;/p&gt;

&lt;p&gt;The obvious substitute, &lt;code&gt;tokio::io::copy&lt;/code&gt;, returns bytes copied too, but only when the stream closes. So the counter has to live somewhere that outlives the call.&lt;/p&gt;

&lt;p&gt;I used a pair of atomic counters incremented inside the copy loop:&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;/// Single-direction copy: read, write, immediately accumulate the counter.&lt;/span&gt;
&lt;span class="cd"&gt;/// Equivalent to `tokio::io::copy` but with a live counter.&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;copy_one_way&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;W&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;rd&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;R&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;wr&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;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;counter&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;AtomicU64&lt;/span&gt;&lt;span class="p"&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="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&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="nb"&gt;u64&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt;
    &lt;span class="n"&gt;R&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;io&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AsyncRead&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Unpin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nb"&gt;Sized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;W&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;io&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;AsyncWrite&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Unpin&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="nb"&gt;Sized&lt;/span&gt;&lt;span class="p"&gt;,&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;tokio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;io&lt;/span&gt;&lt;span class="p"&gt;::{&lt;/span&gt;&lt;span class="n"&gt;AsyncReadExt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;AsyncWriteExt&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;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1024&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;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;loop&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;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rd&lt;/span&gt;&lt;span class="nf"&gt;.read&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;buf&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&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;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;wr&lt;/span&gt;&lt;span class="nf"&gt;.write_all&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;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;n&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="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="nf"&gt;.fetch_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&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;p&gt;&lt;code&gt;fetch_add&lt;/code&gt; with &lt;code&gt;Relaxed&lt;/code&gt; ordering is the right choice here. The counters are pure statistics — nobody reads them to establish a happens-before relationship with other memory — so paying for &lt;code&gt;SeqCst&lt;/code&gt; fencing on every 8 KB chunk would be pure waste.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running two directions concurrently
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;copy_bidirectional&lt;/code&gt; runs both directions at once and cancels whichever finishes first. Reproducing that with &lt;code&gt;tokio::select!&lt;/code&gt; is straightforward:&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;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="n"&gt;copy_bidirectional_counted&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;R1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;W1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;R2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;W2&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;user_rd&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;R1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;frpc_wr&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;W1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;in_counter&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;AtomicU64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;frpc_rd&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;R2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;user_wr&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;W2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;out_counter&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;AtomicU64&lt;/span&gt;&lt;span class="p"&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="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;where&lt;/span&gt; &lt;span class="cm"&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;in_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;copy_one_way&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_rd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;frpc_wr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;in_counter&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;out_task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;copy_one_way&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frpc_rd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_wr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_counter&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="nd"&gt;pin!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_task&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="nd"&gt;pin!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out_task&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;in_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&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;out_bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0u64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;loop&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="nd"&gt;select!&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&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;in_task&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&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="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;in_bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&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;out_task&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&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="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;out_bytes&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
                &lt;span class="k"&gt;break&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;in_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_bytes&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;tokio::pin!&lt;/code&gt; calls matter: without pinning, the two futures are dropped at the end of the &lt;code&gt;select!&lt;/code&gt; arm and the surviving copy task is cancelled mid-stream.&lt;/p&gt;

&lt;p&gt;Note the direction convention. I deliberately count in terms of the &lt;strong&gt;end user's&lt;/strong&gt; perspective rather than the process's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;user_conn → frpc_conn&lt;/code&gt; = the user's service uploading = &lt;strong&gt;in&lt;/strong&gt; (up)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;frpc_conn → user_conn&lt;/code&gt; = the user's service downloading = &lt;strong&gt;out&lt;/strong&gt; (down)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Getting this backwards is the classic mistake in this kind of tool. A user watching their NAS upload a backup should see the up arrow move, not the down arrow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connection counting with an RAII guard
&lt;/h3&gt;

&lt;p&gt;Counting open connections is where an early-return bug really bites. Every path out of the connection handler has to decrement, and there are a lot of them — a failed &lt;code&gt;TcpStream::connect&lt;/code&gt;, a read error, a write error, a panic in the copy loop.&lt;/p&gt;

&lt;p&gt;Rather than hand-decrementing at each exit, use a guard whose &lt;code&gt;Drop&lt;/code&gt; does it:&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;/// Connection-count RAII guard: +1 on construction, -1 on drop,&lt;/span&gt;
&lt;span class="cd"&gt;/// so every exit path (early return, error, panic) stays balanced.&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ConnGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;'a&lt;/span&gt; &lt;span class="n"&gt;AtomicI64&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;ConnGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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;fn&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;c&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;AtomicI64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&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;c&lt;/span&gt;&lt;span class="nf"&gt;.fetch_add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&lt;/span&gt;&lt;span class="p"&gt;);&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;c&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;impl&lt;/span&gt; &lt;span class="nb"&gt;Drop&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ConnGuard&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nv"&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;fn&lt;/span&gt; &lt;span class="nf"&gt;drop&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;self&lt;/span&gt;&lt;span class="na"&gt;.0&lt;/span&gt;&lt;span class="nf"&gt;.fetch_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&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;p&gt;Then the handler body is short enough to audit at a glance:&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;async&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;handle_relay_conn&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;frpc_conn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TcpStream&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;entry&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;RelayEntry&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="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;user_conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;TcpStream&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;connect&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;entry&lt;/span&gt;&lt;span class="py"&gt;.upstream&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;Err&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="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="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;_guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;ConnGuard&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.connections&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;frpc_rd&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;frpc_wr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frpc_conn&lt;/span&gt;&lt;span class="nf"&gt;.split&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;user_rd&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;user_wr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_conn&lt;/span&gt;&lt;span class="nf"&gt;.split&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nf"&gt;copy_bidirectional_counted&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;user_rd&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;frpc_wr&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;entry&lt;/span&gt;&lt;span class="py"&gt;.in_bytes&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;frpc_rd&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;user_wr&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;entry&lt;/span&gt;&lt;span class="py"&gt;.out_bytes&lt;/span&gt;&lt;span class="p"&gt;,&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guard is declared &lt;em&gt;after&lt;/em&gt; the connect succeeds, which is important: a failed connection never became a connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deriving a rate from a cumulative counter
&lt;/h2&gt;

&lt;p&gt;The counters are monotonic totals. The UI wants bytes per second. There are two ways to get there, and the difference matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bad:&lt;/strong&gt; sample the counter, and treat the delta as the rate. This produces a value that is really "bytes in the last interval," and it aliases badly — a single 2 MB chunk arriving between two samples reads as either 2 MB/s or 0 B/s depending entirely on where the sampling boundary lands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Better:&lt;/strong&gt; keep the previous sample and difference it explicitly.&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;// Swap-and-difference: the previous value is read and replaced atomically.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;last_in&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.last_in_bytes&lt;/span&gt;&lt;span class="nf"&gt;.swap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&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;last_out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.last_out_bytes&lt;/span&gt;&lt;span class="nf"&gt;.swap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Relaxed&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;in_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_in&lt;/span&gt;&lt;span class="nf"&gt;.saturating_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_in&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;out_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total_out&lt;/span&gt;&lt;span class="nf"&gt;.saturating_sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_out&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;swap&lt;/code&gt; is the important detail. It reads and writes in one atomic operation, so two concurrent samplers can never interleave into a corrupted "last value." &lt;code&gt;saturating_sub&lt;/code&gt; handles the reset case (the tunnel restarted and the counter went back to zero) without underflowing into a 17-exabyte "negative" rate.&lt;/p&gt;

&lt;p&gt;With a 1-second sampling tick, the difference &lt;em&gt;is&lt;/em&gt; bytes per second, which keeps the arithmetic in the backend trivially simple and leaves the frontend to do nothing but plot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it to the frontend
&lt;/h2&gt;

&lt;p&gt;The backend samples once per second and emits an event:&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;poll_traffic&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;start_gen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;TICK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Duration&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;loop&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="n"&gt;TICK&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;let&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="py"&gt;.try_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="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frpc_state&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;FrpcState&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="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&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;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="c1"&gt;// A new start/stop cycle invalidates this sampler.&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.poll_gen&lt;/span&gt;&lt;span class="nf"&gt;.load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Ordering&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Acquire&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;start_gen&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;break&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;payload&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="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.relay&lt;/span&gt;&lt;span class="nf"&gt;.lock&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;guard&lt;/span&gt;&lt;span class="nf"&gt;.as_ref&lt;/span&gt;&lt;span class="p"&gt;()&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;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;sample&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&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="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;break&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;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;app&lt;/span&gt;&lt;span class="nf"&gt;.emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"frpc://traffic"&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="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;Two details here are easy to get wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The generation counter.&lt;/strong&gt; Without &lt;code&gt;poll_gen&lt;/code&gt;, an old sampler keeps running after a new tunnel starts, and you end up with two samplers emitting interleaved traffic events. The frontend would show a sawtooth pattern that looks like a network problem but is actually two samplers fighting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;poll_traffic&lt;/code&gt; must be spawned, not awaited.&lt;/strong&gt; It is an infinite loop. Awaiting it inside a &lt;code&gt;#[tauri::command]&lt;/code&gt; would hang the command forever and the start button would appear to hang with it. Wrap it:&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="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="nf"&gt;poll_traffic&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;gen&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend side is then almost trivial, because the backend already did the differencing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handleTrafficPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrafficPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;totalInBytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_in_bytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;totalOutBytes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_out_bytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TrafficSnapshot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;in_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;in_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;out_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;out_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;connections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;latestTraffic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Keep a fixed-size rolling window for the chart.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;trafficHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;WINDOW_SIZE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;trafficHistory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;next&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;&lt;code&gt;WINDOW_SIZE&lt;/code&gt; is 60, matching the 1-second backend tick — a 60-point rolling window is one minute of history, which is long enough to read a trend and short enough that the chart stays responsive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewriting the client's config
&lt;/h2&gt;

&lt;p&gt;The last piece is the part that makes the whole trick work: pointing frpc at the relay instead of at the user's service.&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;// Bind an ephemeral port on loopback.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="nn"&gt;TcpListener&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;bind&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="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0u16&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="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&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;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;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="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;crate&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;frpc_state&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;emit_log&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="s"&gt;"system"&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;"中转端口绑定失败，跳过 {name}：{e}"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;continue&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;let&lt;/span&gt; &lt;span class="n"&gt;relay_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="nf"&gt;.local_addr&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="n"&gt;a&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;a&lt;/span&gt;&lt;span class="nf"&gt;.port&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="nf"&gt;Err&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="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Rewrite THIS proxy's localIP/localPort to the relay.&lt;/span&gt;
&lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nn"&gt;ProxyConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Tcp&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;local_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_port&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="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;ProxyConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;local_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_port&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="p"&gt;|&lt;/span&gt; &lt;span class="nn"&gt;ProxyConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Https&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;local_ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;local_port&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;=&amp;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;local_ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"127.0.0.1"&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="n"&gt;local_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;relay_port&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nn"&gt;ProxyConfig&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Udp&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;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* unreachable: UDP is skipped above */&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;Two deliberate decisions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loopback only.&lt;/strong&gt; The relay binds &lt;code&gt;127.0.0.1&lt;/code&gt;, never &lt;code&gt;0.0.0.0&lt;/code&gt;. The relay is an internal implementation detail; exposing it on a routable interface would create a new, unintended entry point into the user's local network.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UDP is skipped.&lt;/strong&gt; The relay is TCP-only because it is implemented as a byte stream. UDP would need a separate, connectionless path. Rather than half-implement it, MoonProxy passes UDP proxies through untouched — they simply do not appear in the traffic graph.&lt;/p&gt;

&lt;p&gt;Binding to port &lt;code&gt;0&lt;/code&gt; and asking the OS for an ephemeral port is also worth calling out: it avoids a port collision with a user's actual service, which a fixed port would eventually cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Stopping the tunnel has to tear down the listeners. Because each accept loop owns its &lt;code&gt;TcpListener&lt;/code&gt;, aborting the task closes the socket:&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_relay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&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;RelayState&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;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="py"&gt;.entries&lt;/span&gt;&lt;span class="nf"&gt;.lock&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;for&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="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="nf"&gt;.drain&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="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;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="py"&gt;.accept_task&lt;/span&gt;&lt;span class="nf"&gt;.lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="nf"&gt;.take&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="nf"&gt;.abort&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;p&gt;In-flight connections end on their own: their copy loop hits a read or write error when the peer goes away, and the &lt;code&gt;ConnGuard&lt;/code&gt; drops on the way out, so the connection count returns to zero without any explicit bookkeeping.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this buys you
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Per-tunnel byte counts, attributed exactly, with no host-level guessing&lt;/li&gt;
&lt;li&gt;Rates that survive a slow network because they are differenced from cumulative totals&lt;/li&gt;
&lt;li&gt;A connection count that is correct on every exit path, including panics&lt;/li&gt;
&lt;li&gt;No changes to the tunnel client itself — you only rewrite where it points&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is not FRP-specific. Any time a subprocess owns the socket and you want to observe the traffic, you can insert yourself in the middle, count the bytes, and let the subprocess do what it was always going to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  MoonProxy
&lt;/h2&gt;

&lt;p&gt;MoonProxy is a free, open-source (MIT) cross-platform GUI desktop client for FRP, built with Tauri v2, Vue 3, and Rust. It bundles the &lt;code&gt;frpc&lt;/code&gt; binary, so you do not need to install anything separately.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;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;Homepage: &lt;a href="https://moonproxy.app" rel="noopener noreferrer"&gt;https://moonproxy.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Runs on macOS (Apple Silicon and Intel) and Windows x64&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If this kind of low-level plumbing is your thing, the &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop" rel="noopener noreferrer"&gt;source is on GitHub&lt;/a&gt; and I am happy to answer questions about the details.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>tauri</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Tunneling From a Raspberry Pi: How FRP Actually Behaves on Low-Power ARM Hardware</title>
      <dc:creator>ChenXX</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:18:57 +0000</pubDate>
      <link>https://dev.to/chenxxpro/tunneling-from-a-raspberry-pi-how-frp-actually-behaves-on-low-power-arm-hardware-1kef</link>
      <guid>https://dev.to/chenxxpro/tunneling-from-a-raspberry-pi-how-frp-actually-behaves-on-low-power-arm-hardware-1kef</guid>
      <description>&lt;p&gt;A Raspberry Pi is the most common way people first meet FRP. You've got Home Assistant, a small web dashboard, maybe a Minecraft server for the kids — all sitting on the Pi, all needing to be reachable from outside the house.&lt;/p&gt;

&lt;p&gt;So you install FRP on the Pi and expose it. Then you open a second terminal and think: &lt;em&gt;is this really what I need every time?&lt;/em&gt; And whether you should run &lt;code&gt;frpc&lt;/code&gt; from a config file, or a tray-based GUI client that assumes someone's sitting at the machine.&lt;/p&gt;

&lt;p&gt;Here's what actually happens on real hardware.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;FRP's client (&lt;code&gt;frpc&lt;/code&gt;) is a single static Go binary. On a Pi it's cheap — it's a Go daemon with no runtime dependencies, near-zero CPU when idle, and it doesn't need a desktop environment. For a headless Pi you SSH into, &lt;code&gt;frpc&lt;/code&gt; is genuinely the right tool and I'd keep using it.&lt;/p&gt;

&lt;p&gt;The friction starts when the Pi isn't yours alone. Family members, a partner, or yourself two weeks later — now the SSH + edit + restart loop is the thing that costs time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Pi actually costs you
&lt;/h2&gt;

&lt;p&gt;The tunnel itself is cheap. The &lt;strong&gt;process management around it&lt;/strong&gt; is what isn't:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't survive a reboot.&lt;/strong&gt; Your SSH session is gone, so nothing restarts &lt;code&gt;frpc&lt;/code&gt; unless you wrote a systemd unit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It doesn't tell you it's dead.&lt;/strong&gt; If &lt;code&gt;frpc&lt;/code&gt; crashes, your public address just starts refusing connections, and you find out when a user complains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changing one port means editing TOML over SSH.&lt;/strong&gt; Fine for you. Not fine for someone who isn't sure what &lt;code&gt;localPort&lt;/code&gt; vs &lt;code&gt;remotePort&lt;/code&gt; means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There's no status you can glance at.&lt;/strong&gt; On a headless box you'd run &lt;code&gt;systemctl status frpc&lt;/code&gt; or &lt;code&gt;journalctl&lt;/code&gt; — which is a terminal, every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this is a criticism of FRP. &lt;code&gt;frpc&lt;/code&gt; is a great tool. It's that the &lt;em&gt;operating model&lt;/em&gt; assumes you live in a terminal, and a Pi sitting under a desk is precisely a machine nobody sits at.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ARM reality check
&lt;/h2&gt;

&lt;p&gt;People assume a GUI will be too heavy for a Pi. The tradeoffs to weigh:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;frpc&lt;/code&gt; (headless)&lt;/th&gt;
&lt;th&gt;Tauri GUI client&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Desktop required&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Yes&lt;/strong&gt; — needs a display&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives reboot&lt;/td&gt;
&lt;td&gt;Only via systemd&lt;/td&gt;
&lt;td&gt;Native launch-at-login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Crash visibility&lt;/td&gt;
&lt;td&gt;&lt;code&gt;journalctl&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On-screen state + health polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Edit a tunnel&lt;/td&gt;
&lt;td&gt;Edit TOML over SSH&lt;/td&gt;
&lt;td&gt;Click&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;Modest — a static Go daemon&lt;/td&gt;
&lt;td&gt;Higher — GUI + webview runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important line in that table: &lt;strong&gt;a desktop GUI needs a desktop.&lt;/strong&gt; A headless Pi with no monitor has no window to click into — in that case &lt;code&gt;frpc&lt;/code&gt; + a systemd unit genuinely is the better engineering choice, and MoonProxy isn't meant to replace it.&lt;/p&gt;

&lt;p&gt;MoonProxy runs on &lt;strong&gt;macOS and Windows&lt;/strong&gt;, not Linux/Pi. So think of it as the answer for the machine that &lt;em&gt;has&lt;/em&gt; a screen — your laptop, a desktop in the homelab, the box you actually sit down at — rather than for the Pi itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern that actually works
&lt;/h2&gt;

&lt;p&gt;The setup that's held up well across homelab setups:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Pi keeps the service, and runs headless &lt;code&gt;frpc&lt;/code&gt;&lt;/strong&gt; — it's a single binary plus a systemd unit. Lean and scriptable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The machine with a screen runs MoonProxy&lt;/strong&gt; to manage tunnels in a form instead of a file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You end up with one tunnel config in two places, which sounds like duplication — but in practice each side only owns the tunnels it actually needs, and you get both: unattended reliability on the Pi, and no-SSH convenience at the desk.&lt;/p&gt;

&lt;p&gt;If you &lt;em&gt;only&lt;/em&gt; have the Pi, keep using &lt;code&gt;frpc&lt;/code&gt;. Nothing below changes that recommendation.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you're on a Pi and tired of &lt;code&gt;journalctl&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Three options, roughly in order of how much I'd recommend them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write the systemd unit properly.&lt;/strong&gt; &lt;code&gt;Restart=always&lt;/code&gt;, &lt;code&gt;RestartSec=5&lt;/code&gt;, and a &lt;code&gt;WantedBy=multi-user.target&lt;/code&gt;. This is the fix that makes &lt;code&gt;frpc&lt;/code&gt; boring in the best way. Smallest possible change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a lightweight web UI for &lt;code&gt;frpc&lt;/code&gt;&lt;/strong&gt; if you genuinely want a browser interface on the Pi — that's a real category (see FRP's own docs and community projects).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use MoonProxy on a machine that has a display&lt;/strong&gt; if your tunnels mostly live on laptops/desktops rather than the Pi.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this looks like in MoonProxy, concretely
&lt;/h2&gt;

&lt;p&gt;On a machine with a screen, the tunnel is a form instead of a file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Add a tunnel&lt;/strong&gt; by filling fields — local port, remote port, type, token. No TOML, no syntax to get wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four-state start button&lt;/strong&gt; — stopped / connecting / connected / error. "Connected" is derived from real &lt;code&gt;frpc&lt;/code&gt; output, not an optimistic flag, so you can trust the green state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health polling every 3 seconds&lt;/strong&gt; — it checks whether your local port is actually reachable, so you find out &lt;em&gt;your service is down&lt;/em&gt; before the person using it does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tray-resident&lt;/strong&gt; — close the window and the tunnel keeps running. Launch-at-login brings it back after a reboot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scheduled connect&lt;/strong&gt; — set weekdays and a time window, so a dev tunnel isn't exposed at 3am.&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, SHA256-verifies, and swaps the binary atomically without reinstalling the app.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You bring your own &lt;code&gt;frps&lt;/code&gt; — MoonProxy is strictly the client side, and it doesn't relay your traffic through anyone's infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bottom line
&lt;/h2&gt;

&lt;p&gt;On a Raspberry Pi, FRP works well and &lt;code&gt;frpc&lt;/code&gt; is the correct tool. Don't reach for a GUI to solve a problem a systemd unit solves.&lt;/p&gt;

&lt;p&gt;But if the machine that runs your tunnels is one you actually sit at — a laptop, a desktop — the GUI layer earns its place, because the thing it removes isn't complexity, it's SSH.&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've already run FRP on a Pi, I'd genuinely like to hear what numbers you got — CPU and memory on a Pi 4 vs a Pi 5 is exactly the kind of detail I'd like to add to the article. 👋&lt;/p&gt;

</description>
      <category>frp</category>
      <category>raspberrypi</category>
      <category>selfhosted</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>Sat, 26 Sep 2026 08:06:31 +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-5hji</link>
      <guid>https://dev.to/chenxxpro/how-to-put-a-local-service-on-the-public-internet-with-frp-without-losing-your-mind-over-config-5hji</guid>
      <description>&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;FRP's configuration looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[ssh]&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;local_ip&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;local_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;22&lt;/span&gt;
&lt;span class="py"&gt;remote_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;6000&lt;/span&gt;

&lt;span class="nn"&gt;[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;http&lt;/span&gt;
&lt;span class="py"&gt;local_port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;8080&lt;/span&gt;
&lt;span class="py"&gt;custom_domains&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;myapp.example.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time you add a service, you edit this file, save it, and restart &lt;code&gt;frpc&lt;/code&gt;. For developers, it's manageable. For everyone else, it's a barrier.&lt;/p&gt;

&lt;h2&gt;
  
  
  What MoonProxy Does
&lt;/h2&gt;

&lt;p&gt;MoonProxy wraps FRP's &lt;code&gt;frpc&lt;/code&gt; into a native desktop app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visual configuration&lt;/strong&gt; — Fill a form instead of editing INI files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One-click start/stop&lt;/strong&gt; — Toggle tunnels without touching the terminal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rule management&lt;/strong&gt; — Manage up to 50 proxy rules in one window&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-reconnect&lt;/strong&gt; — Automatically recoble after network interruptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-platform&lt;/strong&gt; — macOS (Intel + Apple Silicon) and Windows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dark mode&lt;/strong&gt; — Easy on the eyes&lt;/li&gt;
&lt;/ul&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&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;Rust&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;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Engine&lt;/td&gt;
&lt;td&gt;frp (frpc)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The installer is ~5MB — a fraction of Electron-based alternatives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Download&lt;/strong&gt; MoonProxy from &lt;a href="https://github.com/MoonProxyHQ/moonproxy-desktop/releases/latest" rel="noopener noreferrer"&gt;GitHub Releases&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install&lt;/strong&gt; the app (DMG for macOS, EXE for Windows)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure&lt;/strong&gt; your FRP server address and port&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add&lt;/strong&gt; proxy rules for your local services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start&lt;/strong&gt; — that's it&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Use Cases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remote NAS access&lt;/strong&gt; — Reach your home file server from anywhere&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev preview&lt;/strong&gt; — Share localhost projects with clients&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Home automation&lt;/strong&gt; — Access Home Assistant while traveling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game servers&lt;/strong&gt; — Host Minecraft or Valheim for friends&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted apps&lt;/strong&gt; — Expose Jellyfin, Gitea, Vaultwarden, etc.&lt;/li&gt;
&lt;/ul&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;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're using FRP or self-hosting services, give MoonProxy a try. Stars and feedback are appreciated! ⭐&lt;/p&gt;

</description>
      <category>frp</category>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <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>
  </channel>
</rss>
