Skip to content

Commit c730a9a

Browse files
authored
Merge pull request #153 from gosuda/feature/tls-termination-detection
Feature/tls termination detection
2 parents 11ef324 + 741ef83 commit c730a9a

19 files changed

Lines changed: 1113 additions & 249 deletions

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,24 @@ Unlike other tunneling services, Portal is self-hosted and permissionless. You c
1818

1919
- **NAT-friendly connectivity**: Works behind NAT or firewalls without opening inbound ports
2020
- **Automatic subdomain routing**: Gives each app its own subdomain (`your-app.<base-domain>`)
21-
- **End-to-end encryption**: Supports TLS passthrough with relay keyless certificates
21+
- **End-to-end tenant TLS**: Relay routes by SNI, while tenant TLS terminates on your side with relay-backed keyless signing
2222
- **Permissionless Hosting**: Anyone can run their own Portal — no approval needed
2323
- **One-Command Setup**: Expose any local app with a single command
2424
- **UDP Relay (Experimental)**: Supports raw UDP relay use cases, but the transport model and operational behavior may still change
2525

26+
## How Portal Provides End-to-End Encryption
27+
28+
Portal is designed so that tenant TLS terminates on your side rather than at the relay. In the normal data path, the relay forwards encrypted traffic without access to tenant TLS plaintext.
29+
30+
1. The relay accepts the public connection and reads only the TLS ClientHello required for SNI-based routing.
31+
2. It forwards the tenant connection as raw encrypted bytes over the reverse session without terminating tenant TLS.
32+
3. The Portal client on your side acts as the TLS server and completes the tenant handshake locally.
33+
4. For relay-hosted domains, the Portal client obtains certificate signatures via `/v1/sign`, using the relay only as a keyless signing oracle.
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`.
38+
2639
## Components
2740

2841
- **Relay**: A server that routes public requests to the right connected app.

cmd/demo-app/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ func main() {
3333
type demoConfig struct {
3434
relayURLs string
3535
discovery bool
36+
banMITM bool
3637
addr string
3738
name string
3839
desc string
@@ -48,6 +49,7 @@ func runTCPCommand(args []string) error {
4849
fs := utils.NewFlagSet("demo-app", printTCPUsage)
4950
utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://gosunuts.xyz", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS")
5051
utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY")
52+
utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
5153
utils.StringFlag(fs, &cfg.addr, "addr", "127.0.0.1:8092", "local demo HTTP listen address (host:port or URL; disable if empty)")
5254
utils.StringFlag(fs, &cfg.name, "name", "demo-app", "public hostname prefix (single DNS label)")
5355
utils.StringFlag(fs, &cfg.desc, "description", "Portal demo connectivity app", "lease description")
@@ -79,6 +81,7 @@ func runUDPCommand(args []string) error {
7981

8082
utils.StringFlagEnv(fs, &cfg.relayURLs, "relays", "https://localhost:4017", "additional relay API URLs (comma-separated; scheme omitted defaults to https; merged with public registry relays when discovery is enabled)", "RELAYS")
8183
utils.BoolFlagEnv(fs, &cfg.discovery, "discovery", true, "include public registry relays and enable discovery", "DISCOVERY")
84+
utils.BoolFlagEnv(fs, &cfg.banMITM, "ban-mitm", false, "ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
8285
utils.StringFlag(fs, &cfg.name, "name", "demo-udp", "public hostname prefix (single DNS label)")
8386
utils.StringFlag(fs, &cfg.desc, "description", "Portal demo UDP echo service", "lease description")
8487
utils.StringFlag(fs, &cfg.tags, "tags", "demo,udp,echo", "comma-separated lease tags")
@@ -133,6 +136,7 @@ func runTCPDemo(ctx context.Context, cfg demoConfig) error {
133136
exposure, err := sdk.Expose(ctx, sdk.ExposeConfig{
134137
RelayURLs: utils.SplitCSV(cfg.relayURLs),
135138
Name: cfg.name,
139+
BanMITM: cfg.banMITM,
136140
Discovery: cfg.discovery,
137141
Metadata: types.LeaseMetadata{
138142
Description: cfg.desc,
@@ -173,6 +177,7 @@ func runUDPDemo(ctx context.Context, cfg demoConfig) error {
173177
RelayURLs: utils.SplitCSV(cfg.relayURLs),
174178
Name: cfg.name,
175179
UDPEnabled: true,
180+
BanMITM: cfg.banMITM,
176181
Discovery: cfg.discovery,
177182
Metadata: types.LeaseMetadata{
178183
Description: cfg.desc,

cmd/portal-tunnel/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,14 @@ portal expose localhost:8080 \
3737
- `--name` is optional. When omitted, the CLI generates a name for that run.
3838
- `--relays` sets the relay API URLs for that run.
3939
- `--discovery=false` disables the public registry seed list and the discovery expansion loop for that run.
40+
- `--ban-mitm` enables strict rejection when the TLS self-probe detects termination in the path.
4041

4142
Flags:
4243

4344
```text
4445
--relays Portal relay API URLs (comma-separated, https only)
4546
--discovery Include public registry relays and discover additional relay bootstraps
47+
--ban-mitm Ban relay when the MITM self-probe detects TLS termination
4648
--name Public hostname prefix (single DNS label); auto-generated when omitted
4749
--description Service description metadata
4850
--tags Service tags metadata (comma-separated)
@@ -80,4 +82,5 @@ Legacy execution compatibility has been removed:
8082
- 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.
8183
- SDK callers that do not set `ListenerConfig.RetryCount` use infinite retry semantics for each relay.
8284
- Tenant TLS is provisioned automatically through the relay keyless signer. The SDK fetches the relay certificate chain and uses `/v1/sign` for remote signing.
85+
- TLS self-probe mismatches log warnings by default. Use `--ban-mitm` to reject relays that terminate tenant TLS.
8386
- When the local service is unreachable, the tunnel returns an HTTP 503 page.

