github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4tls/handler.go (about)

     1  // Copyright 2020 Matthew Holt
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package l4tls
    16  
    17  import (
    18  	"crypto/tls"
    19  	"encoding/json"
    20  	"fmt"
    21  	"math/big"
    22  
    23  	"github.com/caddyserver/caddy/v2"
    24  	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
    25  	"github.com/caddyserver/caddy/v2/modules/caddytls"
    26  	"go.uber.org/zap"
    27  
    28  	"github.com/mholt/caddy-l4/layer4"
    29  )
    30  
    31  func init() {
    32  	caddy.RegisterModule(&Handler{})
    33  }
    34  
    35  // Handler is a connection handler that terminates TLS.
    36  type Handler struct {
    37  	ConnectionPolicies caddytls.ConnectionPolicies `json:"connection_policies,omitempty"`
    38  
    39  	ctx    caddy.Context
    40  	logger *zap.Logger
    41  }
    42  
    43  // CaddyModule returns the Caddy module information.
    44  func (*Handler) CaddyModule() caddy.ModuleInfo {
    45  	return caddy.ModuleInfo{
    46  		ID:  "layer4.handlers.tls",
    47  		New: func() caddy.Module { return new(Handler) },
    48  	}
    49  }
    50  
    51  // Provision sets up the module.
    52  func (t *Handler) Provision(ctx caddy.Context) error {
    53  	t.ctx = ctx
    54  	t.logger = ctx.Logger(t)
    55  
    56  	// ensure there is at least one policy, which will act as default
    57  	if len(t.ConnectionPolicies) == 0 {
    58  		t.ConnectionPolicies = append(t.ConnectionPolicies, new(caddytls.ConnectionPolicy))
    59  	}
    60  
    61  	err := t.ConnectionPolicies.Provision(ctx)
    62  	if err != nil {
    63  		return fmt.Errorf("setting up Handler connection policies: %v", err)
    64  	}
    65  
    66  	return nil
    67  }
    68  
    69  // Handle handles the connections.
    70  func (t *Handler) Handle(cx *layer4.Connection, next layer4.Handler) error {
    71  	// get the TLS config to use for this connection
    72  	tlsCfg := t.ConnectionPolicies.TLSConfig(t.ctx)
    73  
    74  	// capture the ClientHello info when the handshake is performed
    75  	var clientHello ClientHelloInfo
    76  	underlyingGetConfigForClient := tlsCfg.GetConfigForClient
    77  	tlsCfg.GetConfigForClient = func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
    78  		clientHello.ClientHelloInfo = *hello
    79  		return underlyingGetConfigForClient(hello)
    80  	}
    81  
    82  	// terminate TLS by performing the handshake (note that we pass
    83  	// in cx, not cx.Conn; this is because we must read from the
    84  	// connection to perform the handshake, and cx might have some
    85  	// bytes already buffered need to be read first)
    86  	tlsConn := tls.Server(cx, tlsCfg)
    87  	err := tlsConn.Handshake()
    88  	if err != nil {
    89  		return err
    90  	}
    91  	t.logger.Debug("terminated TLS",
    92  		zap.String("remote", cx.RemoteAddr().String()),
    93  		zap.String("server_name", clientHello.ServerName),
    94  	)
    95  
    96  	// preserve this ClientHello info for later, if needed
    97  	appendClientHello(cx, clientHello)
    98  
    99  	// preserve the tls.ConnectionState for use in the http matcher
   100  	connectionState := tlsConn.ConnectionState()
   101  	appendConnectionState(cx, &connectionState)
   102  
   103  	// all future reads/writes will now be decrypted/encrypted
   104  	// (tlsConn, which wraps cx, is wrapped into a new cx so
   105  	// that future I/O succeeds... if we use the same cx, it'd
   106  	// be wrapping itself, and we'd have nested read calls out
   107  	// to the kernel, which creates a deadlock/hang; see #18)
   108  	return next.Handle(cx.Wrap(tlsConn))
   109  }
   110  
   111  // UnmarshalCaddyfile sets up the Handler from Caddyfile tokens. Syntax:
   112  //
   113  //	tls {
   114  //		connection_policy {
   115  //			...
   116  //		}
   117  //		connection_policy {
   118  //			...
   119  //		}
   120  //	}
   121  //	tls
   122  func (t *Handler) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
   123  	_, wrapper := d.Next(), d.Val() // consume wrapper name
   124  
   125  	// No same-line options are supported
   126  	if d.CountRemainingArgs() > 0 {
   127  		return d.ArgErr()
   128  	}
   129  
   130  	for nesting := d.Nesting(); d.NextBlock(nesting); {
   131  		optionName := d.Val()
   132  		switch optionName {
   133  		case "connection_policy":
   134  			cp := &caddytls.ConnectionPolicy{}
   135  			if err := unmarshalCaddyfileConnectionPolicy(d.NewFromNextSegment(), cp); err != nil {
   136  				return err
   137  			}
   138  			t.ConnectionPolicies = append(t.ConnectionPolicies, cp)
   139  		default:
   140  			return d.ArgErr()
   141  		}
   142  
   143  		// No nested blocks are supported
   144  		if d.NextBlock(nesting + 1) {
   145  			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
   146  		}
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  func appendClientHello(cx *layer4.Connection, chi ClientHelloInfo) {
   153  	var clientHellos []ClientHelloInfo
   154  	if val := cx.GetVar("tls_client_hellos"); val != nil {
   155  		clientHellos = val.([]ClientHelloInfo)
   156  	}
   157  	clientHellos = append(clientHellos, chi)
   158  	cx.SetVar("tls_client_hellos", clientHellos)
   159  }
   160  
   161  // GetClientHelloInfos gets ClientHello information for all the terminated TLS connections.
   162  func GetClientHelloInfos(cx *layer4.Connection) []ClientHelloInfo {
   163  	var clientHellos []ClientHelloInfo
   164  	if val := cx.GetVar("tls_client_hellos"); val != nil {
   165  		clientHellos = val.([]ClientHelloInfo)
   166  	}
   167  	return clientHellos
   168  }
   169  
   170  func appendConnectionState(cx *layer4.Connection, cs *tls.ConnectionState) {
   171  	var connectionStates []*tls.ConnectionState
   172  	if val := cx.GetVar("tls_connection_states"); val != nil {
   173  		connectionStates = val.([]*tls.ConnectionState)
   174  	}
   175  	connectionStates = append(connectionStates, cs)
   176  	cx.SetVar("tls_connection_states", connectionStates)
   177  }
   178  
   179  // GetConnectionStates gets the tls.ConnectionState for all the terminated TLS connections.
   180  func GetConnectionStates(cx *layer4.Connection) []*tls.ConnectionState {
   181  	var connectionStates []*tls.ConnectionState
   182  	if val := cx.GetVar("tls_connection_states"); val != nil {
   183  		connectionStates = val.([]*tls.ConnectionState)
   184  	}
   185  	return connectionStates
   186  }
   187  
   188  // Interface guards
   189  var (
   190  	_ caddy.Provisioner     = (*Handler)(nil)
   191  	_ caddyfile.Unmarshaler = (*Handler)(nil)
   192  	_ layer4.NextHandler    = (*Handler)(nil)
   193  )
   194  
   195  // TODO: move to https://github.com/caddyserver/caddy/tree/master/modules/caddytls/connpolicy.go
   196  // unmarshalCaddyfileConnectionPolicy sets up the ConnectionPolicy from Caddyfile tokens. Syntax:
   197  //
   198  //	connection_policy {
   199  //		alpn <values...>
   200  //		cert_selection {
   201  //			...
   202  //		}
   203  //		ciphers <cipher_suites...>
   204  //		client_auth {
   205  //			...
   206  //		}
   207  //		curves <curves...>
   208  //		default_sni <server_name>
   209  //		match {
   210  //			...
   211  //		}
   212  //		protocols <min> [<max>]
   213  //		# EXPERIMENTAL:
   214  //		drop
   215  //		fallback_sni <server_name>
   216  //		insecure_secrets_log <log_file>
   217  //	}
   218  func unmarshalCaddyfileConnectionPolicy(d *caddyfile.Dispenser, cp *caddytls.ConnectionPolicy) error {
   219  	_, wrapper := d.Next(), "tls "+d.Val()
   220  
   221  	// No same-line options are supported
   222  	if d.CountRemainingArgs() > 0 {
   223  		return d.ArgErr()
   224  	}
   225  
   226  	var (
   227  		hasCertSelection, hasClientAuth, hasDefaultSNI, hasDrop,
   228  		hasFallbackSNI, hasInsecureSecretsLog, hasMatch, hasProtocols bool
   229  	)
   230  	for nesting := d.Nesting(); d.NextBlock(nesting); {
   231  		optionName := d.Val()
   232  		switch optionName {
   233  		case "alpn":
   234  			if d.CountRemainingArgs() == 0 {
   235  				return d.ArgErr()
   236  			}
   237  			cp.ALPN = append(cp.ALPN, d.RemainingArgs()...)
   238  		case "cert_selection":
   239  			if hasCertSelection {
   240  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   241  			}
   242  			p := &caddytls.CustomCertSelectionPolicy{}
   243  			if err := unmarshalCaddyfileCertSelection(d.NewFromNextSegment(), p); err != nil {
   244  				return err
   245  			}
   246  			cp.CertSelection, hasCertSelection = p, true
   247  		case "client_auth":
   248  			if hasClientAuth {
   249  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   250  			}
   251  			ca := &caddytls.ClientAuthentication{}
   252  			if err := ca.UnmarshalCaddyfile(d.NewFromNextSegment()); err != nil {
   253  				return err
   254  			}
   255  			cp.ClientAuthentication, hasClientAuth = ca, true
   256  		case "ciphers":
   257  			if d.CountRemainingArgs() == 0 {
   258  				return d.ArgErr()
   259  			}
   260  			cp.CipherSuites = append(cp.CipherSuites, d.RemainingArgs()...)
   261  		case "curves":
   262  			if d.CountRemainingArgs() == 0 {
   263  				return d.ArgErr()
   264  			}
   265  			cp.Curves = append(cp.Curves, d.RemainingArgs()...)
   266  		case "default_sni":
   267  			if hasDefaultSNI {
   268  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   269  			}
   270  			if d.CountRemainingArgs() != 1 {
   271  				return d.ArgErr()
   272  			}
   273  			_, cp.DefaultSNI, hasDefaultSNI = d.NextArg(), d.Val(), true
   274  		case "drop": // EXPERIMENTAL
   275  			if hasDrop {
   276  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   277  			}
   278  			cp.Drop, hasDrop = true, true
   279  		case "fallback_sni": // EXPERIMENTAL
   280  			if hasFallbackSNI {
   281  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   282  			}
   283  			if d.CountRemainingArgs() != 1 {
   284  				return d.ArgErr()
   285  			}
   286  			_, cp.FallbackSNI, hasFallbackSNI = d.NextArg(), d.Val(), true
   287  		case "insecure_secrets_log": // EXPERIMENTAL
   288  			if hasInsecureSecretsLog {
   289  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   290  			}
   291  			if d.CountRemainingArgs() != 1 {
   292  				return d.ArgErr()
   293  			}
   294  			_, cp.InsecureSecretsLog, hasInsecureSecretsLog = d.NextArg(), d.Val(), true
   295  		case "match":
   296  			if hasMatch {
   297  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   298  			}
   299  			matcherSet, err := ParseCaddyfileNestedMatcherSet(d)
   300  			if err != nil {
   301  				return err
   302  			}
   303  			cp.MatchersRaw, hasMatch = matcherSet, true
   304  		case "protocols":
   305  			if hasProtocols {
   306  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   307  			}
   308  			if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 2 {
   309  				return d.ArgErr()
   310  			}
   311  			_, cp.ProtocolMin, hasProtocols = d.NextArg(), d.Val(), true
   312  			if d.NextArg() {
   313  				cp.ProtocolMax = d.Val()
   314  			}
   315  		default:
   316  			return d.ArgErr()
   317  		}
   318  
   319  		// No nested blocks are supported
   320  		if d.NextBlock(nesting + 1) {
   321  			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
   322  		}
   323  	}
   324  
   325  	return nil
   326  }
   327  
   328  // TODO: move to https://github.com/caddyserver/caddy/tree/master/modules/caddytls/certselection.go
   329  // unmarshalCaddyfileCertSelection sets up the CustomCertSelectionPolicy from Caddyfile tokens. Syntax:
   330  //
   331  //	cert_selection {
   332  //		all_tags <values...>
   333  //		any_tag <values...>
   334  //		public_key_algorithm <dsa|ecdsa|rsa>
   335  //		serial_number <big_integers...>
   336  //		subject_organization <values...>
   337  //	}
   338  func unmarshalCaddyfileCertSelection(d *caddyfile.Dispenser, p *caddytls.CustomCertSelectionPolicy) error {
   339  	_, wrapper := d.Next(), "tls connection_policy "+d.Val() // consume wrapper name
   340  
   341  	// No same-line options are supported
   342  	if d.CountRemainingArgs() > 0 {
   343  		return d.ArgErr()
   344  	}
   345  
   346  	var hasPublicKeyAlgorithm bool
   347  	serialNumberStrings := make([]string, 0)
   348  	for nesting := d.Nesting(); d.NextBlock(nesting); {
   349  		optionName := d.Val()
   350  		switch optionName {
   351  		case "all_tags":
   352  			if d.CountRemainingArgs() == 0 {
   353  				return d.ArgErr()
   354  			}
   355  			p.AllTags = append(p.AllTags, d.RemainingArgs()...)
   356  		case "any_tag":
   357  			if d.CountRemainingArgs() == 0 {
   358  				return d.ArgErr()
   359  			}
   360  			p.AnyTag = append(p.AnyTag, d.RemainingArgs()...)
   361  		case "public_key_algorithm":
   362  			if hasPublicKeyAlgorithm {
   363  				return d.Errf("duplicate %s option '%s'", wrapper, optionName)
   364  			}
   365  			if d.CountRemainingArgs() != 1 {
   366  				return d.ArgErr()
   367  			}
   368  			d.NextArg()
   369  			if err := p.PublicKeyAlgorithm.UnmarshalJSON([]byte(d.Val())); err != nil {
   370  				return d.Errf("parsing %s option '%s': %v", wrapper, optionName, err)
   371  			}
   372  			hasPublicKeyAlgorithm = true
   373  		case "serial_number":
   374  			if d.CountRemainingArgs() == 0 {
   375  				return d.ArgErr()
   376  			}
   377  			for d.NextArg() {
   378  				val, bi := d.Val(), struct{ big.Int }{}
   379  				_, ok := bi.SetString(val, 10)
   380  				if !ok {
   381  					return d.Errf("parsing %s option '%s': invalid big.int value %s", wrapper, optionName, val)
   382  				}
   383  				serialNumberStrings = append(serialNumberStrings, bi.String())
   384  			}
   385  		case "subject_organization":
   386  			if d.CountRemainingArgs() == 0 {
   387  				return d.ArgErr()
   388  			}
   389  			p.SubjectOrganization = append(p.SubjectOrganization, d.RemainingArgs()...)
   390  		default:
   391  			return d.ArgErr()
   392  		}
   393  
   394  		// No nested blocks are supported
   395  		if d.NextBlock(nesting + 1) {
   396  			return d.Errf("malformed %s option '%s': blocks are not supported", wrapper, optionName)
   397  		}
   398  	}
   399  
   400  	// caddytls.bigInt struct is not exported. That's why we can't append directly to SerialNumber list above.
   401  	// TODO: remove this workaround after the code is moved to caddyserver/caddy repo
   402  	if len(serialNumberStrings) > 0 {
   403  		serialNumbersRaw, err := json.Marshal(serialNumberStrings)
   404  		if err != nil {
   405  			return err
   406  		}
   407  		if err = json.Unmarshal(serialNumbersRaw, &p.SerialNumber); err != nil {
   408  			return err
   409  		}
   410  	}
   411  
   412  	return nil
   413  }