You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A tiny, config-driven local reverse proxy with a stable public HTTPS URL. Edit a JSON file to declare what to forward where, run one command, share the link.
Built on [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/) + [YARP](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/) + [Microsoft Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/).
7
+
A config-driven local reverse proxy with a stable public HTTPS URL. Edit one JSON file, run one command, share the link.
6
8
7
-
## Prerequisites
9
+
Built on [.NET Aspire](https://learn.microsoft.com/dotnet/aspire/), [YARP](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/), and [Microsoft Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/).
- A Microsoft or GitHub account (the `devtunnel` CLI prompts you to sign in on first run; Aspire handles this automatically).
11
+
## Why this exists
16
12
17
-
## Run it
13
+
You need a public HTTPS URL that points at something running on your laptop — to receive a webhook, demo a prototype, or test a mobile app against a local API. ngrok works, but you pay for stable URLs and lose control over headers, CORS, and request shaping. This project is the same idea, free, and you keep full control: add CORS, inject auth headers or JSON fields, hot-reload routes from a config file.
Works identically on macOS, Linux, and Windows (PowerShell or `cmd`). On first run, a browser window opens for `devtunnel` sign-in.
26
-
27
-
## Use it
23
+
First run opens a browser for `devtunnel` sign-in. Then:
28
24
29
25
1. Open the Aspire dashboard URL printed in the terminal.
30
-
2. Click the `tunnel` resource and copy its `https://aspire-tunnel-proxy-*.devtunnels.ms` URL.
31
-
3. Edit `src/Proxy/appsettings.json` — point `Clusters.default.Destinations.primary.Address` at the URL you want to forward to. Save; YARP hot-reloads, no restart.
32
-
4. Edit `src/AppHost/appsettings.json` — change `DevTunnel:Id` to your own globally-unique slug (a-z, 0-9, hyphen) so collaborators get their own stable URL.
26
+
2. Click the `tunnel` resource — copy its `https://*.devtunnels.ms` URL.
27
+
3. Edit `src/Proxy/appsettings.json` → set `Clusters.default.Destinations.primary.Address` to your upstream. YARP hot-reloads — no restart needed.
28
+
4. Edit `src/AppHost/appsettings.json` → change `DevTunnel:Id` to a unique slug (a–z, 0–9, `-`) so collaborators get distinct stable URLs.
29
+
30
+
That's it. Anything hitting the tunnel URL is forwarded to your configured destination.
Stripe, GitHub, Slack, etc. need a public HTTPS endpoint to send events. Point the proxy at your local handler, paste the tunnel URL into the provider's webhook config.
47
+
48
+
```jsonc
49
+
// src/Proxy/appsettings.json
50
+
"Clusters": {
51
+
"default": {
52
+
"Destinations": {
53
+
"primary": { "Address":"http://localhost:5050/" }
54
+
}
55
+
}
56
+
}
57
+
```
58
+
59
+
Now `https://<your-slug>.devtunnels.ms/stripe/events` reaches `http://localhost:5050/stripe/events`.
60
+
61
+
### Test a local API from a real mobile device
33
62
34
-
Optional features: [CORS](#cors), [request header injection](#header-injection), [JSON body field injection](#json-body-field-injection), [private tunnel](#security).
63
+
Phones, tablets, and other dev machines can't reach `localhost:5000`. Run the proxy, give them the tunnel URL — works over cellular, hotel Wi-Fi, anywhere.
Stable across restarts as long as `DevTunnel:Id` doesn't change.
73
+
74
+
### Add CORS to an upstream that doesn't support it
39
75
40
-
Declare named CORS policies under `Cors:Policies`, then reference them from any route via the standard YARP `CorsPolicy` field. The bundled default policy is wide-open:
76
+
Wrap a bare API with browser-friendly CORS without modifying the upstream:
41
77
42
-
```json
78
+
```jsonc
43
79
"Cors": {
44
80
"Policies": {
45
81
"default": {
46
-
"AllowedOrigins": ["*"],
47
-
"AllowedMethods": ["*"],
82
+
"AllowedOrigins": ["https://my-spa.example.com"],
83
+
"AllowedMethods": ["GET", "POST"],
48
84
"AllowedHeaders": ["*"],
49
-
"ExposedHeaders": [],
50
-
"AllowCredentials": false,
51
-
"PreflightMaxAgeSeconds": null
85
+
"AllowCredentials":false
86
+
}
87
+
}
88
+
},
89
+
"ReverseProxy": {
90
+
"Routes": {
91
+
"default": {
92
+
"ClusterId":"default",
93
+
"CorsPolicy":"default",
94
+
"Match": { "Path":"{**catch-all}" }
52
95
}
53
96
}
54
97
}
55
98
```
56
99
57
-
-`["*"]` for `AllowedOrigins`, `AllowedMethods`, or `AllowedHeaders` enables the matching `AllowAny*` rule. An explicit list narrows the policy to those values.
58
-
-`AllowCredentials: true` combined with `AllowedOrigins: ["*"]` is invalid — startup throws `InvalidOperationException` with the policy name. List explicit origins instead.
59
-
-`PreflightMaxAgeSeconds` (optional) sets the `Access-Control-Max-Age` response header for preflight caching.
60
-
- Routes opt in by setting `CorsPolicy: "<name>"`. ASP.NET Core CORS middleware short-circuits `OPTIONS` preflights so YARP never forwards them upstream.
61
-
- CORS policies are read at startup. Editing `Cors:Policies` requires an AppHost restart. `Routes` and `Clusters` continue to hot-reload as before.
`["*"]` enables `AllowAny*`. Combining `AllowedOrigins: ["*"]` with `AllowCredentials: true` is invalid and throws at startup — list explicit origins instead.
64
101
65
-
### Header injection
102
+
### Inject auth headers before forwarding
66
103
67
-
Add a `Transforms` array inside a route to set or append request headers before forwarding:
104
+
Public-facing tunnel, secret-bearing upstream. Use route transforms:
For upstreams that authenticate via body fields, not headers:
90
124
91
-
For upstreams that authenticate via JSON body fields rather than headers, declare the fields to merge under the route's `Metadata` block with the `InjectJsonField:` prefix:
@@ -105,25 +136,41 @@ For upstreams that authenticate via JSON body fields rather than headers, declar
105
136
}
106
137
```
107
138
108
-
- Fields are merged into the top level of the JSON request body before forwarding.
109
-
- Each value is parsed as JSON first: `"true"` becomes a boolean, `"42"` becomes a number, `"{\"k\":\"v\"}"` becomes a nested object. Anything that isn't valid JSON is injected as a raw string.
110
-
- If a client-supplied field collides with an injected key, the injected value wins.
111
-
- Skipped silently when the request `Content-Type` is not `application/json`, the body is empty, or the parsed JSON isn't an object — the body passes through unchanged so the upstream's own validation surfaces the error.
139
+
Each value parses as JSON first — `"42"` becomes a number, `"true"` a boolean, `"{...}"` a nested object. Anything else is injected as a string. Skipped silently when the body isn't JSON, is empty, or isn't an object.
|`src/Proxy/appsettings.json`| Routes, clusters, CORS policies, transforms |
147
+
148
+
YARP routes/clusters fully follow the upstream schema — see [YARP config files](https://learn.microsoft.com/aspnet/core/fundamentals/servers/yarp/config-files).
149
+
150
+
**Hot reload:**`Routes` and `Clusters` reload on save with no restart. `Cors:Policies` are read at startup — changes need an AppHost restart.
112
151
113
152
## Security
114
153
115
-
`DevTunnel:AnonymousAccess: true` (the default) makes the tunnel URL **publicly reachable by anyone who knows it**. Don't proxy anything with secrets, dev databases, or unauthenticated admin surfaces.
154
+
`DevTunnel:AnonymousAccess: true` (the default) makes the tunnel URL **publicly reachable by anyone who knows it**. Don't proxy anything with secrets, dev databases, or unauthenticated admin surfaces over an anonymous tunnel.
155
+
156
+
Set `DevTunnel:AnonymousAccess: false` in `src/AppHost/appsettings.json` for a private tunnel. Recipients then need a Microsoft/GitHub login the owner has authorised, or an `X-Tunnel-Authorization` token from `devtunnel token`. Note: private tunnels block cross-origin browser callers — `fetch()` from a deployed SPA on another origin can't complete the interactive sign-in.
116
157
117
-
To make the tunnel private, set `DevTunnel:AnonymousAccess` to `false` in `src/AppHost/appsettings.json`. Recipients then need a Microsoft or GitHub login the tunnel owner has authorised, or an `X-Tunnel-Authorization` token issued by `devtunnel token`. Note: a private tunnel breaks cross-origin browser callers — `fetch()` from a deployed SPA on another origin cannot complete the interactive sign-in flow.
158
+
To report a vulnerability privately, please open a [GitHub security advisory](https://github.com/LorcanChinnock/aspire-tunnel-proxy/security/advisories/new) rather than a public issue.
118
159
119
-
## Layout
160
+
## Project layout
120
161
121
162
```
122
163
src/
123
-
├── AppHost/ .NET Aspire app host: wires the proxy to a Dev Tunnel
124
-
└── Proxy/ ASP.NET Core + YARP: routes/clusters live in appsettings.json
164
+
├── AppHost/ .NET Aspire app host — wires the proxy to a Dev Tunnel
165
+
└── Proxy/ ASP.NET Core + YARP — routes/clusters live in appsettings.json
166
+
tests/
167
+
└── Proxy.Tests/ xUnit v3 integration + unit tests
125
168
```
126
169
170
+
## Contributing
171
+
172
+
Issues and pull requests welcome. PRs run a build + test gate via [GitHub Actions](.github/workflows/ci.yml) and require one CODEOWNER review before merge.
0 commit comments