Skip to content

Conversation

@grepsuzette
Copy link

@grepsuzette grepsuzette commented Sep 8, 2025

TLDR

Currently, on macOS when GEMINI_SANDBOX_PROXY_COMMAND is set, the WebFetch tool does not utilize that proxy. This PR fixes that by making the WebFetch tool use the sandbox proxy when it's configured via the GEMINI_SANDBOX_PROXY_COMMAND environment variable.

Changes Made:

  1. WebFetch Tool (packages/core/src/tools/web-fetch.ts)

    • Modified to check for GEMINI_SANDBOX_PROXY_COMMAND environment variable
    • When set, uses http://localhost:8877 as the proxy
    • Maintains backward compatibility with existing proxy configurations
  2. Documentation Updates

    • Updated docs/tools/web-fetch.md to document the new proxy support

Technical Details:

  • The WebFetch tool now automatically uses the sandbox proxy when GEMINI_SANDBOX_PROXY_COMMAND is set
  • The sandbox proxy, when configured via GEMINI_SANDBOX_PROXY_COMMAND, listens on http://localhost:8877 by default
  • This ensures that web requests made by the WebFetch tool go through the same proxy that other sandboxed operations use
  • This is particularly important for environments where network access is restricted and all outbound traffic must go through the sandbox proxy

Backward Compatibility:

  • Existing proxy configurations via config.getProxy() are preserved and take precedence
  • Tools that don't have the GEMINI_SANDBOX_PROXY_COMMAND environment variable set continue to work as before
  • No breaking changes to the API or existing functionality

The changes have been tested with npm link and a custom proxy script, and they work correctly.

Reviewer Test Plan

This only applies to macOS (Seatbelt sandbox).

Run a proxy file with GEMINI_SANDBOX_PROXY_COMMAND, for instance save something like this as ~/.qwen/proxy.js:

#!/usr/bin/env node

// Domain-based proxy server that listens on 127.0.0.1:8877.
// Routes traffic for specific domains through SOCKS5 proxy (socks5h://127.0.0.1:1080)
// and allows direct connections for all other domains.
//
// Set `GEMINI_SANDBOX_PROXY_COMMAND=scripts/domain-based-proxy.js` to run proxy alongside sandbox
// Ensure a SOCKS5 proxy is running on 127.0.0.1:1080

import http from 'http';
import net from 'net';
import { URL } from 'url';
import console from 'console';
import fs from 'fs';
import path from 'path';
import { SocksClient } from 'socks'; // This will need to be installed: npm install socks

// Set up logging to file
const logFilePath = path.join(process.env.HOME, 'opt', 'var', 'log', 'qwen_sandbox_proxy.log');
const logDir = path.dirname(logFilePath);
if (!fs.existsSync(logDir)) {
  fs.mkdirSync(logDir, { recursive: true });
}

// Custom logger that writes to both console and file
function log(message) {
  const timestamp = new Date().toISOString();
  const logMessage = `[${timestamp}] ${message}\n`;
  console.log(message);
  fs.appendFileSync(logFilePath, logMessage);
}

const PROXY_PORT = 8877;
// List of domains that should be routed through the SOCKS5 proxy
const SOCKS5_DOMAINS = [
	'googleapis.com',
	'github.com',
	'wikipedia.org',
	'google.com',
	'google.hk.com',
	'reddit.com',
];
const ALLOWED_PORT = '443'; // Only handle HTTPS connections

const server = http.createServer((req, res) => {
  // Deny all requests other than CONNECT for HTTPS
  log(
    `[PROXY] Denying non-CONNECT request for: ${req.method} ${req.url}`,
  );
  res.writeHead(405, { 'Content-Type': 'text/plain' });
  res.end('Method Not Allowed');
});

// Function to check if a hostname should use SOCKS5 proxy
function shouldUseSocks5(hostname) {
  return SOCKS5_DOMAINS.some(
    (domain) => hostname === domain || hostname.endsWith(`.${domain}`),
  );
}

// Function to create a direct connection
function createDirectConnection(hostname, port, clientSocket, head) {
  log(`[PROXY] Creating direct connection to ${hostname}:${port}`);

  const serverSocket = net.connect(port, hostname, () => {
    clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
    serverSocket.write(head);
    serverSocket.pipe(clientSocket);
    clientSocket.pipe(serverSocket);
  });

  serverSocket.on('error', (err) => {
    log(`[PROXY] Error connecting to destination: ${err.message}`);
    clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
  });

  return serverSocket;
}

// Function to create a SOCKS5 proxied connection
async function createSocks5Connection(hostname, port, clientSocket, head) {
  log(`[PROXY] Creating SOCKS5 connection to ${hostname}:${port}`);

  try {
    const { socket } = await SocksClient.createConnection({
      proxy: {
        host: '127.0.0.1',
        port: 1080,
        type: 5,
      },
      command: 'connect',
      destination: {
        host: hostname,
        port: parseInt(port, 10),
      },
    });

    clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
    socket.write(head);
    socket.pipe(clientSocket);
    clientSocket.pipe(socket);

    socket.on('error', (err) => {
      log(`[PROXY] SOCKS5 connection error: ${err.message}`);
      clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
    });
  } catch (err) {
    log(`[PROXY] Failed to establish SOCKS5 connection: ${err.message}`);
    clientSocket.end(`HTTP/1.1 502 Bad Gateway\r\n\r\n`);
  }
}

