Compare commits
1 Commits
macos-mitm
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7455f76351 |
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2366,6 +2366,7 @@ dependencies = [
|
|||||||
"async-stream",
|
"async-stream",
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
|
"boring2",
|
||||||
"brotli 7.0.0",
|
"brotli 7.0.0",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -2386,6 +2387,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-boring2",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ rustls = { version = "0.23", features = ["ring"] }
|
|||||||
tokio-rustls = "0.26"
|
tokio-rustls = "0.26"
|
||||||
rustls-native-certs = "0.8"
|
rustls-native-certs = "0.8"
|
||||||
rustls-pemfile = "2"
|
rustls-pemfile = "2"
|
||||||
|
boring2 = "5.0.0-alpha.12"
|
||||||
|
tokio-boring2 = "5.0.0-alpha.12"
|
||||||
time = "0.3"
|
time = "0.3"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
httparse = "1"
|
httparse = "1"
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ Explain to the user what this project unlocks — not what it _is_, but what bec
|
|||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
The LS or Language Server is Antigravity's closed source Go binary that talks to Google's API over gRPC. The Extension Server is what feeds it auth tokens and settings/configs, we fake it with a stub so the LS thinks it's inside a real Antigravity window. ZeroGravity turns your OpenAI-compatible requests into dummy prompts and tells the LS to make an API call. The MITM proxy intercepts that call before it leaves the machine, swaps in your real prompt, tools, images, and generation params, re-encrypts it with BoringSSL matching Chrome's exact TLS fingerprint, and forwards it to Google. Google sees what looks like a normal Antigravity session. The response streams back as SSE events which the MITM parses for text, thinking tokens, tool calls, and usage. The iptables redirect is a UID-scoped firewall rule that routes only the LS's traffic through the MITM without touching anything else.
|
The LS or Language Server is Antigravity's closed source Go binary that talks to Google's API over gRPC. The Extension Server is what feeds it auth tokens and settings/configs, we fake it with a stub so the LS thinks it's inside a real Antigravity window. ZeroGravity turns your OpenAI-compatible requests into dummy prompts and tells the LS to make an API call. The MITM proxy intercepts that call before it leaves the machine, swaps in your real prompt, tools, images, and generation params, re-encrypts it over TLS, and forwards it to Google. All proxy-to-LS communication uses BoringSSL with Chrome's exact TLS and HTTP/2 fingerprint so the LS can't tell it's not a real Antigravity window. Google sees what looks like a normal Antigravity session. The response streams back as SSE events which the MITM parses for text, thinking tokens, tool calls, and usage. The iptables redirect is a UID-scoped firewall rule that routes only the LS's traffic through the MITM without touching anything else.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#2a2a2a', 'primaryTextColor': '#d0d0d0', 'primaryBorderColor': '#888', 'lineColor': '#888', 'secondaryColor': '#333', 'tertiaryColor': '#3a3a3a', 'edgeLabelBackground': '#2a2a2a', 'nodeTextColor': '#d0d0d0'}}}%%
|
%%{init: {'theme': 'dark', 'themeVariables': {'primaryColor': '#2a2a2a', 'primaryTextColor': '#d0d0d0', 'primaryBorderColor': '#888', 'lineColor': '#888', 'secondaryColor': '#333', 'tertiaryColor': '#3a3a3a', 'edgeLabelBackground': '#2a2a2a', 'nodeTextColor': '#d0d0d0'}}}%%
|
||||||
|
|||||||
@@ -325,9 +325,7 @@ fn svc_stop() -> bool {
|
|||||||
}
|
}
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
{
|
{
|
||||||
let _ = Command::new("pkill")
|
let _ = Command::new("pkill").args(["-f", "zerogravity"]).status();
|
||||||
.args(["-f", "zerogravity"])
|
|
||||||
.status();
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@@ -517,9 +515,8 @@ fn do_test(msg: &str) {
|
|||||||
.replace('\n', "\\n")
|
.replace('\n', "\\n")
|
||||||
.replace('\r', "\\r")
|
.replace('\r', "\\r")
|
||||||
.replace('\t', "\\t");
|
.replace('\t', "\\t");
|
||||||
let body = format!(
|
let body =
|
||||||
r#"{{"model":"gemini-3-flash","input":"{escaped}","stream":false,"timeout":30}}"#
|
format!(r#"{{"model":"gemini-3-flash","input":"{escaped}","stream":false,"timeout":30}}"#);
|
||||||
);
|
|
||||||
match curl_post("/v1/responses", &body) {
|
match curl_post("/v1/responses", &body) {
|
||||||
Some(json) => jq_print(&json),
|
Some(json) => jq_print(&json),
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
@@ -42,15 +42,15 @@ use tracing::{debug, info, trace, warn};
|
|||||||
/// We mirror this by maintaining a single upstream connection per domain.
|
/// We mirror this by maintaining a single upstream connection per domain.
|
||||||
struct UpstreamPool {
|
struct UpstreamPool {
|
||||||
domain: String,
|
domain: String,
|
||||||
tls_config: Arc<rustls::ClientConfig>,
|
tls_connector: boring2::ssl::SslConnector,
|
||||||
sender: Mutex<Option<hyper::client::conn::http2::SendRequest<Full<Bytes>>>>,
|
sender: Mutex<Option<hyper::client::conn::http2::SendRequest<Full<Bytes>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpstreamPool {
|
impl UpstreamPool {
|
||||||
fn new(domain: String, tls_config: Arc<rustls::ClientConfig>) -> Self {
|
fn new(domain: String, tls_connector: boring2::ssl::SslConnector) -> Self {
|
||||||
Self {
|
Self {
|
||||||
domain,
|
domain,
|
||||||
tls_config,
|
tls_connector,
|
||||||
sender: Mutex::new(None),
|
sender: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,17 +82,29 @@ impl UpstreamPool {
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| format!("upstream TCP connect to {} failed: {e}", self.domain))?;
|
.map_err(|e| format!("upstream TCP connect to {} failed: {e}", self.domain))?;
|
||||||
|
|
||||||
let connector = tokio_rustls::TlsConnector::from(self.tls_config.clone());
|
let ssl = self
|
||||||
let server_name = rustls::pki_types::ServerName::try_from(self.domain.clone())
|
.tls_connector
|
||||||
.map_err(|e| format!("invalid domain {}: {e}", self.domain))?;
|
.configure()
|
||||||
|
.map_err(|e| format!("SSL configure: {e}"))?
|
||||||
|
.into_ssl(&self.domain)
|
||||||
|
.map_err(|e| format!("SSL into_ssl: {e}"))?;
|
||||||
|
|
||||||
let upstream_tls = connector
|
let mut upstream_tls = tokio_boring2::SslStream::new(ssl, upstream_tcp)
|
||||||
.connect(server_name, upstream_tcp)
|
|
||||||
.await
|
|
||||||
.map_err(|e| format!("upstream TLS to {} failed: {e}", self.domain))?;
|
.map_err(|e| format!("upstream TLS to {} failed: {e}", self.domain))?;
|
||||||
|
|
||||||
|
std::pin::Pin::new(&mut upstream_tls)
|
||||||
|
.connect()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("TLS handshake to {} failed: {e}", self.domain))?;
|
||||||
|
|
||||||
let upstream_io = TokioIo::new(upstream_tls);
|
let upstream_io = TokioIo::new(upstream_tls);
|
||||||
|
// Configure HTTP/2 SETTINGS to match Go's net/http2 defaults
|
||||||
|
// Source: golang.org/x/net/http2/transport.go
|
||||||
let (sender, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new())
|
let (sender, conn) = hyper::client::conn::http2::Builder::new(TokioExecutor::new())
|
||||||
|
.initial_stream_window_size(4 << 20) // 4MB (Go: transportDefaultStreamFlow)
|
||||||
|
.initial_connection_window_size(1 << 30) // 1GB (Go: transportDefaultConnFlow)
|
||||||
|
.max_header_list_size(10 * 1024 * 1024) // 10MB (Go: defaultMaxHeaderListSize)
|
||||||
|
.adaptive_window(false) // Go doesn't use adaptive windowing
|
||||||
.handshake(upstream_io)
|
.handshake(upstream_io)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("upstream h2 handshake to {} failed: {e}", self.domain))?;
|
.map_err(|e| format!("upstream h2 handshake to {} failed: {e}", self.domain))?;
|
||||||
@@ -140,22 +152,11 @@ where
|
|||||||
{
|
{
|
||||||
info!(domain = %domain, "MITM H2: handling HTTP/2 connection");
|
info!(domain = %domain, "MITM H2: handling HTTP/2 connection");
|
||||||
|
|
||||||
// Build TLS config for upstream connections
|
// Build upstream TLS connector matching Go's crypto/tls fingerprint (with ALPN h2)
|
||||||
let mut root_store = rustls::RootCertStore::empty();
|
let upstream_connector = super::tls::build_go_tls_connector(Some(&[b"h2"]));
|
||||||
let native_certs = rustls_native_certs::load_native_certs();
|
|
||||||
for cert in native_certs.certs {
|
|
||||||
let _ = root_store.add(cert);
|
|
||||||
}
|
|
||||||
let mut upstream_tls_config = rustls::ClientConfig::builder()
|
|
||||||
.with_root_certificates(root_store)
|
|
||||||
.with_no_client_auth();
|
|
||||||
upstream_tls_config.alpn_protocols = vec![b"h2".to_vec()];
|
|
||||||
|
|
||||||
// Shared upstream connection pool (single connection, multiplexed)
|
// Shared upstream connection pool (single connection, multiplexed)
|
||||||
let pool = Arc::new(UpstreamPool::new(
|
let pool = Arc::new(UpstreamPool::new(domain.clone(), upstream_connector));
|
||||||
domain.clone(),
|
|
||||||
Arc::new(upstream_tls_config),
|
|
||||||
));
|
|
||||||
|
|
||||||
let io = TokioIo::new(tls_stream);
|
let io = TokioIo::new(tls_stream);
|
||||||
let domain = Arc::new(domain);
|
let domain = Arc::new(domain);
|
||||||
|
|||||||
@@ -18,3 +18,4 @@ pub mod modify;
|
|||||||
pub mod proto;
|
pub mod proto;
|
||||||
pub mod proxy;
|
pub mod proxy;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
pub mod tls;
|
||||||
|
|||||||
@@ -368,20 +368,11 @@ async fn handle_http_over_tls(
|
|||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut tmp = vec![0u8; 32768];
|
let mut tmp = vec![0u8; 32768];
|
||||||
|
|
||||||
// Build upstream TLS connector once for this connection
|
// Build upstream TLS connector matching Go's crypto/tls fingerprint
|
||||||
let mut root_store = rustls::RootCertStore::empty();
|
let upstream_connector = super::tls::build_go_tls_connector(None);
|
||||||
let native_certs = rustls_native_certs::load_native_certs();
|
|
||||||
for cert in native_certs.certs {
|
|
||||||
let _ = root_store.add(cert);
|
|
||||||
}
|
|
||||||
let upstream_config = Arc::new(
|
|
||||||
rustls::ClientConfig::builder()
|
|
||||||
.with_root_certificates(root_store)
|
|
||||||
.with_no_client_auth(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Reusable upstream connection — created lazily, reconnected if stale
|
// Reusable upstream connection — created lazily, reconnected if stale
|
||||||
let mut upstream: Option<tokio_rustls::client::TlsStream<TcpStream>> = None;
|
let mut upstream: Option<tokio_boring2::SslStream<TcpStream>> = None;
|
||||||
|
|
||||||
// Keep-alive loop: handle multiple requests on this connection
|
// Keep-alive loop: handle multiple requests on this connection
|
||||||
loop {
|
loop {
|
||||||
@@ -575,7 +566,7 @@ async fn handle_http_over_tls(
|
|||||||
let conn = match upstream.as_mut() {
|
let conn = match upstream.as_mut() {
|
||||||
Some(c) => c,
|
Some(c) => c,
|
||||||
None => {
|
None => {
|
||||||
let c = connect_upstream(domain, &upstream_config).await?;
|
let c = connect_upstream(domain, &upstream_connector).await?;
|
||||||
upstream.insert(c)
|
upstream.insert(c)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -583,7 +574,7 @@ async fn handle_http_over_tls(
|
|||||||
// Forward the request — if write fails, reconnect and retry once
|
// Forward the request — if write fails, reconnect and retry once
|
||||||
if let Err(e) = conn.write_all(&request_buf).await {
|
if let Err(e) = conn.write_all(&request_buf).await {
|
||||||
debug!(domain, error = %e, "MITM: upstream write failed, reconnecting");
|
debug!(domain, error = %e, "MITM: upstream write failed, reconnecting");
|
||||||
let c = connect_upstream(domain, &upstream_config).await?;
|
let c = connect_upstream(domain, &upstream_connector).await?;
|
||||||
let conn = upstream.insert(c);
|
let conn = upstream.insert(c);
|
||||||
conn.write_all(&request_buf)
|
conn.write_all(&request_buf)
|
||||||
.await
|
.await
|
||||||
@@ -905,15 +896,15 @@ async fn read_full_request(
|
|||||||
|
|
||||||
/// Connect (or reconnect) to the real upstream via TLS.
|
/// Connect (or reconnect) to the real upstream via TLS.
|
||||||
///
|
///
|
||||||
|
/// Uses BoringSSL configured to match Go's `crypto/tls` fingerprint.
|
||||||
/// Bypasses /etc/hosts by resolving via direct DNS query (dig @8.8.8.8),
|
/// Bypasses /etc/hosts by resolving via direct DNS query (dig @8.8.8.8),
|
||||||
/// then falls back to cached IPs file, then to normal system resolution.
|
/// then falls back to cached IPs file, then to normal system resolution.
|
||||||
async fn connect_upstream(
|
async fn connect_upstream(
|
||||||
domain: &str,
|
domain: &str,
|
||||||
config: &Arc<rustls::ClientConfig>,
|
connector: &boring2::ssl::SslConnector,
|
||||||
) -> Result<tokio_rustls::client::TlsStream<TcpStream>, String> {
|
) -> Result<tokio_boring2::SslStream<TcpStream>, String> {
|
||||||
let connector = tokio_rustls::TlsConnector::from(config.clone());
|
|
||||||
let addr = resolve_upstream(domain).await;
|
let addr = resolve_upstream(domain).await;
|
||||||
info!(domain, addr = %addr, "MITM: connecting upstream");
|
info!(domain, addr = %addr, "MITM: connecting upstream (BoringSSL)");
|
||||||
|
|
||||||
let tcp = match tokio::time::timeout(
|
let tcp = match tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(15),
|
std::time::Duration::from_secs(15),
|
||||||
@@ -926,20 +917,26 @@ async fn connect_upstream(
|
|||||||
Err(_) => return Err(format!("Connect to upstream {domain} ({addr}): timed out")),
|
Err(_) => return Err(format!("Connect to upstream {domain} ({addr}): timed out")),
|
||||||
};
|
};
|
||||||
|
|
||||||
let server_name = rustls::pki_types::ServerName::try_from(domain.to_string())
|
let ssl = connector
|
||||||
.map_err(|e| format!("Invalid server name: {e}"))?;
|
.configure()
|
||||||
|
.map_err(|e| format!("SSL configure: {e}"))?
|
||||||
|
.into_ssl(domain)
|
||||||
|
.map_err(|e| format!("SSL into_ssl: {e}"))?;
|
||||||
|
|
||||||
|
let mut stream = tokio_boring2::SslStream::new(ssl, tcp)
|
||||||
|
.map_err(|e| format!("SslStream::new for {domain}: {e}"))?;
|
||||||
|
|
||||||
match tokio::time::timeout(
|
match tokio::time::timeout(
|
||||||
std::time::Duration::from_secs(15),
|
std::time::Duration::from_secs(15),
|
||||||
connector.connect(server_name, tcp),
|
std::pin::Pin::new(&mut stream).connect(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Ok(s)) => {
|
Ok(Ok(())) => {
|
||||||
info!(domain, "MITM: upstream TLS connected ✓");
|
info!(domain, "MITM: upstream TLS connected ✓ (BoringSSL)");
|
||||||
Ok(s)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => Err(format!("TLS connect to upstream {domain}: {e}")),
|
Ok(Err(e)) => Err(format!("TLS handshake to upstream {domain}: {e}")),
|
||||||
Err(_) => Err(format!("TLS connect to upstream {domain}: timed out")),
|
Err(_) => Err(format!("TLS connect to upstream {domain}: timed out")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
src/mitm/tls.rs
Normal file
86
src/mitm/tls.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
//! Upstream TLS configuration matching Go's `crypto/tls` defaults.
|
||||||
|
//!
|
||||||
|
//! The LS is a Go binary — its outbound TLS to Google uses Go's default
|
||||||
|
//! cipher suites, curves, and signature algorithms. This module configures
|
||||||
|
//! BoringSSL to produce a matching TLS ClientHello so Google sees the same
|
||||||
|
//! JA3/JA4 fingerprint regardless of whether our MITM is active.
|
||||||
|
|
||||||
|
use boring2::ssl::{SslConnector, SslMethod};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// Go's default cipher suites in the exact order Go's `crypto/tls` sends them.
|
||||||
|
///
|
||||||
|
/// TLS 1.3 ciphers (hardcoded in Go, not configurable):
|
||||||
|
/// TLS_AES_128_GCM_SHA256, TLS_AES_256_GCM_SHA384, TLS_CHACHA20_POLY1305_SHA256
|
||||||
|
///
|
||||||
|
/// TLS 1.2 ciphers (Go's default preference order):
|
||||||
|
/// ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-GCM-SHA256,
|
||||||
|
/// ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-GCM-SHA384,
|
||||||
|
/// ECDHE-ECDSA-CHACHA20-POLY1305, ECDHE-RSA-CHACHA20-POLY1305,
|
||||||
|
/// ECDHE-RSA-AES128-SHA, ECDHE-RSA-AES256-SHA,
|
||||||
|
/// AES128-GCM-SHA256, AES256-GCM-SHA384, AES128-SHA, AES256-SHA
|
||||||
|
const GO_CIPHER_LIST: &str = "\
|
||||||
|
TLS_AES_128_GCM_SHA256:\
|
||||||
|
TLS_AES_256_GCM_SHA384:\
|
||||||
|
TLS_CHACHA20_POLY1305_SHA256:\
|
||||||
|
ECDHE-ECDSA-AES128-GCM-SHA256:\
|
||||||
|
ECDHE-RSA-AES128-GCM-SHA256:\
|
||||||
|
ECDHE-ECDSA-AES256-GCM-SHA384:\
|
||||||
|
ECDHE-RSA-AES256-GCM-SHA384:\
|
||||||
|
ECDHE-ECDSA-CHACHA20-POLY1305:\
|
||||||
|
ECDHE-RSA-CHACHA20-POLY1305:\
|
||||||
|
ECDHE-RSA-AES128-SHA:\
|
||||||
|
ECDHE-RSA-AES256-SHA:\
|
||||||
|
AES128-GCM-SHA256:\
|
||||||
|
AES256-GCM-SHA384:\
|
||||||
|
AES128-SHA:\
|
||||||
|
AES256-SHA";
|
||||||
|
|
||||||
|
/// Go's default signature algorithms.
|
||||||
|
const GO_SIGALGS: &str = "\
|
||||||
|
ECDSA+SHA256:\
|
||||||
|
RSA-PSS+SHA256:\
|
||||||
|
RSA+SHA256:\
|
||||||
|
ECDSA+SHA384:\
|
||||||
|
RSA-PSS+SHA384:\
|
||||||
|
RSA+SHA384:\
|
||||||
|
RSA-PSS+SHA512:\
|
||||||
|
RSA+SHA512:\
|
||||||
|
RSA+SHA1";
|
||||||
|
|
||||||
|
/// Build an `SslConnector` that mimics Go's `crypto/tls` defaults.
|
||||||
|
///
|
||||||
|
/// If `alpn` is provided, sets ALPN protocols (e.g. `&[b"h2"]` for HTTP/2).
|
||||||
|
pub fn build_go_tls_connector(alpn: Option<&[&[u8]]>) -> SslConnector {
|
||||||
|
let mut builder =
|
||||||
|
SslConnector::builder(SslMethod::tls_client()).expect("SslConnector::builder");
|
||||||
|
|
||||||
|
// Set Go's cipher list
|
||||||
|
builder
|
||||||
|
.set_cipher_list(GO_CIPHER_LIST)
|
||||||
|
.expect("set_cipher_list");
|
||||||
|
|
||||||
|
// Set Go's signature algorithms
|
||||||
|
builder
|
||||||
|
.set_sigalgs_list(GO_SIGALGS)
|
||||||
|
.expect("set_sigalgs_list");
|
||||||
|
|
||||||
|
// Set Go's default curves: X25519, P-256, P-384
|
||||||
|
// BoringSSL uses set_curves_list with colon-separated names
|
||||||
|
builder
|
||||||
|
.set_curves_list("X25519:P-256:P-384")
|
||||||
|
.expect("set_curves_list");
|
||||||
|
|
||||||
|
// ALPN if requested (for HTTP/2)
|
||||||
|
if let Some(protos) = alpn {
|
||||||
|
let mut wire = Vec::new();
|
||||||
|
for proto in protos {
|
||||||
|
wire.push(proto.len() as u8);
|
||||||
|
wire.extend_from_slice(proto);
|
||||||
|
}
|
||||||
|
builder.set_alpn_protos(&wire).expect("set_alpn_protos");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Built Go-matching TLS connector (BoringSSL)");
|
||||||
|
builder.build()
|
||||||
|
}
|
||||||
@@ -58,11 +58,7 @@ impl Platform {
|
|||||||
let ca_cert_path = env_or("SSL_CERT_FILE", default_ca_cert_path);
|
let ca_cert_path = env_or("SSL_CERT_FILE", default_ca_cert_path);
|
||||||
let ls_user = env_or("ZEROGRAVITY_LS_USER", || "zerogravity-ls".into());
|
let ls_user = env_or("ZEROGRAVITY_LS_USER", || "zerogravity-ls".into());
|
||||||
let state_db_path = env_or("ZEROGRAVITY_STATE_DB", || default_state_db_path(&home));
|
let state_db_path = env_or("ZEROGRAVITY_STATE_DB", || default_state_db_path(&home));
|
||||||
let dns_redirect_so_path = if cfg!(target_os = "macos") {
|
let dns_redirect_so_path = format!("{}/dns-redirect.so", &data_dir);
|
||||||
format!("{}/dns-redirect.dylib", &data_dir)
|
|
||||||
} else {
|
|
||||||
format!("{}/dns-redirect.so", &data_dir)
|
|
||||||
};
|
|
||||||
|
|
||||||
let config_dir = PathBuf::from(&config_dir);
|
let config_dir = PathBuf::from(&config_dir);
|
||||||
let token_path = config_dir.join("token");
|
let token_path = config_dir.join("token");
|
||||||
@@ -108,19 +104,18 @@ fn default_ls_binary_path() -> String {
|
|||||||
fn default_ls_binary_path() -> String {
|
fn default_ls_binary_path() -> String {
|
||||||
let home = home_dir();
|
let home = home_dir();
|
||||||
// Check both /Applications and ~/Applications
|
// Check both /Applications and ~/Applications
|
||||||
let candidates = ["language_server_macos_arm", "language_server_darwin_arm64"];
|
|
||||||
for base in &[
|
for base in &[
|
||||||
"/Applications/Antigravity.app",
|
"/Applications/Antigravity.app",
|
||||||
&format!("{home}/Applications/Antigravity.app"),
|
&format!("{home}/Applications/Antigravity.app"),
|
||||||
] {
|
] {
|
||||||
for name in candidates {
|
let path = format!(
|
||||||
let path = format!("{base}/Contents/Resources/app/extensions/antigravity/bin/{name}");
|
"{base}/Contents/Resources/app/extensions/antigravity/bin/language_server_darwin_arm64"
|
||||||
if std::path::Path::new(&path).exists() {
|
);
|
||||||
return path;
|
if std::path::Path::new(&path).exists() {
|
||||||
}
|
return path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"/Applications/Antigravity.app/Contents/Resources/app/extensions/antigravity/bin/language_server_macos_arm".into()
|
"/Applications/Antigravity.app/Contents/Resources/app/extensions/antigravity/bin/language_server_darwin_arm64".into()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
@@ -300,9 +295,9 @@ pub fn supports_uid_isolation() -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if DNS redirect preload is supported (Linux/macOS).
|
/// Returns true if LD_PRELOAD DNS redirect is supported (Linux only).
|
||||||
pub fn supports_ld_preload() -> bool {
|
pub fn supports_ld_preload() -> bool {
|
||||||
cfg!(any(target_os = "linux", target_os = "macos"))
|
cfg!(target_os = "linux")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -65,12 +65,12 @@ pub fn discover_main_ls_config() -> Result<MainLSConfig, String> {
|
|||||||
discovery::discover_main_ls_config()
|
discovery::discover_main_ls_config()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the dns_redirect preload library if it doesn't already exist.
|
/// Build the dns_redirect.so preload library if it doesn't already exist.
|
||||||
///
|
///
|
||||||
/// Linux/macOS — hooks `getaddrinfo()` via LD_PRELOAD/DYLD_INSERT_LIBRARIES
|
/// Linux only — hooks `getaddrinfo()` via LD_PRELOAD to redirect Google API
|
||||||
/// to redirect Google API domain lookups to 127.0.0.1.
|
/// domain lookups to 127.0.0.1.
|
||||||
///
|
///
|
||||||
/// Returns the path to the shared library on success, None on failure.
|
/// Returns the path to the .so on success, None on failure.
|
||||||
fn build_dns_redirect_so() -> Option<String> {
|
fn build_dns_redirect_so() -> Option<String> {
|
||||||
if !platform::supports_ld_preload() {
|
if !platform::supports_ld_preload() {
|
||||||
return None;
|
return None;
|
||||||
@@ -91,15 +91,10 @@ fn build_dns_redirect_so() -> Option<String> {
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let output = if cfg!(target_os = "macos") {
|
// Compile: gcc -shared -fPIC -o dns_redirect.so dns_redirect.c -ldl
|
||||||
Command::new("cc")
|
let output = Command::new("gcc")
|
||||||
.args(["-dynamiclib", "-o", so_path.as_str(), &c_path])
|
.args(["-shared", "-fPIC", "-o", so_path.as_str(), &c_path, "-ldl"])
|
||||||
.output()
|
.output();
|
||||||
} else {
|
|
||||||
Command::new("gcc")
|
|
||||||
.args(["-shared", "-fPIC", "-o", so_path.as_str(), &c_path, "-ldl"])
|
|
||||||
.output()
|
|
||||||
};
|
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
Ok(out) if out.status.success() => {
|
Ok(out) if out.status.success() => {
|
||||||
|
|||||||
@@ -281,23 +281,15 @@ impl StandaloneLS {
|
|||||||
// even the CodeAssistClient which has Proxy:nil hardcoded.
|
// even the CodeAssistClient which has Proxy:nil hardcoded.
|
||||||
let so_path = build_dns_redirect_so();
|
let so_path = build_dns_redirect_so();
|
||||||
if let Some(ref so) = so_path {
|
if let Some(ref so) = so_path {
|
||||||
info!(path = %so, "Enabling DNS redirect preload for headless MITM");
|
info!(path = %so, "Enabling LD_PRELOAD DNS redirect for headless MITM");
|
||||||
#[cfg(target_os = "macos")]
|
env_vars.push(("LD_PRELOAD".into(), so.clone()));
|
||||||
{
|
|
||||||
env_vars.push(("DYLD_INSERT_LIBRARIES".into(), so.clone()));
|
|
||||||
env_vars.push(("DYLD_FORCE_FLAT_NAMESPACE".into(), "1".into()));
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
|
||||||
env_vars.push(("LD_PRELOAD".into(), so.clone()));
|
|
||||||
}
|
|
||||||
env_vars.push((
|
env_vars.push((
|
||||||
"DNS_REDIRECT_LOG".into(),
|
"DNS_REDIRECT_LOG".into(),
|
||||||
format!("{data_dir}/dns-redirect.log"),
|
format!("{data_dir}/dns-redirect.log"),
|
||||||
));
|
));
|
||||||
// Force Go binaries to use cgo (libc) DNS resolver instead of
|
// Force Go binaries to use cgo (libc) DNS resolver instead of
|
||||||
// the pure-Go resolver. Without this, getaddrinfo hooks are
|
// the pure-Go resolver. Without this, LD_PRELOAD getaddrinfo()
|
||||||
// bypassed because Go resolves DNS internally.
|
// hooks are bypassed because Go resolves DNS internally.
|
||||||
env_vars.push(("GODEBUG".into(), "netdns=cgo".into()));
|
env_vars.push(("GODEBUG".into(), "netdns=cgo".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user