carlos / platform Public

edge: tunnel-backed route dispatch + hub wiring on the existing 443 listener

Routes with Tunnel set now proxy over their named reverse channel, and the
hub answers its reserved hostname on the same 443 listener the fleet already
has. tunnelProxy carries the identical trust discipline as tcpProxy
(SetXForwarded + scrubInboundPurgeHeader inbound, purgeModifier outbound):
a tunnel-backed host is exactly as untrusted as an addr-backed one.

Three decisions the brief left open, taken here:

- The tunnel case sits FIRST in Reload's switch, not after addr. `carlos
  route -tunnel` is a narrow UPDATE that does not clear socket/addr, so a
  route can carry both and the binding the operator just typed is the one
  they meant. It is guarded on e.tunnels so an edge with no hub keeps
  serving such a route from its old backend rather than dropping the host.
- A tunnel-served host is never activator-managed: the process lives on the
  far side of the channel, so a wake here would start a second local copy —
  and leaving it out of the keep-set retires the child a repoint left behind.
- Channel names from CARLOS_TUNNEL_CLIENTS are validated against
  registry.ValidTunnelName at boot: a name the CLI would refuse can never
  match a route, so such a client would connect and silently carry nothing.

registry.Fingerprint now sums the tunnel column too — without it the edge's
2s watch poll was blind to `carlos route -tunnel` and would never reload.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Paul Campbell 3850c83e035e9a11f76a056ad9817bae2d4aa89c parent f74a6a8
4 files changed, +349 −2
  • internal/edge/edge.go +150 −1
  • internal/edge/tunnel_test.go +176 −0
  • internal/registry/registry.go +10 −1
  • internal/registry/registry_test.go +13 −0
diff --git a/internal/edge/edge.go b/internal/edge/edge.go
index 1dc161356..d52f23491 100644
--- a/internal/edge/edge.go
+++ b/internal/edge/edge.go
@@ -23,6 +23,7 @@ import (
"flag"
"fmt"
"log"
+ "maps"
"net"
"net/http"
"net/http/httputil"
@@ -99,6 +100,13 @@ type Edge struct {
staticPub ed25519.PublicKey
staticCacheDir string
staticRecheck time.Duration
+ // tunnels, when non-nil, is the reverse-channel hub (tunnel.go): remote
+ // hosts register on its reserved hostname and routes with Tunnel set are
+ // proxied over the channel they name. nil = tunnel serving not configured
+ // on this box: a route's Tunnel binding is then ignored and it falls back
+ // to its socket/addr, or goes unserved if it has neither — the same
+ // nil-means-disabled convention as act/cache/staticStore.
+ tunnels *TunnelHub
// cache, when non-nil, answers cacheable GET/HEAD requests without ever
// invoking the hibernation activator or the backend — the mitigation
// this whole feature exists for. nil disables caching entirely.
@@ -158,6 +166,14 @@ func (e *Edge) EnableCache(cache *Cache) {
e.cache = cache
}
+// EnableTunnels wires the reverse-channel hub in: the edge answers hub.Host()
+// itself (registration) and routes with Tunnel set proxy over their named
+// channel. Call before Reload, same contract as EnableStatic — without a
+// Reload afterwards, tunnel routes stay skipped from the last table build.
+func (e *Edge) EnableTunnels(hub *TunnelHub) {
+ e.tunnels = hub
+}
+
// SetStaticRecheck tunes how long a static route trusts a resolved channel
// pointer before re-reading it (default 60s; 0 = every request). Call after
// EnableStatic and before Reload; handlers built on the next reload use it.
@@ -212,6 +228,9 @@ func (e *Edge) Reload() error {
continue
}
var proxy http.Handler
+ // viaTunnel: this host's bytes leave the box over a reverse channel,
+ // which is what disqualifies it from hibernation below.
+ viaTunnel := false
switch {
// Both reverse proxies get the Carlos-Purge response hook
// (purge_header.go): the app declares its own cache invalidations on
@@ -231,6 +250,22 @@ func (e *Edge) Reload() error {
// Static and status routes get no hook: their responses are the
// edge's own, built from bucket bytes by static.go/status.go, with no
// upstream in a position to declare anything.
+ //
+ // The tunnel case comes FIRST, ahead of socket/addr, because a route
+ // can carry both: `carlos route -tunnel` is a narrow UPDATE that does
+ // not clear socket/addr, so an operator moving a local app onto a
+ // remote host leaves the old column set — and the binding they just
+ // typed is the one they meant. It is guarded on e.tunnels rather than
+ // skipping unconditionally so that an edge with no hub configured
+ // keeps serving such a route from its old socket/addr (degrade to the
+ // previous backend, never take the host off the air); a tunnel-only
+ // route on such an edge matches nothing and falls to
+ // `default: continue`, un-served and un-certed, until the hub lands.
+ case t.Tunnel != "" && e.tunnels != nil:
+ p := tunnelProxy(t.Tunnel, e.tunnels)
+ p.ModifyResponse = e.purgeModifier(t)
+ proxy = p
+ viaTunnel = true
case t.Socket != "":
p := unixProxy(t.Socket)
p.ModifyResponse = e.purgeModifier(t)
@@ -252,7 +287,15 @@ func (e *Edge) Reload() error {
// flagged, the edge therefore behaves exactly as it does without one —
// so the binary + env can ship fleet-wide first, and the flag flips
// one instance at a time.
- hib := e.act != nil && t.Kind == "instance" && t.Hibernate
+ //
+ // A tunnel-served host is never activator-managed, however it is
+ // flagged: the process lives on the far side of the channel and this
+ // edge cannot start it, so waking would mean launching a SECOND,
+ // local copy of the app beside the remote one. Leaving such a host out
+ // of `managed` is also what retires the local child a repointed route
+ // left running (RetireExcept, below) — the app moved to the remote
+ // host, so the process it left behind here should go.
+ hib := e.act != nil && t.Kind == "instance" && t.Hibernate && !viaTunnel
if hib {
managed[t.Host] = true
}
@@ -280,6 +323,13 @@ func (e *Edge) current() map[string]upstream {
// Allowed reports whether host is configured — used as the autocert
// HostPolicy, so a certificate is only ever requested for a host we serve.
func (e *Edge) Allowed(host string) bool {
+ // The reserved tunnel hostname is not a registry route, but remote hosts
+ // dial it over TLS on this same 443 listener — so it must be Allowed, and
+ // autocert will mint for it. That is intended: it is a hostname the
+ // operator configured on this box, not a stranger-supplied SNI.
+ if e.tunnels != nil && host == e.tunnels.Host() {
+ return true
+ }
_, ok := e.current()[host]
return ok
}
@@ -289,6 +339,20 @@ func (e *Edge) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
+ // The reserved tunnel hostname is answered by the hub itself — before the
+ // route lookup, so registration cannot be intercepted by a route, and
+ // after the host:port split, so a dialer sending "tunnel.host:443" still
+ // lands here. It wins over a registry route of the same name: the operator
+ // owns both names, and a shadowed route is visible (its host stops
+ // answering) where a shadowed hub would be a locked-out fleet.
+ //
+ // Being ahead of the cache path is load-bearing too, not just tidy: the
+ // hub ends its handshake in a hijack, so it must be handed the REAL
+ // ResponseWriter, never a recordingResponseWriter wrapping it.
+ if e.tunnels != nil && host == e.tunnels.Host() {
+ e.tunnels.ServeHTTP(w, r)
+ return
+ }
up, ok := e.current()[host]
if !ok {
e.unknownHost(w, r)
@@ -802,6 +866,27 @@ func tcpProxy(addr string) *httputil.ReverseProxy {
}
}
+// tunnelProxy reverse-proxies over a named reverse channel (TunnelHub).
+// Same Host preservation and purge-header discipline as tcpProxy — a
+// tunnel-backed host is exactly as untrusted as an addr-backed one. The
+// scheme is https because the h2 ClientConn requires it; no dial happens
+// (the channel already exists), so the value is otherwise inert. No channel
+// is resolved here either: hub.transportFor looks it up per request, so a
+// proxy built before a reconnect uses the fresh channel afterwards, and a
+// disconnected one fails fast into the stdlib proxy's 502 rather than hanging.
+func tunnelProxy(name string, hub *TunnelHub) *httputil.ReverseProxy {
+ return &httputil.ReverseProxy{
+ Rewrite: func(pr *httputil.ProxyRequest) {
+ pr.SetXForwarded()
+ pr.Out.URL.Scheme = "https"
+ pr.Out.URL.Host = pr.In.Host // h2 :authority = the real hostname
+ pr.Out.Host = pr.In.Host
+ scrubInboundPurgeHeader(pr)
+ },
+ Transport: hub.transportFor(name),
+ }
+}
+
// dialUnixWithRetry masks the brief gap during an instance restart: while the
// old process has exited and the new one hasn't rebound the socket, dials fail
// with ENOENT/ECONNREFUSED. Retry those for a short window so no in-flight
@@ -843,6 +928,17 @@ func Run(args []string) {
// process. act may be nil: pure proxy, exactly Run's behavior. cache may
// also be nil, disabling response caching entirely.
func RunWithActivator(args []string, act Activator, cache *Cache) {
+ RunWithActivatorReady(args, act, cache, nil)
+}
+
+// RunWithActivatorReady is RunWithActivator plus a ready hook: ready (when
+// non-nil) is called with the fully configured Edge — every EnableXxx done,
+// tunnel hub armed if configured — immediately before the process blocks on
+// its listeners. It is the seam an in-process caller uses to reach the live
+// Edge (e.g. to read tunnel state for status reporting) without racing the
+// configuration it needs. It runs on the calling goroutine, so a hook that
+// blocks holds the front door shut: hand off to a goroutine and return.
+func RunWithActivatorReady(args []string, act Activator, cache *Cache, ready func(*Edge)) {
dataDir := envOr("CARLOS_DATA_DIR", "/data")
fs := flag.NewFlagSet("edge", flag.ExitOnError)
registryPath := fs.String("registry", filepath.Join(dataDir, "registry.db"), "registry SQLite path (default $CARLOS_DATA_DIR/registry.db)")
@@ -873,6 +969,12 @@ func RunWithActivator(args []string, act Activator, cache *Cache) {
log.Fatalf("edge: static serving: %v", err)
}
+ // Reverse channels light up when the environment names a reserved
+ // hostname and at least one client; otherwise tunnel routes stay skipped.
+ if err := enableTunnelsFromEnv(e); err != nil {
+ log.Fatalf("edge: tunnels: %v", err)
+ }
+
go e.Watch(make(chan struct{})) // process-lifetime; never stopped
// Reload routes on SIGHUP too (an operator's explicit "now").
@@ -891,6 +993,12 @@ func RunWithActivator(args []string, act Activator, cache *Cache) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT)
+ // The Edge is fully configured here — every EnableXxx applied and the
+ // route table rebuilt behind them — and nothing is listening yet.
+ if ready != nil {
+ ready(e)
+ }
+
if *dev {
// ReadHeaderTimeout guards against Slowloris-style slow-header
// connection exhaustion. No ReadTimeout/WriteTimeout — those would
@@ -1014,6 +1122,47 @@ func enableStaticFromEnv(e *Edge, dataDir string) error {
return e.Reload()
}
+// enableTunnelsFromEnv arms the reverse-channel hub when the environment
+// names a reserved hostname (CARLOS_TUNNEL_HOST) and its authorized clients
+// (CARLOS_TUNNEL_CLIENTS, "name=token[,name=token]"). No CARLOS_TUNNEL_HOST is
+// not an error: the box simply serves no tunnels, and routes with Tunnel set
+// keep whatever local backend they have (Reload's tunnel case is guarded on
+// the hub). A host WITH no usable clients IS an error — it would open a
+// registration endpoint no one can ever authenticate against, which reads
+// like a working tunnel edge right up until a host tries to dial it.
+//
+// Channel names are validated here, at the only place a name enters the
+// process from config: a name the CLI would refuse (registry.ValidTunnelName,
+// what `carlos route -tunnel` enforces) can never match a route's Tunnel
+// column, so such a client would authenticate, connect, and silently carry no
+// traffic at all. Fail the boot instead, naming the entry.
+func enableTunnelsFromEnv(e *Edge) error {
+ host := os.Getenv("CARLOS_TUNNEL_HOST")
+ if host == "" {
+ return nil
+ }
+ clients, err := parseTunnelClients(os.Getenv("CARLOS_TUNNEL_CLIENTS"))
+ if err != nil {
+ return err
+ }
+ if len(clients) == 0 {
+ return fmt.Errorf("CARLOS_TUNNEL_HOST=%s set but CARLOS_TUNNEL_CLIENTS is empty", host)
+ }
+ for _, name := range slices.Sorted(maps.Keys(clients)) {
+ if !registry.ValidTunnelName(name) {
+ return fmt.Errorf("CARLOS_TUNNEL_CLIENTS: %q is not a valid tunnel name — want [a-z0-9-]{1,32}, and it must match a route's -tunnel to carry anything", name)
+ }
+ }
+ e.EnableTunnels(NewTunnelHub(host, clients))
+ // Rebuild the route table so tunnel-backed routes that were skipped (or
+ // left on their old socket/addr) by the startup reload pick up the hub.
+ if err := e.Reload(); err != nil {
+ return fmt.Errorf("reload after EnableTunnels: %w", err)
+ }
+ log.Printf("edge: tunnel hub armed on %s (%d client(s))", host, len(clients))
+ return nil
+}
+
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
diff --git a/internal/edge/tunnel_test.go b/internal/edge/tunnel_test.go
index d75e97c31..7efe2eb32 100644
--- a/internal/edge/tunnel_test.go
+++ b/internal/edge/tunnel_test.go
@@ -5,6 +5,8 @@ import (
"net/http/httptest"
"strings"
"testing"
+
+ "github.com/carlosframework/platform/internal/registry"
)
// tunnelReq builds a request that is well-formed EXCEPT for the credentials
@@ -124,3 +126,177 @@ func TestParseTunnelClients(t *testing.T) {
t.Fatal("want error on entry without '='")
}
}
+
+// tunnelRoute upserts one route into a fresh registry (openReg, the package's
+// one registry harness — edge_test.go) and returns the registry.
+func tunnelRoute(t *testing.T, rt registry.Route) *registry.Registry {
+ t.Helper()
+ reg := openReg(t)
+ if err := reg.Upsert(rt); err != nil {
+ t.Fatalf("upsert: %v", err)
+ }
+ return reg
+}
+
+func TestReloadBuildsTunnelBackedRoute(t *testing.T) {
+ reg := tunnelRoute(t, registry.Route{
+ Host: "demo.example", Kind: "instance", Status: "ready", Tunnel: "pi-box",
+ })
+ e, err := New(reg, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // Before EnableTunnels the route has no backend at all: it must not be
+ // served, and must not burn an ACME issuance either.
+ if e.Allowed("demo.example") {
+ t.Fatal("tunnel-backed route served with no hub configured")
+ }
+ hub := NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"})
+ e.EnableTunnels(hub)
+ if err := e.Reload(); err != nil {
+ t.Fatal(err)
+ }
+ if !e.Allowed("demo.example") {
+ t.Fatal("tunnel-backed route not served after EnableTunnels+Reload")
+ }
+ if !e.Allowed("tunnel.test") {
+ t.Fatal("reserved tunnel hostname must be Allowed (autocert HostPolicy)")
+ }
+ // Channel down -> 502, not a hang.
+ req := httptest.NewRequest("GET", "http://demo.example/", nil)
+ req.Host = "demo.example"
+ rec := httptest.NewRecorder()
+ e.ServeHTTP(rec, req)
+ if rec.Code != http.StatusBadGateway {
+ t.Fatalf("disconnected tunnel: got %d, want 502", rec.Code)
+ }
+}
+
+// The reserved hostname is answered by the hub itself, ahead of the route
+// table — so a registration attempt reaches the handshake (401 here, no
+// credentials) instead of the unknown-host 404.
+func TestEdgeInterceptsReservedTunnelHost(t *testing.T) {
+ e, err := New(openReg(t), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ e.EnableTunnels(NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}))
+ if err := e.Reload(); err != nil {
+ t.Fatal(err)
+ }
+ req := tunnelReq("pi-box", "nope")
+ req.Host = "tunnel.test:443" // host:port must still match the reserved name
+ rec := httptest.NewRecorder()
+ e.ServeHTTP(rec, req)
+ if rec.Code != http.StatusUnauthorized {
+ t.Fatalf("got %d, want 401 — the reserved host must reach the hub, not the route table", rec.Code)
+ }
+}
+
+// `carlos route -tunnel` is a narrow UPDATE: it does not clear the route's
+// socket/addr. So a route can legitimately carry both, and which one wins is
+// a real decision, not a moot one:
+// - hub configured -> the tunnel wins (the newest, most explicit binding).
+// - no hub on this box -> the old addr keeps serving, rather than the host
+// dropping off the air on an edge that cannot honor the tunnel.
+func TestTunnelBindingWinsOverAddrOnlyWhenHubConfigured(t *testing.T) {
+ backend, _ := hostEchoBackend(t, "local addr backend")
+ reg := tunnelRoute(t, registry.Route{
+ Host: "both.example", Kind: "service", Status: "ready",
+ Addr: backend.Listener.Addr().String(), Tunnel: "pi-box",
+ })
+ e, err := New(reg, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ srv := httptest.NewServer(e)
+ defer srv.Close()
+ code, body := getHost(t, srv.URL, "both.example")
+ if code != http.StatusOK || body != "local addr backend" {
+ t.Fatalf("no hub configured: got %d %q, want the addr backend to keep serving", code, body)
+ }
+ e.EnableTunnels(NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}))
+ if err := e.Reload(); err != nil {
+ t.Fatal(err)
+ }
+ if code, _ := getHost(t, srv.URL, "both.example"); code != http.StatusBadGateway {
+ t.Fatalf("hub configured: got %d, want 502 from the (disconnected) tunnel", code)
+ }
+}
+
+// The env wiring is the only place a channel name enters the process from
+// config, and a name `carlos route -tunnel` would refuse can never match a
+// route — so it must stop the boot, not connect and carry nothing.
+func TestEnableTunnelsFromEnv(t *testing.T) {
+ newEdge := func() *Edge {
+ e, err := New(openReg(t), nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return e
+ }
+ t.Run("unset is not an error", func(t *testing.T) {
+ e := newEdge()
+ if err := enableTunnelsFromEnv(e); err != nil {
+ t.Fatalf("no CARLOS_TUNNEL_HOST: %v", err)
+ }
+ if e.tunnels != nil {
+ t.Fatal("hub armed with no CARLOS_TUNNEL_HOST")
+ }
+ })
+ t.Run("host with no clients", func(t *testing.T) {
+ t.Setenv("CARLOS_TUNNEL_HOST", "tunnel.test")
+ t.Setenv("CARLOS_TUNNEL_CLIENTS", "")
+ if err := enableTunnelsFromEnv(newEdge()); err == nil {
+ t.Fatal("want an error: a registration endpoint nobody can authenticate against")
+ }
+ })
+ t.Run("invalid channel name", func(t *testing.T) {
+ t.Setenv("CARLOS_TUNNEL_HOST", "tunnel.test")
+ t.Setenv("CARLOS_TUNNEL_CLIENTS", "pi-box=sekrit,Pi_Box=other")
+ err := enableTunnelsFromEnv(newEdge())
+ if err == nil || !strings.Contains(err.Error(), "Pi_Box") {
+ t.Fatalf("got %v, want an error naming the bad entry", err)
+ }
+ })
+ t.Run("armed", func(t *testing.T) {
+ t.Setenv("CARLOS_TUNNEL_HOST", "tunnel.test")
+ t.Setenv("CARLOS_TUNNEL_CLIENTS", "pi-box=sekrit")
+ e := newEdge()
+ if err := enableTunnelsFromEnv(e); err != nil {
+ t.Fatal(err)
+ }
+ if e.tunnels == nil || e.tunnels.Host() != "tunnel.test" || !e.Allowed("tunnel.test") {
+ t.Fatalf("hub not armed on the configured host")
+ }
+ })
+}
+
+// A tunnel-served host is never activator-managed: the process lives on the
+// far side of the channel, so a wake here would start a second, local copy.
+// The retire pass must also drop it, killing the child a repointed route left
+// behind.
+func TestTunnelRouteIsNeverActivatorManaged(t *testing.T) {
+ reg := tunnelRoute(t, registry.Route{
+ Host: "moved.example", Kind: "instance", Status: "ready",
+ Tunnel: "pi-box", Hibernate: true,
+ })
+ act := &fakeActivator{}
+ e, err := New(reg, act)
+ if err != nil {
+ t.Fatal(err)
+ }
+ e.EnableTunnels(NewTunnelHub("tunnel.test", map[string]string{"pi-box": "sekrit"}))
+ if err := e.Reload(); err != nil {
+ t.Fatal(err)
+ }
+ if last := act.retired[len(act.retired)-1]; last["moved.example"] {
+ t.Fatalf("tunnel-served host kept in the activator's managed set: %v", last)
+ }
+ req := httptest.NewRequest("GET", "http://moved.example/", nil)
+ req.Host = "moved.example"
+ e.ServeHTTP(httptest.NewRecorder(), req)
+ if act.served != 0 {
+ t.Fatalf("activator woke a local process for a tunnel-served host (%d call(s))", act.served)
+ }
+}
diff --git a/internal/registry/registry.go b/internal/registry/registry.go
index 9070d05ae..05694ab6d 100644
--- a/internal/registry/registry.go
+++ b/internal/registry/registry.go
@@ -762,6 +762,15 @@ WHERE kind = 'instance' AND next_due > 0 AND next_due <= ? ORDER BY host`, now)
// edge's cheap change poll. It must change on any row insert/delete and on
// the column writes reloads care about; it must see other processes' commits
// (fresh connection, same WAL-snapshot quirk as List).
+//
+// Text columns contribute their LENGTH under a per-column weight, so a rewrite
+// to a DIFFERENT value of the SAME length (channel canary/1 -> canary/2,
+// tunnel pi-box -> hq-box) is invisible to this poll. That is a deliberate
+// cost of keeping it a single cheap aggregate: an operator's `carlos route`
+// runs beside an edge they can SIGHUP, and every same-length rewrite reaches
+// the fleet on the next reload for any other reason. Weights (3,5,7,11,13…)
+// keep DIFFERENT columns from cancelling each other out, which is the failure
+// that would actually hide a change forever.
func (r *Registry) Fingerprint() (string, error) {
db, err := sql.Open("sqlite", r.path)
if err != nil {
@@ -776,7 +785,7 @@ func (r *Registry) Fingerprint() (string, error) {
SELECT COUNT(*) || ':' || COALESCE(MAX(rowid),0) || ':' ||
COALESCE(SUM(rowid*17 + LENGTH(status)*3 + LENGTH(socket) + LENGTH(addr)
+ LENGTH(version)*5 + hibernate*7 + LENGTH(channel)
- + LENGTH(backing)*11),0)
+ + LENGTH(backing)*11 + LENGTH(tunnel)*13),0)
FROM routes`).Scan(&fp)
return fp, err
}
diff --git a/internal/registry/registry_test.go b/internal/registry/registry_test.go
index 57fa2fd82..435e9dcc7 100644
--- a/internal/registry/registry_test.go
+++ b/internal/registry/registry_test.go
@@ -168,6 +168,19 @@ func TestFingerprintChangesOnWrite(t *testing.T) {
if withBacking == final {
t.Fatal("fingerprint blind to a backing flip")
}
+ // Tunnel bindings must reload the edge too: this poll is the ONLY thing
+ // that notices a `carlos route -tunnel` written by another process, and a
+ // route the edge never reloads stays pointed at its old backend.
+ if err := reg.SetTunnel("c.example.com", "pi-box"); err != nil {
+ t.Fatal(err)
+ }
+ withTunnel, err := reg.Fingerprint()
+ if err != nil {
+ t.Fatal(err)
+ }
+ if withTunnel == withBacking {
+ t.Fatal("fingerprint blind to a tunnel binding")
+ }
}
func TestSetVersionsAndHibernateAreNarrow(t *testing.T) {