cmd/portal-tunnel/main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ func main() {
3434
type exposeFlags struct {
3535
relayCSV string
3636
discovery bool
37+
banMITM bool
3738
privateKey string
3839
name string
3940
desc string
@@ -52,6 +53,7 @@ func runExposeCommand(args []string) error {
5253

5354
utils.StringFlag(fs, &flags.relayCSV, "relays", "", "Additional Portal relay server API URLs (comma-separated; scheme omitted defaults to https)")
5455
utils.BoolFlag(fs, &flags.discovery, "discovery", true, "Include public registry relays and discover additional relay bootstraps")
56+
utils.BoolFlagEnv(fs, &flags.banMITM, "ban-mitm", true, "Ban relay when the MITM self-probe detects TLS termination", "BAN_MITM")
5557
utils.StringFlag(fs, &flags.privateKey, "private-key", "", "Owner private key used to derive a discovery address")
5658
utils.StringFlag(fs, &flags.name, "name", "", "Public hostname prefix (single DNS label); auto-generated when omitted")
5759
utils.StringFlag(fs, &flags.desc, "description", "", "Service description metadata")
@@ -95,6 +97,7 @@ func runExposeCommand(args []string) error {
9597
TargetAddr: flags.targetAddr,
9698
UDPAddr: flags.udpAddr,
9799
UDPEnabled: flags.udp,
100+
BanMITM: flags.banMITM,
98101
Discovery: flags.discovery,
99102
Metadata: types.LeaseMetadata{
100103
Description: flags.desc,
@@ -252,6 +255,7 @@ func printExposeUsage(w io.Writer) {
252255
"portal expose 3000",
253256
"portal expose localhost:8080 --name my-app",
254257
"portal expose 3000 --udp --udp-addr 127.0.0.1:5353",
258+
"portal expose 3000 --ban-mitm",
255259
"portal expose 3000 --relays https://portal.example.com --discovery=false",
256260
},
257261
)

docs/adr/0001-raw-tcp-reverse-connect-and-autocert-tls.md

Lines changed: 0 additions & 42 deletions
This file was deleted.

docs/adr/0002-remove-websocket-and-legacy-compatibility.md

Lines changed: 0 additions & 40 deletions
This file was deleted.

docs/adr/0003-security-and-anti-abuse-hardening.md

Lines changed: 0 additions & 55 deletions
This file was deleted.

docs/adr/README.md

Lines changed: 0 additions & 23 deletions
This file was deleted.

docs/adr/template.md

Lines changed: 0 additions & 34 deletions
This file was deleted.

0 commit comments

Comments
 (0)