Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: CI

on:
push:
branches: [main]
pull_request:

jobs:
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run build

rust:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The rust job runs cargo check/cargo test on ubuntu-latest, but EnvTunnel is a Windows-only Tauri app (README lists Windows + Visual Studio Build Tools as the only supported build environment and documents a known Windows link failure). Running on Linux never produces or validates the actual shipping artifact and cannot catch Windows-specific breakages such as the documented LNK1181: legacy_stdio_definitions.lib failure, so the CI gives false confidence that the app builds. Consider running this job on windows-latest (with the LIB workaround for the known issue) and/or adding a tauri build step so the release binary is actually compiled in CI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 21:

<comment>The `rust` job runs `cargo check`/`cargo test` on `ubuntu-latest`, but EnvTunnel is a Windows-only Tauri app (README lists Windows + Visual Studio Build Tools as the only supported build environment and documents a known Windows link failure). Running on Linux never produces or validates the actual shipping artifact and cannot catch Windows-specific breakages such as the documented `LNK1181: legacy_stdio_definitions.lib` failure, so the CI gives false confidence that the app builds. Consider running this job on `windows-latest` (with the LIB workaround for the known issue) and/or adding a `tauri build` step so the release binary is actually compiled in CI.</comment>

<file context>
@@ -0,0 +1,41 @@
+      - run: npm run lint
+      - run: npm run build
+
+  rust:
+    runs-on: ubuntu-latest
+    defaults:
</file context>

runs-on: ubuntu-latest
defaults:
run:
working-directory: src-tauri
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Install Tauri system dependencies
working-directory: .
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libssl-dev
- run: cargo check --locked

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The rust job runs cargo check / cargo test on a fresh runner without a frontend build, so dist/ never exists. tauri::generate_context!() (lib.rs:337) and tauri-build require frontendDist (../dist) to exist and panic with "frontendDist configuration is set to '../dist' but this path doesn't exist", so both rust steps fail on a clean checkout. The frontend job builds dist/ but on a separate runner with no shared filesystem. Add actions/setup-node@v4, npm ci and npm run build (before cargo check) to the rust job, or at minimum create the empty dist/ directory, so the tauri context macro has assets to load.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/ci.yml, line 40:

<comment>The `rust` job runs `cargo check` / `cargo test` on a fresh runner without a frontend build, so `dist/` never exists. `tauri::generate_context!()` (lib.rs:337) and tauri-build require `frontendDist` (`../dist`) to exist and panic with "frontendDist configuration is set to '../dist' but this path doesn't exist", so both rust steps fail on a clean checkout. The `frontend` job builds `dist/` but on a separate runner with no shared filesystem. Add `actions/setup-node@v4`, `npm ci` and `npm run build` (before `cargo check`) to the rust job, or at minimum create the empty `dist/` directory, so the tauri context macro has assets to load.</comment>

<file context>
@@ -0,0 +1,41 @@
+            librsvg2-dev \
+            patchelf \
+            libssl-dev
+      - run: cargo check --locked
+      - run: cargo test --locked --lib
</file context>

- run: cargo test --locked --lib
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ src-tauri/target/
src-tauri/gen/
.kimi/
AGENTS.md

# Built installers — publish via GitHub Releases, not git
release/
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,12 @@ You can also **add any custom port** via the in-app input.

---

<a id="installation"></a>
## 🚀 Installation

### For Users (Just want the .exe)

1. Download `EnvTunnel_0.1.0_x64-setup.exe` from [Releases](../../releases)
1. Download the Windows installer from [Releases](../../releases)
2. Run the installer
3. Launch EnvTunnel from Start Menu or Desktop

Expand All @@ -83,7 +84,7 @@ You can also **add any custom port** via the in-app input.

```bash
# Clone
git clone https://github.com/YOUR_USERNAME/envtunnel.git
git clone https://github.com/hsr88/envtunnel.git
cd envtunnel

# Install dependencies
Expand All @@ -93,7 +94,7 @@ npm install
npm run tauri build

# The .exe will be at:
# src-tauri/target/release/app.exe
# src-tauri/target/release/envtunnel.exe
```

