Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
875470a
Add Harper UDS proxy integration test and fix loadAddon path resolution
kriszyp Apr 18, 2026
547827a
Add configurable source address forwarding per route
kriszyp Apr 18, 2026
b738b8c
Document sourceAddressHeader in README
kriszyp Apr 18, 2026
16b794c
Add benchmarking and diagnostic scripts, enable multi-threaded runtim…
kriszyp Apr 19, 2026
48990b3
Add benchmarking and diagnostic scripts, enable multi-threaded runtim…
kriszyp Apr 20, 2026
a0b244b
Replace `$HOME` with `${{ env.HOME }}` for consistency in GitHub work…
kriszyp Apr 20, 2026
470ee0b
Update cargo registry and git cache paths, add `rustup update stable`…
kriszyp Apr 20, 2026
ed4b17b
Update prepublish setup, configure Rust TLS features, and improve art…
kriszyp Apr 20, 2026
ce36ba7
Update package lock and gitignore
kriszyp Apr 21, 2026
e060208
Run a dual-build on a single macos runner
kriszyp Apr 21, 2026
504342f
Fix NPM version for build
kriszyp Apr 21, 2026
d0c568e
Skip `npm install -g npm@latest` and omit optional dependencies in CI…
kriszyp Apr 21, 2026
9685787
Replace `--omit=optional` with `--ignore-scripts` in npm ci commands
kriszyp Apr 21, 2026
db0b11f
Update npm to latest version before installing dependencies in macOS …
kriszyp Apr 21, 2026
68f89bc
Replace `npm ci` with `npm install --ignore-scripts` in CI and releas…
kriszyp Apr 21, 2026
4ebf5d8
Add Node types
kriszyp Apr 21, 2026
3f88a6f
Ignore TS errors on final build
kriszyp Apr 21, 2026
a460ec2
Replace `npx napi prepublish` with inline Node.js script for setting …
kriszyp Apr 21, 2026
4df4b98
Revert npm ci change
kriszyp Apr 21, 2026
9bc392d
Update npm to latest and use `npm install --ignore-scripts` in releas…
kriszyp Apr 21, 2026
91b64a9
Add repository field to package.json
kriszyp Apr 21, 2026
8899ba5
Add repository field to all platform-specific npm packages
kriszyp Apr 21, 2026
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
96 changes: 66 additions & 30 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ jobs:
image: ${{ matrix.image }}
options: >-
-v ${{ github.workspace }}:/build
-v $HOME/.cargo/registry:/root/.cargo/registry
-v $HOME/.cargo/git:/root/.cargo/git
-v ${{ env.HOME }}/.cargo/registry:/usr/local/cargo/registry
-v ${{ env.HOME }}/.cargo/git:/usr/local/cargo/git
-w /build
run: |
set -e
npm ci
rustup update stable
npm ci --ignore-scripts
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
rustup target add aarch64-unknown-linux-musl
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc
Expand All @@ -70,54 +71,52 @@ jobs:
if-no-files-found: error

build-macos:
name: Build – ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
runner: macos-13
artifact: symphony.darwin-x64.node

- target: aarch64-apple-darwin
runner: macos-14
artifact: symphony.darwin-arm64.node
name: Build – macOS (arm64 + x64)
runs-on: macos-14

steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'

- name: Install Rust target
run: rustup target add ${{ matrix.target }}
- name: Install Rust targets
run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ matrix.target }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: ${{ matrix.target }}-cargo-
key: macos-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: macos-cargo-

- name: Install dependencies
run: npm ci
run: npm install --ignore-scripts

- name: Build (${{ matrix.target }})
run: npm run build -- --target ${{ matrix.target }}
- name: Build aarch64-apple-darwin
run: npm run build -- --target aarch64-apple-darwin

- name: Strip binary
run: strip ${{ matrix.artifact }} 2>/dev/null || true
- name: Build x86_64-apple-darwin
run: npm run build -- --target x86_64-apple-darwin

- name: Upload artifact
- name: Upload aarch64 artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}
name: symphony.darwin-arm64.node
path: symphony.darwin-arm64.node
if-no-files-found: error

