local-development-gateway 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8baffa29acc52adfcc3112cacc9be936b24528915f86fbc94c323df081e05fd5
4
- data.tar.gz: 4ab5156e3dc8dae143345b3c58271be2afd4b3485fb92c6bfd18153dd1958734
3
+ metadata.gz: 31c635514c35283b8ebab124d6a9f9ac8e17e8de5faaeb8573b0c73153b3efd4
4
+ data.tar.gz: 946d3a12335cff8a00f10a5ac330de37b5f5ec509a33779e44cefc265b8c0554
5
5
  SHA512:
6
- metadata.gz: 8ed0938cfb210bda2c320f6c481af5ddabf9f32d8f04fcac7ac94f60bbc9df8da98fe71f7929581d54ab5a20b648c339b92cd78f799b50fb74f3300e64997e0c
7
- data.tar.gz: b1acdaefc1c2b69b1e101fb5516ee8de7de866e9d9b1d11bd2390d14a860204a869ca8d90a10123a9b304b954fb03297c26b80482743105bee5a53f3bad06c22
6
+ metadata.gz: 04b07e9aeb612b723744759bebb4f8e19319ac885a9dff3163446eaebcaf9a424cb203c36c89649a2378d1db675c5a167aade5bc0f33e6c82ccb70c11d7bcec6
7
+ data.tar.gz: 28528704687762d7a053b62f7c1fc1e7f5af76700e4c332efadaef32cb5c594e420af8bbd53803a1e3d0cecdafa3d892ceeee5edbac100aa388ea72e98a80de1
data/README.md CHANGED
@@ -112,19 +112,102 @@ The gateway includes protocol drivers for encrypted SQL Server and PostgreSQL
112
112
  connections. No host agent, DNS server, administrator privileges, or
113
113
  `/etc/hosts` changes are required.
114
114
 
115
- A participating project only joins its existing database service to
116
- `local-gateway` and adds three labels:
115
+ ### Three connection paths
116
+
117
+ | Caller | Database hostname | Routing |
118
+ | --- | --- | --- |
119
+ | Application/API, background job, test container | `${COMPOSE_PROJECT_NAME}-<database-service>` | Qualified alias on that checkout's private `default` network; internal database port |
120
+ | Desktop client, such as DBeaver | `db.<worktree>.<project>.localhost` | Encrypted, hostname-labelled gateway endpoint on loopback |
121
+ | Database-local console, backup, clone command | `localhost` | Explicitly select the intended database container first |
122
+
123
+ Do not use bare `db` or `db_trav_com` for application connections in the current
124
+ gateway topology. An API attached to both `default` and `local-gateway` can see
125
+ other projects' database service aliases on the shared network. Docker DNS or
126
+ network ordering cannot reliably choose its own database. Separate database
127
+ volumes do not prevent connecting to the wrong server.
128
+
129
+ The qualified alias belongs **only on the private `default` network**. Compose
130
+ project names must be distinct for concurrent checkouts. `.localhost` desktop
131
+ names resolve to loopback, not to an internal database container; they are not
132
+ substitutes for private aliases. Database cloning is independent of this
133
+ hostname correction: it selects source/destination containers explicitly and
134
+ connects locally.
135
+
136
+ ### Canonical development and test Compose pattern
137
+
138
+ Merge these entries into the project's existing development base and gateway
139
+ overlay. Keep its database images, credentials, volumes, and application
140
+ commands. The base must not publish database host ports. Use the driver's
141
+ existing configuration keys: `AZURE_SQL_HOST` is a SQL Server application
142
+ example; `PGHOST` is the PostgreSQL/libpq equivalent. An application with a
143
+ different PostgreSQL configuration key must set that key instead.
144
+
145
+ For example, a wrapper-derived `COMPOSE_PROJECT_NAME=issue-123-example` gives
146
+ private hosts `issue-123-example-db` and `issue-123-example-db_trav_com`.
147
+ `GATEWAY_HOSTNAME=issue-123.example.localhost` separately identifies desktop
148
+ routes. This requires no new Ruby API, generic database environment variable,
149
+ or consumer runtime.
117
150
 
118
151
  ```yaml
152
+ # docker-compose.development.yml: also safe without the gateway overlay.
119
153
  services:
154
+ api:
155
+ environment:
156
+ AZURE_SQL_HOST: "${COMPOSE_PROJECT_NAME}-db"
157
+ PGHOST: "${COMPOSE_PROJECT_NAME}-db_trav_com"
158
+ jobs:
159
+ environment:
160
+ AZURE_SQL_HOST: "${COMPOSE_PROJECT_NAME}-db"
161
+ PGHOST: "${COMPOSE_PROJECT_NAME}-db_trav_com"
162
+ test_api:
163
+ environment:
164
+ AZURE_SQL_HOST: "${COMPOSE_PROJECT_NAME}-db"
165
+ PGHOST: "${COMPOSE_PROJECT_NAME}-db_trav_com"
120
166
  db:
167
+ networks:
168
+ default:
169
+ aliases:
170
+ - "${COMPOSE_PROJECT_NAME}-db"
171
+ db_trav_com:
172
+ networks:
173
+ default:
174
+ aliases:
175
+ - "${COMPOSE_PROJECT_NAME}-db_trav_com"
176
+ ```
177
+
178
+ The example's driver assignments are illustrative. In
179
+ [Travel Authorization's overlay](https://github.com/icefoganalytics/travel-authorization/blob/main/docker-compose.development.gateway.yml),
180
+ `db` is PostgreSQL and `db_trav_com` is SQL Server: its PostgreSQL host setting
181
+ must use `${COMPOSE_PROJECT_NAME}-db`, and its SQL Server host setting must use
182
+ `${COMPOSE_PROJECT_NAME}-db_trav_com`, with labels/ports matching those drivers.
183
+ Apply the rule separately to **every** database service, even when two services
184
+ use the same driver. Preserve each context's existing credentials, internal
185
+ port, and database name; tests must still select the test database rather than
186
+ development data.
187
+
188
+ ```yaml
189
+ # docker-compose.development.gateway.yml
190
+ services:
191
+ api:
121
192
  networks:
122
193
  - default
123
194
  - local-gateway
195
+ db:
196
+ networks:
197
+ default: {}
198
+ local-gateway: {}
124
199
  labels:
125
200
  - "local-gateway.tcp.driver=sql_server"
126
- - "local-gateway.tcp.hostname=db.${GATEWAY_HOSTNAME:-wrap.localhost}"
201
+ - "local-gateway.tcp.hostname=db.${GATEWAY_HOSTNAME}"
127
202
  - "local-gateway.tcp.port=1433"
203
+ db_trav_com:
204
+ networks:
205
+ default: {}
206
+ local-gateway: {}
207
+ labels:
208
+ - "local-gateway.tcp.driver=postgresql"
209
+ - "local-gateway.tcp.hostname=db.${GATEWAY_HOSTNAME}"
210
+ - "local-gateway.tcp.port=5432"
128
211
 
129
212
  networks:
130
213
  local-gateway:
@@ -132,22 +215,64 @@ networks:
132
215
  name: local-gateway
133
216
  ```
134
217
 
135
- For a worktree whose `GATEWAY_HOSTNAME` is `issue-567.wrap.localhost`, DBeaver
136
- connects to `db.issue-567.wrap.localhost` on the driver's standard port:
218
+ Use **mapping-form** database networks in both files. Compose merges
219
+ `default: {}` with the base's `default.aliases`; replacing that mapping with a
220
+ network list can lose the private alias. Do not add qualified aliases to
221
+ `local-gateway`. Jobs/tests need only `default` unless they themselves expose a
222
+ routed service; qualified database names remain safe if an existing overlay
223
+ also attaches them to the shared network.
224
+
225
+ The shared router owns `127.0.0.1:1433` and `127.0.0.1:5432`. No participating
226
+ database publishes either port, so multiple development/test stacks can start
227
+ concurrently. Internal connections use each server's internal port and existing
228
+ encryption policy; they do not pass through the router or require a published
229
+ port.
230
+
231
+ ### Explicit non-gateway fallback
232
+
233
+ Container-only development/tests work with the base alone. If desktop access
234
+ without the gateway is required, select a separate, project-local publishing
235
+ overlay **instead of** the gateway overlay:
236
+
237
+ ```yaml
238
+ # docker-compose.development.ports.yml: opt-in, non-gateway mode only.
239
+ services:
240
+ db:
241
+ ports:
242
+ - "127.0.0.1:1433:1433"
243
+ db_trav_com:
244
+ ports:
245
+ - "127.0.0.1:5432:5432"
246
+ ```
247
+
248
+ These fallback ports must be free; this mode does not promise parallel desktop
249
+ access. Do not combine it with the gateway or leave these publications in the
250
+ base/test files. When migrating a project that retains base publications, its
251
+ gateway overlay must explicitly clear each database's ports with
252
+ `ports: !reset []` (Docker Compose 2.24.4 or newer); simply omitting `ports` or
253
+ setting `ports: []` does not remove inherited publications. Prefer the
254
+ port-free base plus explicit fallback above.
255
+
256
+ Use the project's documented development wrapper to select Compose modes and
257
+ start/test/stop services. Inspect its rendered `config`: gateway mode must have
258
+ no database host publications and must retain the private aliases. A base-only
259
+ test wrapper must use the same qualified hosts.
260
+
261
+ ### Desktop database clients
262
+
263
+ For `GATEWAY_HOSTNAME=issue-123.example.localhost`, connect to
264
+ `db.issue-123.example.localhost` on the driver's standard port:
137
265
 
138
266
  | Driver label | Host port | Client requirement |
139
267
  | --- | ---: | --- |
140
268
  | `sql_server` | `1433` | Encryption enabled and **Trust server certificate** selected |
141
- | `postgresql` | `5432` | SSL mode `require` |
269
+ | `postgresql` | `5432` | SSL mode `require`, SNI enabled, development certificate accepted |
142
270
 
143
271
  Use the database container's actual internal port in
144
- `local-gateway.tcp.port`; it does not need to match the host listener.
145
-
146
- Remove the database's host `ports` publication when using the gateway; the shared
147
- router owns `127.0.0.1:1433` and `127.0.0.1:5432`. Keep the `default` network so
148
- application and test containers still connect directly to `db` on its internal
149
- port, without TLS or a host-published port. A project not using the gateway must
150
- select its own explicit port-publishing fallback.
272
+ `local-gateway.tcp.port`; it need not match the host listener. Different drivers
273
+ can share a hostname because their listeners differ. Two databases using the
274
+ same driver need distinct labels, for example `db.${GATEWAY_HOSTNAME}` and
275
+ `db_trav_com.${GATEWAY_HOSTNAME}`.
151
276
 
152
277
  All `.localhost` names resolve to loopback. The gateway publishes loopback-only
153
278
  listeners, discovers labelled containers through Docker, and uses the database
@@ -165,6 +290,29 @@ Closed sessions lose their cancellation mapping. See
165
290
  for the complete contract and security boundary, including the reason for
166
291
  retaining cancellation-aware PostgreSQL routing despite Traefik's STARTTLS support.
167
292
 
293
+ ### Downstream adoption checklist
294
+
295
+ 1. Give each concurrent checkout a distinct Compose project name and desktop
296
+ route hostname. Qualify every API/job/test database host with
297
+ `${COMPOSE_PROJECT_NAME}-<database-service>` using its existing driver setting.
298
+ 2. Advertise each qualified alias only on its private `default` network;
299
+ preserve it through mapping-form network merges in base-only and gateway modes.
300
+ 3. Remove fixed database publications from development/test stacks; retain
301
+ desktop fallback publications only in an explicitly selected non-gateway file.
302
+ 4. Recreate database and consumer services so changed aliases and environment
303
+ settings take effect. Inspect both rendered Compose configurations.
304
+ 5. Query real, read-only server/database identity from API, job, and test
305
+ contexts and compare it with the explicitly selected local database
306
+ container. Stop that checkout's database while another is available: its
307
+ consumers must fail, not select the other checkout. Record the actual DNS or
308
+ connection error; there is no universal platform error code.
309
+ 6. Verify both encrypted desktop routes, and restore normal gateway attachments
310
+ after test wrappers recreate services using base-only Compose.
311
+
312
+ Shared development networks are not a hostile-container security boundary.
313
+ Qualified private aliases prevent accidental cross-stack selection; they do not
314
+ prevent an attached, malicious container from connecting to another database.
315
+
168
316
  ## Development checks
169
317
 
170
318
  Install the locked development tools with `bundle install`. Run the Ruby
@@ -206,8 +354,8 @@ The gateway owns the single browser port on `127.0.0.1:80`. Participating projec
206
354
 
207
355
  The gateway:
208
356
 
209
- - listens only on `127.0.0.1:80`;
210
- - creates the external Docker network `local-gateway`;
357
+ - listens only on `127.0.0.1:80`, `127.0.0.1:1433`, and `127.0.0.1:5432`;
358
+ - creates the external Docker network `local-gateway` for HTTP and labelled database ingress;
211
359
  - discovers only services explicitly labelled for routing;
212
360
  - has the Docker label `local-gateway=true` so a project can detect it.
213
361
 
@@ -236,6 +384,14 @@ Use a project- and service-prefixed router/service identifier. Never derive a de
236
384
 
237
385
  Without the gateway, a participating project may use its own loopback port-publishing fallback. That fallback is project-specific and is not part of the shared gateway contract.
238
386
 
387
+ Application/job/test database connections follow the
388
+ [private-alias contract](#three-connection-paths), not desktop gateway hostnames
389
+ or bare database service names. Database services retain their private network
390
+ and add gateway labels without publishing host ports. The
391
+ [architecture decision](docs/architecture/local-tcp-routing.md#internal-database-isolation-decision)
392
+ explains why a future HTTP/database ingress split requires coordinated consumer
393
+ migration rather than a transparent gem update.
394
+
239
395
  ## Authentication callbacks
240
396
 
241
397
  A wildcard hostname lets an identity provider allow one local callback pattern instead of every generated port. For Traditional Knowledge, configure:
@@ -263,6 +419,19 @@ It mounts the Docker socket so it can discover labelled containers. A read-only
263
419
  | A project uses normal ports unexpectedly | Confirm the gateway container is running and has `local-gateway=true`. |
264
420
  | Authentication rejects the callback | Confirm the generated hostname matches the allowed wildcard callback pattern. |
265
421
 
422
+ ## Release 0.2.2
423
+
424
+ - Canonical SQL Server/PostgreSQL Compose integration supports concurrent
425
+ development/test stacks without per-stack database host publications.
426
+ - Application, job, and test connections use checkout-qualified private aliases
427
+ instead of ambiguous shared-network service names. Base-only mode, explicit
428
+ non-gateway fallback, multiple databases, and adoption checks are documented.
429
+ - The packaged architecture reference records the current private-alias
430
+ decision, real-container Linux validation, and coordinated migration required
431
+ for a future HTTP/database ingress split.
432
+ - Gateway routing and the public Ruby API are unchanged. Consumers must adopt
433
+ the Compose contract; upgrading the gem does not rewrite their configuration.
434
+
266
435
  ## Release 0.2.1
267
436
 
268
437
  - PostgreSQL query cancellation works across concurrent worktrees, including
@@ -0,0 +1,356 @@
1
+ # Local TCP Routing Architecture
2
+
3
+ Status: SQL Server and PostgreSQL protocol routing implemented in PR #22 and
4
+ validated on Linux. Windows Docker Desktop validation remains outstanding.
5
+
6
+ ## Request
7
+
8
+ Extend Local Development Gateway so a participating Docker Compose project can
9
+ expose hostname-routed database services with the same simple declarative
10
+ integration it already uses for HTTP services.
11
+
12
+ For WRAP, database routing should require only a small change to
13
+ `docker-compose.development.gateway.yml`. WRAP must not implement address
14
+ allocation, routing, DNS, host-file management, startup orchestration, or
15
+ cleanup.
16
+
17
+ The initial endpoints are:
18
+
19
+ ```text
20
+ db.<worktree>.wrap.localhost:1433 # SQL Server
21
+ db.<worktree>.wrap.localhost:5432 # PostgreSQL
22
+ ```
23
+
24
+ Each must connect from DBeaver to that worktree's labelled database container.
25
+
26
+ ## Consumer Contract
27
+
28
+ A participating project keeps its databases on its private `default` network
29
+ and adds the existing `local-gateway` network plus three gateway-owned labels:
30
+ driver, desktop hostname, and internal port. The copyable SQL Server and
31
+ PostgreSQL base/overlay examples live in the README's
32
+ [canonical Compose pattern](../../README.md#canonical-development-and-test-compose-pattern).
33
+
34
+ | Connection path | Destination selection |
35
+ | --- | --- |
36
+ | API, background job, test | `${COMPOSE_PROJECT_NAME}-<database-service>` on private Docker DNS, using the driver's existing host setting and internal port |
37
+ | Desktop database client | Encrypted `db.<worktree>.<project>.localhost` endpoint; driver/hostname labels select the backend |
38
+ | Database-local console, backup, clone | Explicitly selected container, then `localhost`; not gateway routing |
39
+
40
+ Each concurrent checkout has a distinct Compose project name. Each database
41
+ advertises its qualified alias only on that project's `default` network.
42
+ Use mapping-form database networks in the base and gateway overlay so Compose
43
+ preserves `default.aliases`. Configure all application/job/test contexts with
44
+ their appropriate aliases, existing credentials, and intended database names;
45
+ test contexts still select their own test database.
46
+
47
+ Gateway-mode development and test stacks publish no database host ports.
48
+ Container consumers work with the base alone. Desktop access without the
49
+ gateway is an explicit project-local port-publishing overlay, not an inherited
50
+ base default and not a parallel-stack guarantee. The README covers migration
51
+ of inherited port publications, fallback selection, and adoption checks.
52
+
53
+ Internal database hostnames and desktop `.localhost` hostnames are separate
54
+ contracts. Internal connections need neither a published host port nor the
55
+ gateway's encrypted-handshake protocol. Database cloning does not depend on
56
+ the internal hostname correction.
57
+
58
+ Requirements:
59
+
60
+ - No consumer Ruby API calls.
61
+ - No consumer-side address allocator.
62
+ - No per-worktree host-port selection.
63
+ - No bind-address environment variable passed from application code.
64
+ - No consumer startup or shutdown hook solely for gateway routing.
65
+ - No consumer cleanup call.
66
+ - No manually maintained DBeaver port per worktree.
67
+ - The consumer supplies only declarative service identity and internal-port metadata.
68
+
69
+ - No administrator or root installation.
70
+ - No host `/etc/hosts`, DNS, resolver, systemd, or Windows-service changes.
71
+ - The same gateway Compose stack must run through Docker on Linux and Windows.
72
+
73
+ ## Internal Database Isolation Decision
74
+
75
+ **Decision:** retain the current shared ingress topology and standardize
76
+ checkout-qualified private aliases. This resolves
77
+ [#23](https://github.com/icefoganalytics/local-development-gateway/issues/23)'s
78
+ parallel-stack contract and
79
+ [#26](https://github.com/icefoganalytics/local-development-gateway/issues/26)'s
80
+ internal database selection contract without new runtime machinery or public
81
+ Ruby lifecycle changes.
82
+
83
+ Bare `db` is not safe for an API attached to both its private bridge and
84
+ `local-gateway`: other attached databases advertise the same Compose service
85
+ alias on shared ingress. Docker DNS/network ordering, container IDs, separate
86
+ volumes, and gateway TLS/SNI cannot disambiguate an ordinary internal connection.
87
+ The qualified alias identifies only its own private endpoint. If that endpoint
88
+ is absent, its consumers must fail rather than connect to another checkout.
89
+ Do not advertise the qualified alias on shared ingress.
90
+
91
+ An HTTP/database ingress split is a possible future centralization, not a
92
+ transparent gem-only patch:
93
+
94
+ - HTTP services/APIs: private checkout network plus existing HTTP ingress.
95
+ - Databases: private checkout network plus a distinct gateway-owned database
96
+ ingress network. APIs/jobs/tests must not join database ingress.
97
+ - Traefik: HTTP ingress only. Database router: database ingress only.
98
+ - Route discovery must read backend addresses on database ingress instead of
99
+ `DockerRoutes::NETWORK_NAME`'s current `local-gateway`.
100
+ - Gateway readiness/discovery must check the appropriate networks and services.
101
+ Unused-gateway shutdown must retain running consumers on either ingress,
102
+ including database-only stacks, without counting exited containers.
103
+ - Consumer network membership, installed gem versions, and already-running
104
+ shared gateways require coordinated migration. Old consumers still
105
+ advertising database aliases on HTTP ingress remain ambiguous after a
106
+ gateway-only update.
107
+
108
+ That alternative requires packaged install/readiness, database-only lifecycle,
109
+ multi-project startup/stop/replacement, SQL Server routing, PostgreSQL routing
110
+ and cancellation, mixed-version migration, and unchanged HTTP-routing evidence
111
+ before adoption. No structural split is implemented in this release; downstream
112
+ qualified-alias mitigations must remain until a verified replacement is adopted.
113
+
114
+ These development networks are not hostile-container security boundaries.
115
+ Private qualified aliases prevent accidental selection, not deliberate
116
+ connections by another attached container.
117
+
118
+ ## Gateway Responsibilities
119
+
120
+ All implementation belongs in this repository. The gateway must:
121
+
122
+ 1. Discover participating containers and their routing metadata through Docker.
123
+ 2. Derive or read the requested hostname and target internal port.
124
+ 3. Distinguish concurrent worktrees even when they expose the same internal TCP port.
125
+ 4. Make `db.<worktree>.wrap.localhost` resolve or connect to the correct worktree route on the local machine.
126
+ 5. Forward the raw TCP stream to the labelled container.
127
+ 6. Add, update, and remove routes automatically as containers start, restart, and stop.
128
+ 7. Preserve stable routing for a worktree while it remains active.
129
+ 8. Keep all listeners and generated routes local to the developer machine.
130
+ 9. Preserve the existing HTTP routing behavior.
131
+
132
+ ## Implemented Routing Shape
133
+
134
+ Every `.localhost` name intentionally resolves to loopback on both Linux and
135
+ Windows. A shared port therefore requires a protocol-level hostname. The
136
+ gateway uses one Docker-label discovery path and a small driver for each
137
+ supported database handshake:
138
+
139
+ ```text
140
+ DBeaver
141
+ -> db.<worktree>.wrap.localhost:<driver port>
142
+ -> Docker publishes the database router on loopback
143
+ -> driver reads the encrypted handshake hostname
144
+ -> router selects the matching Docker label
145
+ -> router forwards the database stream to the labelled container
146
+ ```
147
+
148
+ The `sql_server` driver mediates Tabular Data Stream (TDS) PRELOGIN, reads SNI
149
+ from the wrapped TLS ClientHello, then preserves the encrypted client/backend
150
+ stream. The
151
+ `postgresql` driver accepts PostgreSQL's SSLRequest, terminates the client TLS
152
+ session to obtain SNI, and forwards the plaintext PostgreSQL stream on the
153
+ private Docker network.
154
+
155
+ Traefik remains responsible for HTTP. It cannot inspect TLS SNI wrapped inside
156
+ TDS PRELOGIN. It **does** support PostgreSQL SSLRequest/STARTTLS and subsequent
157
+ TLS-SNI routing, as documented in its
158
+ [TCP TLS reference](https://doc.traefik.io/traefik/reference/routing-configuration/tcp/tls/).
159
+
160
+ STARTTLS alone is therefore not a reason to implement PostgreSQL routing here.
161
+ The gateway also owns PostgreSQL query cancellation: `psql` and JDBC can open
162
+ a separate plaintext CancelRequest connection containing only a backend PID and
163
+ secret, with no hostname. A stateless SNI router cannot select that backend.
164
+ Keeping negotiation and cancellation-aware session forwarding together preserves
165
+ the existing three-label consumer contract without per-route internal listeners,
166
+ generated Traefik configuration, or another discovery/control plane.
167
+
168
+ Normal database sessions require encrypted handshakes so the requested hostname
169
+ is present as TLS SNI. PostgreSQL cancellation is a bounded control-message
170
+ exception, not support for plaintext database sessions. The router parses
171
+ PostgreSQL backend messages only until BackendKeyData, replaces that key, then
172
+ forwards the remainder without interpreting query results.
173
+
174
+ ## Code Organization
175
+
176
+ `DatabaseRouter` owns listener concurrency and backend connection lifecycle.
177
+ `Wire` owns bounded handshake I/O and bidirectional forwarding, including
178
+ shutdown that wakes both relay directions. `DockerApi` and `DockerRoutes` form
179
+ the external Docker boundary and produce validated `Route` values.
180
+ Behavior-bearing classes each have one file, and directories map to Ruby
181
+ namespaces: drivers live under `DatabaseRouter::Drivers`, TDS framing and TLS
182
+ parsing under `DatabaseRouter::Tds`, and PostgreSQL session-key forwarding and
183
+ cancellation lookup under `DatabaseRouter::PostgreSql`. Tiny immutable `Route`,
184
+ `Connection`, cancellation destination, and TDS `Packet` records stay with
185
+ their owners.
186
+
187
+ Source and test imports resolve from the gem's `lib` load path and start at
188
+ `local_development_gateway`; domain files never traverse sibling paths with
189
+ `require_relative`.
190
+
191
+ The focused tests mirror those boundaries under `test/database_router/`.
192
+ Protocol scenarios keep their literal setup beside the behavior they verify
193
+ instead of sharing a generic fixture layer.
194
+
195
+ ## Hostname Contract
196
+
197
+ - Keep the established WRAP hostname family.
198
+ - HTTP remains:
199
+ - `<worktree>.wrap.localhost`
200
+ - `api.<worktree>.wrap.localhost`
201
+ - `mail.<worktree>.wrap.localhost`
202
+ - SQL Server is:
203
+ - `db.<worktree>.wrap.localhost`
204
+ - PostgreSQL uses the same hostname on port `5432`.
205
+ - Do not migrate consumers to `.test`, `.alt`, `.local`, `home.arpa`, or an externally registered domain as part of this issue.
206
+ - Do not require host resolver configuration; `.localhost` must retain its native loopback behavior.
207
+
208
+ ## Security Boundary
209
+
210
+ - No routed service may be reachable from another machine.
211
+ - Bind host listeners only to loopback addresses.
212
+ - Do not publish generated names through public DNS.
213
+ - Do not expose SQL Server on `0.0.0.0` or a LAN interface.
214
+ - Do not require privileged host integration.
215
+ - Do not grant a long-running container unrestricted write access to arbitrary host files.
216
+ - The database router receives the same read-only Docker socket already required by Traefik so it can resolve labels to current container addresses.
217
+ - SQL Server remains encrypted end to end. PostgreSQL is plaintext only inside the private Docker network after the gateway terminates client TLS.
218
+
219
+ ## Lifecycle
220
+
221
+ - Normal session selection reads current Docker metadata; starting, restarting,
222
+ or stopping a labelled container requires no stored route configuration or
223
+ consumer cleanup.
224
+ - Containers outside `local-gateway` are excluded before label validation.
225
+ Invalid participating routes are warned about and quarantined individually.
226
+ - Duplicate active driver/hostname identities reject every route for that
227
+ identity, not unrelated identities or database drivers.
228
+ - A connection snapshot never changes backend after selection.
229
+ - PostgreSQL rewrites BackendKeyData with a random, collision-checked virtual
230
+ identity whose PID is positive for older client compatibility. The active
231
+ mapping retains the exact backend address, port, and original cancellation key.
232
+ - Cancellation uses that mapping without Docker discovery or hostname
233
+ negotiation. Unknown and closed-session identities fail closed, and malformed
234
+ cancellation lengths never fall through to normal backend forwarding.
235
+ - Backend EOF, client disconnect, or relay failure removes the session mapping.
236
+ Gateway restart closes sessions and clears their cancellation identities;
237
+ normal route configuration is reconstructed from Docker metadata.
238
+ - Handshakes and active sessions have separate bounded admission pools.
239
+ Cancellation and closed health probes do not consume an active-session slot,
240
+ so cancellation remains available when the normal session limit is reached.
241
+ - SQL Server provisional attempts have a bounded sub-deadline within the shared
242
+ handshake deadline. Connection establishment uses only the remaining budget;
243
+ stalled provisional sockets are closed before a healthy alternative is tried.
244
+
245
+ ## Compatibility
246
+
247
+ - DBeaver connects to `db.<worktree>.wrap.localhost` on `1433` for SQL Server
248
+ or `5432` for PostgreSQL.
249
+ - SQL Server clients must enable encryption and trust the development server
250
+ certificate.
251
+ - PostgreSQL clients must use SSL mode `require`, send SNI, and permit the
252
+ gateway's generated development certificate.
253
+ - Concurrent SQL Server routes must use compatible PRELOGIN encryption
254
+ settings because the client receives a provisional backend's negotiation
255
+ response before SNI identifies the final backend.
256
+ - PostgreSQL backends must accept a plaintext connection from the private
257
+ `local-gateway` Docker network.
258
+ - PostgreSQL cancellation PIDs reported by client APIs are virtual. Use
259
+ `SELECT pg_backend_pid()` when inspecting the physical backend in server views.
260
+ - Existing browser routes through Traefik on `127.0.0.1:80` remain unchanged.
261
+ - Multiple active worktrees can use each database driver's standard port
262
+ simultaneously.
263
+ - Adding another protocol requires another explicit driver; ports are never
264
+ inferred from labels.
265
+
266
+ ## Acceptance Scenarios
267
+
268
+ 1. Start worktree A and worktree B with the same database driver and internal
269
+ port.
270
+ 2. Connect a DBeaver-equivalent client to
271
+ `db.<A>.wrap.localhost:<driver port>`; observe bytes reaching only A.
272
+ 3. Connect to `db.<B>.wrap.localhost:<driver port>`; observe bytes reaching
273
+ only B.
274
+ 4. Repeat the routing check for both `sql_server` and `postgresql`.
275
+ 5. Keep both connections possible concurrently without alternate host ports.
276
+ 6. Recreate A's database container; the same hostname routes to its replacement.
277
+ 7. Stop A; A's route disappears while B remains available.
278
+ 8. Verify existing HTTP hostnames still use Traefik.
279
+ 9. Verify every host listener is loopback-only.
280
+ 10. Demonstrate consumer integration as a Compose-only diff.
281
+
282
+ ## Non-Goals
283
+
284
+ - Changing consumer application code, adding new development wrapper runtimes,
285
+ or automatically mutating consumer networks. Existing Compose connection
286
+ settings and file selection must adopt the documented contract.
287
+ - Requiring consumers to run DNS, a proxy, or host installation.
288
+ - Assigning a different database port to each worktree.
289
+ - Publishing development routes outside the machine.
290
+ - Replacing existing HTTP routing.
291
+ - Building a generic service mesh or full database proxy.
292
+ - Supporting plaintext database sessions, PostgreSQL direct TLS negotiation, or
293
+ database protocols without an implemented driver. Plaintext PostgreSQL
294
+ cancellation control packets are supported.
295
+
296
+ ## Verification Evidence
297
+
298
+ PR #22 demonstrated:
299
+
300
+ 1. Two exact `db.<worktree>.wrap.localhost` names selected different labelled
301
+ containers for SQL Server and PostgreSQL.
302
+ 2. Docker-label discovery updated routes without consumer lifecycle code;
303
+ stopped routes were rejected while surviving routes remained reachable.
304
+ 3. Fragmented and oversized handshakes, stalled clients, duplicate routes,
305
+ unsupported drivers, and unreachable provisional backends failed safely.
306
+ 4. Linux validation required no host resolver, administrator/root, or
307
+ operating-system service setup. Windows Docker Desktop was unavailable for
308
+ equivalent validation.
309
+ 5. Existing HTTP routes remained unchanged and every published listener was
310
+ loopback-only.
311
+
312
+ Issue #24's corrective release additionally demonstrated:
313
+
314
+ 1. PostgreSQL 14.4 `psql` cancellation stopped A's query while B stayed active.
315
+ 2. DBeaver's Java runtime with PostgreSQL JDBC 42.7.13 and 42.2.27 returned
316
+ SQLSTATE `57014` for A while B completed its query normally.
317
+ 3. Two SQL Server 2022 backends remained reachable with encrypted `sqlcmd`
318
+ connections despite a first labelled backend that accepted TCP but never
319
+ answered PRELOGIN.
320
+ 4. Focused regressions cover invalid-route isolation, cancellation identity
321
+ collisions after PID normalization, variable backend keys, session cleanup,
322
+ malformed cancellation packets, and cancellation at full session capacity.
323
+
324
+ Issues #23 and #26's documentation release demonstrated on Linux with Docker
325
+ Compose 5.6.0:
326
+
327
+ 1. Two isolated Compose projects, each with SQL Server 2022 `db` and PostgreSQL
328
+ 17 `db_trav_com`, started concurrently without database host publications.
329
+ The repository's `bin/dev` Compose passthrough selected the temporary
330
+ consumer fixtures; the existing shared gateway was reused.
331
+ 2. Real SQL/PG client containers representing API, job, and test contexts joined
332
+ both private and shared networks. `getent hosts` selected only their private
333
+ qualified aliases. Read-only SQL Server `@@SERVERNAME` / `DB_NAME()` and
334
+ PostgreSQL `cluster_name` / `current_database()` / `inet_server_addr()`
335
+ identified their own servers. Test contexts selected `qa_test`; explicit
336
+ database-container `localhost` queries matched the consumer identities.
337
+ 3. Stopping A's two databases made every A context fail while B remained
338
+ usable. `getent hosts` returned exit 1; SQL Server reported login timeout,
339
+ TCP Provider `0x2AFA`, and server-not-found/not-accessible; PostgreSQL reported
340
+ `could not translate host name ... to address: Try again`. These are observed
341
+ platform errors, not required portable error codes.
342
+ 4. Both projects were recreated using base-only Compose. All consumer identity
343
+ checks passed with private-only attachments. Recreating with the gateway
344
+ overlay restored shared attachments and preserved private aliases and test
345
+ database selection.
346
+ 5. Desktop-equivalent host-network clients selected both labelled worktrees:
347
+ `sqlcmd` 18.6 with `-Nm -C` (mandatory encryption, trusted development
348
+ certificate), and PostgreSQL 17 `psql` with `sslmode=require`, SNI, hostname,
349
+ and explicit loopback `hostaddr`. SQL Server strict/direct TLS is outside the
350
+ supported TDS PRELOGIN contract.
351
+ 6. Docker inspection confirmed the shared gateway publishes only
352
+ `127.0.0.1:80`, `127.0.0.1:1433`, and `127.0.0.1:5432`.
353
+
354
+ This validates real database client contexts, not an automatic downstream
355
+ application migration. Consumers still need to adopt the README checklist.
356
+ Windows Docker Desktop validation remains outstanding.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LocalDevelopmentGateway
4
- VERSION = "0.2.1"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: local-development-gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marlen Brunner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-08 00:00:00.000000000 Z
11
+ date: 2026-10-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Ruby API and CLI for starting, monitoring, and conditionally stopping
14
14
  the shared Docker gateway.
@@ -23,6 +23,7 @@ files:
23
23
  - bin/local-development-gateway
24
24
  - config/traefik.yml
25
25
  - docker-compose.yml
26
+ - docs/architecture/local-tcp-routing.md
26
27
  - lib/local_development_gateway.rb
27
28
  - lib/local_development_gateway/database_router.rb
28
29
  - lib/local_development_gateway/database_router/docker_api.rb