> **Note for Windows builders:** If you get `LNK1181: cannot open input file legacy_stdio_definitions.lib`, add this to your `LIB` environment variable:
Expand Down
Binary file modified docs/images/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
20 changes: 10 additions & 10 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,24 +136,24 @@ <h2 class="section-title">&gt; DOWNLOAD</h2>
<div class="download-grid">
<a class="download-card" href="https://github.com/hsr88/envtunnel/releases/download/v1.0.0/EnvTunnel_0.1.0_x64-setup.exe">
<div class="download-label">INSTALLER</div>
<div class="download-name">EnvTunnel_0.1.0_x64-setup.exe</div>
<div class="download-meta">~2 MB | Recommended</div>
<div class="download-name">EnvTunnel Setup (x64)</div>
<div class="download-meta">v1.0.0 · ~4 MB · Recommended</div>
</a>
<a class="download-card" href="https://github.com/hsr88/envtunnel/releases/download/v1.0.0/EnvTunnel_0.1.0_x64_en-US.msi">
<div class="download-label">MSI</div>
<div class="download-name">EnvTunnel_0.1.0_x64_en-US.msi</div>
<div class="download-meta">~3 MB | Enterprise / IT</div>
<div class="download-name">EnvTunnel MSI (x64)</div>
<div class="download-meta">v1.0.0 · ~5 MB · Enterprise / IT</div>
</a>
<a class="download-card secondary" href="https://github.com/hsr88/envtunnel/releases/download/v1.0.0/EnvTunnel.exe">
<div class="download-label">PORTABLE</div>
<div class="download-name">EnvTunnel.exe</div>
<div class="download-meta">~9 MB | No install</div>
<div class="download-meta">v1.0.0 · ~12 MB · No install</div>
</a>
</div>
<div class="download-note">
Direct download links from GitHub Releases.
Direct downloads from <a href="https://github.com/hsr88/envtunnel/releases/latest" target="_blank" rel="noopener noreferrer">GitHub Releases</a>.
<br>
Looking for macOS or Linux? <a href="https://github.com/hsr88/envtunnel#-installation" target="_blank">Build from source</a>.
Looking for macOS or Linux? <a href="https://github.com/hsr88/envtunnel/blob/main/README.md#installation" target="_blank" rel="noopener noreferrer">Build from source</a>.
</div>
</div>
</section>
Expand All @@ -166,9 +166,9 @@ <h2 class="section-title">&gt; DOWNLOAD</h2>
<span class="footer-copy">Open source under MIT License</span>
</div>
<div class="footer-right">
<a href="https://github.com/hsr88/envtunnel" target="_blank">GitHub</a>
<a href="https://github.com/hsr88/envtunnel/issues" target="_blank">Issues</a>
<a href="https://github.com/hsr88/envtunnel/releases" target="_blank">Releases</a>
<a href="https://github.com/hsr88/envtunnel" target="_blank" rel="noopener noreferrer">GitHub</a>
<a href="https://github.com/hsr88/envtunnel/issues" target="_blank" rel="noopener noreferrer">Issues</a>
<a href="https://github.com/hsr88/envtunnel/releases/latest" target="_blank" rel="noopener noreferrer">Releases</a>
</div>
</div>
</footer>
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "envtunnel",
"private": true,
"version": "0.0.0",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
Binary file modified public/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed release/EnvTunnel.exe
Binary file not shown.
Binary file removed release/EnvTunnel_0.1.0_x64-setup.exe
Binary file not shown.
Binary file removed release/EnvTunnel_0.1.0_x64_en-US.msi
Binary file not shown.
Binary file removed screenshot-1.png
Binary file not shown.
Binary file removed screenshot-2.png
Binary file not shown.
Binary file removed screenshot.png
Binary file not shown.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
[package]
name = "envtunnel"
version = "0.1.0"
version = "1.0.0"
description = "EnvTunnel - Local Development Port Scanner with QR Codes"
authors = ["envtunnel"]
license = "MIT"
repository = ""
repository = "https://github.com/hsr88/envtunnel"
edition = "2021"
rust-version = "1.77.2"

Expand Down
103 changes: 103 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,117 @@ pub struct PortStatus {
pub framework: Option<String>,
}

fn is_skipped_iface(name: &str) -> bool {
let n = name.to_lowercase();
const SKIP: &[&str] = &[
"vpn",
"tun",
"tap",
"wsl",
"vethernet",
"docker",
"hyper-v",
"vbox",
"vmware",
"virtual",
"loopback",
"isatap",
"teredo",
"bluetooth",
"nordlynx",
"tailscale",
"wg0",
"wireguard",
"hamachi",
"zerotier",
"br-",
];
SKIP.iter().any(|s| n.contains(s))
}

/// Prefer typical LAN ranges (192.168, then 10.x, then 172.16-31) and skip
/// VPN/WSL/CGNAT addresses so QR codes point at an IP phones can actually reach.
fn lan_priority(v4: std::net::Ipv4Addr) -> Option<u8> {
if v4.is_loopback() || v4.is_unspecified() || v4.is_link_local() || v4.is_multicast() {
return None;
}
let o = v4.octets();
// CGNAT / Tailscale 100.64.0.0/10
if o[0] == 100 && (64..=127).contains(&o[1]) {
return None;
}
if o[0] == 192 && o[1] == 168 {
Some(0)
} else if o[0] == 10 {
Some(1)
} else if o[0] == 172 && (16..=31).contains(&o[1]) {
Some(2)
} else {
None
}
}