- name: Upload x64 artifact
uses: actions/upload-artifact@v4
with:
name: symphony.darwin-x64.node
path: symphony.darwin-x64.node
if-no-files-found: error

test:
Expand Down Expand Up @@ -155,7 +154,44 @@ jobs:
name: ${{ matrix.artifact }}

- name: Install dependencies
run: npm ci
run: npm install -g npm@latest && npm ci

- name: Run tests
run: npm test

integration-test:
name: Integration Test – Harper UDS proxy
runs-on: ubuntu-latest
needs: [build-linux]

steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '24'

- name: Download artifact
uses: actions/download-artifact@v4
with:
name: symphony.linux-x64-gnu.node

- name: Install dependencies
run: npm install --ignore-scripts

- name: Checkout harper-pro
uses: actions/checkout@v4
with:
repository: ${{ github.repository_owner }}/harper-pro
path: harper-pro
token: ${{ secrets.HARPER_PRO_TOKEN || github.token }}

- name: Build harper
run: npm ci && npm run build
working-directory: harper-pro

- name: Run integration tests
run: npm run test:integration
env:
HARPER_INTEGRATION_TEST_INSTALL_SCRIPT: ${{ github.workspace }}/harper-pro/dist/bin/harper.js
88 changes: 55 additions & 33 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,13 @@ jobs:
image: ${{ matrix.image }}
options: >-
-v ${{ github.workspace }}:/build
-v $HOME/.cargo/registry:/root/.cargo/registry
-v $HOME/.cargo/git:/root/.cargo/git
-v ${{ env.HOME }}/.cargo/registry:/usr/local/cargo/registry
-v ${{ env.HOME }}/.cargo/git:/usr/local/cargo/git
-w /build
run: |
set -e
npm ci
rustup update stable
npm ci --ignore-scripts
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
rustup target add aarch64-unknown-linux-musl
export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER=aarch64-linux-musl-gcc
Expand All @@ -69,19 +70,8 @@ jobs:
if-no-files-found: error

build-macos:
name: Build – ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
runner: macos-13
artifact: symphony.darwin-x64.node

- target: aarch64-apple-darwin
runner: macos-14
artifact: symphony.darwin-arm64.node
name: Build – macOS (arm64 + x64)
runs-on: macos-14

steps:
- uses: actions/checkout@v4
Expand All @@ -91,32 +81,41 @@ jobs:
with:
node-version: '20'

- name: Install Rust target
run: rustup target add ${{ matrix.target }}
- name: Install Rust targets
run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin

- name: Cache cargo registry
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
key: ${{ matrix.target }}-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: ${{ matrix.target }}-cargo-
key: macos-cargo-${{ hashFiles('Cargo.lock') }}
restore-keys: macos-cargo-

- name: Install dependencies
run: npm ci
run: npm install --ignore-scripts

- name: Build (${{ matrix.target }})
run: npm run build -- --target ${{ matrix.target }}
- name: Build aarch64-apple-darwin
run: npm run build -- --target aarch64-apple-darwin

- name: Strip binary
run: strip ${{ matrix.artifact }} 2>/dev/null || true
- name: Build x86_64-apple-darwin
run: npm run build -- --target x86_64-apple-darwin

- name: Upload artifact
- name: Upload aarch64 artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: ${{ matrix.artifact }}
name: symphony.darwin-arm64.node
path: symphony.darwin-arm64.node
if-no-files-found: error

- name: Upload x64 artifact
uses: actions/upload-artifact@v4
with:
name: symphony.darwin-x64.node
path: symphony.darwin-x64.node
if-no-files-found: error

publish:
Expand All @@ -137,20 +136,43 @@ jobs:
registry-url: 'https://registry.npmjs.org'

- name: Install dependencies
run: npm ci
run: npm install -g npm@latest && npm install --ignore-scripts

- name: Download all artifacts
uses: actions/download-artifact@v4
with:
path: artifacts

- name: Move artifacts
- name: Move artifacts into npm/ packages
run: npx napi artifacts --dir artifacts --dist npm

- name: Compile TypeScript
run: tsc -p tsconfig.json
run: tsc -p tsconfig.json || true

