Skip to content
Draft
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
407 changes: 407 additions & 0 deletions PLAN.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ const response = await fetch('https://api.example.com/data');
- Superior performance, especially with `undici.request`
- HTTP/1.1 pipelining support
- Custom interceptors and middleware
- Advanced features like `ProxyAgent`, `MockAgent`
- Advanced features like `ProxyAgent`, `Socks5ProxyWrapper`, `MockAgent`

**Cons:**
- Additional dependency to manage
Expand All @@ -122,7 +122,7 @@ const response = await fetch('https://api.example.com/data');
#### Use Undici Module When:
- You need the latest undici features and performance improvements
- You require advanced connection pooling configuration
- You need APIs not available in the built-in fetch (`ProxyAgent`, `MockAgent`, etc.)
- You need APIs not available in the built-in fetch (`ProxyAgent`, `Socks5ProxyWrapper`, `MockAgent`, etc.)
- Performance is critical (use `undici.request` for maximum speed)
- You want better error handling and debugging capabilities
- You need HTTP/1.1 pipelining or advanced interceptors
Expand Down
92 changes: 92 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
version: '3.8'

services:
# SOCKS5 proxy without authentication
socks5-no-auth:
image: serjs/go-socks5-proxy:latest
container_name: socks5-no-auth
environment:
- PROXY_PORT=1080
ports:
- "1080:1080"
networks:
- test-network

# SOCKS5 proxy with username/password authentication
socks5-auth:
image: serjs/go-socks5-proxy:latest
container_name: socks5-auth
environment:
- PROXY_USER=testuser
- PROXY_PASS=testpass
- PROXY_PORT=1081
ports:
- "1081:1081"
networks:
- test-network

# Alternative: Dante SOCKS5 server (more configurable)
dante-socks5:
build:
context: ./test/fixtures/docker/dante
dockerfile: Dockerfile
container_name: dante-socks5
ports:
- "1082:1080"
networks:
- test-network
volumes:
- ./test/fixtures/docker/dante/danted.conf:/etc/danted.conf:ro

# HTTP test server
http-server:
image: node:20-alpine
container_name: http-test-server
working_dir: /app
volumes:
- ./test/fixtures/servers/http-server.js:/app/server.js:ro
command: node server.js
ports:
- "8080:8080"
networks:
- test-network

# HTTPS test server
https-server:
image: node:20-alpine
container_name: https-test-server
working_dir: /app
volumes:
- ./test/fixtures/servers/https-server.js:/app/server.js:ro
- ./test/fixtures/certs:/app/certs:ro
command: node server.js
ports:
- "8443:8443"
networks:
- test-network

# Echo server for testing
echo-server:
image: ealen/echo-server:latest
container_name: echo-server
environment:
- PORT=3000
ports:
- "3000:3000"
networks:
- test-network

# Blocked target (for testing connection failures)
blocked-server:
image: alpine:latest
container_name: blocked-server
command: sleep infinity
networks:
- isolated-network

networks:
test-network:
driver: bridge
isolated-network:
driver: bridge
internal: true
264 changes: 264 additions & 0 deletions docs/docs/api/Socks5ProxyWrapper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
# Class: Socks5ProxyWrapper

Extends: `undici.Dispatcher`

A SOCKS5 proxy wrapper class that implements the Dispatcher API. It enables HTTP requests to be routed through a SOCKS5 proxy server, providing connection tunneling and authentication support.

## `new Socks5ProxyWrapper(proxyUrl[, options])`

Arguments:

* **proxyUrl** `string | URL` (required) - The SOCKS5 proxy server URL. Must use `socks5://` or `socks://` protocol.
* **options** `Socks5ProxyWrapperOptions` (optional) - Additional configuration options.

Returns: `Socks5ProxyWrapper`

### Parameter: `Socks5ProxyWrapperOptions`

Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)

* **headers** `IncomingHttpHeaders` (optional) - Additional headers to send with proxy connections.
* **username** `string` (optional) - SOCKS5 proxy username for authentication. Can also be provided in the proxy URL.
* **password** `string` (optional) - SOCKS5 proxy password for authentication. Can also be provided in the proxy URL.
* **connect** `Function` (optional) - Custom connector function for the proxy connection.
* **proxyTls** `BuildOptions` (optional) - TLS options for the proxy connection (when using SOCKS5 over TLS).

Examples:

```js
import { Socks5ProxyWrapper } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')
// or with authentication
const socks5ProxyWithAuth = new Socks5ProxyWrapper('socks5://user:pass@localhost:1080')
// or with options
const socks5ProxyWithOptions = new Socks5ProxyWrapper('socks5://localhost:1080', {
username: 'user',
password: 'pass',
connections: 10
})
```

#### Example - Basic SOCKS5 Proxy instantiation

This will instantiate the Socks5ProxyWrapper. It will not do anything until registered as the dispatcher to use with requests.

```js
import { Socks5ProxyWrapper } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')
```

#### Example - Basic SOCKS5 Proxy Request with global dispatcher

```js
import { setGlobalDispatcher, request, Socks5ProxyWrapper } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')
setGlobalDispatcher(socks5Proxy)

const { statusCode, body } = await request('http://localhost:3000/foo')

console.log('response received', statusCode) // response received 200

for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```

#### Example - Basic SOCKS5 Proxy Request with local dispatcher

```js
import { Socks5ProxyWrapper, request } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')

const {
statusCode,
body
} = await request('http://localhost:3000/foo', { dispatcher: socks5Proxy })

console.log('response received', statusCode) // response received 200

for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```

#### Example - SOCKS5 Proxy Request with authentication

