FileSharer Development Notes 7 — An External Sharing URL Does Not Mean the Connection Is Ready
FileSharer Development Notes · 07 / 10

Sending an internal-network sharing address to someone outside that network requires another route. FileSharer keeps file transfer in the C# application and delegates the external connection to a separate tunnel. The interesting part of this structure is less the command that starts the tunnel than the point at which the connection is considered usable.

This installment examines connection-state handling and reconnection in FileSharer. Creating a tunnel with a real account, issuing a public link, and testing relay-service performance are outside its verification scope.
Why bundle a Go executable inside a C# application?
Alongside the C# application, the project includes tsproxy/main.go, a small relay program that uses Tailscale's Go library. The main application extracts the embedded, compressed tsproxy.exe.gz, runs it as a child process, and passes it an address for connecting to the local HTTP server.
Separating these roles avoids having to rebuild file selection and the Windows UI in Go. It also avoids forcing the relay library's use into C#. In exchange, it introduces new responsibilities: bundling the executable, terminating the process, and communicating status.
The current code passes authentication values through the child process's environment variables instead of appending them directly to the command-line string. This does not guarantee that the values are hidden from every form of local access. The fact established here is that the transfer channel is separate from command-line arguments.
Standard output is an interface between the two programs
C# reads the Go process's standard output and standard error. When it detects a predefined prefix, it handles the result.
FUNNEL_URL=<public address>
TSPROXY_ERROR=<error description>
This distinguishes general progress logs from machine-readable results. If the convention changes, the relay program could be running normally while the main application fails to receive its address. This message format therefore needs to be managed alongside the versions of the executable and the main application.
Break readiness into four stages
Whether a process exists, whether a public name resolves, whether HTTPS responds, and whether an actual file can be downloaded are separate checks. The Go source contains functions that wait for DNS and HTTPS readiness, but they do not return a distinct failure result when readiness cannot be verified or the wait times out. The caller prints readiness-stage logs and the public address without a warning, so the appearance of an address alone does not establish that the readiness checks succeeded.
| Observation | What it establishes |
|---|---|
| Child process has not exited | Local process liveness |
| DNS check succeeds | The name can be resolved to an address |
| HTTPS readiness check succeeds | A response from the path used by that check |
| Sample file received and contents compared | The result of that file's delivery path |
Distinguishing these stages in the product UI would also help users understand why an address exists but does not open. Packing every meaning into one green indicator makes the source of failure harder to identify.
Monitoring choices intended to avoid interrupting working transfers
A WinForms timer monitors the connection every 25 seconds and attempts a deeper reachability check on every third tick. It counts consecutive failures instead of reconnecting immediately after a single delay. The intent is to avoid interrupting an existing download when a check request runs slowly during a large transfer.
However, the activeTransfers value used to decide whether to skip a check counts open transfer operations, not bytes that have flowed recently. LAN transfers use the same counter. “A transfer operation exists” therefore cannot be expanded into “data continues to flow through the external tunnel.” Recording the latest progress time separately from the state of the external route is a candidate for future improvement.
Reconnection also includes rebuilding the link
Comparing the source paths revealed a difference in recovery behavior. The initial external link includes a fragment containing decryption information, but OnTunnelReconnected constructs the new link using only the public address and the share ID. The browser's decryption startup code stops if the required fragment is missing.
This difference is why reconnection tests should go beyond checking the process and address, continuing through the PIN page to the final file. The code indicates a possible failure; reproduction through a live external tunnel still requires separate verification.
The Windows child-process cleanup code also attempts to associate the process with a Job Object. It sets the JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE flag. When the last handle to a Job with this flag applied is closed, its associated processes are also terminated. This condition is described in Microsoft's Job Objects documentation. Association failures and exceptions during shutdown still need to be tested before the entire lifecycle can be considered managed.
External sharing involves more than obtaining an address. Making startup, readiness, use, reconnection, and shutdown individually observable helps narrow down problems.
FileSharer.cs:1601–1689 TailscaleTunnel.Start · FileSharer.cs:1468–1553 ChildProcessGuard · FileSharer.cs:3290–3421 Tunnel monitoring and reconnection · FileSharer.cs:4288 Initial external link construction · tsproxy/main.go:110–232 DNS and HTTPS checks · tsproxy/main.go:393–407 Server startup and URL output
Proposed improvements are distinguished from implemented behavior.
댓글
댓글 쓰기