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 }