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  )