github.com/mholt/caddy-l4@v0.0.0-20241104153248-ec8fae209322/modules/l4openvpn/matcher.go (about) 1 // Copyright 2024 VNXME 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 l4openvpn 16 17 import ( 18 "encoding/binary" 19 "errors" 20 "io" 21 "net" 22 "strings" 23 24 "github.com/caddyserver/caddy/v2" 25 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" 26 27 "github.com/mholt/caddy-l4/layer4" 28 ) 29 30 func init() { 31 caddy.RegisterModule(&MatchOpenVPN{}) 32 } 33 34 // MatchOpenVPN is able to match OpenVPN connections. 35 type MatchOpenVPN struct { 36 37 // Modes contains a list of supported OpenVPN modes to match against incoming client reset messages: 38 // 39 // - `plain` mode messages have no replay protection, authentication or encryption; 40 // 41 // - `auth` mode messages have no encryption, but provide for replay protection and authentication 42 // with a pre-shared 2048-bit group key, a variable key direction, and plenty digest algorithms; 43 // 44 // - 'crypt' mode messages feature replay protection, authentication and encryption with 45 // a pre-shared 2048-bit group key, a fixed key direction, and SHA-256 + AES-256-CTR algorithms; 46 // 47 // - `crypt2` mode messages are essentially `crypt` messages with an individual 2048-bit client key 48 // used for authentication and encryption attached to client reset messages in a protected form 49 // (a 1024-bit server key is used for its authentication end encryption). 50 // 51 // Notes: Each mode shall only be present once in the list. Values in the list are case-insensitive. 52 // If the list is empty, MatchOpenVPN will consider all modes as accepted and try them one by one. 53 Modes []string `json:"modes,omitempty"` 54 55 /* 56 * Fields relevant to the auth, crypt and crypt2 modes: 57 */ 58 59 // IgnoreCrypto makes MatchOpenVPN skip decryption and authentication if set to true. 60 // 61 // Notes: IgnoreCrypto impacts the auth, crypt and crypt2 modes at once and makes sense only if/when 62 // the relevant static keys are provided. If neither GroupKey nor GroupKeyFile is set, decryption 63 // (if applicable) and authentication are automatically skipped in the auth and crypt modes only. If 64 // neither ServerKey nor ServerKeyFile is provided, decryption and authentication are automatically 65 // skipped in the crypt2 mode (unless there is a client key). If neither ClientKeys nor ClientKeyFiles 66 // are provided, decryption and authentication are automatically skipped in the crypt2 mode (unless 67 // there is a server key). In the crypt2 mode, when there is a client key and there is no server key, 68 // decryption of a WrappedKey is impossible, and this part of the incoming message is authenticated by 69 // comparing it with what has been included in the matching client key. 70 IgnoreCrypto bool `json:"ignore_crypto,omitempty"` 71 // IgnoreTimestamp makes MatchOpenVPN skip replay timestamps validation if set to true. 72 // 73 // Note: A 30-seconds time window is applicable by default, i.e. a timestamp of up to 15 seconds behind 74 // or ahead of now is accepted. 75 IgnoreTimestamp bool `json:"ignore_timestamp,omitempty"` 76 77 /* 78 * Fields relevant to the auth and crypt modes: 79 */ 80 81 // GroupKey contains a hex string representing a pre-shared 2048-bit group key. This key may be 82 // present in OpenVPN config files inside `<tls-auth/>` or `<tls-crypt/>` blocks or generated with 83 // `openvpn --genkey tls-auth|tls-crypt` command. No comments (starting with '#' or '-') are allowed. 84 GroupKey string `json:"group_key,omitempty"` 85 // GroupKeyFile is a path to a file containing a pre-shared 2048-bit group key which may be present 86 // in OpenVPN config files after `tls-auth` or `tls-crypt` directives. It is the same key as the one 87 // GroupKey introduces, so these fields are mutually exclusive. If both are set, GroupKey always takes 88 // precedence. Any comments in the file (starting with '#' or '-') are ignored. 89 GroupKeyFile string `json:"group_key_file,omitempty"` 90 91 /* 92 * Fields relevant to the auth mode only: 93 */ 94 95 // AuthDigest is a name of a digest algorithm used for authentication (HMAC generation and validation) of 96 // the auth mode messages. If no value is provided, MatchOpenVPN will try all the algorithms it supports. 97 // 98 // Notes: OpenVPN binaries may support a larger number of digest algorithms thanks to the OpenSSL library 99 // used under the hood. A few legacy and exotic digest algorithms are known to be missing, so IgnoreCrypto 100 // may be set to true to ensure successful message matching if a desired digest algorithm isn't listed below. 101 // 102 // List of the supported digest algorithms: 103 // - MD5 104 // - SHA-1 105 // - RIPEMD-160 106 // - SHA-224 107 // - SHA-256 108 // - SHA-384 109 // - SHA-512 110 // - SHA-512/224 111 // - SHA-512/256 112 // - SHA3-224 113 // - SHA3-256 114 // - SHA3-384 115 // - SHA3-512 116 // - BLAKE2s-256 117 // - BLAKE2b-512 118 // - SHAKE-128 119 // - SHAKE-256 120 // 121 // Note: Digest algorithm names are recognised in a number of popular notations, including lowercase. 122 // Please, refer to the source code (AuthDigests variable in crypto.go) for details. 123 AuthDigest string `json:"auth_digest,omitempty"` 124 // GroupKeyDirection is a group key direction and may contain one of the following three values: 125 // 126 // - `normal` means the server config has `tls-auth [...] 0` or `key-direction 0`, 127 // while the client configs have `tls-auth [...] 1` or `key-direction 1`; 128 // 129 // - `inverse` means the server config has `tls-auth [...] 1` or `key-direction 1`, 130 // while the client config have `tls-auth [...] 0` or `key-direction 0`; 131 // 132 // - `bidi` or `bidirectional` means key direction is omitted (e.g. `tls-auth [...]`) 133 // in both the server config and client configs. 134 // 135 // Notes: Values are case-insensitive. If no value is specified, the normal key direction is implied. 136 // The inverse key direction is a violation of the OpenVPN official recommendations, and the bidi one 137 // provides for a lower level of DoS and message replay attacks resilience. 138 GroupKeyDirection string `json:"group_key_direction,omitempty"` 139 140 /* 141 * Fields relevant to the crypt2 mode only: 142 */ 143 144 // ClientKeys contains a list of base64 strings representing 2048-bit client keys (each one in a decrypted 145 // form followed by an encrypted and authenticated form also known as WKc in the OpenVPN docs). These keys 146 // may be present in OpenVPN client config files inside `<tls-crypt-v2/>` block or generated with `openvpn 147 // --tls-crypt-v2 [server.key] --genkey tls-crypt-v2-client` command. No comments (starting with '#' or '-') 148 // are allowed. 149 ClientKeys []string `json:"client_keys,omitempty"` 150 // ClientKeyFiles is a list of paths to files containing 2048-bit client key which may be present in OpenVPN 151 // config files after `tls-crypt-v2` directive. These are the same keys as those ClientKeys introduce, but 152 // these fields are complementary. If both are set, a joint list of client keys is created. Any comments in 153 // the files (starting with '#' or '-') are ignored. 154 ClientKeyFiles []string `json:"client_key_files,omitempty"` 155 156 // ServerKey contains a base64 string representing a 1024-bit server key used only for authentication and 157 // encryption of client keys. This key may be present in OpenVPN server config files inside `<tls-crypt-v2/>` 158 // block or generated with `openvpn --genkey tls-crypt-v2-server` command. No comments (starting with '#' 159 // or '-') are allowed. 160 ServerKey string `json:"server_key,omitempty"` 161 // ServerKeyFile is a path to a file containing a 1024-bit server key which may be present in OpenVPN 162 // config files after `tls-crypt-v2` directive. It is the same key as the one ServerKey introduces, so 163 // these fields are mutually exclusive. If both are set, ServerKey always takes precedence. Any comments 164 // in the file (starting with '#' or '-') are ignored. 165 ServerKeyFile string `json:"server_key_file,omitempty"` 166 167 /* 168 * Internal fields: 169 */ 170 171 acceptAuth bool 172 acceptCrypt bool 173 acceptCrypt2 bool 174 acceptPlain bool 175 176 groupKeyAuth *StaticKey 177 groupKeyCrypt *StaticKey 178 179 authDigest *AuthDigest 180 lastDigest *AuthDigest 181 182 clientKeys []*WrappedKey 183 serverKey *StaticKey 184 } 185 186 // CaddyModule returns the Caddy module information. 187 func (m *MatchOpenVPN) CaddyModule() caddy.ModuleInfo { 188 return caddy.ModuleInfo{ 189 ID: "layer4.matchers.openvpn", 190 New: func() caddy.Module { return new(MatchOpenVPN) }, 191 } 192 } 193 194 // Match returns true if the connection looks like OpenVPN. 195 func (m *MatchOpenVPN) Match(cx *layer4.Connection) (bool, error) { 196 var err error 197 var l, n int 198 199 // Prepare a 3-byte buffer 200 buf := make([]byte, LengthBytesTotal+OpcodeKeyIDBytesTotal) 201 202 // Do TCP-specific reads and checks 203 _, isTCP := cx.LocalAddr().(*net.TCPAddr) 204 if isTCP { 205 // Read 2 bytes containing the remaining bytes length 206 _, err = io.ReadFull(cx, buf[:LengthBytesTotal]) 207 if err != nil { 208 return false, err 209 } 210 211 // Validate the remaining bytes length 212 l = int(binary.BigEndian.Uint16(buf[:LengthBytesTotal])) 213 if l < MessagePlainBytesTotal || l > MessageCrypt2BytesMax { 214 return false, nil 215 } 216 } 217 218 // Read 1 byte containing MessageHeader 219 _, err = io.ReadFull(cx, buf[LengthBytesTotal:]) 220 if err != nil { 221 return false, err 222 } 223 224 // Parse MessageHeader 225 hdr := &MessageHeader{} 226 if err = hdr.FromBytes(buf[LengthBytesTotal:]); err != nil { 227 return false, nil 228 } 229 230 // Validate MessageHeader.KeyID 231 if hdr.KeyID > 0 { 232 return false, nil 233 } 234 235 var mp *MessagePlain 236 var ma *MessageAuth 237 var mc *MessageCrypt 238 var mr *MessageCrypt2 239 240 if hdr.Opcode == OpcodeControlHardResetClientV2 && (m.acceptPlain || m.acceptAuth || m.acceptCrypt) { 241 if isTCP { 242 if l > MessageAuthBytesMax { 243 return false, nil 244 } 245 246 buf = make([]byte, l-OpcodeKeyIDBytesTotal+1) 247 n, err = io.ReadAtLeast(cx, buf, l-OpcodeKeyIDBytesTotal) 248 if err != nil || n > l-OpcodeKeyIDBytesTotal { 249 return false, err 250 } 251 } else { 252 buf = make([]byte, MessageAuthBytesMaxHL+1) 253 n, err = io.ReadAtLeast(cx, buf, 1) 254 if err != nil || n < MessagePlainBytesTotalHL || n > MessageAuthBytesMaxHL { 255 return false, err 256 } 257 } 258 259 if m.acceptPlain { 260 // Parse and validate MessagePlain 261 mp = &MessagePlain{} 262 err = mp.FromBytesHeadless(buf[:n], hdr) 263 if err == nil && mp.Match() { 264 return true, nil 265 } 266 } 267 268 if m.acceptAuth { 269 // Parse and validate MessageAuth 270 ma = &MessageAuth{MessageTraitAuth: MessageTraitAuth{Digest: m.lastDigest}} 271 err = ma.FromBytesHeadless(buf[:n], hdr) 272 if err == nil && ma.Match(m.IgnoreTimestamp, m.IgnoreCrypto, m.authDigest, m.groupKeyAuth) { 273 m.lastDigest = ma.Digest 274 return true, nil 275 } 276 } 277 278 if m.acceptCrypt { 279 // Parse and validate MessageCrypt 280 mc = &MessageCrypt{} 281 err = mc.FromBytesHeadless(buf[:n], hdr) 282 if err == nil && mc.Match(m.IgnoreTimestamp, m.IgnoreCrypto, nil, m.groupKeyCrypt) { 283 return true, nil 284 } 285 } 286 } 287 288 if hdr.Opcode == OpcodeControlHardResetClientV3 && m.acceptCrypt2 { 289 if isTCP { 290 if l < MessageCrypt2BytesMin { 291 return false, nil 292 } 293 294 buf = make([]byte, l-OpcodeKeyIDBytesTotal+1) 295 n, err = io.ReadAtLeast(cx, buf, l-OpcodeKeyIDBytesTotal) 296 if err != nil || n > l-OpcodeKeyIDBytesTotal { 297 return false, err 298 } 299 } else { 300 buf = make([]byte, MessageCrypt2BytesMaxHL+1) 301 n, err = io.ReadAtLeast(cx, buf, 1) 302 if err != nil || n < MessageCrypt2BytesMinHL || n > MessageCrypt2BytesMaxHL { 303 return false, err 304 } 305 } 306 307 // Parse and validate MessageCrypt2 308 mr = &MessageCrypt2{} 309 err = mr.FromBytesHeadless(buf[:n], hdr) 310 if err == nil && mr.Match(m.IgnoreTimestamp, m.IgnoreCrypto, nil, m.serverKey, m.clientKeys) { 311 return true, nil 312 } 313 } 314 315 return false, nil 316 } 317 318 // Provision prepares m's internal structures. 319 func (m *MatchOpenVPN) Provision(_ caddy.Context) error { 320 repl := caddy.NewReplacer() 321 322 if len(m.Modes) > 0 { 323 for _, mode := range m.Modes { 324 mode = strings.ToLower(repl.ReplaceAll(mode, "")) 325 switch mode { 326 case ModeAuth: 327 m.acceptAuth = true 328 case ModeCrypt: 329 m.acceptCrypt = true 330 case ModeCrypt2: 331 m.acceptCrypt2 = true 332 case ModePlain: 333 m.acceptPlain = true 334 default: 335 return ErrInvalidMode 336 } 337 } 338 } else { 339 m.acceptAuth, m.acceptCrypt, m.acceptCrypt2, m.acceptPlain = true, true, true, true 340 } 341 342 var gkdBidi, gkdInverse bool 343 m.GroupKeyDirection = strings.ToLower(repl.ReplaceAll(m.GroupKeyDirection, "")) 344 if len(m.GroupKeyDirection) > 0 { 345 switch m.GroupKeyDirection { 346 case GroupKeyDirectionBidi, GroupKeyDirectionBidi2: 347 gkdBidi = true 348 case GroupKeyDirectionInverse: 349 gkdInverse = true 350 case GroupKeyDirectionNormal: 351 break 352 default: 353 return ErrInvalidGroupKeyDirection 354 } 355 } 356 357 m.GroupKey = repl.ReplaceAll(m.GroupKey, "") 358 if len(m.GroupKey) > 0 { 359 sk := &StaticKey{} 360 if err := sk.FromHex(m.GroupKey); err != nil { 361 return err 362 } 363 if len(sk.KeyBytes) != StaticKeyBytesTotal { 364 return ErrInvalidGroupKey 365 } 366 m.groupKeyAuth, m.groupKeyCrypt = &StaticKey{Bidi: gkdBidi, Inverse: gkdInverse, KeyBytes: sk.KeyBytes}, sk 367 } else { 368 m.GroupKeyFile = repl.ReplaceAll(m.GroupKeyFile, "") 369 if len(m.GroupKeyFile) > 0 { 370 sk := &StaticKey{} 371 if err := sk.FromGroupKeyFile(m.GroupKeyFile); err != nil { 372 return err 373 } 374 if len(sk.KeyBytes) != StaticKeyBytesTotal { 375 return ErrInvalidGroupKey 376 } 377 m.groupKeyAuth, m.groupKeyCrypt = &StaticKey{Bidi: gkdBidi, Inverse: gkdInverse, KeyBytes: sk.KeyBytes}, sk 378 } 379 } 380 381 m.AuthDigest = repl.ReplaceAll(m.AuthDigest, "") 382 if len(m.AuthDigest) > 0 { 383 m.authDigest = AuthDigestFindByName(m.AuthDigest) 384 if m.authDigest == nil { 385 return ErrInvalidAuthDigest 386 } 387 } 388 389 m.ServerKey = repl.ReplaceAll(m.ServerKey, "") 390 if len(m.ServerKey) > 0 { 391 sk := &StaticKey{} 392 if err := sk.FromBase64(m.ServerKey); err != nil { 393 return err 394 } 395 if len(sk.KeyBytes) != StaticKeyBytesHalf { 396 return ErrInvalidServerKey 397 } 398 m.serverKey = sk 399 } else { 400 m.ServerKeyFile = repl.ReplaceAll(m.ServerKeyFile, "") 401 if len(m.ServerKeyFile) > 0 { 402 sk := &StaticKey{} 403 if err := sk.FromServerKeyFile(m.ServerKeyFile); err != nil { 404 return err 405 } 406 if len(sk.KeyBytes) != StaticKeyBytesHalf { 407 return ErrInvalidServerKey 408 } 409 m.serverKey = sk 410 } 411 } 412 413 if len(m.ClientKeys) > 0 { 414 for _, clientKey := range m.ClientKeys { 415 clientKey = repl.ReplaceAll(clientKey, "") 416 if len(clientKey) > 0 { 417 ck := &WrappedKey{} 418 if err := ck.FromBase64(clientKey); err != nil { 419 return err 420 } 421 422 if len(ck.StaticKey.KeyBytes) != StaticKeyBytesTotal || 423 (m.serverKey != nil && !ck.DecryptAndAuthenticate(nil, m.serverKey)) { 424 return ErrInvalidClientKey 425 } 426 427 m.clientKeys = append(m.clientKeys, ck) 428 } 429 } 430 } else if len(m.ClientKeyFiles) > 0 { 431 for _, clientKeyFile := range m.ClientKeyFiles { 432 clientKeyFile = repl.ReplaceAll(clientKeyFile, "") 433 if len(clientKeyFile) > 0 { 434 ck := &WrappedKey{} 435 if err := ck.FromClientKeyFile(clientKeyFile); err != nil { 436 return err 437 } 438 439 if len(ck.StaticKey.KeyBytes) != StaticKeyBytesTotal || 440 (m.serverKey != nil && !ck.DecryptAndAuthenticate(nil, m.serverKey)) { 441 return ErrInvalidClientKey 442 } 443 444 m.clientKeys = append(m.clientKeys, ck) 445 } 446 } 447 } 448 449 return nil 450 } 451 452 // UnmarshalCaddyfile sets up the MatchOpenVPN from Caddyfile tokens. Syntax: 453 // 454 // openvpn { 455 // modes <plain|auth|crypt|crypt2> [<...>] 456 // 457 // ignore_crypto 458 // ignore_timestamp 459 // 460 // group_key <hex> 461 // group_key_file <path> 462 // 463 // auth_digest <digest> 464 // group_key_direction <normal|inverse|bidi|bidirectional> 465 // 466 // server_key <base64> 467 // server_key_file <path> 468 // 469 // client_key <base64> 470 // client_key_file <path> 471 // } 472 // openvpn 473 // 474 // Note: multiple 'client_key' and 'client_key_file' options are allowed. 475 func (m *MatchOpenVPN) UnmarshalCaddyfile(d *caddyfile.Dispenser) error { 476 _, wrapper := d.Next(), d.Val() // consume wrapper name 477 478 // No same-line arguments are supported 479 if d.CountRemainingArgs() > 0 { 480 return d.ArgErr() 481 } 482 483 errDuplicate := func(optionName string) error { 484 return d.Errf("duplicate %s option '%s'", wrapper, optionName) 485 } 486 487 errGroupKeyMutex := func() error { 488 return d.Errf("%s options 'group_key' and `group_key_file` are mutually exclusive", wrapper) 489 } 490 491 errServerKeyMutex := func() error { 492 return d.Errf("%s options 'server_key' and `server_key_file` are mutually exclusive", wrapper) 493 } 494 495 var hasAuthDigest, hasGroupKey, hasGroupKeyDirection, hasGroupKeyFile, hasIgnoreCrypto, hasIgnoreTimestamp, 496 hasModes, hasServerKey, hasServerKeyFile bool 497 for nesting := d.Nesting(); d.NextBlock(nesting); { 498 optionName := d.Val() 499 switch optionName { 500 case "modes": 501 if hasModes { 502 return errDuplicate(optionName) 503 } 504 if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 4 { 505 return d.ArgErr() 506 } 507 m.Modes, hasModes = append(m.Modes, d.RemainingArgs()...), true 508 case "ignore_crypto": 509 if hasIgnoreCrypto { 510 return errDuplicate(optionName) 511 } 512 if d.CountRemainingArgs() > 0 { 513 return d.ArgErr() 514 } 515 m.IgnoreCrypto, hasIgnoreCrypto = true, true 516 case "ignore_timestamp": 517 if hasIgnoreTimestamp { 518 return errDuplicate(optionName) 519 } 520 if d.CountRemainingArgs() > 0 { 521 return d.ArgErr() 522 } 523 m.IgnoreTimestamp, hasIgnoreTimestamp = true, true 524 case "group_key": 525 if hasGroupKeyFile { 526 return errGroupKeyMutex() 527 } 528 if hasGroupKey { 529 return errDuplicate(optionName) 530 } 531 if d.CountRemainingArgs() != 1 { 532 return d.ArgErr() 533 } 534 _, m.GroupKey, hasGroupKey = d.NextArg(), d.Val(), true 535 case "group_key_file": 536 if hasGroupKey { 537 return errGroupKeyMutex() 538 } 539 if hasGroupKeyFile { 540 return errDuplicate(optionName) 541 } 542 if d.CountRemainingArgs() != 1 { 543 return d.ArgErr() 544 } 545 _, m.GroupKeyFile, hasGroupKeyFile = d.NextArg(), d.Val(), true 546 case "auth_digest": 547 if hasAuthDigest { 548 return errDuplicate(optionName) 549 } 550 if d.CountRemainingArgs() != 1 { 551 return d.ArgErr() 552 } 553 _, m.AuthDigest, hasAuthDigest = d.NextArg(), d.Val(), true 554 case "group_key_direction": 555 if hasGroupKeyDirection { 556 return errDuplicate(optionName) 557 } 558 if d.CountRemainingArgs() != 1 { 559 return d.ArgErr() 560 } 561 _, m.GroupKeyDirection, hasGroupKeyDirection = d.NextArg(), d.Val(), true 562 case "server_key": 563 if hasServerKeyFile { 564 return errServerKeyMutex() 565 } 566 if hasServerKey { 567 return errDuplicate(optionName) 568 } 569 if d.CountRemainingArgs() != 1 { 570 return d.ArgErr() 571 } 572 _, m.ServerKey, hasServerKey = d.NextArg(), d.Val(), true 573 case "server_key_file": 574 if hasServerKey { 575 return errServerKeyMutex() 576 } 577 if hasServerKeyFile { 578 return errDuplicate(optionName) 579 } 580 if d.CountRemainingArgs() != 1 { 581 return d.ArgErr() 582 } 583 _, m.ServerKeyFile, hasServerKeyFile = d.NextArg(), d.Val(), true 584 case "client_key": 585 if d.CountRemainingArgs() != 1 { 586 return d.ArgErr() 587 } 588 m.ClientKeys = append(m.ClientKeys, d.RemainingArgs()...) 589 case "client_key_file": 590 if d.CountRemainingArgs() != 1 { 591 return d.ArgErr() 592 } 593 m.ClientKeyFiles = append(m.ClientKeyFiles, d.RemainingArgs()...) 594 default: 595 return d.ArgErr() 596 } 597 598 // No nested blocks are supported 599 if d.NextBlock(nesting + 1) { 600 return d.Errf("malformed %s option '%s': nested blocks are not supported", wrapper, optionName) 601 } 602 } 603 604 return nil 605 } 606 607 // Interface guards 608 var ( 609 _ caddy.Provisioner = (*MatchOpenVPN)(nil) 610 _ caddyfile.Unmarshaler = (*MatchOpenVPN)(nil) 611 _ layer4.ConnMatcher = (*MatchOpenVPN)(nil) 612 ) 613 614 var ( 615 ErrInvalidAuthDigest = errors.New("invalid auth digest") 616 ErrInvalidClientKey = errors.New("invalid client key") 617 ErrInvalidGroupKey = errors.New("invalid group key") 618 ErrInvalidGroupKeyDirection = errors.New("invalid group key direction") 619 ErrInvalidMode = errors.New("invalid mode") 620 ErrInvalidServerKey = errors.New("invalid server key") 621 ) 622 623 const ( 624 GroupKeyDirectionBidi = "bidi" 625 GroupKeyDirectionBidi2 = "bidirectional" 626 GroupKeyDirectionInverse = "inverse" 627 GroupKeyDirectionNormal = "normal" 628 629 ModeAuth = "auth" 630 ModeCrypt = "crypt" 631 ModeCrypt2 = "crypt2" 632 ModePlain = "plain" 633 )