#[tauri::command]
fn get_local_ip() -> Result<String, String> {
if let Ok(ifaces) = local_ip_address::list_afinet_netifas() {
let mut best: Option<(u8, String)> = None;
for (name, ip) in ifaces {
if is_skipped_iface(&name) {
continue;
}
let std::net::IpAddr::V4(v4) = ip else {
continue;
};
let Some(prio) = lan_priority(v4) else {
continue;
};
match &best {
None => best = Some((prio, v4.to_string())),
Some((best_prio, _)) if prio < *best_prio => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When multiple adapters share the same priority (e.g. Ethernet and Wi-Fi both on 192.168.x.x), the _ => {} arm keeps whichever the OS enumerated first, and list_afinet_netifas() order is not guaranteed stable. The chosen IP can therefore change between launches without any network change, producing an inconsistent QR/localhost IP. Break ties deterministically rather than relying on enumeration order.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src-tauri/src/lib.rs, line 82:

<comment>When multiple adapters share the same priority (e.g. Ethernet and Wi-Fi both on 192.168.x.x), the `_ => {}` arm keeps whichever the OS enumerated first, and `list_afinet_netifas()` order is not guaranteed stable. The chosen IP can therefore change between launches without any network change, producing an inconsistent QR/localhost IP. Break ties deterministically rather than relying on enumeration order.</comment>

<file context>
@@ -13,14 +13,117 @@ pub struct PortStatus {
+            };
+            match &best {
+                None => best = Some((prio, v4.to_string())),
+                Some((best_prio, _)) if prio < *best_prio => {
+                    best = Some((prio, v4.to_string()));
+                }
</file context>

best = Some((prio, v4.to_string()));
}
_ => {}
}
}
if let Some((_, ip)) = best {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When no eligible RFC1918 interface exists, this new filtered path falls back to local_ip() and can return the VPN, Tailscale, or CGNAT address that the code claims to skip. Keep the fallback subject to the same interface/address filtering, or return an error when no reachable LAN address is available.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src-tauri/src/lib.rs, line 88:

<comment>When no eligible RFC1918 interface exists, this new filtered path falls back to `local_ip()` and can return the VPN, Tailscale, or CGNAT address that the code claims to skip. Keep the fallback subject to the same interface/address filtering, or return an error when no reachable LAN address is available.</comment>

<file context>
@@ -13,14 +13,117 @@ pub struct PortStatus {
+                _ => {}
+            }
+        }
+        if let Some((_, ip)) = best {
+            return Ok(ip);
+        }
</file context>

return Ok(ip);
}
}

match local_ip_address::local_ip() {
Ok(ip) => Ok(ip.to_string()),
Err(e) => Err(format!("Failed to get local IP: {}", e)),
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::net::Ipv4Addr;

#[test]
fn lan_priority_prefers_rfc1918() {
assert_eq!(lan_priority(Ipv4Addr::new(192, 168, 1, 15)), Some(0));
assert_eq!(lan_priority(Ipv4Addr::new(10, 0, 0, 2)), Some(1));
assert_eq!(lan_priority(Ipv4Addr::new(172, 16, 0, 1)), Some(2));
assert_eq!(lan_priority(Ipv4Addr::new(100, 64, 0, 1)), None);
assert_eq!(lan_priority(Ipv4Addr::new(8, 8, 8, 8)), None);
assert_eq!(lan_priority(Ipv4Addr::LOCALHOST), None);
assert_eq!(lan_priority(Ipv4Addr::new(169, 254, 1, 1)), None);
}

#[test]
fn skips_virtual_ifaces() {
assert!(is_skipped_iface("vEthernet (WSL)"));
assert!(is_skipped_iface("NordLynx"));
assert!(is_skipped_iface("Tailscale"));
assert!(is_skipped_iface("Docker Bridge"));
assert!(!is_skipped_iface("Wi-Fi"));
assert!(!is_skipped_iface("Ethernet"));
assert!(!is_skipped_iface("en0"));
}
}

async fn detect_framework(url: &str) -> Option<String> {
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(2))
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"productName": "EnvTunnel",
"version": "0.1.0",
"version": "1.0.0",
"identifier": "com.envtunnel.app",
"build": {
"frontendDist": "../dist",
Expand Down
Loading
Loading