Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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: 2 additions & 1 deletion modules/caddyhttp/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"maps"
"net"
"net/http"
"slices"
"strconv"
"sync"
"time"
Expand Down Expand Up @@ -235,7 +236,7 @@ func (app *App) Provision(ctx caddy.Context) error {

// if no protocols configured explicitly, enable all except h2c
if len(srv.Protocols) == 0 {
srv.Protocols = []string{"h1", "h2", "h3"}
srv.Protocols = slices.Clone(srv.protocolsWithDefaults())
Comment thread
steadytao marked this conversation as resolved.
Outdated
}

srvProtocolsUnique := map[string]struct{}{}
Expand Down
45 changes: 44 additions & 1 deletion modules/caddyhttp/autohttps.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
for d := range serverDomainSet {
echDomains = append(echDomains, d)
}
app.tlsApp.RegisterServerNames(echDomains)
app.tlsApp.RegisterServerNames(echDomains, httpsRRALPNs(srv))

// nothing more to do here if there are no domains that qualify for
// automatic HTTPS and there are no explicit TLS connection policies:
Expand Down Expand Up @@ -550,6 +550,49 @@ func (app *App) makeRedirRoute(redirToPort uint, matcherSet MatcherSet) Route {
}
}

func httpsRRALPNs(srv *Server) []string {
Comment thread
steadytao marked this conversation as resolved.
// Automatic HTTPS runs before server provisioning fills in the default
// protocols, so derive the effective set directly from the raw config here.
serverProtocols := srv.protocolsWithDefaults()

protocols := make(map[string]struct{}, len(serverProtocols))
if srv.ListenProtocols == nil {
for _, protocol := range serverProtocols {
protocols[protocol] = struct{}{}
}
} else {
for _, lnProtocols := range srv.ListenProtocols {
if len(lnProtocols) == 0 {
for _, protocol := range serverProtocols {
protocols[protocol] = struct{}{}
}
continue
}
for _, protocol := range lnProtocols {
if protocol == "" {
for _, inherited := range serverProtocols {
protocols[inherited] = struct{}{}
}
continue
}
protocols[protocol] = struct{}{}
}
}
}

alpn := make(map[string]struct{}, 3)
if _, ok := protocols["h3"]; ok {
alpn["h3"] = struct{}{}
}
if _, ok := protocols["h2"]; ok {
alpn["h2"] = struct{}{}
}
if _, ok := protocols["h1"]; ok {
alpn["http/1.1"] = struct{}{}
}
return caddytls.OrderedHTTPSRRALPN(alpn)
}

// createAutomationPolicies ensures that automated certificates for this
// app are managed properly. This adds up to two automation policies:
// one for the public names, and one for the internal names. If a catch-all
Expand Down
46 changes: 46 additions & 0 deletions modules/caddyhttp/autohttps_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package caddyhttp

import (
"reflect"
"testing"
)

func TestHTTPSRRALPNsDefaultProtocols(t *testing.T) {
srv := &Server{}

got := httpsRRALPNs(srv)
want := []string{"h3", "h2", "http/1.1"}

if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
}
}

func TestHTTPSRRALPNsListenProtocolOverrides(t *testing.T) {
srv := &Server{
Protocols: []string{"h1", "h2"},
ListenProtocols: [][]string{
{"h1"},
nil,
{"h2c", "h3"},
},
}

got := httpsRRALPNs(srv)
want := []string{"h3", "h2", "http/1.1"}

if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %v want %v", got, want)
}
}

func TestHTTPSRRALPNsIgnoresH2COnly(t *testing.T) {
srv := &Server{
Protocols: []string{"h2c"},
}

got := httpsRRALPNs(srv)
if len(got) != 0 {
t.Fatalf("unexpected ALPN values: got %v want none", got)
}
}
14 changes: 12 additions & 2 deletions modules/caddyhttp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,8 @@ type Server struct {
onStopFuncs []func(context.Context) error // TODO: Experimental (Nov. 2023)
}

var defaultProtocols = []string{"h1", "h2", "h3"}