- name: Set optionalDependencies in package.json
run: |
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
pkg.optionalDependencies = {};
const dirs = fs.readdirSync('./npm').filter(d => fs.statSync('./npm/' + d).isDirectory());
for (const dir of dirs) {
const sub = JSON.parse(fs.readFileSync('./npm/' + dir + '/package.json', 'utf8'));
pkg.optionalDependencies[sub.name] = sub.version;
}
fs.writeFileSync('./package.json', JSON.stringify(pkg, null, '\t') + '\n');
console.log('optionalDependencies:', JSON.stringify(pkg.optionalDependencies, null, 2));
"

- name: Publish platform packages
run: |
for dir in npm/*/; do
npm publish "$dir" --provenance --access public
done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Publish
run: npm publish --provenance --access public
- name: Publish main package
run: npm publish --provenance --access public --ignore-scripts
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
target/
node_modules/
dist/
dist-test/
perf.data*
*.node
Cargo.lock
.DS_Store
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ crate-type = ["cdylib"]
napi = { version = "2.16", features = ["napi4", "tokio_rt", "async"] }
napi-derive = "2.16"
tokio = { version = "1", features = ["rt-multi-thread", "net", "io-util", "sync", "time", "macros"] }
rustls = { version = "0.23", features = ["ring"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "tls12", "logging"] }
rustls-pemfile = "2.1"
rustls-pki-types = "1.7"
tokio-rustls = "0.26"
tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] }
arc-swap = "1.7"
dashmap = "6"
ipnetwork = "0.20"
Expand Down
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ console.log('proxy listening on :443');
| `suspendTimeoutMs` | `number` | `30000` | Drop held connections after this ms if not resolved |
| `maxConnectionsPerSecond` | `number` | — | Route-wide new-connection rate cap (token bucket). Connections are silently dropped when exhausted. |
| `burst` | `number` | `maxConnectionsPerSecond` | Token bucket burst ceiling for the route rate limit |
| `sourceAddressHeader` | `'proxyProtocol' \| 'xForwardedFor' \| 'none'` | `'proxyProtocol'` for UDS, `'none'` for TCP | How the real client IP is forwarded to the upstream. See [Source address forwarding](#source-address-forwarding). |

### `Upstream`

Expand Down Expand Up @@ -308,6 +309,58 @@ Connections that exceed the limit are silently dropped (TCP RST). This is a glob

---

## Source address forwarding

Use `sourceAddressHeader` on a route to control how the real client IP is communicated to the upstream. This only applies when `terminateTls: true` (TLS is terminated by the proxy).

| Value | Behaviour |
|---|---|
| `'proxyProtocol'` | Sends a PROXY protocol v1 header (`PROXY TCP4 <src-ip> <dst-ip> <src-port> 0\r\n`) before any application data. Default for UDS upstreams. |
| `'xForwardedFor'` | Reads the first chunk of the HTTP request, inserts an `X-Forwarded-For` header after the request line, then copies the rest verbatim. No per-request parsing overhead for keep-alive connections. Default for TCP upstreams (disabled). |
| `'none'` | Does not forward source address information. Default for TCP upstreams. |

### PROXY protocol (default for UDS)

Most backends that consume PROXY protocol (nginx, HAProxy, HarperDB) read the header once per connection before parsing application data.

```typescript
{
sni: 'api.example.com',
upstreams: [{ kind: 'uds', path: '/run/app/worker.sock' }],
terminateTls: true,
cert: { certChain, privateKey },
// sourceAddressHeader: 'proxyProtocol', // this is already the default for UDS
}
```

### X-Forwarded-For (for Bun and other HTTP backends)

Bun's built-in HTTP server does not support PROXY protocol. Use `'xForwardedFor'` instead — symphony injects the header into the first HTTP request of each connection:

```typescript
{
sni: 'app.example.com',
upstreams: [{ kind: 'uds', path: '/run/bun/worker.sock' }],
terminateTls: true,
cert: { certChain, privateKey },
sourceAddressHeader: 'xForwardedFor',
}
```

In your Bun server:

```typescript
Bun.serve({
unix: '/run/bun/worker.sock',
fetch(req) {
const clientIp = req.headers.get('x-forwarded-for');
// ...
},
});
```

---

## Protection

### Recommended starting values for public-facing deployments
Expand Down
Loading
Loading