# multi-protocol-proxy-chain

Chain multiple proxies in strict order (HTTP/HTTPS/SOCKS4/SOCKS5/SSH) for Node.js. TypeScript-first library to build multi-hop forward proxy chains.

多协议链式代理库（HTTP/HTTPS/SOCKS4/SOCKS5/SSH），用于在 Node.js 中构建严格顺序的多跳前向代理链，提供 TypeScript 类型支持。

## Features / 特性

- ✅ **Strict-order proxy chain**: `client → local → p0 → p1 → … → target`
- ✅ **Multi-protocol support**: `HTTP`, `HTTPS`, `SOCKS5`, `SOCKS4`, `SSH (raw passthrough)`
- ✅ **HTTP CONNECT tunneling** and plain HTTP proxying
- ✅ **TypeScript** types and `.d.ts` declarations
- ✅ **Async lifecycle**: `start()` returns the actual bound URL; `stopAll()` awaits graceful shutdown
- ✅ **Port selection helper**: auto-find available port from a start value
- ✅ **Cross-platform**: Windows, macOS, Linux support
- ✅ **EventEmitter**: Listen to connection errors and lifecycle events
- ✅ **Custom logger**: Support custom logging functions

## Use Cases / 适用场景

- 通过多级上游代理实现分层转发与出站路由
- 将 `SOCKS5/SOCKS4` 与 `HTTP/HTTPS` 混合链式连接
- 在受限网络中通过 `HTTP CONNECT` 建立隧道
- 将浏览器或 CLI 流量统一引导到本地入口后多跳转发

多协议链式代理库，支持严格顺序的代理链转发：

`client -> local proxy -> proxies[0] -> proxies[1] -> ... -> target`

## 安装

```bash
npm install multi-protocol-proxy-chain
```

## 系统要求

- **Node.js**: >= 14.0.0
- **操作系统**: Windows 10+, macOS 10.14+, Linux (所有主流发行版)
- **依赖**: `ssh2` (用于 SSH 代理支持)

### 跨平台支持

本库完全支持跨平台运行：

- **Windows**: 使用 `netstat -ano` 进行端口检测
- **macOS**: 使用 `lsof` 进行端口检测
- **Linux**: 优先使用 `ss` 命令，回退到 `lsof`，最后使用绑定检测
- **其他 Unix-like 系统**: 使用通用的绑定检测方法

所有平台都支持自动端口查找和代理链功能。

## 快速开始

### 基础示例

```ts
import { MultiProtocolProxyChain } from 'multi-protocol-proxy-chain';

const chain = new MultiProtocolProxyChain();
chain.setProxies([
  { protocol: 'http', host: '192.168.101.166', port: 7890 },
  { protocol: 'socks5', host: '128.121.59.167', port: 36168, userId: 'user', password: 'pass' },
]);

// 启动本地入口（例如 socks5）
(async () => {
  try {
    const url = await chain.start('socks5', 2000);
    console.log('本地代理入口:', url);
    // 输出: socks5://127.0.0.1:2000 (或自动找到的可用端口)
  } catch (err) {
    console.error('启动失败:', err);
  }
})();
```

### 完整示例（包含错误处理和优雅关闭）

```ts
import { MultiProtocolProxyChain } from 'multi-protocol-proxy-chain';

const chain = new MultiProtocolProxyChain({
  logger: {
    log: (...args) => console.log('[ProxyChain]', ...args),
    error: (...args) => console.error('[ProxyChain Error]', ...args),
  },
});

// 设置代理链（严格按顺序）
chain.setProxies([
  { protocol: 'http', host: 'proxy1.example.com', port: 8080 },
  { protocol: 'socks5', host: 'proxy2.example.com', port: 1080, userId: 'user', password: 'pass' },
  { protocol: 'https', host: 'proxy3.example.com', port: 443, userId: 'user2', password: 'pass2' },
]);

// 监听错误事件
chain.on('error', (err) => {
  console.error('代理链错误:', err);
});

(async () => {
  try {
    // 启动本地 SOCKS5 代理入口
    const url = await chain.start('socks5', 2000, '127.0.0.1');
    console.log('✅ 本地代理入口已启动:', url);
    console.log('📝 使用示例:');
    console.log(`   curl --socks5-hostname 127.0.0.1:2000 https://httpbin.org/ip`);
    
    // 也可以启动多个不同协议的入口
    const httpUrl = await chain.start('http', 3128);
    console.log('✅ HTTP 代理入口:', httpUrl);
  } catch (err) {
    console.error('❌ 启动失败:', err);
    process.exit(1);
  }
})();

