fix readme and routes path

rabbitprincess committed Mar 25, 2026 at 23:29 UTC dce86b2df7d7c171928e85d3a7146f7642b3b3bc
3 files changed +15 -10
README.md
+2 -2
@@ -21,7 +21,7 @@ Unlike other tunneling services, Portal is self-hosted and permissionless. You c
21 - **End-to-end tenant TLS**: Relay routes by SNI, while tenant TLS terminates on your side with relay-backed keyless signing
22 - **Permissionless Hosting**: Anyone can run their own Portal — no approval needed
23 - **One-Command Setup**: Expose any local app with a single command
24 -- **UDP Relay (Experimental)**: Supports raw UDP relay use cases, but the transport model and operational behavior may still change
24 +- **UDP Relay (Experimental)**: Supports raw UDP relay
25
26 ## How Portal Provides End-to-End Encryption
27
@@ -34,7 +34,7 @@ Portal is designed so that tenant TLS terminates on your side rather than at the
34 5. Session keys are derived entirely on your side. The relay provides certificate signatures only and does not receive tenant traffic secrets.
35 6. After the handshake, the relay continues forwarding ciphertext without needing tenant TLS plaintext to keep routing traffic.
36
37 -Portal also checks that the relay is preserving TLS passthrough. The Portal client connects to its own public endpoint and compares TLS exporter values observed on both client-controlled ends. If they differ, Portal logs suspected TLS termination by default. You can switch to strict enforcement with `portal expose --ban-mitm`.
37 +Portal also checks that the relay is preserving TLS passthrough. The Portal client connects to its own public endpoint and compares TLS exporter values observed on both client-controlled ends. If they differ, `portal expose` rejects the relay by default.
38
39 ## Components
40
cmd/portal-tunnel/README.md
+1 -1
@@ -94,6 +94,6 @@ Legacy execution compatibility has been removed:
94 - With discovery enabled, the configured relay list starts with `public registry + --relays values` and can expand through relay discovery. With `--discovery=false`, only the explicit relay URLs are used. Published public URLs appear only for relays that have registered successfully.
95 - SDK callers that do not set `ListenerConfig.RetryCount` use infinite retry semantics for each relay.
96 - Tenant TLS is provisioned automatically through the relay keyless signer. The SDK fetches the relay certificate chain and uses `/v1/sign` for remote signing.
97 -- TLS self-probe mismatches log warnings by default. Use `--ban-mitm` to reject relays that terminate tenant TLS.
97 +- `portal expose` enables MITM strict enforcement by default. Use `--ban-mitm=false` to keep warning-only behavior when the TLS self-probe suspects relay termination.
98 - When the local service is unreachable, the tunnel returns an HTTP 503 page.
99 - `--http-route` mode is HTTP-only and cannot be combined with `--udp`.
cmd/portal-tunnel/http_routes.go
+12 -7
@@ -216,6 +216,7 @@ func (r *httpRoute) rewriteLocation(header http.Header, meta httpRouteMeta) {
216 return
217 }
218
219 + var mappedPath string
220 switch {
221 case parsed.IsAbs():
222 if !strings.EqualFold(parsed.Scheme, r.upstream.Scheme) || !strings.EqualFold(parsed.Host, r.upstream.Host) {
@@ -223,14 +224,18 @@ func (r *httpRoute) rewriteLocation(header http.Header, meta httpRouteMeta) {
224 }
225 parsed.Scheme = meta.publicScheme
226 parsed.Host = meta.publicHost
226 - parsed.Path = r.mapUpstreamPathToPublic(parsed.Path)
227 - parsed.RawPath = ""
228 - header.Set("Location", parsed.String())
229 - case strings.HasPrefix(location, "/"):
230 - parsed.Path = r.mapUpstreamPathToPublic(parsed.Path)
231 - parsed.RawPath = ""
232 - header.Set("Location", parsed.String())
227 + case strings.HasPrefix(location, "/") && (len(location) == 1 || (location[1] != '/' && location[1] != '\\')):
228 + default:
229 + return
230 + }
231 +
232 + mappedPath = r.mapUpstreamPathToPublic(parsed.Path)
233 + if !strings.HasPrefix(mappedPath, "/") || (len(mappedPath) > 1 && (mappedPath[1] == '/' || mappedPath[1] == '\\')) {
234 + return
235 }
236 + parsed.Path = mappedPath
237 + parsed.RawPath = ""
238 + header.Set("Location", parsed.String())
239 }
240
241 func (r *httpRoute) rewriteSetCookies(header http.Header, publicHost string) {