Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ A hardening round. What an upgrading operator notices first:
upstream paths must be absolute and clean.
- **The TCP control listeners refuse browsers** (any `Origin`, and on
`local` any non-loopback `Host`).
- **Under the service edge a dedicated mdns `listen` is refused:** it
would serve the status page on a port the exposure mode does not
govern. The front door shares the HTTP port, as the seed configures it.
- **Caddy 2.11.7**, current quic-go, webtransport-go, and `x/` modules,
built with Go 1.27.2; `govulncheck` gates every pull request.

Expand Down
8 changes: 8 additions & 0 deletions docs/20260722-034619-capability-mdns.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,14 @@ unauthorized responses take precedence over this route table:
| Caching | `Cache-Control: no-store` on every route |
| Encoding | **All dynamic values render as DOM text nodes, never markup.** Today every rendered field is charset-inert by construction (registry name/host validation admits no HTML metacharacters; `canonical` is operator cold config; a conflict rename appends `-<digits>`), and this row keeps that true for every future field |

**Dedicated mode is refused under the service edge.** The exposure mode
governs ports 80 and 443: the firewall rule and the socket watch cover
nothing else, so a dedicated `listen` would serve the status page outside
the mode, on every address it names, a global IPv6 one included. When the
service edge has published a mode, a `listen` is a Start error naming the
mode, and the front door shares the HTTP port. Dedicated mode remains for a
hand-run Caddyfile (development and CI), which has no exposure contract.

**Bind failures are hard Start errors (dedicated mode).** If the
front-door address is held by another process, Start refuses the
config with a precise error naming the address and the conflict. If
Expand Down
7 changes: 7 additions & 0 deletions mdns.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,13 @@ func (a *App) startMdns() error {
zap.String("name", ms.Name),
)
} else {
// Under the service edge the exposure mode governs ports 80 and 443
// alone (its firewall rule and its socket watch): a dedicated
// front-door port would serve the status page outside both, on
// every address the listen names, a global IPv6 one included.
if scope := ExposureScope(); scope != "" {
return fmt.Errorf("janus mdns: `listen %s` gives the front door a port of its own, which the %s exposure mode does not govern (it covers ports 80 and 443); under the service edge the front door shares the HTTP port: remove `listen`", ms.Listen, scope)
}
if err := a.checkMdnsListenCollision(); err != nil {
return err
}
Expand Down
22 changes: 22 additions & 0 deletions mdns_exposure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package janus
import (
"net"
"net/netip"
"strings"
"testing"

"github.com/brutella/dnssd"
Expand Down Expand Up @@ -113,3 +114,24 @@ func TestMdnsStandaloneKeepsAddressPolicy(t *testing.T) {
t.Fatalf("standalone address policy changed: %+v", srv)
}
}

// Under the service edge (a published exposure mode), a dedicated front
// door is refused in every mode: its port lies outside the ports the mode
// governs, so it would serve the status page past the firewall rule and
// the socket watch, on a global IPv6 address too.
func TestMdnsDedicatedFrontDoorRefusedUnderTheServiceEdge(t *testing.T) {
t.Cleanup(func() { SetExposure("", "", netip.Addr{}) })
for _, scope := range []string{"localhost", "lan", "wan"} {
lan := netip.Addr{}
iface := ""
if scope == "lan" {
lan, iface = netip.MustParseAddr("10.0.0.211"), "en0"
}
SetExposure(scope, iface, lan)
app := newTestMdnsApp(t, &MdnsSettings{Listen: ":7680"})
err := app.startMdns()
if err == nil || !strings.Contains(err.Error(), "remove `listen`") || !strings.Contains(err.Error(), scope) {
t.Errorf("%s: want the dedicated front door refused, got %v", scope, err)
}
}
}
Loading