// 优雅关闭
process.on('SIGINT', async () => {
  console.log('\n收到退出信号，正在关闭代理入口...');
  await chain.stopAll();
  console.log('✅ 已关闭所有代理入口');
  process.exit(0);
});

process.on('SIGTERM', async () => {
  await chain.stopAll();
  process.exit(0);
});
```

### 使用不同协议的本地入口

```ts
const chain = new MultiProtocolProxyChain();
chain.setProxies([/* ... */]);

// 启动 SOCKS5 入口（浏览器常用）
const socks5Url = await chain.start('socks5', 2000);

// 启动 HTTP 入口（支持 CONNECT 和普通 HTTP 请求）
const httpUrl = await chain.start('http', 3128);

// 启动 HTTPS 入口（需要 TLS 证书，本库使用自签名证书）
const httpsUrl = await chain.start('https', 8443);

// 启动 SOCKS4 入口
const socks4Url = await chain.start('socks4', 1080);
```

### 错误处理示例

```ts
const chain = new MultiProtocolProxyChain();

try {
  // 尝试启动代理
  const url = await chain.start('socks5', 2000);
  console.log('启动成功:', url);
} catch (err) {
  if (err instanceof Error) {
    if (err.message.includes('EADDRINUSE')) {
      console.error('端口已被占用，尝试其他端口...');
      // 可以尝试其他端口或使用 PortOccupiedCheck
    } else if (err.message.includes('Unsupported')) {
      console.error('不支持的协议');
    } else {
      console.error('启动失败:', err.message);
    }
  }
}
```

### 监听连接事件

```ts
const chain = new MultiProtocolProxyChain();

// 监听代理链错误
chain.on('error', (err) => {
  console.error('代理链连接错误:', err.message);
  // 可以在这里实现重连逻辑
});

chain.setProxies([/* ... */]);
await chain.start('socks5', 2000);
```

## API 文档

### `MultiProtocolProxyChain`

#### 构造函数

```ts
new MultiProtocolProxyChain(options?: {
  logger?: {
    log: (...args: any[]) => void;
    error: (...args: any[]) => void;
  };
})
```

**参数：**
- `options.logger` (可选): 自定义日志函数，默认使用 `console`

**示例：**
```ts
const chain = new MultiProtocolProxyChain({
  logger: {
    log: (...args) => myLogger.info(...args),
    error: (...args) => myLogger.error(...args),
  },
});
```

#### `setProxies(proxies: ProxySpec[])`

设置上游代理链，顺序严格遵循数组顺序。每次调用会替换之前的代理链。

**参数：**
- `proxies`: 代理配置数组，按顺序执行

**示例：**
```ts
chain.setProxies([
  { protocol: 'http', host: 'proxy1.com', port: 8080 },
  { protocol: 'socks5', host: 'proxy2.com', port: 1080, userId: 'user', password: 'pass' },
]);
```

#### `start(protocol, port, host?): Promise<string>`

启动本地代理入口服务器。

**参数：**
- `protocol`: `'http' | 'https' | 'socks5' | 'socks4' | 'ssh'` - 本地入口协议
- `port`: `number` - 起始端口号（如果被占用会自动查找下一个可用端口）
- `host`: `string` (可选) - 绑定地址，默认 `'127.0.0.1'`

**返回：**
- `Promise<string>` - 返回实际绑定的代理 URL，例如 `'socks5://127.0.0.1:2000'`

**示例：**
```ts
const url = await chain.start('socks5', 2000);
// url: 'socks5://127.0.0.1:2000'