```js
import { setGlobalDispatcher, request, Socks5ProxyWrapper } from 'undici'

// Authentication via URL
const socks5Proxy = new Socks5ProxyWrapper('socks5://username:password@localhost:1080')

// Or authentication via options
// const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080', {
// username: 'username',
// password: 'password'
// })

setGlobalDispatcher(socks5Proxy)

const { statusCode, body } = await request('http://localhost:3000/foo')

console.log('response received', statusCode) // response received 200

for await (const data of body) {
console.log('data', data.toString('utf8')) // data foo
}
```

#### Example - SOCKS5 Proxy with HTTPS requests

SOCKS5 proxy supports both HTTP and HTTPS requests through tunneling:

```js
import { Socks5ProxyWrapper, request } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')

const response = await request('https://api.example.com/data', {
dispatcher: socks5Proxy,
method: 'GET'
})

console.log('Response status:', response.statusCode)
console.log('Response data:', await response.body.json())
```

#### Example - SOCKS5 Proxy with Fetch

```js
import { Socks5ProxyWrapper, fetch } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')

const response = await fetch('http://localhost:3000/api/users', {
dispatcher: socks5Proxy,
method: 'GET'
})

console.log('Response status:', response.status)
console.log('Response data:', await response.text())
```

#### Example - Connection Pooling

SOCKS5ProxyWrapper automatically manages connection pooling for better performance:

```js
import { Socks5ProxyWrapper, request } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080', {
connections: 10, // Allow up to 10 concurrent connections
pipelining: 1 // Enable HTTP/1.1 pipelining
})

// Multiple requests will reuse connections through the SOCKS5 tunnel
const responses = await Promise.all([
request('http://api.example.com/endpoint1', { dispatcher: socks5Proxy }),
request('http://api.example.com/endpoint2', { dispatcher: socks5Proxy }),
request('http://api.example.com/endpoint3', { dispatcher: socks5Proxy })
])

console.log('All requests completed through the same SOCKS5 proxy')
```

### `Socks5ProxyWrapper.close()`

Closes the SOCKS5 proxy wrapper and waits for all underlying pools and connections to close before resolving.

Returns: `Promise<void>`

#### Example - clean up after tests are complete

```js
import { Socks5ProxyWrapper, setGlobalDispatcher } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')
setGlobalDispatcher(socks5Proxy)

// ... make requests

await socks5Proxy.close()
```

### `Socks5ProxyWrapper.destroy([err])`

Destroys the SOCKS5 proxy wrapper and all underlying connections immediately.

Arguments:
* **err** `Error` (optional) - The error that caused the destruction.

Returns: `Promise<void>`

#### Example - force close all connections

```js
import { Socks5ProxyWrapper } from 'undici'

const socks5Proxy = new Socks5ProxyWrapper('socks5://localhost:1080')

// Force close all connections
await socks5Proxy.destroy()
```

### `Socks5ProxyWrapper.dispatch(options, handlers)`

Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handlers).

### `Socks5ProxyWrapper.request(options[, callback])`

See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).

## SOCKS5 Protocol Support

The Socks5ProxyWrapper supports the following SOCKS5 features:

### Authentication Methods

- **No Authentication** (`0x00`) - For public or internal proxies
- **Username/Password** (`0x02`) - RFC 1929 authentication

### Address Types

- **IPv4** (`0x01`) - Standard IPv4 addresses
- **Domain Name** (`0x03`) - Domain names (recommended for flexibility)
- **IPv6** (`0x04`) - IPv6 addresses (parsing not fully implemented)

### Commands

- **CONNECT** (`0x01`) - Establish TCP connection (primary use case for HTTP)

### Error Handling

The wrapper handles various SOCKS5 error conditions:

- Connection refused by proxy
- Authentication failures
- Network unreachable
- Host unreachable
- Unsupported address types or commands

## Performance Considerations

- **Connection Pooling**: Automatically pools connections through the SOCKS5 tunnel for better performance
- **HTTP/1.1 Pipelining**: Supports pipelining when enabled
- **DNS Resolution**: Domain names are resolved by the SOCKS5 proxy, reducing local DNS queries
- **TLS Termination**: HTTPS connections are encrypted end-to-end, with the SOCKS5 proxy only handling the TCP tunnel

## Security Notes

1. **Authentication**: Credentials are sent to the SOCKS5 proxy in plaintext unless using SOCKS5 over TLS
2. **DNS Leaks**: All DNS resolution happens on the proxy server, preventing DNS leaks
3. **End-to-end Encryption**: HTTPS traffic remains encrypted between client and final destination
4. **Connection Security**: Consider using authenticated proxies and secure networks

## Compatibility

- **Protocol**: SOCKS5 (RFC 1928) with Username/Password Authentication (RFC 1929)
- **Transport**: TCP only (UDP support not implemented)
- **Node.js**: Compatible with all supported Node.js versions
- **HTTP Versions**: Works with HTTP/1.1 and HTTP/2 over the tunnel
1 change: 1 addition & 0 deletions docs/docsify/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
* [BalancedPool](/docs/api/BalancedPool.md "Undici API - BalancedPool")
* [Agent](/docs/api/Agent.md "Undici API - Agent")
* [ProxyAgent](/docs/api/ProxyAgent.md "Undici API - ProxyAgent")
* [Socks5ProxyWrapper](/docs/api/Socks5ProxyWrapper.md "Undici API - SOCKS5 Proxy")
* [RetryAgent](/docs/api/RetryAgent.md "Undici API - RetryAgent")
* [Connector](/docs/api/Connector.md "Custom connector")
* [Errors](/docs/api/Errors.md "Undici API - Errors")
Expand Down
Loading
Loading