server.on('connect', (req, clientSocket, head) => {
  // req.url will be in the format "hostname:port" for a CONNECT request.
  const { port, hostname } = new URL(`http://${req.url}`);

  log(`[PROXY] Intercepted CONNECT request for: ${hostname}:${port}`);

  if (port !== ALLOWED_PORT) {
    log(`[PROXY] Denying connection to ${hostname}:${port} (port not allowed)`);
    clientSocket.end('HTTP/1.1 403 Forbidden\r\n\r\n');
    return;
  }

  if (shouldUseSocks5(hostname)) {
    // Route through SOCKS5 proxy
    createSocks5Connection(hostname, port, clientSocket, head);
  } else {
    // Direct connection
    createDirectConnection(hostname, port, clientSocket, head);
  }

  clientSocket.on('error', (err) => {
    // This can happen if the client hangs up.
    log(`[PROXY] Client socket error: ${err.message}`);
  });
});

// Listen on IPv4 localhost only
server.listen(PROXY_PORT, '127.0.0.1', () => {
  log(`[PROXY] Proxy listening on 127.0.0.1:${PROXY_PORT} (IPv4)`);
  log(
    `[PROXY] Routing domains through SOCKS5 (127.0.0.1:1080): ${SOCKS5_DOMAINS.join(', ')}`,
  );
  log(`[PROXY] All other domains will use direct connections.`);
});

// Also listen on IPv6 localhost only
server.listen(PROXY_PORT, '::1', () => {
  log(`[PROXY] Proxy listening on ::1:${PROXY_PORT} (IPv6)`);
});

Have a file ~/.qwen/proxy.sh:

#!/bin/bash
/opt/homebrew/bin/node ~/.qwen/proxy.js

Then npm run build && npm link.
Then launch qwen like this:

GEMINI_SANDBOX_PROXY_COMMAND=~/.qwen/proxy.sh qwen -s

Ask qwen to use tool web_fetch on bing, wikipedia, google, baidu.
The log file ~/var/opt/log/qwen_sandbox_proxy.log will show something like:

[2025-09-08T18:43:51.290Z] [PROXY] Intercepted CONNECT request for: cn.bing.com:443
[2025-09-08T18:43:51.290Z] [PROXY] Creating direct connection to cn.bing.com:443
[2025-09-08T18:44:08.743Z] [PROXY] Intercepted CONNECT request for: portal.qwen.ai:443
[2025-09-08T18:44:08.744Z] [PROXY] Creating direct connection to portal.qwen.ai:443
[2025-09-08T18:44:52.769Z] [PROXY] Intercepted CONNECT request for: portal.qwen.ai:443
[2025-09-08T18:44:52.769Z] [PROXY] Creating direct connection to portal.qwen.ai:443
[2025-09-08T18:44:56.341Z] [PROXY] Intercepted CONNECT request for: google.com:443
[2025-09-08T18:44:56.342Z] [PROXY] Creating SOCKS5 connection to google.com:443
[2025-09-08T18:44:56.617Z] [PROXY] Intercepted CONNECT request for: www.google.com:443
[2025-09-08T18:44:56.619Z] [PROXY] Creating SOCKS5 connection to www.google.com:443
[2025-09-08T18:44:56.921Z] [PROXY] Intercepted CONNECT request for: www.google.com.hk:443
[2025-09-08T18:44:56.922Z] [PROXY] Creating direct connection to www.google.com.hk:443
[2025-09-08T18:45:06.375Z] [PROXY] Intercepted CONNECT request for: portal.qwen.ai:443
[2025-09-08T18:45:06.375Z] [PROXY] Creating direct connection to portal.qwen.ai:443
[2025-09-08T18:46:11.936Z] [PROXY] Error connecting to destination: connect ETIMEDOUT 31.13.94.49:443
[2025-09-08T18:46:34.601Z] [PROXY] Intercepted CONNECT request for: portal.qwen.ai:443
[2025-09-08T18:46:34.602Z] [PROXY] Creating direct connection to portal.qwen.ai:443
[2025-09-08T18:46:39.935Z] [PROXY] Intercepted CONNECT request for: en.wikipedia.org:443
[2025-09-08T18:46:39.935Z] [PROXY] Creating SOCKS5 connection to en.wikipedia.org:443
[2025-09-08T18:46:41.030Z] [PROXY] Intercepted CONNECT request for: portal.qwen.ai:443
[2025-09-08T18:46:41.030Z] [PROXY] Creating direct connection to portal.qwen.ai:443

Testing Matrix

🍏 🪟 🐧
npm run OK
npx
Docker
Podman - -
Seatbelt OK - -

Linked issues / bugs

Resolves #556.

@grepsuzette grepsuzette changed the title feat(cli): Allow SEATBELT profiles in user settings directory Make WebFetch tool use GEMINI_SANDBOX_PROXY_COMMAND proxy when set Sep 8, 2025
@github-actions github-actions bot added the bug label Sep 8, 2025
@github-actions github-actions bot added type/bug Something isn't working as expected and removed bug labels Oct 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/bug Something isn't working as expected

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Tool web_fetch does not use proxy launched when GEMINI_SANDBOX_PROXY_COMMAND is set

1 participant