const url2 = await chain.start('http', 3128, '0.0.0.0');
// url2: 'http://0.0.0.0:3128'
```

#### `stopAll(): Promise<void>`

停止所有已启动的本地代理入口，等待所有服务器优雅关闭。

**示例：**
```ts
await chain.stopAll();
console.log('所有代理入口已关闭');
```

#### 事件

`MultiProtocolProxyChain` 继承自 `EventEmitter`，支持以下事件：

- `'error'`: 当代理链连接出错时触发
  ```ts
  chain.on('error', (err: Error) => {
    console.error('代理链错误:', err);
  });
  ```

### `ProxySpec` 类型

```ts
type ProxySpec = {
  protocol: 'http' | 'https' | 'socks5' | 'socks4' | 'ssh';
  host: string;           // 代理服务器地址
  port: number;            // 代理服务器端口
  userId?: string;         // 认证用户名（可选）
  password?: string;       // 认证密码（可选）
};
```

**协议说明：**
- `http`: HTTP 代理，支持 CONNECT 隧道和普通 HTTP 请求
- `https`: HTTPS 代理（TLS 加密连接）
- `socks5`: SOCKS5 代理，支持用户名/密码认证
- `socks4`: SOCKS4 代理，支持用户名认证
- `ssh`: SSH 隧道代理（使用 ssh2 库）

### `PortOccupiedCheck`

端口占用检测工具类。

#### `checkPort(initPort, maxPort?, host?): Promise<number>`

检查并返回一个未被占用的端口号。

**参数：**
- `initPort`: `number` - 起始端口号
- `maxPort`: `number` (可选) - 最大端口号，默认 `65535`
- `host`: `string` (可选) - 绑定检测的主机地址，默认 `'127.0.0.1'`

**返回：**
- `Promise<number>` - 第一个可用的端口号

**抛出：**
- 如果从 `initPort` 到 `maxPort` 范围内没有可用端口，抛出错误

**示例：**
```ts
import { PortOccupiedCheck } from 'multi-protocol-proxy-chain';

const checker = new PortOccupiedCheck();
const port = await checker.checkPort(2000, 2100); // 在 2000-2100 范围内查找
console.log('可用端口:', port);
```

## 使用场景示例

### 场景 1: 浏览器代理设置

```ts
const chain = new MultiProtocolProxyChain();
chain.setProxies([
  { protocol: 'http', host: 'corporate-proxy.com', port: 8080 },
  { protocol: 'socks5', host: 'exit-node.com', port: 1080 },
]);

const url = await chain.start('socks5', 2000);
console.log('在浏览器中设置 SOCKS5 代理:', url);
// 浏览器设置: SOCKS5 127.0.0.1:2000
```

### 场景 2: CLI 工具代理

```ts
const chain = new MultiProtocolProxyChain();
chain.setProxies([/* ... */]);

await chain.start('http', 3128);

// 使用 curl
// curl --proxy http://127.0.0.1:3128 https://example.com

// 使用环境变量
// export http_proxy=http://127.0.0.1:3128
// export https_proxy=http://127.0.0.1:3128
```

### 场景 3: Node.js 应用代理

```ts
import { HttpsProxyAgent } from 'https-proxy-agent';

const chain = new MultiProtocolProxyChain();
chain.setProxies([/* ... */]);

const url = await chain.start('http', 3128);
const agent = new HttpsProxyAgent(url);

// 在 axios 中使用
import axios from 'axios';
const response = await axios.get('https://api.example.com', {
  httpsAgent: agent,
});
```

### 场景 4: 多协议混合链

```ts
// 通过 HTTP 代理 -> SOCKS5 代理 -> HTTPS 代理 -> 目标
chain.setProxies([
  { protocol: 'http', host: 'proxy1.com', port: 8080 },
  { protocol: 'socks5', host: 'proxy2.com', port: 1080, userId: 'user', password: 'pass' },
  { protocol: 'https', host: 'proxy3.com', port: 443 },
]);
```

## 注意事项

### 代理链顺序

- ⚠️ **重要**: 代理链的顺序是严格的，数组中的第一个代理会先连接，然后依次连接后续代理
- 确保代理链的顺序正确，错误的顺序可能导致连接失败

### 端口管理

- `start()` 方法会自动查找可用端口，从指定的 `port` 开始，最大到 `65535`
- 如果需要限制端口范围，建议先使用 `PortOccupiedCheck.checkPort(initPort, maxPort)` 获取端口
- 在极端并发场景下，端口可能被抢占，建议使用 `try/catch` 处理错误并重试

### 安全性

- ⚠️ **HTTPS 代理**: 默认 `rejectUnauthorized: false`，不验证上游 HTTPS 代理的证书
- 生产环境建议根据安全需求调整 TLS 验证策略
- SSH 代理需要正确的用户名和密码（或密钥）

### 错误处理

- 代理链中的任何连接错误都会触发 `error` 事件
- 建议监听 `error` 事件并实现适当的错误处理逻辑
- 网络问题可能导致代理链中断，需要实现重连机制

### 性能考虑

- 多级代理会增加延迟，每增加一级代理大约增加 50-200ms 延迟
- 建议在生产环境中监控代理链的性能和可用性
- 对于高并发场景，考虑使用连接池或限流机制

### 资源清理

- 使用 `stopAll()` 确保所有服务器正确关闭
- 在应用退出时（如 `SIGINT`、`SIGTERM`）调用 `stopAll()`
- 未正确关闭可能导致端口占用问题

### 端口占用检测（PortOccupiedCheck）

`PortOccupiedCheck` 是一个跨平台的端口可用性检测工具，支持：

- ✅ **跨平台支持**: Windows (netstat), macOS (lsof), Linux (ss/lsof)
- ✅ **自动回退**: 如果系统工具不可用，自动使用绑定检测方法
- ✅ **从起始端口开始逐个尝试**，返回第一个可用端口
- ✅ **默认从 `initPort` 检查到 `65535`**，可通过第二个参数设定上限
- ✅ **已被 `start()` 内部使用**用于自动寻找可用端口

**使用示例：**

```ts
import { MultiProtocolProxyChain, PortOccupiedCheck } from 'multi-protocol-proxy-chain';

