Skip to content

Implement HTTP/2 CONNECT-IP fallback over TLS #981

Closed

Description

@privateip

Parent: #970

Deliverable

An HTTP/2 listener on TCP/443 that carries CONNECT-IP packets in capsules and uses the same service authority and gVPC packet path as HTTP/3.

Dependencies

#980

Galactic Edge integration (#992)

Implement this milestone as a consumer of the Galactic Edge provider contract. #992 owns provider authorization, host attachment lifecycle, shared-state fencing and maintenance, independently enforced forwarding expiry, prefix conflict arbitration/publication, and gVPC traffic policy. MASQUE owns client authentication, connector /96 assignment and retention, session packet checks, transport behavior and its provider adapter. Use the foundation's versioned contract and existing provider helpers; do not introduce a second host-networking controller, lease store or BGP publisher. Any required foundation change must be tracked and qualified under #992 before this milestone claims integrated acceptance.

Work

  • Implement Extended CONNECT negotiation and capsule transport over HTTP/2 with TLS using the selected stack.
  • Reuse admission, prefix grants, policy and per-connector ownership from the HTTP/3 service; fallback must not create a second concurrent forwarding owner.
  • Implement bounded incremental capsule parsing, stream backpressure and independent cancellation, including capsules split across reads and multiple capsules per read.
  • Apply the agreed reconnect and address-retention behavior when a new HTTP/2 tunnel replaces a QUIC tunnel. Automatic product-client transport selection remains out of scope.

Acceptance criteria

  • With UDP/443 blocked, an independent client exchanges bidirectional 1280-byte IPv6 packets with real gVPC workloads over TCP/443.
  • Packet or negotiated-protocol evidence confirms HTTP/2 over TLS/TCP with no QUIC dependency.
  • The same authorization, spoofing, tenant-isolation and expiry cases pass on both transports.
  • Truncated, malformed and oversized capsule inputs are bounded; slow readers and cancellation cannot cause unbounded buffering or disrupt another connector.
  • A controlled QUIC-to-HTTP/2 reconnect follows the documented continuity contract and never permits dual ownership.

Additional integration acceptance

  • Route HTTP/2 through exactly the same foundation-backed authorization, packet backend and connector session generation as HTTP/3; fallback cannot acquire authority from a second store or bypass readiness.
  • Test replacement both within the same provider instance and across nodes while the old QUIC path remains usable. Shared instance epochs alone do not prevent duplicate sessions.
  • Apply CONNECT-IP server lacks overload limits and tenant isolation checks #991 pre-admission limits and authenticate before probes on TCP as well as UDP; HTTP/2 stream cancellation or slow readers must preserve other sessions and attachment users.

Scope follows #970: CONNECT-IP with HTTP/2 fallback; no public API development, plugin/portal integration, product-client development or production rollout.

Activity

  1. changed the title [-]MASQUE: implement HTTP/2 CONNECT-IP fallback over TLS[/-] [+]Implement HTTP/2 CONNECT-IP fallback over TLS[/+] on Oct 22, 2026
  2. added theissue type on Oct 22, 2026
  3. self-assigned this
    on Oct 28, 2026
  4. privateip commented on Oct 28, 2026

    @privateip
    CollaboratorAuthor

    Verdict: valid. No production HTTP/2 CONNECT-IP listener exists.

    Checked against main at d902d55:

    Dependency: the HTTP/3 service from #980 (cmd/masque-provider, service.Run, config/masque) is still an open stack. The HTTP/2 listener has to run inside that service, so the fix stacks on it.

  5. privateip commented on Oct 28, 2026

    @privateip
    CollaboratorAuthor

    Approved plan. Two PRs stacked on #980's stack (#1202 → #1203 → #1204), because the listener runs inside the service.Run that #1203 adds. Both PRs say "Related to #981". This issue stays open for the lab acceptance rows: real gVPC workloads with UDP/443 blocked, and replacement across nodes.

    Root cause. connectip.Server serves only HTTP/3. Its request path runs through connect-ip-go, which parses only HTTP/3 requests. The one HTTP/2 server is the #973 prototype in tests/connectipinterop.

    PR 1, internal/edge/connectip:

    • Shared request path. ServeHTTP becomes a transport parse followed by one shared path: admission, lease watch, backend, tunnel and proxySession. Both transports share the same Admission, the session, admission and per-credential caps, and packetPolicy. HTTP/2 cannot get a second admission path or forwarding owner.
    • Capsule codec. Parses incrementally, so a capsule may be split across reads or share a read with others. Oversized DATAGRAM capsules and unknown capsules are discarded without being buffered. Control capsules are capped in size. A malformed capsule ends only its own request.
    • Toward the client. The HTTP/2 tunnel checks and decrements the hop limit. A packet over 1500 bytes gets an ICMPv6 Packet Too Big with MTU 1280, which goes back through packetPolicy. Each capsule write has a 5 s deadline, so a client that stops reading ends only its own session.
    • Server.ServeTLS(net.Listener). HTTP/2 only, ALPN h2.
      • A listener wrapper applies MaxConnections and MaxConnectionsPerSource.
      • Limits sets the stream, header and handshake limits.
      • Authentication runs before any per-request work.
      • It refuses to start unless GODEBUG contains http2xconnect=1.

    PR 2, service:

    • --listen-tcp (default :8443). Startup fails if Extended CONNECT is off, and /readyz needs both listeners.
    • GODEBUG is set in the image and the Deployment. The Service gets TCP/443.
    • Docs are updated.
    • The interop harness's HTTP/2 path moves to the production server, and the prototype is deleted.

    Tests:

    • HTTP/2 round trip with 1280-byte packets.
    • HTTP/2 repeats the HTTP/3 authorization, spoofing, isolation and expiry cases.
    • Capsule input that is truncated, malformed, oversized, split or coalesced.
    • A client that stops reading, and RST_STREAM on one stream while another stream keeps forwarding.
    • The TCP limits.
    • QUIC to HTTP/2 replacement under a single-owner admission: the old session ends, and two backend sessions are never open at once.

    Risk. The stack merges after #980's stack. Extended CONNECT is process-wide, which only matters to masque-provider. The Service exposes mixed UDP and TCP ports.

  6. privateip commented on Oct 28, 2026

    @privateip
    CollaboratorAuthor

    The fix is open as two drafts on top of #980's stack, in stack #1205:

    1. feat(connectip): Serve CONNECT-IP over HTTP/2 with TLS #1206 adds the HTTP/2 listener to connectip.Server, sharing one request path with HTTP/3.
    2. feat(masque): Serve the HTTP/2 CONNECT-IP fallback from masque-provider #1207 runs it in masque-provider on TCP/443 with GODEBUG=http2xconnect=1, and moves the interop harness onto the production server.

    One change from the plan: the HTTP/2 tunnel clears its write deadline after each write. net/http resets an HTTP/2 stream whose write deadline passes even when no write is pending, so without this every session idle for 5 s would end. A regression test covers it.

    task test:connectip-interop passed all 33 tests against the pinned Xray client over both transports, including HTTP/2 with UDP/443 blocked. The acceptance rows that need real gVPC workloads and replacement across nodes are not run. This issue stays open for them.

  7. privateip commented on Oct 29, 2026

    @privateip
    CollaboratorAuthor

    Every acceptance criterion now has evidence, so I'm closing this. The HTTP/2 listener merged in #1206, and #1207 runs it in the service on TCP/443. #1228 adds an HTTP/2 transport to the lab probe, and #1240 adds the HTTP/2 rows to the containerlab lab suite. All of those rows passed.

    Criterion Evidence
    With UDP/443 blocked, 1280-byte packets both ways with real gVPC workloads over TCP/443 h2-lifecycle, with UDP to the service port dropped by an nft rule: two VPCs, connect and reconnect, 500/500 1280-byte replies from each workload, and 40/40 1280-byte pings from the workloads to the connector. The independent Xray client also passed with UDP/443 blocked in the interop suite (#1207)
    HTTP/2 over TLS/TCP with no QUIC dependency h2-lifecycle negotiated h2. No UDP datagrams to the service port reached the provider pod, and the block dropped none during the run. An HTTP/3 control attempt, by contrast, had 2 dropped
    Same authorization, spoofing, isolation and expiry cases on both transports h2-authz-isolation: an unknown credential got 401 and a credential for another VPC got 403. The server counted 500/500 spoofed packets as refused, and none was delivered. The expiring credential stopped 2.02 s before expiry, and the other connector answered 2701/2701. The HTTP/3 rows from #1238 cover the same cases
    Malformed, truncated and oversized capsules are bounded, and slow readers and cancellation stay isolated Unit tests cover capsule framing, split reads, malformed input that ends only its own request, a cancelled request that leaves its sibling running, and a slow reader that ends only its own request. A stalled HTTP/2 client ends after 5.5 s, within the 6.25 s bound (#1230)
    QUIC-to-HTTP/2 reconnect follows the continuity contract with no dual ownership h3-to-h2-same-pod: a 19 ms gap and no stale packets in the new request. The claim epoch did not change
    Same foundation-backed path as HTTP/3, with no second authority store and readiness not bypassed Both transports share one request path, the session limit, the adapter and the session generation (#1206)
    Replacement within one instance and across nodes while the old QUIC path still works The same-pod row as above. h3-to-h2-cross-node: the old HTTP/3 request was still answering when the HTTP/2 request reached the other node. The server closed it, and the new request got 503 until the authority released the old authorization. The two never forwarded together: the gap was 32.6 s, then 500/500
    #991 limits and authentication before probes on TCP Unit tests cover connection, per-source, handshake and session limits on HTTP/2, and that no probe runs before admission
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

Fields

Priority

None yet

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions