feat: agnostic tunnel system — bring your own tunnel provider
New Tunnel trait + 5 implementations:
- NoneTunnel: local-only, no external exposure (default)
- CloudflareTunnel: wraps cloudflared binary, extracts public URL
- TailscaleTunnel: tailscale serve (tailnet) or funnel (public)
- NgrokTunnel: wraps ngrok binary, supports custom domains
- CustomTunnel: user-provided command with {port}/{host} placeholders
Config schema:
- [tunnel] section with provider selector
- Provider-specific sub-configs: cloudflare, tailscale, ngrok, custom
- Backward compatible (serde default = "none")
Gateway integration:
- Tunnel starts automatically on 'zeroclaw gateway'
- Prints public URL on success, falls back to local on failure
20 new tests (factory, constructors, NoneTunnel async start/health)
649 tests passing, 0 clippy warnings, cargo fmt clean
This commit is contained in:
parent
bc31e4389b
commit
390cbc0a6c
12 changed files with 967 additions and 4 deletions
|
|
@ -33,6 +33,9 @@ pub struct Config {
|
|||
|
||||
#[serde(default)]
|
||||
pub memory: MemoryConfig,
|
||||
|
||||
#[serde(default)]
|
||||
pub tunnel: TunnelConfig,
|
||||
}
|
||||
|
||||
// ── Memory ───────────────────────────────────────────────────
|
||||
|
|
@ -146,6 +149,72 @@ impl Default for HeartbeatConfig {
|
|||
}
|
||||
}
|
||||
|
||||
// ── Tunnel ──────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TunnelConfig {
|
||||
/// "none", "cloudflare", "tailscale", "ngrok", "custom"
|
||||
pub provider: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub cloudflare: Option<CloudflareTunnelConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub tailscale: Option<TailscaleTunnelConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub ngrok: Option<NgrokTunnelConfig>,
|
||||
|
||||
#[serde(default)]
|
||||
pub custom: Option<CustomTunnelConfig>,
|
||||
}
|
||||
|
||||
impl Default for TunnelConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: "none".into(),
|
||||
cloudflare: None,
|
||||
tailscale: None,
|
||||
ngrok: None,
|
||||
custom: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CloudflareTunnelConfig {
|
||||
/// Cloudflare Tunnel token (from Zero Trust dashboard)
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TailscaleTunnelConfig {
|
||||
/// Use Tailscale Funnel (public internet) vs Serve (tailnet only)
|
||||
#[serde(default)]
|
||||
pub funnel: bool,
|
||||
/// Optional hostname override
|
||||
pub hostname: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct NgrokTunnelConfig {
|
||||
/// ngrok auth token
|
||||
pub auth_token: String,
|
||||
/// Optional custom domain
|
||||
pub domain: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CustomTunnelConfig {
|
||||
/// Command template to start the tunnel. Use {port} and {host} placeholders.
|
||||
/// Example: "bore local {port} --to bore.pub"
|
||||
pub start_command: String,
|
||||
/// Optional URL to check tunnel health
|
||||
pub health_url: Option<String>,
|
||||
/// Optional regex to extract public URL from command stdout
|
||||
pub url_pattern: Option<String>,
|
||||
}
|
||||
|
||||
// ── Channels ─────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -236,6 +305,7 @@ impl Default for Config {
|
|||
heartbeat: HeartbeatConfig::default(),
|
||||
channels_config: ChannelsConfig::default(),
|
||||
memory: MemoryConfig::default(),
|
||||
tunnel: TunnelConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -373,6 +443,7 @@ mod tests {
|
|||
matrix: None,
|
||||
},
|
||||
memory: MemoryConfig::default(),
|
||||
tunnel: TunnelConfig::default(),
|
||||
};
|
||||
|
||||
let toml_str = toml::to_string_pretty(&config).unwrap();
|
||||
|
|
@ -432,6 +503,7 @@ default_temperature = 0.7
|
|||
heartbeat: HeartbeatConfig::default(),
|
||||
channels_config: ChannelsConfig::default(),
|
||||
memory: MemoryConfig::default(),
|
||||
tunnel: TunnelConfig::default(),
|
||||
};
|
||||
|
||||
config.save().unwrap();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue