feat(resources): add TCP and UDP resources #4

Merged
ryan.mcgee merged 21 commits from feat/tcp-udp into main 2026-10-15 18:06:44 +00:00
Owner

Summary

  • Resources can carry raw TCP or UDP traffic (databases, DNS, game servers) in addition to HTTP, on a per-protocol listen port, rendered as Traefik tcp/udp routers and managed in the UI.
  • Stacked on #3 (feat/resource-access); retarget to main once #2 and #3 are merged.

Changes

  • Migration adds protocol and listen_port, makes domain nullable, with a partial unique index on HTTP domains and a unique (organization, protocol, port) index. SQLite rebuilds the table (foreign keys off on a pinned connection, foreign_key_check); PostgreSQL uses ALTER. down refuses while TCP/UDP resources exist.
  • Resource gets an immutable protocol and per-protocol validation; access settings, rules and share links are HTTP-only ({:error, :not_http}).
  • DynamicConfig renders tcp and udp sections on entrypoints tcp-<port> / udp-<port>. The lowest id wins a contested port; invalid ports/targets are dropped; a raw resource that carries access settings is not served (fail-closed).
  • UI: protocol select on create, port and optional hostname fields, protocol badge, plain host:port targets, Access page and button HTTP-only, and a node page snippet listing the entrypoints that node's Traefik must declare (live).
  • Fixes a latent crash when a blank name was submitted for a node, resource or target (String.trim/1 on nil), with regression tests.
  • Dev stack: whoamitcp/whoamiudp, extra entrypoints and published ports, idempotent seeds. README documents the entrypoint convention and limits.

Verification

  • 493 tests pass on SQLite and on PostgreSQL; mix format --check-formatted, mix compile --warnings-as-errors, mix credo --strict clean; mix dialyzer 0 errors on both backends.
  • Migration up/down/up on populated SQLite and PostgreSQL databases: row counts, integrity, cascades and the down guard checked.
  • Compose stack against the real Traefik: the migration applied to an existing populated dev database (HTTP data intact); Traefik lists the tcp and udp routers and services; TCP and UDP echo through Traefik; creating, disabling and deleting a TCP resource in the browser adds and removes the router and the node snippet entry; an undeclared entrypoint disables only that router (others keep serving).
  • Observed: a TCP client that half-closes right after writing got no reply through Traefik (works with the connection held open); documented in the README.

Not included

  • SNI routing of several TCP services on one port, TCP TLS passthrough, IP allow-lists for raw resources.
## Summary - Resources can carry raw TCP or UDP traffic (databases, DNS, game servers) in addition to HTTP, on a per-protocol listen port, rendered as Traefik tcp/udp routers and managed in the UI. - Stacked on #3 (`feat/resource-access`); retarget to `main` once #2 and #3 are merged. ## Changes - Migration adds `protocol` and `listen_port`, makes `domain` nullable, with a partial unique index on HTTP domains and a unique `(organization, protocol, port)` index. SQLite rebuilds the table (foreign keys off on a pinned connection, `foreign_key_check`); PostgreSQL uses `ALTER`. `down` refuses while TCP/UDP resources exist. - `Resource` gets an immutable protocol and per-protocol validation; access settings, rules and share links are HTTP-only (`{:error, :not_http}`). - `DynamicConfig` renders `tcp` and `udp` sections on entrypoints `tcp-<port>` / `udp-<port>`. The lowest id wins a contested port; invalid ports/targets are dropped; a raw resource that carries access settings is not served (fail-closed). - UI: protocol select on create, port and optional hostname fields, protocol badge, plain `host:port` targets, Access page and button HTTP-only, and a node page snippet listing the entrypoints that node's Traefik must declare (live). - Fixes a latent crash when a blank name was submitted for a node, resource or target (`String.trim/1` on nil), with regression tests. - Dev stack: whoamitcp/whoamiudp, extra entrypoints and published ports, idempotent seeds. README documents the entrypoint convention and limits. ## Verification - 493 tests pass on SQLite and on PostgreSQL; `mix format --check-formatted`, `mix compile --warnings-as-errors`, `mix credo --strict` clean; `mix dialyzer` 0 errors on both backends. - Migration up/down/up on populated SQLite and PostgreSQL databases: row counts, integrity, cascades and the `down` guard checked. - Compose stack against the real Traefik: the migration applied to an existing populated dev database (HTTP data intact); Traefik lists the tcp and udp routers and services; TCP and UDP echo through Traefik; creating, disabling and deleting a TCP resource in the browser adds and removes the router and the node snippet entry; an undeclared entrypoint disables only that router (others keep serving). - Observed: a TCP client that half-closes right after writing got no reply through Traefik (works with the connection held open); documented in the README. ## Not included - SNI routing of several TCP services on one port, TCP TLS passthrough, IP allow-lists for raw resources.
- resources gain a protocol (http, tcp, udp, fixed at creation) and a
  listen_port; TCP/UDP resources need a port, have an optional
  informational domain, and never use TLS
- ports are unique per organization and protocol; domains are unique
  for HTTP resources only, so an HTTP app and a TCP service can share
  a hostname
- access control stays HTTP-only: settings, rules and share links
  return :not_http for raw resources, and they have no login surface
  or auth endpoint
- the migration rebuilds the table on SQLite (which cannot drop NOT
  NULL) with foreign keys verified off first, so dependent rows are
  never cascade-deleted; up, down and up again were checked against
  populated databases on SQLite and PostgreSQL, and rolling back is
  refused while TCP/UDP resources exist
- fix: blanking a node, resource or target field on edit no longer
  crashes the changeset (update_change runs with nil)
- TCP resources become catch-all TCP routers (HostSNI(`*`)) and UDP
  resources rule-less UDP routers, each on the tcp-<port> / udp-<port>
  entrypoint, with the targets as load-balanced host:port servers
  (IPv6 bracketed, scheme ignored)
- one shared function decides which raw resources are servable, and
  both the rendered config and the new required_entrypoints/1 use it,
  so the list of entrypoints a node must declare matches what is served
- a contested port keeps the lowest resource id on that node; unusable
  ports and targets are dropped; a raw resource that somehow carries
  access settings is not served rather than served unprotected
- sections appear only when non-empty; HTTP rendering is unchanged
- the tenancy suite puts both tenants on the same raw port
- the new-resource form picks HTTP, TCP or UDP and shows the matching
  fields (domain and TLS, or listen port and an optional hostname);
  the type is shown but fixed when editing
- lists and detail pages label raw resources by port with a protocol
  badge; raw targets are plain host:port addresses with no scheme
- the Access page and button are HTTP-only
- the node page lists the tcp-<port> / udp-<port> entrypoints its
  Traefik must declare, from the same logic that decides what is
  rendered, and follows resource changes live
- compose gets whoamitcp and whoamiudp, and publishes the two extra
  Traefik ports on loopback
- the dev Traefik declares the tcp-9000 and udp-9001 entrypoints
- the dev seed creates a TCP resource (9000) and a UDP resource (9001)
  routed to them, idempotently
- README: TCP/UDP resources (entrypoint convention, port uniqueness,
  catch-all routing, no access control) and the dev commands
- hold the client connection open while waiting for the reply;
  closing the sending side immediately got no reply through Traefik
- note this in the README
ryan.mcgee changed target branch from feat/resource-access to main 2026-10-15 18:06:17 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
White_Ghost_Cyber_Systems/traefik-control!4
No description provided.