// 方式一：先检测端口范围，再启动（推荐用于生产环境）
const checker = new PortOccupiedCheck();
try {
  const availablePort = await checker.checkPort(2000, 2100); // 限制在 2000-2100 范围内
  console.log('找到可用端口:', availablePort);
  
  const chain = new MultiProtocolProxyChain();
  chain.setProxies([
    { protocol: 'http', host: '192.168.101.166', port: 7890 },
  ]);
  
  const url = await chain.start('socks5', availablePort, '127.0.0.1');
  console.log('本地代理入口:', url); // 例如：socks5://127.0.0.1:2003
} catch (err) {
  console.error('端口检测失败:', err);
}

// 方式二：直接调用 start（内部会自动从起始端口查找可用端口）
const chain2 = new MultiProtocolProxyChain();
chain2.setProxies([/* ... */]);
const url2 = await chain2.start('socks5', 2000);
console.log('本地代理入口（自动选择）:', url2);
```

**跨平台端口检测机制：**

- **Windows**: 使用 `netstat -ano` 快速检测端口占用
- **macOS**: 使用 `lsof` 命令检测端口占用
- **Linux**: 优先使用 `ss` 命令，回退到 `lsof`，最后使用绑定检测
- **其他平台**: 使用通用的绑定检测方法（尝试绑定端口来判断是否可用）

所有平台在检测失败时都会自动回退到可靠的绑定检测方法，确保功能可用。

## 常见问题 (FAQ)

### Q: 支持 IPv6 吗？

A: 是的，支持 IPv6 地址。在设置代理时可以使用 IPv6 地址，本地入口也支持绑定到 IPv6 地址。

```ts
await chain.start('socks5', 2000, '::1'); // IPv6 localhost
```

### Q: 可以同时启动多个本地入口吗？

A: 可以，可以多次调用 `start()` 启动不同协议或端口的入口。

```ts
await chain.start('socks5', 2000);
await chain.start('http', 3128);
await chain.start('socks5', 2001); // 甚至可以启动多个相同协议的入口
```

### Q: 代理链中的某个代理连接失败会怎样？

A: 连接失败会触发 `error` 事件，并可能导致整个请求失败。建议监听 `error` 事件并实现重试或降级逻辑。

### Q: 如何验证代理链是否正常工作？

A: 可以使用以下方法测试：

```bash
# 使用 curl 测试 SOCKS5 代理
curl --socks5-hostname 127.0.0.1:2000 https://httpbin.org/ip

# 使用 curl 测试 HTTP 代理
curl --proxy http://127.0.0.1:3128 https://httpbin.org/ip

# 检查返回的 IP 地址是否为最后一个代理的出口 IP
```

### Q: 性能如何？

A: 每增加一级代理大约增加 50-200ms 延迟。对于大多数应用场景，3-5 级代理链是可以接受的。如果需要更高性能，建议：
- 减少代理链层级
- 选择地理位置较近的代理
- 使用更快的代理服务器

### Q: 支持代理认证吗？

A: 支持。所有协议都支持用户名/密码认证（通过 `userId` 和 `password` 字段）。

```ts
chain.setProxies([
  { protocol: 'http', host: 'proxy.com', port: 8080, userId: 'user', password: 'pass' },
  { protocol: 'socks5', host: 'proxy2.com', port: 1080, userId: 'user2', password: 'pass2' },
]);
```

## 许可证

ISC

## 贡献

欢迎提交 Issue 和 Pull Request！

## 更新日志

### v1.0.4
- ✅ 添加跨平台支持（Windows/macOS/Linux）
- ✅ 改进端口检测机制
- ✅ 完善文档和示例