var (
ServerHeader = "Caddy"
serverHeader = []string{ServerHeader}
Expand Down Expand Up @@ -900,13 +902,14 @@ func (s *Server) logRequest(
// protocol returns true if the protocol proto is configured/enabled.
func (s *Server) protocol(proto string) bool {
if s.ListenProtocols == nil {
if slices.Contains(s.Protocols, proto) {
if slices.Contains(s.protocolsWithDefaults(), proto) {
return true
}
} else {
serverProtocols := s.protocolsWithDefaults()
for _, lnProtocols := range s.ListenProtocols {
for _, lnProtocol := range lnProtocols {
if lnProtocol == "" && slices.Contains(s.Protocols, proto) || lnProtocol == proto {
if lnProtocol == "" && slices.Contains(serverProtocols, proto) || lnProtocol == proto {
return true
}
}
Expand All @@ -916,6 +919,13 @@ func (s *Server) protocol(proto string) bool {
return false
}

func (s *Server) protocolsWithDefaults() []string {
if len(s.Protocols) == 0 {
return defaultProtocols
}
return s.Protocols
}

// Listeners returns the server's listeners. These are active listeners,
// so calling Accept() or Close() on them will probably break things.
// They are made available here for read-only purposes (e.g. Addr())
Expand Down
4 changes: 2 additions & 2 deletions modules/caddytls/connpolicy.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
// in its config (remember, TLS connection policies are used by *other* apps to
// run TLS servers) -- we skip names with placeholders
if tlsApp.EncryptedClientHello.Publication == nil {
var echNames []string
repl := caddy.NewReplacer()
for _, p := range cp {
var echNames []string
for _, m := range p.matchers {
if sni, ok := m.(MatchServerName); ok {
for _, name := range sni {
Expand All @@ -164,8 +164,8 @@ func (cp ConnectionPolicies) TLSConfig(ctx caddy.Context) *tls.Config {
}
}
}
tlsApp.RegisterServerNames(echNames, p.ALPN)
}
tlsApp.RegisterServerNames(echNames)
}

tlsCfg.GetEncryptedClientHelloKeys = func(chi *tls.ClientHelloInfo) ([]tls.EncryptedClientHelloKey, error) {
Expand Down
33 changes: 26 additions & 7 deletions modules/caddytls/ech.go
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,10 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error {
zap.Strings("domains", dnsNamesToPublish),
zap.Uint8s("config_ids", configIDs))

if dnsPublisher, ok := publisher.(*ECHDNSPublisher); ok {
dnsPublisher.alpnByDomain = t.alpnValuesForServerNames(dnsNamesToPublish)
}

// publish this ECH config list with this publisher
pubTime := time.Now()
err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin)
Expand Down Expand Up @@ -776,7 +780,8 @@ type ECHDNSPublisher struct {
ProviderRaw json.RawMessage `json:"provider,omitempty" caddy:"namespace=dns.providers inline_key=name"`
provider ECHDNSProvider

logger *zap.Logger
alpnByDomain map[string][]string
logger *zap.Logger
}

// CaddyModule returns the Caddy module information.
Expand Down Expand Up @@ -872,12 +877,7 @@ nextName:
continue
}
params := httpsRec.Params
if params == nil {
params = make(libdns.SvcParams)
}

// overwrite only the "ech" SvcParamKey
params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}
params = dnsPub.publishedSvcParams(domain, params, configListBin)

// publish record
_, err = dnsPub.provider.SetRecords(ctx, zone, []libdns.Record{
Expand All @@ -903,6 +903,25 @@ nextName:
return nil
}

func (dnsPub *ECHDNSPublisher) publishedSvcParams(domain string, existing libdns.SvcParams, configListBin []byte) libdns.SvcParams {
params := make(libdns.SvcParams, len(existing)+2)
for key, values := range existing {
params[key] = append([]string(nil), values...)
}

params["ech"] = []string{base64.StdEncoding.EncodeToString(configListBin)}

if len(dnsPub.alpnByDomain) == 0 {
return params
}

if alpn := dnsPub.alpnByDomain[strings.ToLower(domain)]; len(alpn) > 0 {
params["alpn"] = append([]string(nil), alpn...)
}

return params
}

// echConfig represents an ECHConfig from the specification,
// [draft-ietf-tls-esni-22](https://www.ietf.org/archive/id/draft-ietf-tls-esni-22.html).
type echConfig struct {
Expand Down
65 changes: 65 additions & 0 deletions modules/caddytls/ech_dns_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package caddytls

import (
"encoding/base64"
"reflect"
"sync"
"testing"

"github.com/libdns/libdns"
)

func TestRegisterServerNamesWithALPN(t *testing.T) {
tlsApp := &TLS{
serverNames: make(map[string]serverNameRegistration),
serverNamesMu: new(sync.Mutex),
}

tlsApp.RegisterServerNames([]string{
"Example.com:443",
"example.com",
"127.0.0.1:443",
}, []string{"h2", "http/1.1"})
tlsApp.RegisterServerNames([]string{"EXAMPLE.COM"}, []string{"h3"})

got := tlsApp.alpnValuesForServerNames([]string{"example.com:443", "127.0.0.1:443"})
want := map[string][]string{
"example.com": {"h3", "h2", "http/1.1"},
}

if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected ALPN values: got %#v want %#v", got, want)
}
}

func TestECHDNSPublisherPublishedSvcParams(t *testing.T) {
dnsPub := &ECHDNSPublisher{
alpnByDomain: map[string][]string{
"example.com": {"h3", "h2", "http/1.1"},
},
}

existing := libdns.SvcParams{
"alpn": {"h2"},
"ipv4hint": {"203.0.113.10"},
}

got := dnsPub.publishedSvcParams("Example.com", existing, []byte{0x01, 0x02, 0x03})

if !reflect.DeepEqual(existing["alpn"], []string{"h2"}) {
t.Fatalf("existing params mutated: got %v", existing["alpn"])
}

if !reflect.DeepEqual(got["alpn"], []string{"h3", "h2", "http/1.1"}) {
t.Fatalf("unexpected ALPN params: got %v", got["alpn"])
}

if !reflect.DeepEqual(got["ipv4hint"], []string{"203.0.113.10"}) {
t.Fatalf("unexpected preserved params: got %v", got["ipv4hint"])
}

wantECH := base64.StdEncoding.EncodeToString([]byte{0x01, 0x02, 0x03})
if !reflect.DeepEqual(got["ech"], []string{wantECH}) {
t.Fatalf("unexpected ECH params: got %v want %v", got["ech"], wantECH)
}
}
Loading
Loading