mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-21 18:11:27 -07:00
update skills
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
# Workers VPC Connectivity
|
||||
|
||||
Connect Cloudflare Workers to private networks and internal infrastructure using TCP Sockets.
|
||||
|
||||
## Overview
|
||||
|
||||
Workers VPC connectivity enables outbound TCP connections from Workers to private resources in AWS, Azure, GCP, on-premises datacenters, or any private network. This is achieved through the **TCP Sockets API** (`cloudflare:sockets`), which provides low-level network access for custom protocols and services.
|
||||
|
||||
**Key capabilities:**
|
||||
- Direct TCP connections to private IPs and hostnames
|
||||
- TLS/StartTLS support for encrypted connections
|
||||
- Integration with Cloudflare Tunnel for secure private network access
|
||||
- Full control over wire protocols (database protocols, SSH, MQTT, custom TCP)
|
||||
|
||||
**Note:** This reference documents the TCP Sockets API. For the newer Workers VPC Services product (HTTP-only service bindings with built-in SSRF protection), refer to separate documentation when available. VPC Services is currently in beta (2025+).
|
||||
|
||||
## Quick Decision: Which Technology?
|
||||
|
||||
Need private network connectivity from Workers?
|
||||
|
||||
| Requirement | Use | Why |
|
||||
|------------|-----|-----|
|
||||
| HTTP/HTTPS APIs in private network | VPC Services (beta, separate docs) | SSRF-safe, declarative bindings |
|
||||
| PostgreSQL/MySQL databases | [Hyperdrive](../hyperdrive/) | Connection pooling, caching, optimized |
|
||||
| Custom TCP protocols (SSH, MQTT, proprietary) | **TCP Sockets (this doc)** | Full protocol control |
|
||||
| Simple HTTP with lowest latency | TCP Sockets + [Smart Placement](../smart-placement/) | Manual optimization |
|
||||
| Expose on-prem to internet (inbound) | [Cloudflare Tunnel](../tunnel/) | Not Worker-specific |
|
||||
|
||||
## When to Use TCP Sockets
|
||||
|
||||
**Use TCP Sockets when you need:**
|
||||
- ✅ Direct control over wire protocols (e.g., Postgres wire protocol, SSH, Redis RESP)
|
||||
- ✅ Non-HTTP protocols (MQTT, SMTP, custom binary protocols)
|
||||
- ✅ StartTLS or custom TLS negotiation
|
||||
- ✅ Streaming binary data over TCP
|
||||
|
||||
**Don't use TCP Sockets when:**
|
||||
- ❌ You just need HTTP/HTTPS (use `fetch()` or VPC Services)
|
||||
- ❌ You need PostgreSQL/MySQL (use Hyperdrive for pooling)
|
||||
- ❌ You need WebSocket (use native Workers WebSocket)
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { connect } from 'cloudflare:sockets';
|
||||
|
||||
export default {
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
// Connect to private service
|
||||
const socket = connect(
|
||||
{ hostname: "db.internal.company.net", port: 5432 },
|
||||
{ secureTransport: "on" }
|
||||
);
|
||||
|
||||
try {
|
||||
await socket.opened; // Wait for connection
|
||||
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode("QUERY\r\n"));
|
||||
await writer.close();
|
||||
|
||||
const reader = socket.readable.getReader();
|
||||
const { value } = await reader.read();
|
||||
|
||||
return new Response(value);
|
||||
} finally {
|
||||
await socket.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Architecture Pattern: Workers + Tunnel
|
||||
|
||||
Most private network connectivity combines TCP Sockets with Cloudflare Tunnel:
|
||||
|
||||
```
|
||||
┌─────────┐ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ Worker │────▶│ TCP Socket │────▶│ Tunnel │────▶│ Private │
|
||||
│ │ │ (this API) │ │ (cloudflared)│ │ Network │
|
||||
└─────────┘ └─────────────┘ └──────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
1. Worker opens TCP socket to Tunnel hostname
|
||||
2. Tunnel endpoint routes to private IP
|
||||
3. Response flows back through Tunnel to Worker
|
||||
|
||||
See [configuration.md](./configuration.md) for Tunnel setup details.
|
||||
|
||||
## Reading Order
|
||||
|
||||
1. **Start here (README.md)** - Overview and decision guide
|
||||
2. **[api.md](./api.md)** - Socket interface, types, methods
|
||||
3. **[configuration.md](./configuration.md)** - Wrangler setup, Tunnel integration
|
||||
4. **[patterns.md](./patterns.md)** - Real-world examples (databases, protocols, error handling)
|
||||
5. **[gotchas.md](./gotchas.md)** - Limits, blocked ports, common errors
|
||||
|
||||
## Key Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Max concurrent sockets per request | 6 |
|
||||
| Blocked destinations | Cloudflare IPs, localhost, port 25 |
|
||||
| Scope requirement | Must create in handler (not global) |
|
||||
|
||||
See [gotchas.md](./gotchas.md) for complete limits and troubleshooting.
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always close sockets** - Use try/finally blocks
|
||||
2. **Validate destinations** - Prevent SSRF by allowlisting hosts
|
||||
3. **Use Hyperdrive for databases** - Better performance than raw TCP
|
||||
4. **Prefer fetch() for HTTP** - Only use TCP when necessary
|
||||
5. **Combine with Smart Placement** - Reduce latency to private networks
|
||||
|
||||
## Related Technologies
|
||||
|
||||
- **[Hyperdrive](../hyperdrive/)** - PostgreSQL/MySQL with connection pooling
|
||||
- **[Cloudflare Tunnel](../tunnel/)** - Secure private network access
|
||||
- **[Smart Placement](../smart-placement/)** - Auto-locate Workers near backends
|
||||
- **VPC Services (beta)** - HTTP-only service bindings with SSRF protection (separate docs)
|
||||
|
||||
## Reference
|
||||
|
||||
- [TCP Sockets API Documentation](https://developers.cloudflare.com/workers/runtime-apis/tcp-sockets/)
|
||||
- [Connect to databases guide](https://developers.cloudflare.com/workers/tutorials/connect-to-postgres/)
|
||||
- [Cloudflare Tunnel setup](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/)
|
||||
202
.agents/skills/cloudflare-deploy/references/workers-vpc/api.md
Normal file
202
.agents/skills/cloudflare-deploy/references/workers-vpc/api.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# TCP Sockets API Reference
|
||||
|
||||
Complete API reference for the Cloudflare Workers TCP Sockets API (`cloudflare:sockets`).
|
||||
|
||||
## Core Function: `connect()`
|
||||
|
||||
```typescript
|
||||
function connect(
|
||||
address: SocketAddress,
|
||||
options?: SocketOptions
|
||||
): Socket
|
||||
```
|
||||
|
||||
Creates an outbound TCP connection to the specified address.
|
||||
|
||||
### Parameters
|
||||
|
||||
#### `SocketAddress`
|
||||
|
||||
```typescript
|
||||
interface SocketAddress {
|
||||
hostname: string; // DNS hostname or IP address
|
||||
port: number; // TCP port (1-65535, excluding blocked ports)
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description | Example |
|
||||
|-------|------|-------------|---------|
|
||||
| `hostname` | `string` | Target hostname or IP | `"db.internal.net"`, `"10.0.1.50"` |
|
||||
| `port` | `number` | TCP port number | `5432`, `443`, `22` |
|
||||
|
||||
DNS names are resolved at connection time. IPv4, IPv6, and private IPs (10.x, 172.16.x, 192.168.x) supported.
|
||||
|
||||
#### `SocketOptions`
|
||||
|
||||
```typescript
|
||||
interface SocketOptions {
|
||||
secureTransport?: "off" | "on" | "starttls";
|
||||
allowHalfOpen?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `secureTransport` | `"off" \| "on" \| "starttls"` | `"off"` | TLS mode |
|
||||
| `allowHalfOpen` | `boolean` | `false` | Allow half-closed connections |
|
||||
|
||||
**`secureTransport` modes:**
|
||||
|
||||
| Mode | Behavior | Use Case |
|
||||
|------|----------|----------|
|
||||
| `"off"` | Plain TCP, no encryption | Testing, internal trusted networks |
|
||||
| `"on"` | Immediate TLS handshake | HTTPS, secure databases, SSH |
|
||||
| `"starttls"` | Start plain, upgrade later with `startTls()` | Postgres, SMTP, IMAP |
|
||||
|
||||
**`allowHalfOpen`:** When `false` (default), closing read stream auto-closes write stream. When `true`, streams are independent.
|
||||
|
||||
### Returns
|
||||
|
||||
A `Socket` object with readable/writable streams.
|
||||
|
||||
## Socket Interface
|
||||
|
||||
```typescript
|
||||
interface Socket {
|
||||
// Streams
|
||||
readable: ReadableStream<Uint8Array>;
|
||||
writable: WritableStream<Uint8Array>;
|
||||
|
||||
// Connection state
|
||||
opened: Promise<SocketInfo>;
|
||||
closed: Promise<void>;
|
||||
|
||||
// Methods
|
||||
close(): Promise<void>;
|
||||
startTls(): Socket;
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### `readable: ReadableStream<Uint8Array>`
|
||||
|
||||
Stream for reading data from the socket. Use `getReader()` to consume data.
|
||||
|
||||
```typescript
|
||||
const reader = socket.readable.getReader();
|
||||
const { done, value } = await reader.read(); // Read one chunk
|
||||
```
|
||||
|
||||
#### `writable: WritableStream<Uint8Array>`
|
||||
|
||||
Stream for writing data to the socket. Use `getWriter()` to send data.
|
||||
|
||||
```typescript
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode("HELLO\r\n"));
|
||||
await writer.close();
|
||||
```
|
||||
|
||||
#### `opened: Promise<SocketInfo>`
|
||||
|
||||
Promise that resolves when connection succeeds, rejects on failure.
|
||||
|
||||
```typescript
|
||||
interface SocketInfo {
|
||||
remoteAddress?: string; // May be undefined
|
||||
localAddress?: string; // May be undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const info = await socket.opened;
|
||||
} catch (error) {
|
||||
// Connection failed
|
||||
}
|
||||
```
|
||||
|
||||
#### `closed: Promise<void>`
|
||||
|
||||
Promise that resolves when socket is fully closed (both directions).
|
||||
|
||||
### Methods
|
||||
|
||||
#### `close(): Promise<void>`
|
||||
|
||||
Closes the socket gracefully, waiting for pending writes to complete.
|
||||
|
||||
```typescript
|
||||
const socket = connect({ hostname: "api.internal", port: 443 });
|
||||
try {
|
||||
// Use socket
|
||||
} finally {
|
||||
await socket.close(); // Always call in finally block
|
||||
}
|
||||
```
|
||||
|
||||
#### `startTls(): Socket`
|
||||
|
||||
Upgrades connection to TLS. Only available when `secureTransport: "starttls"` was specified.
|
||||
|
||||
```typescript
|
||||
const socket = connect(
|
||||
{ hostname: "db.internal", port: 5432 },
|
||||
{ secureTransport: "starttls" }
|
||||
);
|
||||
|
||||
// Send protocol-specific StartTLS command
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode("STARTTLS\r\n"));
|
||||
|
||||
// Upgrade to TLS - use returned socket, not original
|
||||
const secureSocket = socket.startTls();
|
||||
const secureWriter = secureSocket.writable.getWriter();
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
import { connect } from 'cloudflare:sockets';
|
||||
|
||||
export default {
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
const socket = connect({ hostname: "echo.example.com", port: 7 }, { secureTransport: "on" });
|
||||
|
||||
try {
|
||||
await socket.opened;
|
||||
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode("Hello, TCP!\n"));
|
||||
await writer.close();
|
||||
|
||||
const reader = socket.readable.getReader();
|
||||
const { value } = await reader.read();
|
||||
|
||||
return new Response(value);
|
||||
} finally {
|
||||
await socket.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
See [patterns.md](./patterns.md) for multi-chunk reading, error handling, and protocol implementations.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Task | Code |
|
||||
|------|------|
|
||||
| Import | `import { connect } from 'cloudflare:sockets';` |
|
||||
| Connect | `connect({ hostname: "host", port: 443 })` |
|
||||
| With TLS | `connect(addr, { secureTransport: "on" })` |
|
||||
| StartTLS | `socket.startTls()` after handshake |
|
||||
| Write | `await writer.write(data); await writer.close();` |
|
||||
| Read | `const { value } = await reader.read();` |
|
||||
| Error handling | `try { await socket.opened; } catch { }` |
|
||||
| Always close | `try { } finally { await socket.close(); }` |
|
||||
|
||||
## See Also
|
||||
|
||||
- [patterns.md](./patterns.md) - Real-world protocol implementations
|
||||
- [configuration.md](./configuration.md) - Wrangler setup and environment variables
|
||||
- [gotchas.md](./gotchas.md) - Limits and error handling
|
||||
@@ -0,0 +1,147 @@
|
||||
# Configuration
|
||||
|
||||
Setup and configuration for TCP Sockets in Cloudflare Workers.
|
||||
|
||||
## Wrangler Configuration
|
||||
|
||||
### Basic Setup
|
||||
|
||||
TCP Sockets are available by default in Workers runtime. No special configuration required in `wrangler.jsonc`:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"name": "private-network-worker",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01"
|
||||
}
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Store connection details as env vars:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"vars": { "DB_HOST": "10.0.1.50", "DB_PORT": "5432" }
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
interface Env { DB_HOST: string; DB_PORT: string; }
|
||||
|
||||
export default {
|
||||
async fetch(req: Request, env: Env): Promise<Response> {
|
||||
const socket = connect({ hostname: env.DB_HOST, port: parseInt(env.DB_PORT) });
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Per-Environment Configuration
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"vars": { "DB_HOST": "localhost" },
|
||||
"env": {
|
||||
"staging": { "vars": { "DB_HOST": "staging-db.internal.net" } },
|
||||
"production": { "vars": { "DB_HOST": "prod-db.internal.net" } }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Deploy: `wrangler deploy --env staging` or `wrangler deploy --env production`
|
||||
|
||||
## Integration with Cloudflare Tunnel
|
||||
|
||||
To connect Workers to private networks, combine TCP Sockets with Cloudflare Tunnel:
|
||||
|
||||
```
|
||||
Worker (TCP Socket) → Tunnel hostname → cloudflared → Private Network
|
||||
```
|
||||
|
||||
### Quick Setup
|
||||
|
||||
1. **Install cloudflared** on a server inside your private network
|
||||
2. **Create tunnel**: `cloudflared tunnel create my-private-network`
|
||||
3. **Configure routing** in `config.yml`:
|
||||
|
||||
```yaml
|
||||
tunnel: <TUNNEL_ID>
|
||||
credentials-file: /path/to/<TUNNEL_ID>.json
|
||||
ingress:
|
||||
- hostname: db.internal.example.com
|
||||
service: tcp://10.0.1.50:5432
|
||||
- service: http_status:404 # Required catch-all
|
||||
```
|
||||
|
||||
4. **Run tunnel**: `cloudflared tunnel run my-private-network`
|
||||
5. **Connect from Worker**:
|
||||
|
||||
```typescript
|
||||
const socket = connect(
|
||||
{ hostname: "db.internal.example.com", port: 5432 }, // Tunnel hostname
|
||||
{ secureTransport: "on" }
|
||||
);
|
||||
```
|
||||
|
||||
For detailed Tunnel setup, see [Tunnel configuration reference](../tunnel/configuration.md).
|
||||
|
||||
## Smart Placement Integration
|
||||
|
||||
Reduce latency by auto-placing Workers near backends:
|
||||
|
||||
```jsonc
|
||||
{ "placement": { "mode": "smart" } }
|
||||
```
|
||||
|
||||
Workers automatically relocate closer to TCP socket destinations after observing connection latency. See [Smart Placement reference](../smart-placement/).
|
||||
|
||||
## Secrets Management
|
||||
|
||||
Store sensitive credentials as secrets (not in wrangler.jsonc):
|
||||
|
||||
```bash
|
||||
wrangler secret put DB_PASSWORD # Enter value when prompted
|
||||
```
|
||||
|
||||
Access in Worker via `env.DB_PASSWORD`. Use in protocol handshake or authentication.
|
||||
|
||||
## Local Development
|
||||
|
||||
Test with `wrangler dev`. Note: Local mode may not access private networks. Use public endpoints or mock servers for development:
|
||||
|
||||
```typescript
|
||||
const config = process.env.NODE_ENV === 'dev'
|
||||
? { hostname: 'localhost', port: 5432 } // Mock
|
||||
: { hostname: 'db.internal.example.com', port: 5432 }; // Production
|
||||
```
|
||||
|
||||
## Connection String Patterns
|
||||
|
||||
Parse connection strings to extract host and port:
|
||||
|
||||
```typescript
|
||||
function parseConnectionString(connStr: string): SocketAddress {
|
||||
const url = new URL(connStr); // e.g., "postgres://10.0.1.50:5432/mydb"
|
||||
return { hostname: url.hostname, port: parseInt(url.port) || 5432 };
|
||||
}
|
||||
```
|
||||
|
||||
## Hyperdrive Integration
|
||||
|
||||
For PostgreSQL/MySQL, prefer Hyperdrive over raw TCP sockets (includes connection pooling):
|
||||
|
||||
```jsonc
|
||||
{ "hyperdrive": [{ "binding": "DB", "id": "<HYPERDRIVE_ID>" }] }
|
||||
```
|
||||
|
||||
See [Hyperdrive reference](../hyperdrive/) for complete setup.
|
||||
|
||||
## Compatibility
|
||||
|
||||
TCP Sockets available in all modern Workers. Use current date: `"compatibility_date": "2025-01-01"`. No special flags required.
|
||||
|
||||
## Related Configuration
|
||||
|
||||
- **[Tunnel Configuration](../tunnel/configuration.md)** - Detailed cloudflared setup
|
||||
- **[Smart Placement](../smart-placement/configuration.md)** - Placement mode options
|
||||
- **[Hyperdrive](../hyperdrive/configuration.md)** - Database connection pooling setup
|
||||
@@ -0,0 +1,167 @@
|
||||
# Gotchas and Troubleshooting
|
||||
|
||||
Common pitfalls, limitations, and solutions for TCP Sockets in Cloudflare Workers.
|
||||
|
||||
## Platform Limits
|
||||
|
||||
### Connection Limits
|
||||
|
||||
| Limit | Value |
|
||||
|-------|-------|
|
||||
| Max concurrent sockets per request | 6 (hard limit) |
|
||||
| Socket lifetime | Request duration |
|
||||
| Connection timeout | Platform-dependent, no setting |
|
||||
|
||||
**Problem:** Exceeding 6 connections throws error
|
||||
|
||||
**Solution:** Process in batches of 6
|
||||
|
||||
```typescript
|
||||
for (let i = 0; i < hosts.length; i += 6) {
|
||||
const batch = hosts.slice(i, i + 6).map(h => connect({ hostname: h, port: 443 }));
|
||||
await Promise.all(batch.map(async s => { /* use */ await s.close(); }));
|
||||
}
|
||||
```
|
||||
|
||||
### Blocked Destinations
|
||||
|
||||
Cloudflare IPs (1.1.1.1), localhost (127.0.0.1), port 25 (SMTP), Worker's own URL blocked for security.
|
||||
|
||||
**Solution:** Use public IPs or Tunnel hostnames: `connect({ hostname: "db.internal.company.net", port: 5432 })`
|
||||
|
||||
### Scope Requirements
|
||||
|
||||
**Problem:** Sockets created in global scope fail
|
||||
|
||||
**Cause:** Sockets tied to request lifecycle
|
||||
|
||||
**Solution:** Create inside handler: `export default { async fetch() { const socket = connect(...); } }`
|
||||
|
||||
## Common Errors
|
||||
|
||||
### Error: "proxy request failed"
|
||||
|
||||
**Causes:** Blocked destination (Cloudflare IP, localhost, port 25), DNS failure, network unreachable
|
||||
|
||||
**Solution:** Validate destinations, use Tunnel hostnames, catch errors with try/catch
|
||||
|
||||
### Error: "TCP Loop detected"
|
||||
|
||||
**Cause:** Worker connecting to itself
|
||||
|
||||
**Solution:** Connect to external service, not Worker's own hostname
|
||||
|
||||
### Error: "Port 25 prohibited"
|
||||
|
||||
**Cause:** SMTP port blocked
|
||||
|
||||
**Solution:** Use Email Workers API for email
|
||||
|
||||
### Error: "socket is not open"
|
||||
|
||||
**Cause:** Read/write after close
|
||||
|
||||
**Solution:** Always use try/finally to ensure proper closure order
|
||||
|
||||
### Error: Connection timeout
|
||||
|
||||
**Cause:** No built-in timeout
|
||||
|
||||
**Solution:** Use `Promise.race()`:
|
||||
|
||||
```typescript
|
||||
const socket = connect(addr, opts);
|
||||
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000));
|
||||
await Promise.race([socket.opened, timeout]);
|
||||
```
|
||||
|
||||
## TLS/SSL Issues
|
||||
|
||||
### StartTLS Timing
|
||||
|
||||
**Problem:** Calling `startTls()` too early
|
||||
|
||||
**Solution:** Send protocol-specific STARTTLS command, wait for server OK, then call `socket.startTls()`
|
||||
|
||||
### Certificate Validation
|
||||
|
||||
**Problem:** Self-signed certs fail
|
||||
|
||||
**Solution:** Use proper certs or Tunnel (handles TLS termination)
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Not Using Connection Pooling
|
||||
|
||||
**Problem:** New connection overhead per request
|
||||
|
||||
**Solution:** Use [Hyperdrive](../hyperdrive/) for databases (built-in pooling)
|
||||
|
||||
### Not Using Smart Placement
|
||||
|
||||
**Problem:** High latency to backend
|
||||
|
||||
**Solution:** Enable: `{ "placement": { "mode": "smart" } }` in wrangler.jsonc
|
||||
|
||||
### Forgetting to Close Sockets
|
||||
|
||||
**Problem:** Resource leaks
|
||||
|
||||
**Solution:** Always use try/finally:
|
||||
|
||||
```typescript
|
||||
const socket = connect({ hostname: "api.internal", port: 443 });
|
||||
try {
|
||||
// Use socket
|
||||
} finally {
|
||||
await socket.close();
|
||||
}
|
||||
```
|
||||
|
||||
## Data Handling Issues
|
||||
|
||||
### Assuming Single Read Gets All Data
|
||||
|
||||
**Problem:** Only reading once may miss chunked data
|
||||
|
||||
**Solution:** Loop `reader.read()` until `done === true` (see patterns.md)
|
||||
|
||||
### Text Encoding Issues
|
||||
|
||||
**Problem:** Using wrong encoding
|
||||
|
||||
**Solution:** Specify encoding: `new TextDecoder('iso-8859-1').decode(data)`
|
||||
|
||||
## Security Issues
|
||||
|
||||
### SSRF Vulnerability
|
||||
|
||||
**Problem:** User-controlled destinations allow access to internal services
|
||||
|
||||
**Solution:** Validate against strict allowlist:
|
||||
|
||||
```typescript
|
||||
const ALLOWED = ['api1.internal.net', 'api2.internal.net'];
|
||||
const host = new URL(req.url).searchParams.get('host');
|
||||
if (!host || !ALLOWED.includes(host)) return new Response('Forbidden', { status: 403 });
|
||||
```
|
||||
|
||||
## When to Use Alternatives
|
||||
|
||||
| Use Case | Alternative | Reason |
|
||||
|----------|-------------|--------|
|
||||
| PostgreSQL/MySQL | [Hyperdrive](../hyperdrive/) | Connection pooling, caching |
|
||||
| HTTP/HTTPS | `fetch()` | Simpler, built-in |
|
||||
| HTTP with SSRF protection | VPC Services (beta 2025+) | Declarative bindings |
|
||||
|
||||
## Debugging Tips
|
||||
|
||||
1. **Log connection details:** `const info = await socket.opened; console.log(info.remoteAddress);`
|
||||
2. **Test with public services first:** Use tcpbin.com:4242 echo server
|
||||
3. **Verify Tunnel:** `cloudflared tunnel info <name>` and `cloudflared tunnel route ip list`
|
||||
|
||||
## Related
|
||||
|
||||
- [Hyperdrive](../hyperdrive/) - Database connections
|
||||
- [Smart Placement](../smart-placement/) - Latency optimization
|
||||
- [Tunnel Troubleshooting](../tunnel/gotchas.md)
|
||||
@@ -0,0 +1,209 @@
|
||||
# Common Patterns
|
||||
|
||||
Real-world patterns and examples for TCP Sockets in Cloudflare Workers.
|
||||
|
||||
```typescript
|
||||
import { connect } from 'cloudflare:sockets';
|
||||
```
|
||||
|
||||
## Basic Patterns
|
||||
|
||||
### Simple Request-Response
|
||||
|
||||
```typescript
|
||||
const socket = connect({ hostname: "echo.example.com", port: 7 }, { secureTransport: "on" });
|
||||
try {
|
||||
await socket.opened;
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode("Hello\n"));
|
||||
await writer.close();
|
||||
|
||||
const reader = socket.readable.getReader();
|
||||
const { value } = await reader.read();
|
||||
return new Response(value);
|
||||
} finally {
|
||||
await socket.close();
|
||||
}
|
||||
```
|
||||
|
||||
### Reading All Data
|
||||
|
||||
```typescript
|
||||
async function readAll(socket: Socket): Promise<Uint8Array> {
|
||||
const reader = socket.readable.getReader();
|
||||
const chunks: Uint8Array[] = [];
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
}
|
||||
const total = chunks.reduce((sum, c) => sum + c.length, 0);
|
||||
const result = new Uint8Array(total);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; }
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
### Streaming Response
|
||||
|
||||
```typescript
|
||||
// Stream socket data directly to HTTP response
|
||||
const socket = connect({ hostname: "stream.internal", port: 9000 }, { secureTransport: "on" });
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode("STREAM\n"));
|
||||
await writer.close();
|
||||
return new Response(socket.readable);
|
||||
```
|
||||
|
||||
## Protocol Examples
|
||||
|
||||
### Redis RESP
|
||||
|
||||
```typescript
|
||||
// Send: *2\r\n$3\r\nGET\r\n$<keylen>\r\n<key>\r\n
|
||||
// Recv: $<len>\r\n<data>\r\n or $-1\r\n for null
|
||||
const socket = connect({ hostname: "redis.internal", port: 6379 });
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode(`*2\r\n$3\r\nGET\r\n$3\r\nkey\r\n`));
|
||||
```
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
**Use [Hyperdrive](../hyperdrive/) for production.** Raw Postgres protocol is complex (startup, auth, query messages).
|
||||
|
||||
### MQTT
|
||||
|
||||
```typescript
|
||||
const socket = connect({ hostname: "mqtt.broker", port: 1883 });
|
||||
const writer = socket.writable.getWriter();
|
||||
// CONNECT: 0x10 <len> 0x00 0x04 "MQTT" 0x04 <flags> ...
|
||||
// PUBLISH: 0x30 <len> <topic_len> <topic> <message>
|
||||
```
|
||||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Retry with Backoff
|
||||
|
||||
```typescript
|
||||
async function connectWithRetry(addr: SocketAddress, opts: SocketOptions, maxRetries = 3): Promise<Socket> {
|
||||
for (let i = 1; i <= maxRetries; i++) {
|
||||
try {
|
||||
const socket = connect(addr, opts);
|
||||
await socket.opened;
|
||||
return socket;
|
||||
} catch (error) {
|
||||
if (i === maxRetries) throw error;
|
||||
await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i - 1))); // Exponential backoff
|
||||
}
|
||||
}
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
```
|
||||
|
||||
### Timeout
|
||||
|
||||
```typescript
|
||||
async function connectWithTimeout(addr: SocketAddress, opts: SocketOptions, ms = 5000): Promise<Socket> {
|
||||
const socket = connect(addr, opts);
|
||||
const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));
|
||||
await Promise.race([socket.opened, timeout]);
|
||||
return socket;
|
||||
}
|
||||
```
|
||||
|
||||
### Fallback
|
||||
|
||||
```typescript
|
||||
async function connectWithFallback(primary: string, fallback: string, port: number): Promise<Socket> {
|
||||
try {
|
||||
const socket = connect({ hostname: primary, port }, { secureTransport: "on" });
|
||||
await socket.opened;
|
||||
return socket;
|
||||
} catch {
|
||||
return connect({ hostname: fallback, port }, { secureTransport: "on" });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Patterns
|
||||
|
||||
### Destination Allowlist (Prevent SSRF)
|
||||
|
||||
```typescript
|
||||
const ALLOWED_HOSTS = ['db.internal.company.net', 'api.internal.company.net', /^10\.0\.1\.\d+$/];
|
||||
|
||||
function isAllowed(hostname: string): boolean {
|
||||
return ALLOWED_HOSTS.some(p => p instanceof RegExp ? p.test(hostname) : p === hostname);
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
const target = new URL(req.url).searchParams.get('host');
|
||||
if (!target || !isAllowed(target)) return new Response('Forbidden', { status: 403 });
|
||||
const socket = connect({ hostname: target, port: 443 });
|
||||
// Use socket...
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### Connection Pooling
|
||||
|
||||
```typescript
|
||||
class SocketPool {
|
||||
private pool = new Map<string, Socket[]>();
|
||||
|
||||
async acquire(hostname: string, port: number): Promise<Socket> {
|
||||
const key = `${hostname}:${port}`;
|
||||
const sockets = this.pool.get(key) || [];
|
||||
if (sockets.length > 0) return sockets.pop()!;
|
||||
const socket = connect({ hostname, port }, { secureTransport: "on" });
|
||||
await socket.opened;
|
||||
return socket;
|
||||
}
|
||||
|
||||
release(hostname: string, port: number, socket: Socket): void {
|
||||
const key = `${hostname}:${port}`;
|
||||
const sockets = this.pool.get(key) || [];
|
||||
if (sockets.length < 3) { sockets.push(socket); this.pool.set(key, sockets); }
|
||||
else socket.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Multi-Protocol Gateway
|
||||
|
||||
```typescript
|
||||
interface Protocol { name: string; defaultPort: number; test(host: string, port: number): Promise<string>; }
|
||||
|
||||
const PROTOCOLS: Record<string, Protocol> = {
|
||||
redis: {
|
||||
name: 'redis',
|
||||
defaultPort: 6379,
|
||||
async test(host, port) {
|
||||
const socket = connect({ hostname: host, port });
|
||||
try {
|
||||
const writer = socket.writable.getWriter();
|
||||
await writer.write(new TextEncoder().encode('*1\r\n$4\r\nPING\r\n'));
|
||||
writer.releaseLock();
|
||||
const reader = socket.readable.getReader();
|
||||
const { value } = await reader.read();
|
||||
return new TextDecoder().decode(value || new Uint8Array());
|
||||
} finally { await socket.close(); }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
async fetch(req: Request): Promise<Response> {
|
||||
const url = new URL(req.url);
|
||||
const proto = url.pathname.slice(1); // /redis
|
||||
const host = url.searchParams.get('host');
|
||||
if (!host || !PROTOCOLS[proto]) return new Response('Invalid', { status: 400 });
|
||||
const result = await PROTOCOLS[proto].test(host, parseInt(url.searchParams.get('port') || '') || PROTOCOLS[proto].defaultPort);
|
||||
return new Response(result);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user