From e7828c4ddf54e5a0df421f1d8915e4b04cd67c4d Mon Sep 17 00:00:00 2001
From: vnxme <46669194+vnxme@users.noreply.github.com>
Date: Wed, 2 Oct 2024 23:42:47 +0300
Subject: [PATCH] OpenVPN matcher: initial commit
---
README.md | 1 +
imports.go | 1 +
.../gd_matcher_openvpn.caddytest | 169 ++++
modules/l4openvpn/crypto.go | 489 +++++++++
modules/l4openvpn/matcher.go | 633 ++++++++++++
modules/l4openvpn/matcher_test.go | 551 ++++++++++
modules/l4openvpn/messages.go | 948 ++++++++++++++++++
7 files changed, 2792 insertions(+)
create mode 100644 integration/caddyfile_adapt/gd_matcher_openvpn.caddytest
create mode 100644 modules/l4openvpn/crypto.go
create mode 100644 modules/l4openvpn/matcher.go
create mode 100644 modules/l4openvpn/matcher_test.go
create mode 100644 modules/l4openvpn/messages.go
diff --git a/README.md b/README.md
index a7600ae..d7f6a56 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ Current matchers:
- **layer4.matchers.http** - matches connections that start with HTTP requests. In addition, any [`http.matchers` modules](https://caddyserver.com/docs/modules/) can be used for matching on HTTP-specific properties of requests, such as header or path. Note that only the first request of each connection can be used for matching.
- **layer4.matchers.local_ip** - matches connections based on local IP (or CIDR range).
- **layer4.matchers.not** - matches connections that aren't matched by inner matcher sets.
+- **layer4.matchers.openvpn** - matches connections that look like [OpenVPN](https://openvpn.net/community-resources/openvpn-protocol/) connections.
- **layer4.matchers.postgres** - matches connections that look like Postgres connections.
- **layer4.matchers.proxy_protocol** - matches connections that start with [HAPROXY proxy protocol](https://www.haproxy.org/download/1.8/doc/proxy-protocol.txt).
- **layer4.matchers.rdp** - matches connections that look like [RDP](https://winprotocoldoc.blob.core.windows.net/productionwindowsarchives/MS-RDPBCGR/%5BMS-RDPBCGR%5D.pdf).
diff --git a/imports.go b/imports.go
index 83274d4..6b4c2f8 100644
--- a/imports.go
+++ b/imports.go
@@ -21,6 +21,7 @@ import (
_ "github.com/mholt/caddy-l4/modules/l4dns"
_ "github.com/mholt/caddy-l4/modules/l4echo"
_ "github.com/mholt/caddy-l4/modules/l4http"
+ _ "github.com/mholt/caddy-l4/modules/l4openvpn"
_ "github.com/mholt/caddy-l4/modules/l4postgres"
_ "github.com/mholt/caddy-l4/modules/l4proxy"
_ "github.com/mholt/caddy-l4/modules/l4proxyprotocol"
diff --git a/integration/caddyfile_adapt/gd_matcher_openvpn.caddytest b/integration/caddyfile_adapt/gd_matcher_openvpn.caddytest
new file mode 100644
index 0000000..8cfe4db
--- /dev/null
+++ b/integration/caddyfile_adapt/gd_matcher_openvpn.caddytest
@@ -0,0 +1,169 @@
+{
+ layer4 {
+ :8843 {
+ @plain openvpn {
+ modes plain
+ }
+ route @plain {
+ proxy localhost:1194
+ }
+ @auth openvpn {
+ modes auth
+ auth_digest sha256
+ group_key_direction normal
+ group_key_file /etc/openvpn/ta.key
+ }
+ route @auth {
+ proxy localhost:1195
+ }
+ @crypt openvpn {
+ modes crypt
+ group_key 21d94830510107f8753d3b6f3145e01ded37075115afcb0538ecdd8503ee96637218c9ed38d908d594231d7d143c73da5055310f89d336da99c8b3dcb18909c79dd44f540670ebc0f120beb7211e96839cb542572c48bfa7ffaa9a22cb8304b7869b92f4442918e598745bb78ac8877f02b00a7cdef3f2446c130d39a7c451269ef399fd6029cdfc80a7c604041312ab0a969bc906bdee6e6d707afdcbe8c7fb97beb66049c3d328340775025433ceba1e38008a826cf92443d903106199373bdadd9c2c735cf481e580db4e81b99f12e3f46b6159c687cd1b9e689f7712573c0f02735a45573dfb5cd55cf4649423892c7e91f439bdd7337a8ceebd302cfbfa
+ }
+ route @crypt {
+ proxy localhost:1196
+ }
+ @crypt2 openvpn {
+ modes crypt2
+ server_key_file /etc/openvpn/v2-server.key
+ }
+ route @crypt2 {
+ proxy localhost:1197
+ }
+ route {
+ tls
+ proxy localhost:8080
+ }
+ }
+ }
+}
+----------
+{
+ "apps": {
+ "layer4": {
+ "servers": {
+ "srv0": {
+ "listen": [
+ ":8843"
+ ],
+ "routes": [
+ {
+ "match": [
+ {
+ "openvpn": {
+ "modes": [
+ "plain"
+ ]
+ }
+ }
+ ],
+ "handle": [
+ {
+ "handler": "proxy",
+ "upstreams": [
+ {
+ "dial": [
+ "localhost:1194"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "openvpn": {
+ "modes": [
+ "auth"
+ ],
+ "group_key_file": "/etc/openvpn/ta.key",
+ "auth_digest": "sha256",
+ "group_key_direction": "normal"
+ }
+ }
+ ],
+ "handle": [
+ {
+ "handler": "proxy",
+ "upstreams": [
+ {
+ "dial": [
+ "localhost:1195"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "openvpn": {
+ "modes": [
+ "crypt"
+ ],
+ "group_key": "21d94830510107f8753d3b6f3145e01ded37075115afcb0538ecdd8503ee96637218c9ed38d908d594231d7d143c73da5055310f89d336da99c8b3dcb18909c79dd44f540670ebc0f120beb7211e96839cb542572c48bfa7ffaa9a22cb8304b7869b92f4442918e598745bb78ac8877f02b00a7cdef3f2446c130d39a7c451269ef399fd6029cdfc80a7c604041312ab0a969bc906bdee6e6d707afdcbe8c7fb97beb66049c3d328340775025433ceba1e38008a826cf92443d903106199373bdadd9c2c735cf481e580db4e81b99f12e3f46b6159c687cd1b9e689f7712573c0f02735a45573dfb5cd55cf4649423892c7e91f439bdd7337a8ceebd302cfbfa"
+ }
+ }
+ ],
+ "handle": [
+ {
+ "handler": "proxy",
+ "upstreams": [
+ {
+ "dial": [
+ "localhost:1196"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "match": [
+ {
+ "openvpn": {
+ "modes": [
+ "crypt2"
+ ],
+ "server_key_file": "/etc/openvpn/v2-server.key"
+ }
+ }
+ ],
+ "handle": [
+ {
+ "handler": "proxy",
+ "upstreams": [
+ {
+ "dial": [
+ "localhost:1197"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "handle": [
+ {
+ "handler": "tls"
+ },
+ {
+ "handler": "proxy",
+ "upstreams": [
+ {
+ "dial": [
+ "localhost:8080"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/modules/l4openvpn/crypto.go b/modules/l4openvpn/crypto.go
new file mode 100644
index 0000000..03a6274
--- /dev/null
+++ b/modules/l4openvpn/crypto.go
@@ -0,0 +1,489 @@
+// Copyright 2024 VNXME
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package l4openvpn
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/hmac"
+ "crypto/md5"
+ "crypto/sha1"
+ "crypto/sha256"
+ "crypto/sha512"
+ "encoding/base64"
+ "encoding/hex"
+ "errors"
+ "golang.org/x/crypto/blake2b"
+ "golang.org/x/crypto/blake2s"
+ "golang.org/x/crypto/ripemd160"
+ "golang.org/x/crypto/sha3"
+ "hash"
+ "io"
+ "os"
+ "regexp"
+ "slices"
+ "strings"
+)
+
+// AuthDigest represents a digest used for computing HMACs of control messages.
+type AuthDigest struct {
+ // Creator is a function returning something that implements hash.Hash interface.
+ // Creator is required whenever Generator is nil.
+ Creator func() hash.Hash
+ // Generator is a function returning an HMAC for a given set of key and plain bytes.
+ // Generator is optional, but it takes precedence over Creator.
+ Generator func(key, plain []byte) []byte
+ // Names contains a list of digest names in various notations.
+ Names []string
+ // Size is a number of bytes all computed HMACs will have.
+ Size int
+}
+
+// HMACGenerateOnClient computes an HMAC of a given plain text with a part of a given StaticKey
+// (to be sent by the client to the server).
+func (ad *AuthDigest) HMACGenerateOnClient(sk *StaticKey, plain []byte) []byte {
+ key := sk.GetClientAuthKey(ad.Size)
+ if ad.Generator != nil {
+ return ad.Generator(key, plain)
+ }
+ return HMACCreateAndGenerate(ad.Creator, key, plain)
+}
+
+// HMACGenerateOnServer computes an HMAC of a given plain text with a part of a given StaticKey
+// (to be sent by the server to the client).
+func (ad *AuthDigest) HMACGenerateOnServer(sk *StaticKey, plain []byte) []byte {
+ key := sk.GetServerAuthKey(ad.Size)
+ if ad.Generator != nil {
+ return ad.Generator(key, plain)
+ }
+ return HMACCreateAndGenerate(ad.Creator, key, plain)
+}
+
+// HMACValidateOnClient compares an expected HMAC (as received by the client from the server)
+// with an actual HMAC of a given plain text it computes with a part of a given StaticKey.
+func (ad *AuthDigest) HMACValidateOnClient(sk *StaticKey, plain, expected []byte) bool {
+ actual := ad.HMACGenerateOnServer(sk, plain)
+ return hmac.Equal(actual, expected)
+}
+
+// HMACValidateOnServer compares an expected HMAC (as received by the server from the client)
+// with an actual HMAC of a given plain text it computes with a part of a given StaticKey.
+func (ad *AuthDigest) HMACValidateOnServer(sk *StaticKey, plain, expected []byte) bool {
+ actual := ad.HMACGenerateOnClient(sk, plain)
+ return hmac.Equal(actual, expected)
+}
+
+// CryptCipher represents a cipher used for en/decrypting control messages.
+type CryptCipher struct {
+ // Decryptor is a function returning plain bytes from encrypted ones.
+ Decryptor func(key, iv, encrypted []byte) []byte
+ // Encryptor is a function returning encrypted bytes from plain ones.
+ Encryptor func(key, iv, plain []byte) []byte
+ // Names contains a list of cipher names in various notations.
+ Names []string
+ // SizeBlock is a number of bytes an initialization vector (IV) must have.
+ SizeBlock int
+ // SizeKey is a number of bytes an en/decryption key must have.
+ SizeKey int
+}
+
+// DecryptOnClient decrypts given encrypted bytes with a part of a given StaticKey
+// (as received by the client from the server).
+func (cc *CryptCipher) DecryptOnClient(sk *StaticKey, iv, encrypted []byte) []byte {
+ key := sk.GetClientDecryptKey(cc.SizeKey)
+ return cc.Decryptor(key, iv, encrypted)
+}
+
+// DecryptOnServer decrypts given encrypted bytes with a part of a given StaticKey
+// (as received by the server from the client).
+func (cc *CryptCipher) DecryptOnServer(sk *StaticKey, iv, encrypted []byte) []byte {
+ key := sk.GetServerDecryptKey(cc.SizeKey)
+ return cc.Decryptor(key, iv, encrypted)
+}
+
+// EncryptOnClient encrypts given plain bytes with a part of a given StaticKey
+// (to be sent by the client to the server).
+func (cc *CryptCipher) EncryptOnClient(sk *StaticKey, iv, plain []byte) []byte {
+ key := sk.GetClientEncryptKey(cc.SizeKey)
+ return cc.Encryptor(key, iv, plain)
+}
+
+// EncryptOnServer encrypts given plain bytes with a part of a given StaticKey
+// (to be sent by the server to the client).
+func (cc *CryptCipher) EncryptOnServer(sk *StaticKey, iv, plain []byte) []byte {
+ key := sk.GetServerEncryptKey(cc.SizeKey)
+ return cc.Encryptor(key, iv, plain)
+}
+
+// StaticKey is an OpenVPN static key used for authentication and encryption of control messages.
+//
+// Notes:
+//
+// Authentication. If no key direction is set (i.e. a bidirectional key), OpenVPN uses key[64:64+size] for
+// computing and validating HMACs on both the client and the server. If the client has `key-direction 1`
+// and the server has `key-direction 0`, OpenVPN uses key[192:192+size] for computing HMACs on the client and
+// validating them on the server. If the client has `key-direction 0` and the server has `key-direction 1`
+// (i.e. an inverse direction in violation of the recommendations), OpenVPN uses key[64:64+size] for computing
+// HMACs on the client and validating them on the server. Inverse and Bidi are mutually exclusive. If both are
+// set, Bidi takes precedence.
+//
+// En/decryption. OpenVPN always takes 2 different keys for encryption and decryption, so Bidi is completely
+// ignored. Unless Inverse is set, key[128:128+size] is used for encryption on the client and key[0:0+size] is
+// used for decryption on the client with the server applying these keys in the other way.
+type StaticKey struct {
+ // Bidi mimics `key-direction` omitted for both the server and the client.
+ Bidi bool
+ // Inverse mimics `key-direction 1` set for the server and `key-direction 0` set for the client.
+ Inverse bool
+ // KeyBytes must contain 128 or 256 bytes of the static key.
+ KeyBytes []byte
+}
+
+// FromBase64 fills sk's KeyBytes from a given base64 string.
+func (sk *StaticKey) FromBase64(s string) (err error) {
+ sk.KeyBytes, err = base64.StdEncoding.DecodeString(s)
+ return
+}
+
+// FromGroupKeyFile fills sk's KeyBytes from a given group key file.
+func (sk *StaticKey) FromGroupKeyFile(path string) error {
+ return sk.FromFile(path, StaticKeyFromFileHex, StaticKeyBytesTotal*2, sk.FromHex)
+}
+
+// FromServerKeyFile fills sk's KeyBytes from a given server key file.
+func (sk *StaticKey) FromServerKeyFile(path string) error {
+ return sk.FromFile(path, StaticKeyFromFileBase64, base64.StdEncoding.EncodedLen(StaticKeyBytesHalf), sk.FromBase64)
+}
+
+// FromFile fills sk's KeyBytes from a given file.
+func (sk *StaticKey) FromFile(path string, re *regexp.Regexp, size int, from func(string) error) error {
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = file.Close() }()
+
+ n := 1024
+ buf := make([]byte, n)
+ n, err = file.Read(buf)
+ if err != nil && !errors.Is(err, io.EOF) {
+ return err
+ }
+
+ if n > 0 {
+ var s string
+ if r := re.FindStringSubmatch(string(buf[:n])); r != nil && len(r) == 2 {
+ s = strings.ReplaceAll(r[1], "\r", "")
+ s = strings.ReplaceAll(s, "\n", "")
+ if size == 0 || len(s) == size {
+ return from(s)
+ }
+ }
+ }
+
+ return ErrInvalidStaticKeyFileContents
+}
+
+// FromHex fills sk's KeyBytes from a given hex string.
+func (sk *StaticKey) FromHex(s string) (err error) {
+ sk.KeyBytes, err = hex.DecodeString(s)
+ return
+}
+
+// GetClientAuthBytes returns a quarter of KeyBytes to be used for authentication of control messages
+// composed by the client.
+func (sk *StaticKey) GetClientAuthBytes() []byte {
+ if sk.Inverse || sk.Bidi {
+ return sk.GetQuarterBytes(1)
+ }
+ return sk.GetQuarterBytes(3)
+}
+
+// GetClientAuthKey returns a key of a given size from GetClientAuthBytes.
+func (sk *StaticKey) GetClientAuthKey(size int) []byte {
+ return sk.GetClientAuthBytes()[:min(size, StaticKeyBytesQuarter)]
+}
+
+// GetClientEncryptBytes returns a quarter of KeyBytes to be used for encryption of control messages
+// composed by the client.
+func (sk *StaticKey) GetClientEncryptBytes() []byte {
+ if sk.Inverse {
+ return sk.GetQuarterBytes(0)
+ }
+ return sk.GetQuarterBytes(2)
+}
+
+// GetClientEncryptKey returns a key of a given size from GetClientEncryptBytes.
+func (sk *StaticKey) GetClientEncryptKey(size int) []byte {
+ return sk.GetClientEncryptBytes()[:min(size, StaticKeyBytesQuarter)]
+}
+
+// GetClientDecryptBytes returns a quarter of KeyBytes to be used for decryption of control messages
+// received by the client.
+func (sk *StaticKey) GetClientDecryptBytes() []byte {
+ if sk.Inverse {
+ return sk.GetQuarterBytes(2)
+ }
+ return sk.GetQuarterBytes(0)
+}
+
+// GetClientDecryptKey returns a key of a given size from GetClientDecryptBytes.
+func (sk *StaticKey) GetClientDecryptKey(size int) []byte {
+ return sk.GetClientDecryptBytes()[:min(size, StaticKeyBytesQuarter)]
+}
+
+// GetServerAuthBytes returns a quarter of KeyBytes to be used for authentication of control messages
+// composed by the server.
+func (sk *StaticKey) GetServerAuthBytes() []byte {
+ if sk.Inverse && !sk.Bidi {
+ return sk.GetQuarterBytes(3)
+ }
+ return sk.GetQuarterBytes(1)
+}
+
+// GetServerAuthKey returns a key of a given size from GetServerAuthKey.
+func (sk *StaticKey) GetServerAuthKey(size int) []byte {
+ return sk.GetServerAuthBytes()[:min(size, StaticKeyBytesQuarter)]
+}
+
+// GetServerEncryptBytes returns a quarter of KeyBytes to be used for encryption of control messages
+// composed by the server.
+func (sk *StaticKey) GetServerEncryptBytes() []byte {
+ return sk.GetClientDecryptBytes()
+}
+
+// GetServerEncryptKey returns a key of a given size from GetServerEncryptBytes.
+func (sk *StaticKey) GetServerEncryptKey(size int) []byte {
+ return sk.GetClientDecryptKey(size)
+}
+
+// GetServerDecryptBytes returns a quarter of KeyBytes to be used for decryption of control messages
+// received by the server.
+func (sk *StaticKey) GetServerDecryptBytes() []byte {
+ return sk.GetClientEncryptBytes()
+}
+
+// GetServerDecryptKey returns a key of a given size from GetServerDecryptBytes.
+func (sk *StaticKey) GetServerDecryptKey(size int) []byte {
+ return sk.GetClientEncryptKey(size)
+}
+
+// GetQuarterBytes returns a nth (0-based) quarter of KeyBytes
+func (sk *StaticKey) GetQuarterBytes(q uint) []byte {
+ q = q % 4
+ if len(sk.KeyBytes) < StaticKeyBytesTotal {
+ q = q % 2
+ }
+ if len(sk.KeyBytes) < StaticKeyBytesHalf {
+ q = 0
+ }
+ if len(sk.KeyBytes) < StaticKeyBytesQuarter {
+ return sk.KeyBytes
+ }
+ return sk.KeyBytes[q*StaticKeyBytesQuarter : (q+1)*StaticKeyBytesQuarter]
+}
+
+// ToBase64 returns a base64 string representing KeyBytes.
+func (sk *StaticKey) ToBase64() string {
+ return base64.StdEncoding.EncodeToString(sk.KeyBytes)
+}
+
+// ToHex returns a hex string representing KeyBytes.
+func (sk *StaticKey) ToHex() string {
+ return hex.EncodeToString(sk.KeyBytes)
+}
+
+// AuthDigests contains all the supported items of AuthDigest type.
+var AuthDigests = []*AuthDigest{
+ // Legacy digests
+ {Creator: md5.New, Names: []string{"MD5", "SSL3-MD5", "md5", "ssl3-md5"}, Size: md5.Size},
+ {Creator: sha1.New, Names: []string{"SHA-1", "SHA1", "SSL3-SHA1", "sha-1", "sha1", "ssl3-sha1"}, Size: sha1.Size},
+ {Creator: ripemd160.New, Names: []string{"RIPEMD-160", "RMD-160", "RIPEMD160", "RIPEMD", "RMD160", "ripemd-160", "rmd-160", "ripemd160", "ripemd", "rmd160"}, Size: ripemd160.Size},
+ // SHA2 digests
+ {Creator: sha256.New224, Names: []string{"SHA-224", "SHA2-224", "SHA224", "sha-224", "sha2-224", "sha224"}, Size: sha256.Size224},
+ {Creator: sha256.New, Names: []string{"SHA-256", "SHA2-256", "SHA256", "sha-256", "sha2-256", "sha256"}, Size: sha256.Size},
+ {Creator: sha512.New384, Names: []string{"SHA-384", "SHA2-384", "SHA384", "sha-384", "sha2-384", "sha384"}, Size: sha512.Size384},
+ {Creator: sha512.New, Names: []string{"SHA-512", "SHA2-512", "SHA512", "sha-512", "sha2-512", "sha512"}, Size: sha512.Size},
+ {Creator: sha512.New512_224, Names: []string{"SHA-512/224", "SHA2-512/224", "SHA512-224", "sha-512/224", "sha2-512/224", "sha512-224"}, Size: sha512.Size224},
+ {Creator: sha512.New512_256, Names: []string{"SHA-512/256", "SHA2-512/256", "SHA512-256", "sha-512/256", "sha2-512/256", "sha512-256"}, Size: sha512.Size256},
+ // SHA3 digests
+ {Creator: sha3.New224, Names: []string{"SHA3-224", "sha3-224"}, Size: 28},
+ {Creator: sha3.New256, Names: []string{"SHA3-256", "sha3-256"}, Size: 32},
+ {Creator: sha3.New384, Names: []string{"SHA3-384", "sha3-384"}, Size: 48},
+ {Creator: sha3.New512, Names: []string{"SHA3-512", "sha3-512"}, Size: 64},
+ // BLAKE digests
+ {
+ Creator: func() hash.Hash {
+ h, _ := blake2s.New256(nil)
+ return h
+ },
+ Names: []string{"BLAKE2s-256", "BLAKE2S-256", "blake2s-256", "blake2S-256"},
+ Size: blake2s.Size,
+ },
+ {
+ Creator: func() hash.Hash {
+ h, _ := blake2b.New512(nil)
+ return h
+ },
+ Names: []string{"BLAKE2b-512", "BLAKE2B-512", "blake2b-512", "blake2B-512"},
+ Size: blake2b.Size,
+ },
+ // SHAKE digests
+ {
+ Creator: func() hash.Hash {
+ return sha3.NewShake128()
+ },
+ Names: []string{"SHAKE-128", "SHAKE128", "shake-128", "shake128"},
+ Size: 32,
+ },
+ {
+ Creator: func() hash.Hash {
+ return sha3.NewShake256()
+ },
+ Names: []string{"SHAKE-256", "SHAKE256", "shake-256", "shake256"},
+ Size: 64,
+ },
+ // Custom digests
+ {
+ // This MD5-SHA1 implementation outputs bytes that match many online generators. However,
+ // its output never matches the HMACs of the sample packets generated on Windows and macOS.
+ // Since OpenVPN uses the OpenSSL library under the hood, the reason for this hash mismatch
+ // should most likely be traced there. Assuming the MD5-SHA1 digest has a very limited use,
+ // there is little probability anyone will ever face this issue.
+ Generator: func(key, plain []byte) []byte {
+ hmacMD5 := hmac.New(md5.New, key[:md5.Size])
+ hmacMD5.Write(plain)
+ hmacSHA1 := hmac.New(sha1.New, key[:sha1.Size])
+ hmacSHA1.Write(plain)
+ result := make([]byte, 0, md5.Size+sha1.Size)
+ return hmacSHA1.Sum(hmacMD5.Sum(result))
+ },
+ Names: []string{"MD5+SHA1", "MD5-SHA1", "MD5SHA1", "md5+sha1", "md5-sha1", "md5sha1"},
+ Size: md5.Size + sha1.Size,
+ },
+}
+
+// AuthDigestSizes contains sizes of all the supported items of AuthDigest type.
+var AuthDigestSizes = func() []int {
+ presence := make([]bool, AuthHMACBytesMax+1)
+ n := 0
+ for _, ad := range AuthDigests {
+ if !presence[ad.Size] {
+ n++
+ presence[ad.Size] = true
+ }
+ }
+ sizes := make([]int, 0, n)
+ for i, present := range presence {
+ if present {
+ sizes = append(sizes, i)
+ }
+ }
+ return sizes
+}()
+
+// CryptCiphers contains all the supported items of CryptCipher type.
+var CryptCiphers = []*CryptCipher{
+ {
+ Decryptor: func(key, iv, encrypted []byte) []byte {
+ plain := make([]byte, len(encrypted))
+ block, _ := aes.NewCipher(key)
+ ctr := cipher.NewCTR(block, iv)
+ ctr.XORKeyStream(plain, encrypted)
+ return plain
+ },
+ Encryptor: func(key, iv, plain []byte) []byte {
+ encrypted := make([]byte, len(plain))
+ block, _ := aes.NewCipher(key)
+ ctr := cipher.NewCTR(block, iv)
+ ctr.XORKeyStream(encrypted, plain)
+ return encrypted
+ },
+ Names: []string{"AES-256-CTR", "aes-256-ctr"},
+ SizeBlock: 16,
+ SizeKey: 32,
+ },
+}
+
+// AuthDigestDefault is the default AuthDigest used in the crypt and crypt2 modes (SHA-256).
+var AuthDigestDefault = AuthDigestFindByName("SHA-256")
+
+// CryptCipherDefault is the default CryptCipher used in the crypt and crypt2 modes (AES-256-CTR).
+var CryptCipherDefault = CryptCipherFindByName("AES-256-CTR")
+
+var StaticKeyFromFileBase64 = regexp.MustCompile("^(?:#.*?\\r?\\n)*" +
+ "-----BEGIN OpenVPN tls-crypt-v2 (?:client|server) key-----\\r?\\n" +
+ "([0-9a-zA-Z+=\\/\\r\\n]+)" +
+ "-----END OpenVPN tls-crypt-v2 (?:client|server) key-----(?:\\r?\\n)?$")
+var StaticKeyFromFileHex = regexp.MustCompile("^(?:#.*?\\r?\\n)*" +
+ "-----BEGIN OpenVPN Static key V1-----\\r?\\n" +
+ "([0-9a-fA-F\\r\\n]+)" +
+ "-----END OpenVPN Static key V1-----(?:\\r?\\n)?$")
+
+var (
+ ErrInvalidStaticKeyFileContents = errors.New("invalid static key file contents")
+)
+
+const (
+ AuthHMACBytesMax = sha512.Size
+ AuthHMACBytesMin = md5.Size
+
+ CryptHMACBytesTotal = sha256.Size
+
+ StaticKeyBytesTotal = 256
+ StaticKeyBytesHalf = StaticKeyBytesTotal / 2
+ StaticKeyBytesQuarter = StaticKeyBytesTotal / 4
+)
+
+// AuthDigestFindByName returns a pointer to AuthDigest having a given name or nil.
+func AuthDigestFindByName(name string) *AuthDigest {
+ for _, ad := range AuthDigests {
+ if slices.Contains(ad.Names, name) {
+ return ad
+ }
+ }
+ return nil
+}
+
+// CryptCipherFindByName returns a pointer to CryptCipher having a given name or nil.
+func CryptCipherFindByName(name string) *CryptCipher {
+ for _, cc := range CryptCiphers {
+ if slices.Contains(cc.Names, name) {
+ return cc
+ }
+ }
+ return nil
+}
+
+// HMACCreateAndGenerate uses a given digest creator to compute an HMAC of a given plain text with a given key.
+func HMACCreateAndGenerate(creator func() hash.Hash, key, plain []byte) []byte {
+ hmacDigest := hmac.New(creator, key)
+ hmacDigest.Write(plain)
+ return hmacDigest.Sum(nil)
+}
+
+// StaticKeyNewFromBase64 returns a pointer to StaticKey filled with bytes from a given base64 string.
+func StaticKeyNewFromBase64(s string, inverse bool, bidi bool) *StaticKey {
+ sk := &StaticKey{Inverse: inverse, Bidi: bidi}
+ _ = sk.FromBase64(s)
+ return sk
+}
+
+// StaticKeyNewFromHex returns a pointer to StaticKey filled with bytes from a given hex string.
+func StaticKeyNewFromHex(s string, inverse bool, bidi bool) *StaticKey {
+ sk := &StaticKey{Inverse: inverse, Bidi: bidi}
+ _ = sk.FromHex(s)
+ return sk
+}
diff --git a/modules/l4openvpn/matcher.go b/modules/l4openvpn/matcher.go
new file mode 100644
index 0000000..d28066d
--- /dev/null
+++ b/modules/l4openvpn/matcher.go
@@ -0,0 +1,633 @@
+// Copyright 2024 VNXME
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package l4openvpn
+
+import (
+ "encoding/binary"
+ "errors"
+ "io"
+ "net"
+ "strings"
+
+ "github.com/caddyserver/caddy/v2"
+ "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
+
+ "github.com/mholt/caddy-l4/layer4"
+)
+
+func init() {
+ caddy.RegisterModule(&MatchOpenVPN{})
+}
+
+// MatchOpenVPN is able to match OpenVPN connections.
+type MatchOpenVPN struct {
+
+ // Modes contains a list of supported OpenVPN modes to match against incoming client reset messages:
+ //
+ // - `plain` mode messages have no replay protection, authentication or encryption;
+ //
+ // - `auth` mode messages have no encryption, but provide for replay protection and authentication
+ // with a pre-shared 2048-bit group key, a variable key direction, and plenty digest algorithms;
+ //
+ // - 'crypt' mode messages feature replay protection, authentication and encryption with
+ // a pre-shared 2048-bit group key, a fixed key direction, and SHA-256 + AES-256-CTR algorithms;
+ //
+ // - `crypt2` mode messages are essentially `crypt` messages with an individual 2048-bit client key
+ // used for authentication and encryption attached to client reset messages in a protected form
+ // (a 1024-bit server key is used for its authentication end encryption).
+ //
+ // Notes: Each mode shall only be present once in the list. Values in the list are case-insensitive.
+ // If the list is empty, MatchOpenVPN will consider all modes as accepted and try them one by one.
+ Modes []string `json:"modes,omitempty"`
+
+ /*
+ * Fields relevant to the auth, crypt and crypt2 modes:
+ */
+
+ // IgnoreCrypto makes MatchOpenVPN skip decryption and authentication if set to true.
+ //
+ // Notes: IgnoreCrypto impacts the auth, crypt and crypt2 modes at once and makes sense only if/when
+ // the relevant static keys are provided. If neither GroupKey nor GroupKeyFile is set, decryption
+ // (if applicable) and authentication are automatically skipped in the auth and crypt modes only. If
+ // neither ServerKey nor ServerKeyFile is provided, decryption and authentication are automatically
+ // skipped in the crypt2 mode (unless there is a client key). If neither ClientKeys nor ClientKeyFiles
+ // are provided, decryption and authentication are automatically skipped in the crypt2 mode (unless
+ // there is a server key). In the crypt2 mode, when there is a client key and there is no server key,
+ // decryption of a WrappedKey is impossible, and this part of the incoming message is authenticated by
+ // comparing it with what has been included in the matching client key.
+ IgnoreCrypto bool `json:"ignore_crypto,omitempty"`
+ // IgnoreTimestamp makes MatchOpenVPN skip replay timestamps validation if set to true.
+ //
+ // Note: A 30-seconds time window is applicable by default, i.e. a timestamp of up to 15 seconds behind
+ // or ahead of now is accepted.
+ IgnoreTimestamp bool `json:"ignore_timestamp,omitempty"`
+
+ /*
+ * Fields relevant to the auth and crypt modes:
+ */
+
+ // GroupKey contains a hex string representing a pre-shared 2048-bit group key. This key may be
+ // present in OpenVPN config files inside `` or `` blocks or generated with
+ // `openvpn --genkey tls-auth|tls-crypt` command. No comments (starting with '#' or '-') are allowed.
+ GroupKey string `json:"group_key,omitempty"`
+ // GroupKeyFile is a path to a file containing a pre-shared 2048-bit group key which may be present
+ // in OpenVPN config files after `tls-auth` or `tls-crypt` directives. It is the same key as the one
+ // GroupKey introduces, so these fields are mutually exclusive. If both are set, GroupKey always takes
+ // precedence. Any comments in the file (starting with '#' or '-') are ignored.
+ GroupKeyFile string `json:"group_key_file,omitempty"`
+
+ /*
+ * Fields relevant to the auth mode only:
+ */
+
+ // AuthDigest is a name of a digest algorithm used for authentication (HMAC generation and validation) of
+ // the auth mode messages. If no value is provided, MatchOpenVPN will try all the algorithms it supports.
+ //
+ // Notes: OpenVPN binaries may support a larger number of digest algorithms thanks to the OpenSSL library
+ // used under the hood. A few legacy and exotic digest algorithms are known to be missing, so IgnoreCrypto
+ // may be set to true to ensure successful message matching if a desired digest algorithm isn't listed below.
+ //
+ // List of the supported digest algorithms:
+ // - MD5
+ // - SHA-1
+ // - RIPEMD-160
+ // - SHA-224
+ // - SHA-256
+ // - SHA-384
+ // - SHA-512
+ // - SHA-512/224
+ // - SHA-512/256
+ // - SHA3-224
+ // - SHA3-256
+ // - SHA3-384
+ // - SHA3-512
+ // - BLAKE2s-256
+ // - BLAKE2b-512
+ // - SHAKE-128
+ // - SHAKE-256
+ //
+ // Note: Digest algorithm names are recognised in a number of popular notations, including lowercase.
+ // Please, refer to the source code (AuthDigests variable in crypto.go) for details.
+ AuthDigest string `json:"auth_digest,omitempty"`
+ // GroupKeyDirection is a group key direction and may contain one of the following three values:
+ //
+ // - `normal` means the server config has `tls-auth [...] 0` or `key-direction 0`,
+ // while the client configs have `tls-auth [...] 1` or `key-direction 1`;
+ //
+ // - `inverse` means the server config has `tls-auth [...] 1` or `key-direction 1`,
+ // while the client config have `tls-auth [...] 0` or `key-direction 0`;
+ //
+ // - `bidi` or `bidirectional` means key direction is omitted (e.g. `tls-auth [...]`)
+ // in both the server config and client configs.
+ //
+ // Notes: Values are case-insensitive. If no value is specified, the normal key direction is implied.
+ // The inverse key direction is a violation of the OpenVPN official recommendations, and the bidi one
+ // provides for a lower level of DoS and message replay attacks resilience.
+ GroupKeyDirection string `json:"group_key_direction,omitempty"`
+
+ /*
+ * Fields relevant to the crypt2 mode only:
+ */
+
+ // ClientKeys contains a list of base64 strings representing 2048-bit client keys (each one in a decrypted
+ // form followed by an encrypted and authenticated form also known as WKc in the OpenVPN docs). These keys
+ // may be present in OpenVPN client config files inside `` block or generated with `openvpn
+ // --tls-crypt-v2 [server.key] --genkey tls-crypt-v2-client` command. No comments (starting with '#' or '-')
+ // are allowed.
+ ClientKeys []string `json:"client_keys,omitempty"`
+ // ClientKeyFiles is a list of paths to files containing 2048-bit client key which may be present in OpenVPN
+ // config files after `tls-crypt-v2` directive. These are the same keys as those ClientKeys introduce, but
+ // these fields are complementary. If both are set, a joint list of client keys is created. Any comments in
+ // the files (starting with '#' or '-') are ignored.
+ ClientKeyFiles []string `json:"client_key_files,omitempty"`
+
+ // ServerKey contains a base64 string representing a 1024-bit server key used only for authentication and
+ // encryption of client keys. This key may be present in OpenVPN server config files inside ``
+ // block or generated with `openvpn --genkey tls-crypt-v2-server` command. No comments (starting with '#'
+ // or '-') are allowed.
+ ServerKey string `json:"server_key,omitempty"`
+ // ServerKeyFile is a path to a file containing a 1024-bit server key which may be present in OpenVPN
+ // config files after `tls-crypt-v2` directive. It is the same key as the one ServerKey introduces, so
+ // these fields are mutually exclusive. If both are set, ServerKey always takes precedence. Any comments
+ // in the file (starting with '#' or '-') are ignored.
+ ServerKeyFile string `json:"server_key_file,omitempty"`
+
+ /*
+ * Internal fields:
+ */
+
+ acceptAuth bool
+ acceptCrypt bool
+ acceptCrypt2 bool
+ acceptPlain bool
+
+ groupKeyAuth *StaticKey
+ groupKeyCrypt *StaticKey
+
+ authDigest *AuthDigest
+ lastDigest *AuthDigest
+
+ clientKeys []*WrappedKey
+ serverKey *StaticKey
+}
+
+// CaddyModule returns the Caddy module information.
+func (m *MatchOpenVPN) CaddyModule() caddy.ModuleInfo {
+ return caddy.ModuleInfo{
+ ID: "layer4.matchers.openvpn",
+ New: func() caddy.Module { return new(MatchOpenVPN) },
+ }
+}
+
+// Match returns true if the connection looks like OpenVPN.
+func (m *MatchOpenVPN) Match(cx *layer4.Connection) (bool, error) {
+ var err error
+ var l, n int
+
+ // Prepare a 3-byte buffer
+ buf := make([]byte, LengthBytesTotal+OpcodeKeyIDBytesTotal)
+
+ // Do TCP-specific reads and checks
+ _, isTCP := cx.LocalAddr().(*net.TCPAddr)
+ if isTCP {
+ // Read 2 bytes containing the remaining bytes length
+ _, err = io.ReadFull(cx, buf[:LengthBytesTotal])
+ if err != nil {
+ return false, err
+ }
+
+ // Validate the remaining bytes length
+ l = int(binary.BigEndian.Uint16(buf[:LengthBytesTotal]))
+ if l < MessagePlainBytesTotal || l > MessageCrypt2BytesMax {
+ return false, nil
+ }
+ }
+
+ // Read 1 byte containing MessageHeader
+ _, err = io.ReadFull(cx, buf[LengthBytesTotal:])
+ if err != nil {
+ return false, err
+ }
+
+ // Parse MessageHeader
+ hdr := &MessageHeader{}
+ if err = hdr.FromBytes(buf[LengthBytesTotal:]); err != nil {
+ return false, nil
+ }
+
+ // Validate MessageHeader.KeyID
+ if hdr.KeyID > 0 {
+ return false, nil
+ }
+
+ var mp *MessagePlain
+ var ma *MessageAuth
+ var mc *MessageCrypt
+ var mr *MessageCrypt2
+
+ if hdr.Opcode == OpcodeControlHardResetClientV2 && (m.acceptPlain || m.acceptAuth || m.acceptCrypt) {
+ if isTCP {
+ if l > MessageAuthBytesMax {
+ return false, nil
+ }
+
+ buf = make([]byte, l-OpcodeKeyIDBytesTotal+1)
+ n, err = io.ReadAtLeast(cx, buf, l-OpcodeKeyIDBytesTotal)
+ if err != nil || n > l-OpcodeKeyIDBytesTotal {
+ return false, err
+ }
+ } else {
+ buf = make([]byte, MessageAuthBytesMaxHL+1)
+ n, err = io.ReadAtLeast(cx, buf, 1)
+ if err != nil || n < MessagePlainBytesTotalHL || n > MessageAuthBytesMaxHL {
+ return false, err
+ }
+ }
+
+ if m.acceptPlain {
+ // Parse and validate MessagePlain
+ mp = &MessagePlain{}
+ err = mp.FromBytesHeadless(buf[:n], hdr)
+ if err == nil && mp.Match() {
+ return true, nil
+ }
+ }
+
+ if m.acceptAuth {
+ // Parse and validate MessageAuth
+ ma = &MessageAuth{MessageTraitAuth: MessageTraitAuth{Digest: m.lastDigest}}
+ err = ma.FromBytesHeadless(buf[:n], hdr)
+ if err == nil && ma.Match(m.IgnoreTimestamp, m.IgnoreCrypto, m.authDigest, m.groupKeyAuth) {
+ m.lastDigest = ma.Digest
+ return true, nil
+ }
+ }
+
+ if m.acceptCrypt {
+ // Parse and validate MessageCrypt
+ mc = &MessageCrypt{}
+ err = mc.FromBytesHeadless(buf[:n], hdr)
+ if err == nil && mc.Match(m.IgnoreTimestamp, m.IgnoreCrypto, nil, m.groupKeyCrypt) {
+ return true, nil
+ }
+ }
+ }
+
+ if hdr.Opcode == OpcodeControlHardResetClientV3 && m.acceptCrypt2 {
+ if isTCP {
+ if l < MessageCrypt2BytesMin {
+ return false, nil
+ }
+
+ buf = make([]byte, l-OpcodeKeyIDBytesTotal+1)
+ n, err = io.ReadAtLeast(cx, buf, l-OpcodeKeyIDBytesTotal)
+ if err != nil || n > l-OpcodeKeyIDBytesTotal {
+ return false, err
+ }
+ } else {
+ buf = make([]byte, MessageCrypt2BytesMaxHL+1)
+ n, err = io.ReadAtLeast(cx, buf, 1)
+ if err != nil || n < MessageCrypt2BytesMinHL || n > MessageCrypt2BytesMaxHL {
+ return false, err
+ }
+ }
+
+ // Parse and validate MessageCrypt2
+ mr = &MessageCrypt2{}
+ err = mr.FromBytesHeadless(buf[:n], hdr)
+ if err == nil && mr.Match(m.IgnoreTimestamp, m.IgnoreCrypto, nil, m.serverKey, m.clientKeys) {
+ return true, nil
+ }
+ }
+
+ return false, nil
+}
+
+// Provision prepares m's internal structures.
+func (m *MatchOpenVPN) Provision(_ caddy.Context) error {
+ repl := caddy.NewReplacer()
+
+ if len(m.Modes) > 0 {
+ for _, mode := range m.Modes {
+ mode = strings.ToLower(repl.ReplaceAll(mode, ""))
+ switch mode {
+ case ModeAuth:
+ m.acceptAuth = true
+ case ModeCrypt:
+ m.acceptCrypt = true
+ case ModeCrypt2:
+ m.acceptCrypt2 = true
+ case ModePlain:
+ m.acceptPlain = true
+ default:
+ return ErrInvalidMode
+ }
+ }
+ } else {
+ m.acceptAuth, m.acceptCrypt, m.acceptCrypt2, m.acceptPlain = true, true, true, true
+ }
+
+ var gkdBidi, gkdInverse bool
+ m.GroupKeyDirection = strings.ToLower(repl.ReplaceAll(m.GroupKeyDirection, ""))
+ if len(m.GroupKeyDirection) > 0 {
+ switch m.GroupKeyDirection {
+ case GroupKeyDirectionBidi, GroupKeyDirectionBidi2:
+ gkdBidi = true
+ case GroupKeyDirectionInverse:
+ gkdInverse = true
+ case GroupKeyDirectionNormal:
+ break
+ default:
+ return ErrInvalidGroupKeyDirection
+ }
+ }
+
+ m.GroupKey = repl.ReplaceAll(m.GroupKey, "")
+ if len(m.GroupKey) > 0 {
+ sk := &StaticKey{}
+ if err := sk.FromHex(m.GroupKey); err != nil {
+ return err
+ }
+ if len(sk.KeyBytes) != StaticKeyBytesTotal {
+ return ErrInvalidGroupKey
+ }
+ m.groupKeyAuth, m.groupKeyCrypt = &StaticKey{Bidi: gkdBidi, Inverse: gkdInverse, KeyBytes: sk.KeyBytes}, sk
+ } else {
+ m.GroupKeyFile = repl.ReplaceAll(m.GroupKeyFile, "")
+ if len(m.GroupKeyFile) > 0 {
+ sk := &StaticKey{}
+ if err := sk.FromGroupKeyFile(m.GroupKeyFile); err != nil {
+ return err
+ }
+ if len(sk.KeyBytes) != StaticKeyBytesTotal {
+ return ErrInvalidGroupKey
+ }
+ m.groupKeyAuth, m.groupKeyCrypt = &StaticKey{Bidi: gkdBidi, Inverse: gkdInverse, KeyBytes: sk.KeyBytes}, sk
+ }
+ }
+
+ m.AuthDigest = repl.ReplaceAll(m.AuthDigest, "")
+ if len(m.AuthDigest) > 0 {
+ m.authDigest = AuthDigestFindByName(m.AuthDigest)
+ if m.authDigest == nil {
+ return ErrInvalidAuthDigest
+ }
+ }
+
+ m.ServerKey = repl.ReplaceAll(m.ServerKey, "")
+ if len(m.ServerKey) > 0 {
+ sk := &StaticKey{}
+ if err := sk.FromBase64(m.ServerKey); err != nil {
+ return err
+ }
+ if len(sk.KeyBytes) != StaticKeyBytesHalf {
+ return ErrInvalidServerKey
+ }
+ m.serverKey = sk
+ } else {
+ m.ServerKeyFile = repl.ReplaceAll(m.ServerKeyFile, "")
+ if len(m.ServerKeyFile) > 0 {
+ sk := &StaticKey{}
+ if err := sk.FromServerKeyFile(m.ServerKeyFile); err != nil {
+ return err
+ }
+ if len(sk.KeyBytes) != StaticKeyBytesHalf {
+ return ErrInvalidServerKey
+ }
+ m.serverKey = sk
+ }
+ }
+
+ if len(m.ClientKeys) > 0 {
+ for _, clientKey := range m.ClientKeys {
+ clientKey = repl.ReplaceAll(clientKey, "")
+ if len(clientKey) > 0 {
+ ck := &WrappedKey{}
+ if err := ck.FromBase64(clientKey); err != nil {
+ return err
+ }
+
+ if len(ck.StaticKey.KeyBytes) != StaticKeyBytesTotal ||
+ (m.serverKey != nil && !ck.DecryptAndAuthenticate(nil, m.serverKey)) {
+ return ErrInvalidClientKey
+ }
+
+ m.clientKeys = append(m.clientKeys, ck)
+ }
+ }
+ } else if len(m.ClientKeyFiles) > 0 {
+ for _, clientKeyFile := range m.ClientKeyFiles {
+ clientKeyFile = repl.ReplaceAll(clientKeyFile, "")
+ if len(clientKeyFile) > 0 {
+ ck := &WrappedKey{}
+ if err := ck.FromClientKeyFile(clientKeyFile); err != nil {
+ return err
+ }
+
+ if len(ck.StaticKey.KeyBytes) != StaticKeyBytesTotal ||
+ (m.serverKey != nil && !ck.DecryptAndAuthenticate(nil, m.serverKey)) {
+ return ErrInvalidClientKey
+ }
+
+ m.clientKeys = append(m.clientKeys, ck)
+ }
+ }
+ }
+
+ return nil
+}
+
+// UnmarshalCaddyfile sets up the MatchOpenVPN from Caddyfile tokens. Syntax:
+//
+// openvpn {
+// modes [<...>]
+//
+// ignore_crypto
+// ignore_timestamp
+//
+// group_key
+// group_key_file
+//
+// auth_digest
+// group_key_direction
+//
+// server_key
+// server_key_file
+//
+// client_key
+// client_key_file
+// }
+// openvpn
+//
+// Note: multiple 'client_key' and 'client_key_file' options are allowed.
+func (m *MatchOpenVPN) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
+ _, wrapper := d.Next(), d.Val() // consume wrapper name
+
+ // No same-line arguments are supported
+ if d.CountRemainingArgs() > 0 {
+ return d.ArgErr()
+ }
+
+ errDuplicate := func(optionName string) error {
+ return d.Errf("duplicate %s option '%s'", wrapper, optionName)
+ }
+
+ errGroupKeyMutex := func() error {
+ return d.Errf("%s options 'group_key' and `group_key_file` are mutually exclusive", wrapper)
+ }
+
+ errServerKeyMutex := func() error {
+ return d.Errf("%s options 'server_key' and `server_key_file` are mutually exclusive", wrapper)
+ }
+
+ var hasAuthDigest, hasGroupKey, hasGroupKeyDirection, hasGroupKeyFile, hasIgnoreCrypto, hasIgnoreTimestamp,
+ hasModes, hasServerKey, hasServerKeyFile bool
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
+ optionName := d.Val()
+ switch optionName {
+ case "modes":
+ if hasModes {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() == 0 || d.CountRemainingArgs() > 4 {
+ return d.ArgErr()
+ }
+ m.Modes, hasModes = append(m.Modes, d.RemainingArgs()...), true
+ case "ignore_crypto":
+ if hasIgnoreCrypto {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() > 0 {
+ return d.ArgErr()
+ }
+ m.IgnoreCrypto, hasIgnoreCrypto = true, true
+ case "ignore_timestamp":
+ if hasIgnoreTimestamp {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() > 0 {
+ return d.ArgErr()
+ }
+ m.IgnoreTimestamp, hasIgnoreTimestamp = true, true
+ case "group_key":
+ if hasGroupKeyFile {
+ return errGroupKeyMutex()
+ }
+ if hasGroupKey {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ _, m.GroupKey, hasGroupKey = d.NextArg(), d.Val(), true
+ case "group_key_file":
+ if hasGroupKey {
+ return errGroupKeyMutex()
+ }
+ if hasGroupKeyFile {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ _, m.GroupKeyFile, hasGroupKeyFile = d.NextArg(), d.Val(), true
+ case "auth_digest":
+ if hasAuthDigest {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ _, m.AuthDigest, hasAuthDigest = d.NextArg(), d.Val(), true
+ case "group_key_direction":
+ if hasGroupKeyDirection {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ _, m.GroupKeyDirection, hasGroupKeyDirection = d.NextArg(), d.Val(), true
+ case "server_key":
+ if hasServerKeyFile {
+ return errServerKeyMutex()
+ }
+ if hasServerKey {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ _, m.ServerKey, hasServerKey = d.NextArg(), d.Val(), true
+ case "server_key_file":
+ if hasServerKey {
+ return errServerKeyMutex()
+ }
+ if hasServerKeyFile {
+ return errDuplicate(optionName)
+ }
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ _, m.ServerKeyFile, hasServerKeyFile = d.NextArg(), d.Val(), true
+ case "client_key":
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ m.ClientKeys = append(m.ClientKeys, d.RemainingArgs()...)
+ case "client_key_file":
+ if d.CountRemainingArgs() != 1 {
+ return d.ArgErr()
+ }
+ m.ClientKeyFiles = append(m.ClientKeyFiles, d.RemainingArgs()...)
+ default:
+ return d.ArgErr()
+ }
+
+ // No nested blocks are supported
+ if d.NextBlock(nesting + 1) {
+ return d.Errf("malformed %s option '%s': nested blocks are not supported", wrapper, optionName)
+ }
+ }
+
+ return nil
+}
+
+// Interface guards
+var (
+ _ caddy.Provisioner = (*MatchOpenVPN)(nil)
+ _ caddyfile.Unmarshaler = (*MatchOpenVPN)(nil)
+ _ layer4.ConnMatcher = (*MatchOpenVPN)(nil)
+)
+
+var (
+ ErrInvalidAuthDigest = errors.New("invalid auth digest")
+ ErrInvalidClientKey = errors.New("invalid client key")
+ ErrInvalidGroupKey = errors.New("invalid group key")
+ ErrInvalidGroupKeyDirection = errors.New("invalid group key direction")
+ ErrInvalidMode = errors.New("invalid mode")
+ ErrInvalidServerKey = errors.New("invalid server key")
+)
+
+const (
+ GroupKeyDirectionBidi = "bidi"
+ GroupKeyDirectionBidi2 = "bidirectional"
+ GroupKeyDirectionInverse = "inverse"
+ GroupKeyDirectionNormal = "normal"
+
+ ModeAuth = "auth"
+ ModeCrypt = "crypt"
+ ModeCrypt2 = "crypt2"
+ ModePlain = "plain"
+)
diff --git a/modules/l4openvpn/matcher_test.go b/modules/l4openvpn/matcher_test.go
new file mode 100644
index 0000000..bc617ee
--- /dev/null
+++ b/modules/l4openvpn/matcher_test.go
@@ -0,0 +1,551 @@
+// Copyright 2024 VNXME
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package l4openvpn
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "net"
+ "slices"
+ "testing"
+
+ "github.com/caddyserver/caddy/v2"
+ "go.uber.org/zap"
+
+ "github.com/mholt/caddy-l4/layer4"
+)
+
+func assertNoError(t *testing.T, err error) {
+ t.Helper()
+ if err != nil && !errors.Is(err, io.EOF) && !errors.Is(err, io.ErrUnexpectedEOF) {
+ t.Fatalf("Unexpected error: %s\n", err)
+ }
+}
+
+func Test_AuthDigests_CheckAll(t *testing.T) {
+ plain := plainPacket1[9:14]
+ names := make(map[string]int, len(AuthDigests))
+ hashes := make([][]byte, 0, len(AuthDigests))
+ var hmac []byte
+ for i, ad := range AuthDigests {
+ if len(ad.Names) == 0 {
+ t.Fatalf("Test %d: there must be at least one name", i)
+ }
+ for j, name := range ad.Names {
+ if len(name) == 0 {
+ t.Fatalf("Test %d [%s]: empty name %d", i, ad.Names[0], j)
+ }
+ if k, existing := names[name]; existing {
+ t.Fatalf("Test %d [%s]: name %d [%s] used by %s", i, ad.Names[0], j, name, AuthDigests[k].Names[0])
+ }
+ names[name] = i
+ }
+ if ad.Size == 0 {
+ t.Fatalf("Test %d [%s]: zero size", i, ad.Names[0])
+ }
+ if !slices.Contains(AuthDigestSizes, ad.Size) {
+ t.Fatalf("Test %d [%s]: size missing in AuthDigestSizes", i, ad.Names[0])
+ }
+ if ad.Creator == nil && ad.Generator == nil {
+ t.Fatalf("Test %d [%s]: missing a creator or a generator", i, ad.Names[0])
+ }
+ if ad.Generator != nil {
+ hmac = ad.Generator(groupKey12.GetClientAuthKey(ad.Size), plain)
+ } else {
+ hmac = HMACCreateAndGenerate(ad.Creator, groupKey12.GetClientAuthKey(ad.Size), plain)
+ }
+ if len(hmac) != ad.Size {
+ t.Fatalf("Test %d [%s]: HMAC length doesn't match its size", i, ad.Names[0])
+ }
+ for j, existing := range hashes {
+ if bytes.Equal(hmac, existing) {
+ t.Fatalf("Test %d [%s]: HMAC bytes same as %s", i, ad.Names[0], AuthDigests[j].Names[0])
+ }
+ }
+ hashes = append(hashes, hmac)
+ }
+}
+
+func Test_MessagePlain_FromBytes_Match_ToBytes(t *testing.T) {
+ for i, packet := range [][]byte{
+ plainPacket1, plainPacket2,
+ plainPacket3, plainPacket4,
+ } {
+ msg := &MessagePlain{}
+ if err := msg.FromBytes(packet); err != nil {
+ t.Fatalf("Test %d: failed to unpack: %s", i, err)
+ }
+ if !bytes.Equal(packet, msg.ToBytes()) {
+ t.Fatalf("Test %d: failed to pack", i)
+ }
+ if !msg.Match() {
+ t.Fatalf("Test %d: failed to match", i)
+ }
+ }
+}
+
+func Test_MessageAuth_FromBytes_Match_ToBytes(t *testing.T) {
+ for i, packet := range [][]byte{
+ // Legacy digests
+ authMD5Packet1, authMD5Packet2,
+ authSHA1Packet1, authSHA1Packet2,
+ authRIPEMD160Packet1, authRIPEMD160Packet2,
+ // SHA2 digests
+ authSHA224Packet1, authSHA224Packet2,
+ authSHA256Packet1, authSHA256Packet2,
+ authSHA384Packet1, authSHA384Packet2,
+ authSHA512Packet1, authSHA512Packet2,
+ authSHA512224Packet1, authSHA512224Packet2,
+ authSHA512256Packet1, authSHA512256Packet2,
+ // SHA3 digests
+ authSHA3224Packet1, authSHA3224Packet2,
+ authSHA3256Packet1, authSHA3256Packet2,
+ authSHA3384Packet1, authSHA3384Packet2,
+ authSHA3512Packet1, authSHA3512Packet2,
+ // BLAKE digests
+ authBLAKE2s256Packet1, authBLAKE2s256Packet2,
+ authBLAKE2b512Packet1, authBLAKE2b512Packet2,
+ } {
+ msg := &MessageAuth{}
+ if err := msg.FromBytes(packet); err != nil {
+ t.Fatalf("Test %d: failed to unpack: %s", i, err)
+ }
+ if !msg.Match(true, false, nil, groupKey12) {
+ t.Fatalf("Test %d: failed to match", i)
+ }
+ if !bytes.Equal(packet, msg.ToBytes()) {
+ t.Fatalf("Test %d: failed to pack", i)
+ }
+ }
+}
+
+func Test_MessageAuth_FromBytes_Match(t *testing.T) {
+ for i, packet := range [][]byte{
+ // Legacy digests
+ authMD5Packet3, authMD5Packet4,
+ authSHA1Packet3, authSHA1Packet4,
+ // SHA2 digests
+ authSHA224Packet3, authSHA224Packet4,
+ authSHA256Packet3, authSHA256Packet4,
+ authSHA384Packet3, authSHA384Packet4,
+ authSHA512Packet3, authSHA512Packet4,
+ } {
+ msg := &MessageAuth{}
+ if err := msg.FromBytes(packet); err != nil {
+ t.Fatalf("Test %d: failed to unpack: %s", i, err)
+ }
+ if !msg.Match(true, true, nil, groupKey12) {
+ t.Fatalf("Test %d: failed to match", i)
+ }
+ }
+
+}
+
+func Test_MessageCrypt_FromBytes_Match_ToBytes(t *testing.T) {
+ for i, packet := range [][]byte{
+ cryptPacket1, cryptPacket2,
+ } {
+ msg := &MessageCrypt{}
+ if err := msg.FromBytes(packet); err != nil {
+ t.Fatalf("Test %d: failed to unpack: %s", i, err)
+ }
+ if !msg.Match(true, false, nil, groupKey12) {
+ t.Fatalf("Test %d: failed to match", i)
+ }
+ if !bytes.Equal(packet, msg.ToBytes()) {
+ t.Fatalf("Test %d: failed to pack", i)
+ }
+ }
+}
+
+func Test_MessageCrypt_FromBytes_Match(t *testing.T) {
+ for i, packet := range [][]byte{
+ cryptPacket3, cryptPacket4,
+ } {
+ msg := &MessageCrypt{}
+ if err := msg.FromBytes(packet); err != nil {
+ t.Fatalf("Test %d: failed to unpack: %s", i, err)
+ }
+ if !msg.Match(true, true, nil, groupKey12) {
+ t.Fatalf("Test %d: failed to match", i)
+ }
+ }
+}
+
+func Test_MessageCrypt2_FromBytes_Match_ToBytes(t *testing.T) {
+ for i, packet := range [][]byte{
+ crypt2Packet5, crypt2Packet6,
+ } {
+ msg := &MessageCrypt2{}
+ if err := msg.FromBytes(packet); err != nil {
+ t.Fatalf("Test %d: failed to unpack: %s", i, err)
+ }
+ if !msg.Match(true, false, nil, serverKey56, nil) {
+ t.Fatalf("Test %d: failed to match with a server key", i)
+ }
+ if !msg.Match(true, false, nil, nil, []*WrappedKey{clientKey56}) {
+ t.Fatalf("Test %d: failed to match with a client key", i)
+ }
+ if !bytes.Equal(packet, msg.ToBytes()) {
+ fmt.Printf("%x\n%x\n", packet, msg.ToBytes())
+ t.Fatalf("Test %d: failed to pack", i)
+ }
+ }
+}
+
+func Test_MessageCrypt2_FromBytes_Match(t *testing.T) {
+ for i, packet := range [][]byte{
+ crypt2Packet5, crypt2Packet6,
+ } {
+ msg := &MessageCrypt2{}
+ if err := msg.FromBytes(packet); err != nil {
+ t.Fatalf("Test %d: failed to unpack: %s", i, err)
+ }
+ if !msg.Match(true, false, nil, nil, nil) {
+ t.Fatalf("Test %d: failed to match", i)
+ }
+ }
+}
+
+func Test_MatchOpenVPN_Match(t *testing.T) {
+ type test struct {
+ matcher *MatchOpenVPN
+ data []byte
+ shouldMatch bool
+ }
+
+ modesNotAuth := []string{"crypt", "crypt2", "plain"}
+ modesNotPlain := []string{"auth", "crypt", "crypt2"}
+ modesNotCrypt := []string{"auth", "crypt2", "plain"}
+ modesNotCrypt2 := []string{"auth", "crypt", "plain"}
+
+ testsPlain := func() []test {
+ m0 := &MatchOpenVPN{}
+ m1 := &MatchOpenVPN{Modes: modesNotPlain}
+ tests := make([]test, 0, 3*2*2)
+ for i, packet := range [][]byte{
+ plainPacket1, plainPacket2,
+ plainPacket3, plainPacket4,
+ } {
+ tests = append(tests,
+ test{matcher: m0, data: packet[:MessagePlainBytesTotal-i-1], shouldMatch: false},
+ test{matcher: m0, data: packet, shouldMatch: true},
+ test{matcher: m1, data: packet, shouldMatch: false},
+ )
+ }
+ return tests
+ }()
+
+ testsKnownKeyAuth := func() []test {
+ m0 := &MatchOpenVPN{}
+ m1 := &MatchOpenVPN{IgnoreTimestamp: true}
+ m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotAuth}
+ m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex}
+ m4 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex, AuthDigest: "shake128"}
+ m5 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex, GroupKeyDirection: GroupKeyDirectionInverse}
+ tests := make([]test, 0, 6*15*2)
+ for _, packet := range [][]byte{
+ authMD5Packet1, authMD5Packet2,
+ authSHA1Packet1, authSHA1Packet2,
+ authRIPEMD160Packet1, authRIPEMD160Packet2,
+ authSHA224Packet1, authSHA224Packet2,
+ authSHA256Packet1, authSHA256Packet2,
+ authSHA384Packet1, authSHA384Packet2,
+ authSHA512Packet1, authSHA512Packet2,
+ authSHA512224Packet1, authSHA512224Packet2,
+ authSHA512256Packet1, authSHA512256Packet2,
+ authSHA3224Packet1, authSHA3224Packet2,
+ authSHA3256Packet1, authSHA3256Packet2,
+ authSHA3384Packet1, authSHA3384Packet2,
+ authSHA3512Packet1, authSHA3512Packet2,
+ authBLAKE2s256Packet1, authBLAKE2s256Packet2,
+ authBLAKE2b512Packet1, authBLAKE2b512Packet2,
+ } {
+ tests = append(tests,
+ test{matcher: m0, data: packet, shouldMatch: false},
+ test{matcher: m1, data: packet, shouldMatch: true},
+ test{matcher: m2, data: packet, shouldMatch: false},
+ test{matcher: m3, data: packet, shouldMatch: true},
+ test{matcher: m4, data: packet, shouldMatch: false},
+ test{matcher: m5, data: packet, shouldMatch: false},
+ )
+ }
+ return tests
+ }()
+
+ testsUnknownKeyAuth := func() []test {
+ m0 := &MatchOpenVPN{}
+ m1 := &MatchOpenVPN{IgnoreTimestamp: true}
+ m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotAuth}
+ m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex}
+ tests := make([]test, 0, 4*6*2)
+ for _, packet := range [][]byte{
+ authMD5Packet3, authMD5Packet4,
+ authSHA1Packet3, authSHA1Packet4,
+ authSHA224Packet3, authSHA224Packet4,
+ authSHA256Packet3, authSHA256Packet4,
+ authSHA384Packet3, authSHA384Packet4,
+ authSHA512Packet3, authSHA512Packet4,
+ } {
+ tests = append(tests,
+ test{matcher: m0, data: packet, shouldMatch: false},
+ test{matcher: m1, data: packet, shouldMatch: true},
+ test{matcher: m2, data: packet, shouldMatch: false},
+ test{matcher: m3, data: packet, shouldMatch: false},
+ )
+ }
+ return tests
+ }()
+
+ testsUnsupportedDigestsAuth := func() []test {
+ m0 := &MatchOpenVPN{}
+ m1 := &MatchOpenVPN{IgnoreTimestamp: true}
+ m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotAuth}
+ m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex}
+ tests := make([]test, 0, 4*6*2)
+ for _, packet := range [][]byte{
+ authMD5SHA1Packet1, authMD5SHA1Packet2,
+ authSM3Packet1, authSM3Packet2,
+ authWhirlpoolPacket1, authWhirlpoolPacket2,
+ authMD5SHA1Packet3, authMD5SHA1Packet4,
+ } {
+ tests = append(tests,
+ test{matcher: m0, data: packet, shouldMatch: false},
+ test{matcher: m1, data: packet, shouldMatch: true},
+ test{matcher: m2, data: packet, shouldMatch: false},
+ test{matcher: m3, data: packet, shouldMatch: false},
+ )
+ }
+ return tests
+ }()
+
+ testsKnownKeyCrypt := func() []test {
+ m0 := &MatchOpenVPN{}
+ m1 := &MatchOpenVPN{IgnoreTimestamp: true}
+ m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotCrypt}
+ m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex}
+ tests := make([]test, 0, 4*1*2)
+ for _, packet := range [][]byte{
+ cryptPacket1, cryptPacket2,
+ } {
+ tests = append(tests,
+ test{matcher: m0, data: packet, shouldMatch: false},
+ test{matcher: m1, data: packet, shouldMatch: true},
+ test{matcher: m2, data: packet, shouldMatch: false},
+ test{matcher: m3, data: packet, shouldMatch: true},
+ )
+ }
+ return tests
+ }()
+
+ testsUnknownKeyCrypt := func() []test {
+ m0 := &MatchOpenVPN{}
+ m1 := &MatchOpenVPN{IgnoreTimestamp: true}
+ m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotCrypt}
+ m3 := &MatchOpenVPN{IgnoreTimestamp: true, GroupKey: groupKey12Hex}
+ tests := make([]test, 0, 4*1*2)
+ for _, packet := range [][]byte{
+ cryptPacket3, cryptPacket4,
+ } {
+ tests = append(tests,
+ test{matcher: m0, data: packet, shouldMatch: false},
+ test{matcher: m1, data: packet, shouldMatch: true},
+ test{matcher: m2, data: packet, shouldMatch: false},
+ test{matcher: m3, data: packet, shouldMatch: false},
+ )
+ }
+ return tests
+ }()
+
+ testsKnownKeyCrypt2 := func() []test {
+ m0 := &MatchOpenVPN{}
+ m1 := &MatchOpenVPN{IgnoreTimestamp: true}
+ m2 := &MatchOpenVPN{IgnoreTimestamp: true, Modes: modesNotCrypt2}
+ m3 := &MatchOpenVPN{IgnoreTimestamp: true, ServerKey: serverKey56Base64}
+ m4 := &MatchOpenVPN{IgnoreTimestamp: true, ClientKeys: []string{clientKey56Base64}}
+ tests := make([]test, 0, 5*1*2)
+ for _, packet := range [][]byte{
+ crypt2Packet5, crypt2Packet6,
+ } {
+ tests = append(tests,
+ test{matcher: m0, data: packet, shouldMatch: false},
+ test{matcher: m1, data: packet, shouldMatch: true},
+ test{matcher: m2, data: packet, shouldMatch: false},
+ test{matcher: m3, data: packet, shouldMatch: true},
+ test{matcher: m4, data: packet, shouldMatch: true},
+ )
+ }
+ return tests
+ }()
+
+ tests := make([]test, 0, len(testsPlain)+len(testsKnownKeyAuth)+len(testsUnknownKeyAuth)+
+ len(testsUnsupportedDigestsAuth)+len(testsKnownKeyCrypt)+len(testsUnknownKeyCrypt)+len(testsKnownKeyCrypt2))
+ tests = append(tests, testsPlain...)
+ tests = append(tests, testsKnownKeyAuth...)
+ tests = append(tests, testsUnknownKeyAuth...)
+ tests = append(tests, testsUnsupportedDigestsAuth...)
+ tests = append(tests, testsKnownKeyCrypt...)
+ tests = append(tests, testsUnknownKeyCrypt...)
+ tests = append(tests, testsKnownKeyCrypt2...)
+
+ ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
+ defer cancel()
+
+ for i, tc := range tests {
+ func() {
+ err := tc.matcher.Provision(ctx)
+ assertNoError(t, err)
+
+ in, out := net.Pipe()
+ defer func() {
+ _, _ = io.Copy(io.Discard, out)
+ _ = out.Close()
+ }()
+
+ cx := layer4.WrapConnection(out, []byte{}, zap.NewNop())
+ go func() {
+ _, err := in.Write(tc.data)
+ assertNoError(t, err)
+ _ = in.Close()
+ }()
+
+ matched, err := tc.matcher.Match(cx)
+ assertNoError(t, err)
+
+ if matched != tc.shouldMatch {
+ if tc.shouldMatch {
+ t.Fatalf("Test %d: matcher did not match | %+v\n", i, tc.matcher)
+ } else {
+ t.Fatalf("Test %d: matcher should not match | %+v\n", i, tc.matcher)
+ }
+ }
+ }()
+ }
+}
+
+// https://github.com/OpenVPN/openvpn/blob/master/sample/sample-keys/ta.key
+var groupKey12Hex = "" +
+ "21d94830510107f8753d3b6f3145e01d" +
+ "ed37075115afcb0538ecdd8503ee9663" +
+ "7218c9ed38d908d594231d7d143c73da" +
+ "5055310f89d336da99c8b3dcb18909c7" +
+ "9dd44f540670ebc0f120beb7211e9683" +
+ "9cb542572c48bfa7ffaa9a22cb8304b7" +
+ "869b92f4442918e598745bb78ac8877f" +
+ "02b00a7cdef3f2446c130d39a7c45126" +
+ "9ef399fd6029cdfc80a7c604041312ab" +
+ "0a969bc906bdee6e6d707afdcbe8c7fb" +
+ "97beb66049c3d328340775025433ceba" +
+ "1e38008a826cf92443d903106199373b" +
+ "dadd9c2c735cf481e580db4e81b99f12" +
+ "e3f46b6159c687cd1b9e689f7712573c" +
+ "0f02735a45573dfb5cd55cf464942389" +
+ "2c7e91f439bdd7337a8ceebd302cfbfa"
+
+var groupKey12 = StaticKeyNewFromHex(groupKey12Hex, false, false)
+
+/*
+ * All the sample packets below are generated with the sample static key above.
+ */
+
+var plainPacket1 = []byte{56, 131, 30, 193, 48, 89, 179, 111, 104, 0, 0, 0, 0, 0}
+var plainPacket2 = []byte{56, 48, 212, 183, 154, 72, 13, 92, 194, 0, 0, 0, 0, 0}
+
+var authMD5Packet1 = []byte{56, 108, 88, 142, 73, 58, 114, 77, 35, 45, 192, 5, 145, 148, 66, 118, 118, 229, 204, 60, 174, 162, 74, 50, 78, 0, 0, 0, 1, 102, 234, 243, 9, 0, 0, 0, 0, 0}
+var authMD5Packet2 = []byte{56, 31, 34, 72, 211, 219, 0, 85, 46, 200, 142, 75, 104, 53, 70, 109, 234, 137, 253, 29, 138, 148, 218, 83, 141, 0, 0, 0, 1, 102, 234, 243, 39, 0, 0, 0, 0, 0}
+var authSHA1Packet1 = []byte{56, 38, 129, 217, 92, 90, 2, 14, 97, 123, 32, 15, 106, 140, 112, 232, 206, 242, 138, 133, 246, 151, 31, 71, 44, 140, 201, 188, 248, 0, 0, 0, 1, 102, 234, 241, 204, 0, 0, 0, 0, 0}
+var authSHA1Packet2 = []byte{56, 200, 170, 60, 164, 170, 196, 13, 56, 240, 33, 30, 131, 14, 244, 151, 16, 1, 7, 173, 226, 133, 237, 132, 58, 101, 188, 6, 132, 0, 0, 0, 1, 102, 234, 242, 139, 0, 0, 0, 0, 0}
+var authRIPEMD160Packet1 = []byte{56, 30, 52, 26, 73, 175, 7, 96, 168, 184, 22, 237, 90, 206, 228, 15, 190, 115, 56, 133, 2, 91, 146, 56, 141, 94, 239, 86, 106, 0, 0, 0, 1, 102, 235, 48, 209, 0, 0, 0, 0, 0}
+var authRIPEMD160Packet2 = []byte{56, 248, 184, 244, 38, 24, 29, 19, 178, 189, 169, 134, 190, 27, 29, 4, 48, 38, 158, 6, 149, 140, 127, 148, 8, 199, 15, 254, 191, 0, 0, 0, 1, 102, 235, 48, 246, 0, 0, 0, 0, 0}
+
+var authSHA224Packet1 = []byte{56, 120, 162, 216, 21, 223, 131, 234, 134, 80, 127, 130, 174, 30, 102, 244, 238, 216, 176, 213, 66, 172, 6, 45, 221, 153, 93, 227, 228, 70, 180, 76, 82, 233, 176, 242, 229, 0, 0, 0, 1, 102, 234, 243, 221, 0, 0, 0, 0, 0}
+var authSHA224Packet2 = []byte{56, 122, 21, 55, 125, 40, 237, 190, 189, 15, 86, 80, 48, 61, 30, 49, 106, 231, 188, 22, 247, 221, 163, 252, 20, 146, 229, 246, 134, 11, 85, 67, 57, 90, 81, 233, 82, 0, 0, 0, 1, 102, 234, 243, 251, 0, 0, 0, 0, 0}
+var authSHA256Packet1 = []byte{56, 241, 168, 141, 190, 188, 201, 75, 111, 199, 1, 198, 27, 138, 167, 106, 34, 70, 142, 66, 147, 64, 216, 37, 38, 62, 8, 150, 42, 120, 226, 65, 81, 10, 81, 27, 180, 47, 147, 125, 81, 0, 0, 0, 1, 102, 234, 244, 77, 0, 0, 0, 0, 0}
+var authSHA256Packet2 = []byte{56, 198, 120, 101, 184, 178, 101, 227, 112, 52, 242, 119, 31, 128, 235, 50, 107, 58, 233, 34, 122, 77, 17, 220, 196, 226, 154, 108, 211, 182, 10, 155, 196, 199, 66, 72, 174, 72, 44, 220, 36, 0, 0, 0, 1, 102, 235, 45, 106, 0, 0, 0, 0, 0}
+var authSHA384Packet1 = []byte{56, 160, 37, 56, 112, 116, 89, 66, 24, 154, 38, 199, 92, 228, 209, 62, 141, 171, 224, 61, 218, 223, 221, 98, 33, 77, 134, 136, 40, 146, 36, 112, 30, 207, 152, 170, 2, 216, 227, 212, 205, 1, 115, 113, 22, 3, 11, 8, 208, 81, 97, 20, 191, 64, 202, 169, 249, 0, 0, 0, 1, 102, 234, 244, 185, 0, 0, 0, 0, 0}
+var authSHA384Packet2 = []byte{56, 175, 250, 66, 242, 105, 89, 222, 222, 108, 120, 226, 236, 11, 225, 251, 172, 175, 118, 219, 249, 225, 140, 228, 111, 129, 234, 103, 248, 34, 220, 49, 65, 99, 241, 43, 235, 15, 49, 216, 249, 41, 140, 75, 231, 56, 33, 200, 228, 255, 207, 231, 234, 189, 248, 105, 1, 0, 0, 0, 1, 102, 235, 46, 153, 0, 0, 0, 0, 0}
+var authSHA512Packet1 = []byte{56, 161, 94, 244, 194, 238, 125, 66, 225, 158, 56, 169, 182, 153, 161, 60, 52, 18, 97, 185, 50, 29, 118, 249, 132, 174, 102, 134, 41, 219, 138, 47, 121, 94, 151, 157, 117, 100, 50, 28, 187, 17, 127, 71, 193, 79, 142, 107, 174, 210, 123, 68, 207, 70, 40, 98, 73, 118, 125, 217, 193, 236, 245, 181, 36, 237, 68, 214, 150, 103, 239, 47, 69, 0, 0, 0, 1, 102, 234, 245, 25, 0, 0, 0, 0, 0}
+var authSHA512Packet2 = []byte{56, 216, 77, 99, 2, 63, 78, 109, 231, 0, 10, 151, 81, 69, 223, 219, 180, 247, 218, 140, 170, 125, 79, 34, 161, 70, 29, 172, 91, 88, 45, 168, 55, 171, 209, 42, 255, 50, 38, 254, 254, 69, 190, 54, 201, 7, 176, 188, 231, 178, 32, 104, 23, 230, 139, 31, 109, 7, 74, 23, 204, 111, 15, 47, 184, 142, 42, 87, 177, 229, 241, 249, 5, 0, 0, 0, 1, 102, 234, 255, 102, 0, 0, 0, 0, 0}
+var authSHA512224Packet1 = []byte{56, 120, 93, 159, 15, 39, 241, 197, 215, 124, 49, 249, 190, 40, 30, 103, 24, 237, 160, 8, 161, 166, 93, 197, 148, 86, 250, 10, 149, 235, 99, 28, 241, 101, 144, 232, 87, 0, 0, 0, 1, 102, 235, 51, 45, 0, 0, 0, 0, 0}
+var authSHA512224Packet2 = []byte{56, 53, 50, 102, 177, 253, 154, 44, 246, 173, 13, 203, 52, 177, 212, 190, 163, 163, 56, 75, 12, 35, 102, 36, 104, 173, 105, 79, 88, 155, 95, 205, 120, 223, 140, 149, 46, 0, 0, 0, 1, 102, 235, 51, 79, 0, 0, 0, 0, 0}
+var authSHA512256Packet1 = []byte{56, 156, 159, 218, 246, 224, 82, 248, 79, 214, 160, 218, 53, 181, 49, 88, 113, 34, 84, 237, 38, 173, 51, 70, 73, 213, 141, 198, 137, 96, 146, 164, 20, 250, 51, 190, 127, 193, 138, 146, 150, 0, 0, 0, 1, 102, 235, 51, 120, 0, 0, 0, 0, 0}
+var authSHA512256Packet2 = []byte{56, 186, 196, 224, 32, 227, 61, 79, 121, 104, 237, 52, 167, 134, 171, 65, 80, 24, 151, 202, 16, 228, 171, 154, 174, 102, 41, 163, 190, 181, 203, 116, 114, 212, 38, 182, 6, 85, 139, 204, 151, 0, 0, 0, 1, 102, 235, 51, 146, 0, 0, 0, 0, 0}
+
+var authSHA3224Packet1 = []byte{56, 75, 11, 103, 27, 91, 109, 41, 244, 55, 38, 214, 34, 145, 221, 10, 39, 122, 95, 31, 247, 145, 61, 200, 0, 50, 20, 138, 13, 157, 64, 45, 229, 228, 103, 188, 122, 0, 0, 0, 1, 102, 235, 49, 75, 0, 0, 0, 0, 0}
+var authSHA3224Packet2 = []byte{56, 27, 135, 234, 94, 136, 16, 205, 183, 224, 158, 35, 33, 167, 179, 186, 129, 221, 189, 91, 145, 254, 97, 214, 73, 168, 98, 178, 238, 57, 164, 23, 10, 231, 232, 228, 130, 0, 0, 0, 1, 102, 235, 49, 166, 0, 0, 0, 0, 0}
+var authSHA3256Packet1 = []byte{56, 102, 44, 44, 250, 78, 239, 197, 24, 141, 207, 4, 172, 243, 182, 248, 89, 85, 126, 211, 221, 77, 58, 132, 232, 210, 92, 100, 224, 138, 249, 189, 233, 173, 65, 107, 247, 44, 12, 44, 25, 0, 0, 0, 1, 102, 235, 49, 221, 0, 0, 0, 0, 0}
+var authSHA3256Packet2 = []byte{56, 111, 169, 160, 21, 96, 100, 39, 245, 239, 232, 248, 180, 118, 223, 2, 151, 181, 0, 11, 135, 228, 62, 200, 44, 74, 41, 61, 165, 219, 6, 140, 9, 232, 100, 126, 61, 31, 78, 112, 114, 0, 0, 0, 1, 102, 235, 49, 255, 0, 0, 0, 0, 0}
+var authSHA3384Packet1 = []byte{56, 218, 149, 197, 79, 218, 74, 75, 4, 109, 230, 99, 239, 20, 110, 58, 247, 115, 155, 16, 0, 63, 246, 58, 163, 117, 183, 254, 124, 158, 57, 90, 135, 115, 197, 127, 124, 240, 153, 252, 185, 94, 204, 83, 67, 204, 234, 217, 139, 253, 229, 231, 48, 124, 223, 87, 116, 0, 0, 0, 1, 102, 235, 50, 44, 0, 0, 0, 0, 0}
+var authSHA3384Packet2 = []byte{56, 182, 206, 108, 83, 191, 147, 63, 158, 59, 66, 8, 109, 134, 242, 142, 55, 34, 184, 130, 40, 104, 48, 136, 113, 123, 93, 177, 36, 111, 185, 151, 14, 35, 42, 200, 95, 241, 192, 218, 171, 88, 217, 108, 229, 133, 112, 162, 157, 218, 83, 149, 212, 102, 179, 173, 248, 0, 0, 0, 1, 102, 235, 50, 76, 0, 0, 0, 0, 0}
+var authSHA3512Packet1 = []byte{56, 56, 51, 121, 50, 97, 82, 27, 97, 63, 0, 247, 55, 225, 68, 192, 42, 21, 209, 75, 160, 240, 219, 32, 232, 98, 25, 109, 168, 157, 235, 66, 58, 236, 39, 211, 113, 42, 63, 83, 156, 141, 128, 86, 58, 252, 72, 252, 160, 4, 49, 100, 226, 2, 23, 206, 245, 74, 243, 19, 37, 96, 95, 45, 66, 114, 214, 204, 242, 76, 169, 149, 188, 0, 0, 0, 1, 102, 235, 50, 116, 0, 0, 0, 0, 0}
+var authSHA3512Packet2 = []byte{56, 35, 221, 191, 87, 222, 230, 175, 109, 149, 129, 74, 255, 48, 70, 110, 253, 142, 246, 140, 49, 20, 10, 69, 34, 12, 194, 51, 250, 81, 94, 127, 4, 33, 56, 80, 212, 219, 18, 173, 203, 113, 67, 16, 10, 252, 253, 100, 209, 106, 164, 171, 174, 200, 225, 141, 218, 251, 169, 147, 139, 91, 67, 114, 19, 67, 180, 240, 81, 96, 189, 75, 126, 0, 0, 0, 1, 102, 235, 50, 144, 0, 0, 0, 0, 0}
+
+var authBLAKE2s256Packet1 = []byte{56, 117, 185, 201, 73, 60, 231, 94, 83, 79, 58, 65, 198, 193, 150, 251, 40, 240, 186, 67, 214, 103, 173, 128, 71, 85, 169, 180, 57, 185, 190, 142, 169, 29, 70, 15, 227, 16, 233, 122, 248, 0, 0, 0, 1, 102, 235, 52, 237, 0, 0, 0, 0, 0}
+var authBLAKE2s256Packet2 = []byte{56, 113, 195, 33, 169, 198, 218, 173, 209, 221, 244, 170, 234, 51, 121, 193, 200, 71, 196, 195, 124, 161, 83, 34, 216, 32, 220, 169, 217, 119, 173, 198, 111, 212, 180, 207, 239, 133, 126, 23, 73, 0, 0, 0, 1, 102, 235, 53, 26, 0, 0, 0, 0, 0}
+var authBLAKE2b512Packet1 = []byte{56, 85, 88, 174, 47, 95, 41, 94, 246, 10, 210, 140, 132, 252, 217, 139, 220, 49, 214, 53, 127, 38, 150, 43, 148, 226, 184, 99, 168, 70, 117, 193, 144, 71, 193, 51, 66, 175, 1, 199, 90, 168, 165, 252, 200, 183, 163, 149, 209, 150, 81, 156, 180, 62, 40, 71, 169, 157, 115, 55, 227, 142, 235, 232, 186, 16, 163, 153, 165, 225, 206, 65, 218, 0, 0, 0, 1, 102, 235, 53, 76, 0, 0, 0, 0, 0}
+var authBLAKE2b512Packet2 = []byte{56, 81, 167, 144, 212, 181, 169, 247, 183, 247, 121, 237, 117, 202, 78, 166, 17, 78, 117, 65, 216, 21, 66, 25, 153, 188, 93, 74, 87, 190, 188, 117, 121, 177, 19, 55, 28, 183, 97, 209, 51, 33, 207, 175, 170, 42, 132, 26, 136, 166, 120, 154, 94, 36, 33, 5, 43, 244, 234, 80, 16, 72, 109, 54, 200, 77, 191, 229, 251, 101, 62, 90, 10, 0, 0, 0, 1, 102, 235, 53, 114, 0, 0, 0, 0, 0}
+
+var authMD5SHA1Packet1 = []byte{56, 179, 179, 82, 117, 88, 75, 7, 103, 207, 125, 244, 183, 3, 111, 46, 96, 79, 33, 216, 220, 7, 46, 234, 213, 182, 38, 87, 48, 131, 127, 227, 208, 13, 246, 26, 169, 220, 143, 161, 18, 68, 67, 179, 92, 0, 0, 0, 1, 102, 235, 48, 73, 0, 0, 0, 0, 0}
+var authMD5SHA1Packet2 = []byte{56, 72, 91, 69, 91, 123, 156, 4, 2, 200, 34, 158, 108, 40, 218, 3, 95, 19, 203, 170, 36, 86, 29, 207, 40, 251, 124, 79, 93, 174, 221, 45, 22, 18, 125, 250, 150, 82, 37, 64, 100, 108, 251, 29, 114, 0, 0, 0, 1, 102, 235, 48, 132, 0, 0, 0, 0, 0}
+
+var authSM3Packet1 = []byte{56, 61, 163, 194, 51, 225, 14, 218, 181, 76, 125, 35, 206, 68, 24, 66, 176, 84, 237, 88, 38, 121, 213, 67, 33, 172, 83, 167, 103, 89, 82, 122, 7, 166, 156, 79, 26, 67, 191, 210, 226, 0, 0, 0, 1, 102, 235, 50, 211, 0, 0, 0, 0, 0}
+var authSM3Packet2 = []byte{56, 216, 20, 196, 148, 131, 37, 73, 181, 55, 166, 108, 86, 143, 5, 25, 20, 21, 149, 77, 221, 237, 110, 232, 237, 23, 40, 231, 192, 225, 197, 160, 172, 49, 130, 178, 78, 12, 143, 130, 229, 0, 0, 0, 1, 102, 235, 50, 243, 0, 0, 0, 0, 0}
+
+var authWhirlpoolPacket1 = []byte{56, 36, 245, 218, 233, 15, 158, 142, 144, 116, 207, 71, 60, 35, 210, 55, 90, 223, 92, 122, 87, 53, 131, 122, 248, 84, 50, 42, 69, 254, 231, 197, 30, 216, 65, 242, 173, 160, 127, 229, 165, 224, 32, 16, 118, 45, 197, 158, 145, 0, 130, 72, 78, 104, 107, 247, 100, 54, 185, 151, 70, 116, 219, 15, 112, 175, 78, 30, 177, 222, 41, 223, 29, 0, 0, 0, 1, 102, 235, 55, 2, 0, 0, 0, 0, 0}
+var authWhirlpoolPacket2 = []byte{56, 31, 112, 117, 225, 131, 182, 114, 117, 242, 139, 201, 7, 108, 19, 165, 164, 120, 243, 26, 152, 195, 228, 36, 162, 249, 105, 116, 114, 121, 232, 215, 253, 153, 249, 243, 54, 55, 83, 70, 28, 19, 87, 233, 54, 164, 69, 52, 96, 28, 107, 220, 236, 226, 35, 224, 155, 100, 78, 59, 174, 220, 107, 120, 56, 100, 160, 122, 182, 96, 94, 83, 71, 0, 0, 0, 1, 102, 235, 55, 144, 0, 0, 0, 0, 0}
+
+var cryptPacket1 = []byte{56, 76, 98, 159, 244, 184, 134, 148, 158, 0, 0, 0, 1, 102, 237, 91, 50, 14, 141, 87, 40, 125, 165, 204, 227, 61, 5, 91, 201, 99, 44, 253, 7, 202, 200, 84, 124, 48, 80, 144, 250, 52, 248, 173, 26, 201, 173, 67, 166, 16, 189, 73, 203, 12}
+var cryptPacket2 = []byte{56, 162, 49, 153, 71, 88, 124, 182, 93, 0, 0, 0, 1, 102, 237, 91, 95, 84, 154, 63, 127, 63, 175, 65, 227, 69, 45, 146, 14, 64, 81, 56, 239, 162, 229, 54, 81, 103, 167, 133, 38, 57, 83, 119, 60, 149, 149, 218, 201, 144, 193, 202, 149, 111}
+
+/*
+ * All the sample packets below are generated with another static key.
+ */
+
+var cryptPacket3 = []byte{56, 114, 151, 86, 204, 204, 137, 212, 215, 0, 0, 0, 1, 102, 231, 24, 196, 58, 184, 197, 69, 200, 222, 132, 120, 248, 163, 68, 112, 17, 137, 97, 240, 56, 122, 62, 49, 172, 177, 176, 86, 180, 187, 148, 69, 17, 251, 38, 0, 31, 203, 0, 237, 122}
+var cryptPacket4 = []byte{56, 49, 193, 232, 78, 82, 175, 151, 76, 0, 0, 0, 1, 102, 231, 25, 92, 82, 125, 47, 131, 35, 217, 41, 164, 145, 71, 178, 38, 218, 194, 60, 100, 167, 212, 8, 160, 131, 22, 61, 246, 52, 20, 100, 6, 16, 108, 18, 127, 24, 185, 240, 99, 156}
+
+var authSHA512Packet3 = []byte{56, 9, 137, 51, 217, 234, 95, 85, 78, 254, 110, 108, 95, 38, 212, 11, 224, 47, 57, 16, 51, 199, 136, 76, 111, 191, 16, 107, 75, 219, 113, 162, 191, 67, 46, 146, 184, 246, 177, 52, 53, 53, 127, 191, 5, 184, 24, 166, 146, 223, 234, 222, 239, 9, 92, 227, 241, 225, 196, 46, 230, 138, 3, 5, 85, 186, 65, 251, 189, 11, 16, 28, 102, 0, 0, 0, 1, 102, 231, 16, 138, 0, 0, 0, 0, 0}
+var authSHA512Packet4 = []byte{56, 178, 216, 6, 201, 115, 66, 0, 252, 112, 99, 34, 163, 140, 85, 246, 137, 75, 183, 212, 159, 38, 251, 25, 190, 253, 36, 249, 198, 196, 70, 177, 201, 14, 65, 227, 248, 77, 108, 115, 189, 160, 244, 174, 98, 107, 141, 70, 231, 120, 91, 118, 74, 229, 197, 11, 34, 193, 58, 35, 253, 148, 135, 235, 90, 101, 6, 152, 24, 139, 17, 204, 33, 0, 0, 0, 1, 102, 231, 16, 206, 0, 0, 0, 0, 0}
+var authSHA384Packet3 = []byte{56, 219, 201, 226, 49, 70, 125, 55, 178, 191, 78, 40, 216, 206, 58, 20, 224, 132, 135, 191, 172, 205, 188, 24, 176, 48, 143, 139, 127, 225, 202, 39, 8, 196, 77, 57, 9, 41, 94, 103, 73, 169, 38, 206, 220, 2, 48, 62, 228, 47, 75, 97, 94, 55, 92, 204, 186, 0, 0, 0, 1, 102, 231, 17, 154, 0, 0, 0, 0, 0}
+var authSHA384Packet4 = []byte{56, 16, 226, 49, 167, 146, 222, 219, 13, 3, 106, 151, 111, 210, 227, 142, 102, 16, 216, 234, 94, 111, 244, 11, 94, 253, 12, 186, 117, 92, 196, 92, 65, 107, 141, 17, 229, 249, 197, 17, 103, 41, 223, 153, 181, 117, 29, 117, 56, 22, 175, 162, 64, 31, 77, 122, 72, 0, 0, 0, 1, 102, 231, 17, 193, 0, 0, 0, 0, 0}
+var authMD5SHA1Packet3 = []byte{56, 99, 167, 201, 107, 123, 246, 212, 180, 87, 93, 31, 188, 10, 53, 149, 139, 232, 13, 207, 71, 108, 154, 143, 114, 180, 196, 221, 157, 16, 106, 225, 14, 219, 137, 223, 222, 146, 106, 226, 168, 120, 86, 22, 124, 0, 0, 0, 1, 102, 231, 23, 250, 0, 0, 0, 0, 0}
+var authMD5SHA1Packet4 = []byte{56, 37, 80, 49, 19, 199, 20, 62, 202, 74, 99, 211, 73, 42, 204, 120, 193, 83, 126, 182, 7, 159, 177, 126, 206, 70, 29, 198, 68, 211, 249, 15, 123, 201, 45, 193, 38, 134, 223, 186, 236, 58, 235, 55, 130, 0, 0, 0, 1, 102, 231, 24, 40, 0, 0, 0, 0, 0}
+var authSHA256Packet3 = []byte{56, 102, 151, 183, 239, 253, 36, 110, 23, 150, 73, 73, 166, 35, 204, 199, 240, 149, 243, 16, 8, 55, 68, 108, 31, 11, 74, 186, 254, 65, 15, 81, 5, 222, 184, 12, 106, 72, 2, 114, 154, 0, 0, 0, 1, 102, 231, 17, 9, 0, 0, 0, 0, 0}
+var authSHA256Packet4 = []byte{56, 95, 17, 119, 145, 64, 76, 195, 82, 28, 120, 32, 8, 114, 93, 80, 206, 88, 123, 172, 37, 73, 97, 54, 221, 37, 7, 157, 39, 147, 73, 251, 107, 61, 52, 76, 11, 97, 161, 3, 96, 0, 0, 0, 1, 102, 231, 17, 93, 0, 0, 0, 0, 0}
+var authSHA224Packet3 = []byte{56, 27, 28, 60, 231, 13, 31, 116, 190, 88, 126, 12, 34, 137, 96, 59, 7, 91, 163, 246, 60, 2, 38, 129, 69, 217, 9, 24, 18, 36, 210, 88, 86, 2, 226, 0, 96, 0, 0, 0, 1, 102, 231, 20, 101, 0, 0, 0, 0, 0}
+var authSHA224Packet4 = []byte{56, 248, 223, 183, 225, 174, 116, 5, 214, 134, 211, 177, 21, 142, 215, 9, 8, 164, 55, 40, 10, 206, 40, 254, 173, 235, 176, 126, 12, 67, 35, 221, 219, 209, 30, 244, 178, 0, 0, 0, 1, 102, 231, 20, 137, 0, 0, 0, 0, 0}
+var authSHA1Packet3 = []byte{56, 237, 200, 39, 23, 233, 70, 6, 161, 241, 68, 106, 124, 33, 176, 55, 84, 222, 250, 76, 156, 191, 179, 213, 51, 159, 4, 62, 210, 0, 0, 0, 1, 102, 231, 18, 45, 0, 0, 0, 0, 0}
+var authSHA1Packet4 = []byte{56, 130, 31, 160, 196, 239, 31, 197, 116, 52, 169, 99, 61, 200, 67, 148, 130, 219, 9, 88, 119, 50, 245, 69, 146, 204, 29, 211, 206, 0, 0, 0, 1, 102, 231, 18, 99, 0, 0, 0, 0, 0}
+var authMD5Packet3 = []byte{56, 215, 5, 24, 98, 120, 183, 161, 99, 207, 88, 65, 149, 207, 91, 106, 49, 202, 38, 190, 180, 159, 186, 132, 12, 0, 0, 0, 1, 102, 231, 18, 140, 0, 0, 0, 0, 0}
+var authMD5Packet4 = []byte{56, 54, 43, 62, 107, 142, 244, 8, 206, 17, 95, 99, 7, 97, 10, 102, 63, 210, 191, 101, 209, 192, 183, 94, 67, 0, 0, 0, 1, 102, 231, 18, 188, 0, 0, 0, 0, 0}
+
+var plainPacket3 = []uint8{56, 232, 90, 55, 186, 10, 31, 142, 127, 0, 0, 0, 0, 0}
+var plainPacket4 = []uint8{56, 177, 22, 70, 225, 86, 175, 190, 204, 0, 0, 0, 0, 0}
+
+/*
+ * All the sample packets below are generated with a pair of tls-crypt-v2 server key and tls-crypt-v2 client key.
+ */
+
+var serverKey56Base64 = "U2hihe8H77pInpRzMEWNZ/NwM1CBSSVSw5HyXT7/+1pspISJzKBiECs+LRvE6QlwgKm606H1wLv0defgJRNU1UG1fi25oMPqjFcYybU+wOgY8eX6OWM0EWI6d2XaL6Neu1E9fMGDAWnzQFsFZhMQH80xv0kzzLm13UjL7lrdQnM="
+var clientKey56Base64 = "HZVyTZ3S2YMR9UFUei+kWmNcCaxxT31StqhozQFXVQ41WK203PFtunbuA7HZNPaBLQbyC3aaxwGcEsqW1Jnm/3WptcPWFYhFGhW+H37x2howQyAGj6IIsjZQyS9gwYgGr8bVNTZIywz3hw+KLRCAzkhTqk6ONen1wf5rewu03g2RNq/suLU6V31OTDOxeyb1WkUA25Ych7le6FJzO8YqI5jOosokID3ueT05vCdMIDa6FsHR3BmPjX2OYLquV+wBF7IBykKcxrCvrT2Qf/tBpv7PtUkKx6pCApKiUuPxLALMxqv3ATa7vrCB8qZQWmO4dRY40ORYZ622MiqCDmDFGKQca1bPxdHdpFCr5o8gvYHR9p0Xm2t6KcCvAO+CUAMKA1DsUqIYrrhQIkkZI/dbffBGnxyrb/XlzGnSynv5d8oAl7QSrW292JLtm7VCGE7NePEMvAx9iAXNTwxpdwNNfPmb3ZNWq7H+mGUaYQpcs7xW45zzyUkV9fESFEnlePxeJ3pjqxawcjnOBy1yLk/RKyFOakNJ/Z6r7IFjgI6usooo1i7+Tp6O5MHQasWcpyLWzW5KvjGPIKH0SFl6VJdBMkWWYm1D5KdgqXX8716uNvpY0dVpXgE+KfJPxIRQDGCuS8zyrxAHCvm8MAbiOpi9g35/4vKFj+cvar4EssHrvSX+l0Z7q5kGnMbnvStH7MEzTQXCcMT30vDWBn2W9SEkOh9KIq1z3/46owEr"
+
+var serverKey56 = StaticKeyNewFromBase64(serverKey56Base64, false, false)
+var clientKey56 = WrappedKeyNewFromBase64(clientKey56Base64)
+
+var crypt2Packet5 = []byte{80, 100, 224, 45, 159, 27, 166, 162, 220, 15, 0, 0, 1, 102, 240, 100, 162, 53, 85, 10, 213, 183, 32, 34, 176, 186, 16, 66, 59, 48, 128, 24, 240, 143, 116, 59, 133, 18, 152, 241, 84, 81, 95, 195, 181, 88, 112, 148, 217, 127, 200, 222, 197, 88, 164, 28, 107, 86, 207, 197, 209, 221, 164, 80, 171, 230, 143, 32, 189, 129, 209, 246, 157, 23, 155, 107, 122, 41, 192, 175, 0, 239, 130, 80, 3, 10, 3, 80, 236, 82, 162, 24, 174, 184, 80, 34, 73, 25, 35, 247, 91, 125, 240, 70, 159, 28, 171, 111, 245, 229, 204, 105, 210, 202, 123, 249, 119, 202, 0, 151, 180, 18, 173, 109, 189, 216, 146, 237, 155, 181, 66, 24, 78, 205, 120, 241, 12, 188, 12, 125, 136, 5, 205, 79, 12, 105, 119, 3, 77, 124, 249, 155, 221, 147, 86, 171, 177, 254, 152, 101, 26, 97, 10, 92, 179, 188, 86, 227, 156, 243, 201, 73, 21, 245, 241, 18, 20, 73, 229, 120, 252, 94, 39, 122, 99, 171, 22, 176, 114, 57, 206, 7, 45, 114, 46, 79, 209, 43, 33, 78, 106, 67, 73, 253, 158, 171, 236, 129, 99, 128, 142, 174, 178, 138, 40, 214, 46, 254, 78, 158, 142, 228, 193, 208, 106, 197, 156, 167, 34, 214, 205, 110, 74, 190, 49, 143, 32, 161, 244, 72, 89, 122, 84, 151, 65, 50, 69, 150, 98, 109, 67, 228, 167, 96, 169, 117, 252, 239, 94, 174, 54, 250, 88, 209, 213, 105, 94, 1, 62, 41, 242, 79, 196, 132, 80, 12, 96, 174, 75, 204, 242, 175, 16, 7, 10, 249, 188, 48, 6, 226, 58, 152, 189, 131, 126, 127, 226, 242, 133, 143, 231, 47, 106, 190, 4, 178, 193, 235, 189, 37, 254, 151, 70, 123, 171, 153, 6, 156, 198, 231, 189, 43, 71, 236, 193, 51, 77, 5, 194, 112, 196, 247, 210, 240, 214, 6, 125, 150, 245, 33, 36, 58, 31, 74, 34, 173, 115, 223, 254, 58, 163, 1, 43}
+var crypt2Packet6 = []byte{80, 136, 240, 154, 124, 25, 199, 138, 59, 15, 0, 0, 1, 102, 240, 107, 4, 103, 91, 3, 26, 182, 97, 79, 186, 12, 192, 49, 251, 104, 205, 177, 215, 107, 141, 155, 102, 232, 247, 246, 206, 142, 216, 230, 20, 218, 58, 153, 248, 131, 173, 105, 2, 213, 164, 28, 107, 86, 207, 197, 209, 221, 164, 80, 171, 230, 143, 32, 189, 129, 209, 246, 157, 23, 155, 107, 122, 41, 192, 175, 0, 239, 130, 80, 3, 10, 3, 80, 236, 82, 162, 24, 174, 184, 80, 34, 73, 25, 35, 247, 91, 125, 240, 70, 159, 28, 171, 111, 245, 229, 204, 105, 210, 202, 123, 249, 119, 202, 0, 151, 180, 18, 173, 109, 189, 216, 146, 237, 155, 181, 66, 24, 78, 205, 120, 241, 12, 188, 12, 125, 136, 5, 205, 79, 12, 105, 119, 3, 77, 124, 249, 155, 221, 147, 86, 171, 177, 254, 152, 101, 26, 97, 10, 92, 179, 188, 86, 227, 156, 243, 201, 73, 21, 245, 241, 18, 20, 73, 229, 120, 252, 94, 39, 122, 99, 171, 22, 176, 114, 57, 206, 7, 45, 114, 46, 79, 209, 43, 33, 78, 106, 67, 73, 253, 158, 171, 236, 129, 99, 128, 142, 174, 178, 138, 40, 214, 46, 254, 78, 158, 142, 228, 193, 208, 106, 197, 156, 167, 34, 214, 205, 110, 74, 190, 49, 143, 32, 161, 244, 72, 89, 122, 84, 151, 65, 50, 69, 150, 98, 109, 67, 228, 167, 96, 169, 117, 252, 239, 94, 174, 54, 250, 88, 209, 213, 105, 94, 1, 62, 41, 242, 79, 196, 132, 80, 12, 96, 174, 75, 204, 242, 175, 16, 7, 10, 249, 188, 48, 6, 226, 58, 152, 189, 131, 126, 127, 226, 242, 133, 143, 231, 47, 106, 190, 4, 178, 193, 235, 189, 37, 254, 151, 70, 123, 171, 153, 6, 156, 198, 231, 189, 43, 71, 236, 193, 51, 77, 5, 194, 112, 196, 247, 210, 240, 214, 6, 125, 150, 245, 33, 36, 58, 31, 74, 34, 173, 115, 223, 254, 58, 163, 1, 43}
diff --git a/modules/l4openvpn/messages.go b/modules/l4openvpn/messages.go
new file mode 100644
index 0000000..ed101eb
--- /dev/null
+++ b/modules/l4openvpn/messages.go
@@ -0,0 +1,948 @@
+// Copyright 2024 VNXME
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package l4openvpn
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/binary"
+ "errors"
+ "slices"
+ "time"
+)
+
+// MessageHeader is an OpenVPN message header.
+type MessageHeader struct {
+ // Opcode is a type of message that follows.
+ Opcode uint8
+ // KeyID refers to an already negotiated TLS session. It must equal zero for client reset messages.
+ KeyID uint8
+}
+
+// FromBytes fills msg's internal structures from source bytes.
+func (msg *MessageHeader) FromBytes(src []byte) error {
+ // Any MessageHeader has exactly 1 byte.
+ if len(src) != OpcodeKeyIDBytesTotal {
+ return ErrInvalidSourceLength
+ }
+
+ // Opcode occupies the high 5 bits, KeyID extends over the low 3 bits.
+ msg.KeyID, msg.Opcode = src[0]&KeyIDMask, src[0]>>OpcodeShift
+
+ return nil
+}
+
+// ToBytes return a slice of bytes representing msg's internal structures.
+func (msg *MessageHeader) ToBytes() []byte {
+ dst := make([]byte, 0, OpcodeKeyIDBytesTotal)
+ dst = append(dst, msg.KeyID|(msg.Opcode< 0 && msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0
+}
+
+// ToBytes return a slice of bytes representing msg's internal structures.
+func (msg *MessagePlain) ToBytes() []byte {
+ dst := make([]byte, 0, MessagePlainBytesTotal)
+ dst = append(dst, msg.MessageHeader.ToBytes()...)
+ dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID)
+ dst = append(dst, msg.PrevPacketIDsCount)
+ dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID)
+ return dst
+}
+
+// MessageAuth is a P_CONTROL_HARD_RESET_CLIENT_V2-type OpenVPN message with authentication.
+type MessageAuth struct {
+ MessagePlain
+ MessageTraitAuth
+ MessageTraitReplay
+}
+
+// Authenticate returns true if msg has a valid HMAC.
+func (msg *MessageAuth) Authenticate(ad *AuthDigest, sk *StaticKey) bool {
+ return msg.MessageTraitAuth.AuthenticateOnServer(ad, sk, msg.ToBytesAuth)
+}
+
+// FromBytes fills msg's internal structures from a slice of bytes.
+func (msg *MessageAuth) FromBytes(src []byte) error {
+ // Any MessageAuth is between 38 and 86 bytes (with a header).
+ if len(src) < MessageAuthBytesMin || len(src) > MessageAuthBytesMax {
+ return ErrInvalidSourceLength
+ }
+
+ // Parse MessageHeader
+ hdr := &MessageHeader{}
+ if err := hdr.FromBytes(src[:OpcodeKeyIDBytesTotal]); err != nil {
+ return err
+ }
+
+ // Match MessageHeader.Opcode
+ if hdr.Opcode != OpcodeControlHardResetClientV2 {
+ return ErrInvalidHeaderOpcode
+ }
+
+ // Parse everything else
+ return msg.FromBytesHeadless(src[OpcodeKeyIDBytesTotal:], hdr)
+}
+
+// FromBytesHeadless fills msg's internal structures from a slice of bytes and uses a pre-filled header.
+func (msg *MessageAuth) FromBytesHeadless(src []byte, hdr *MessageHeader) error {
+ // Any MessageAuth is between 37 and 85 bytes long (without a header).
+ if len(src) < MessageAuthBytesMinHL || len(src) > MessageAuthBytesMaxHL {
+ return ErrInvalidSourceLength
+ }
+
+ // Check for a non-empty MessageHeader.
+ if hdr == nil {
+ return ErrMissingReusableHeader
+ }
+
+ // Assign Header to the specified pointer.
+ msg.MessageHeader = *hdr
+
+ // Any MessageAuth has an 8-byte LocalSessionID at the beginning.
+ msg.LocalSessionID = BytesOrder.Uint64(src[:SessionIDBytesTotal])
+
+ // Any MessageAuth has a 16-to-64-byte HMAC between LocalSessionID and ReplayPacketID.
+ off1, off2 := SessionIDBytesTotal, len(src)-2*PacketIDBytesTotal-OpcodeKeyIDBytesTotal-TimestampBytesTotal
+ msg.HMAC = src[off1:off2]
+
+ // Check whether a valid HMAC length is provided.
+ if !slices.Contains(AuthDigestSizes, len(msg.HMAC)) {
+ return ErrInvalidHMACLength
+ }
+
+ // Any MessageAuth has a 4-byte ReplayPacketID between HMAC and ReplayTimestamp.
+ off1, off2 = off2, off2+PacketIDBytesTotal
+ msg.ReplayPacketID = BytesOrder.Uint32(src[off1:off2])
+
+ // Any MessageAuth has a 4-byte ReplayTimestamp between ReplayPacketID and PrevPacketIDsCount.
+ off1, off2 = off2, off2+TimestampBytesTotal
+ msg.ReplayTimestamp = BytesOrder.Uint32(src[off1:off2])
+
+ // Any MessageAuth has a 1-byte PrevPacketIDsCount between ReplayTimestamp and ThisPacketID.
+ off1, off2 = off2, off2+OpcodeKeyIDBytesTotal
+ msg.PrevPacketIDsCount = src[off1]
+
+ // Any MessageAuth has a 4-byte ThisPacketID at the end.
+ msg.ThisPacketID = BytesOrder.Uint32(src[off2:])
+
+ return nil
+}
+
+// Match returns true if msg's internal structures have valid values.
+func (msg *MessageAuth) Match(ignoreTimestamp, ignoreCrypto bool, ad *AuthDigest, sk *StaticKey) bool {
+ return msg.MessagePlain.Match() &&
+ msg.ReplayPacketID == 1 && (ignoreTimestamp || msg.ValidateReplayTimestamp(time.Now())) &&
+ (ad == nil || ad.Size == len(msg.MessageTraitAuth.HMAC)) &&
+ (ignoreCrypto || sk == nil || msg.Authenticate(ad, sk))
+}
+
+// Sign computes and fills msg's HMAC.
+func (msg *MessageAuth) Sign(ad *AuthDigest, sk *StaticKey) error {
+ return msg.MessageTraitAuth.SignOnClient(ad, sk, msg.ToBytesAuth)
+}
+
+// ToBytes returns a slice of bytes representing msg's internal structures.
+func (msg *MessageAuth) ToBytes() []byte {
+ dst := make([]byte, 0, MessagePlainBytesTotal+len(msg.HMAC)+PacketIDBytesTotal+TimestampBytesTotal)
+ dst = append(dst, msg.MessageHeader.ToBytes()...)
+ dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID)
+ dst = append(dst, msg.HMAC...)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp)
+ dst = append(dst, msg.PrevPacketIDsCount)
+ dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID)
+ return dst
+}
+
+// ToBytesAuth returns a slice of bytes representing msg's internal structures without HMAC.
+func (msg *MessageAuth) ToBytesAuth() []byte {
+ dst := make([]byte, 0, MessagePlainBytesTotal+PacketIDBytesTotal+TimestampBytesTotal)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp)
+ dst = append(dst, msg.MessageHeader.ToBytes()...)
+ dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID)
+ dst = append(dst, msg.PrevPacketIDsCount)
+ dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID)
+ return dst
+}
+
+// MessageCrypt is a P_CONTROL_HARD_RESET_CLIENT_V2-type OpenVPN message with authentication and encryption.
+type MessageCrypt struct {
+ MessageAuth
+ MessageTraitCrypt
+}
+
+// Authenticate returns true if msg has a valid HMAC.
+func (msg *MessageCrypt) Authenticate(ad *AuthDigest, sk *StaticKey) bool {
+ return msg.MessageTraitAuth.AuthenticateOnServer(ad, sk, msg.ToBytesAuth)
+}
+
+// DecryptAndAuthenticate decrypts msg's encrypted bytes before calling Authenticate.
+func (msg *MessageCrypt) DecryptAndAuthenticate(ad *AuthDigest, sk *StaticKey) bool {
+ if len(msg.Encrypted) != OpcodeKeyIDBytesTotal+PacketIDBytesTotal ||
+ msg.MessageTraitCrypt.DecryptOnServer(sk, &msg.MessageTraitAuth, msg.FromBytesCrypt) != nil {
+ return false
+ }
+ return msg.Authenticate(ad, sk)
+}
+
+// EncryptAndSign encrypts msg's plain bytes before calling Sign.
+func (msg *MessageCrypt) EncryptAndSign(ad *AuthDigest, sk *StaticKey) error {
+ if err := msg.MessageTraitCrypt.EncryptOnClient(sk, &msg.MessageTraitAuth, msg.ToBytesCrypt); err != nil {
+ return err
+ }
+ return msg.Sign(ad, sk)
+}
+
+// FromBytes fills msg's internal structures from a slice of bytes.
+func (msg *MessageCrypt) FromBytes(src []byte) error {
+ // Any MessageCrypt is between 54 bytes (with a header).
+ if len(src) != MessageCryptBytesTotal {
+ return ErrInvalidSourceLength
+ }
+
+ // Parse MessageHeader
+ hdr := &MessageHeader{}
+ if err := hdr.FromBytes(src[:OpcodeKeyIDBytesTotal]); err != nil {
+ return err
+ }
+
+ // Match MessageHeader.Opcode
+ if hdr.Opcode != OpcodeControlHardResetClientV2 {
+ return ErrInvalidHeaderOpcode
+ }
+
+ // Parse everything else
+ return msg.FromBytesHeadless(src[OpcodeKeyIDBytesTotal:], hdr)
+}
+
+// FromBytesHeadless fills msg's internal structures from a slice of bytes and uses a pre-filled header.
+func (msg *MessageCrypt) FromBytesHeadless(src []byte, hdr *MessageHeader) error {
+ // Any MessageCrypt is exactly 53 bytes long (without a header).
+ if len(src) != MessageCryptBytesTotalHL {
+ return ErrInvalidSourceLength
+ }
+
+ // Check for a non-empty MessageHeader.
+ if hdr == nil {
+ return ErrMissingReusableHeader
+ }
+
+ // Assign Header to the specified pointer.
+ msg.MessageHeader = *hdr
+
+ // Any MessageCrypt has an 8-byte LocalSessionID at the beginning.
+ msg.LocalSessionID = BytesOrder.Uint64(src[:SessionIDBytesTotal])
+
+ // Any MessageCrypt has a 4-byte ReplayPacketID between LocalSessionID and ReplayTimestamp.
+ off1, off2 := SessionIDBytesTotal, SessionIDBytesTotal+PacketIDBytesTotal
+ msg.ReplayPacketID = BytesOrder.Uint32(src[off1:off2])
+
+ // Any MessageCrypt has a 4-byte ReplayTimestamp between ReplayPacketID and HMAC.
+ off1, off2 = off2, off2+TimestampBytesTotal
+ msg.ReplayTimestamp = BytesOrder.Uint32(src[off1:off2])
+
+ // Any MessageCrypt has a 32-byte SHA-256 HMAC between ReplayTimestamp and Encrypted bytes.
+ off1, off2 = off2, off2+AuthDigestDefault.Size
+ msg.Digest, msg.HMAC = AuthDigestDefault, src[off1:off2]
+
+ // Any MessageCrypt has a 5-byte Encrypted part at the end.
+ msg.Cipher, msg.Encrypted = CryptCipherDefault, src[off2:]
+
+ return nil
+}
+
+// FromBytesCrypt fills msg's internal structures from a slice of bytes after decryption.
+func (msg *MessageCrypt) FromBytesCrypt(plain []byte) error {
+ if len(plain) != len(msg.Encrypted) {
+ return ErrInvalidPlainLength
+ }
+
+ // Any MessageCrypt has a PrevPacketIDsCount at the beginning of the plain text.
+ msg.PrevPacketIDsCount = plain[0]
+
+ // Any MessageCrypt has a ThisPacketID at the end of the plain text.
+ msg.ThisPacketID = BytesOrder.Uint32(plain[OpcodeKeyIDBytesTotal:])
+
+ return nil
+}
+
+// Match returns true if msg's internal structures have valid values.
+func (msg *MessageCrypt) Match(ignoreTimestamp, ignoreCrypto bool, ad *AuthDigest, sk *StaticKey) bool {
+ return msg.LocalSessionID > 0 &&
+ msg.ReplayPacketID == 1 && (ignoreTimestamp || msg.ValidateReplayTimestamp(time.Now())) &&
+ (ignoreCrypto || sk == nil || (msg.DecryptAndAuthenticate(ad, sk) &&
+ msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0))
+}
+
+// Sign computes and fills msg's HMAC.
+func (msg *MessageCrypt) Sign(ad *AuthDigest, sk *StaticKey) error {
+ return msg.MessageTraitAuth.SignOnClient(ad, sk, msg.ToBytesAuth)
+}
+
+// ToBytes returns a slice of bytes representing msg's internal structures.
+func (msg *MessageCrypt) ToBytes() []byte {
+ dst := make([]byte, 0, MessagePlainBytesTotal+len(msg.HMAC)+PacketIDBytesTotal+TimestampBytesTotal)
+ dst = append(dst, msg.MessageHeader.ToBytes()...)
+ dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp)
+ dst = append(dst, msg.HMAC...)
+ dst = append(dst, msg.Encrypted...)
+ return dst
+}
+
+// ToBytesAuth returns a slice of bytes representing msg's internal structures without HMAC.
+func (msg *MessageCrypt) ToBytesAuth() []byte {
+ dst := make([]byte, 0, MessagePlainBytesTotal+PacketIDBytesTotal+TimestampBytesTotal)
+ dst = append(dst, msg.MessageHeader.ToBytes()...)
+ dst = BytesOrder.AppendUint64(dst, msg.LocalSessionID)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayPacketID)
+ dst = BytesOrder.AppendUint32(dst, msg.ReplayTimestamp)
+ dst = append(dst, msg.PrevPacketIDsCount)
+ dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID)
+ return dst
+}
+
+// ToBytesCrypt returns a slice of bytes representing msg's internal structures before encryption.
+func (msg *MessageCrypt) ToBytesCrypt() []byte {
+ dst := make([]byte, 0, OpcodeKeyIDBytesTotal+PacketIDBytesTotal)
+ dst = append(dst, msg.PrevPacketIDsCount)
+ dst = BytesOrder.AppendUint32(dst, msg.ThisPacketID)
+ return dst
+}
+
+// MessageCrypt2 is a P_CONTROL_HARD_RESET_CLIENT_V3-type OpenVPN message with authentication and encryption
+// using a wrapped client key which is authenticated and encrypted with a server key.
+type MessageCrypt2 struct {
+ MessageCrypt
+ WrappedKey
+}
+
+// DecryptAndAuthenticate decrypts and authenticates msg's encrypted bytes (WrappedKey before MessageCrypt).
+func (msg *MessageCrypt2) DecryptAndAuthenticate(ad *AuthDigest, sk *StaticKey) bool {
+ if !msg.WrappedKey.DecryptAndAuthenticate(ad, sk) {
+ return false
+ }
+ return msg.MessageCrypt.DecryptAndAuthenticate(ad, &msg.WrappedKey.StaticKey)
+}
+
+// EncryptAndSign encrypts and signs msg's plain bytes (WrappedKey before MessageCrypt).
+func (msg *MessageCrypt2) EncryptAndSign(ad *AuthDigest, sk *StaticKey) error {
+ if err := msg.WrappedKey.EncryptAndSign(ad, sk); err != nil {
+ return err
+ }
+ return msg.MessageCrypt.EncryptAndSign(ad, &msg.WrappedKey.StaticKey)
+}
+
+// FromBytes fills msg's internal structures from a slice of bytes.
+func (msg *MessageCrypt2) FromBytes(src []byte) error {
+ // Any MessageCrypt2 is between 344 and 600 bytes (with a header).
+ if len(src) < MessageCrypt2BytesMin || len(src) > MessageCrypt2BytesMax {
+ return ErrInvalidSourceLength
+ }
+
+ // Parse MessageHeader
+ hdr := &MessageHeader{}
+ if err := hdr.FromBytes(src[:OpcodeKeyIDBytesTotal]); err != nil {
+ return err
+ }
+
+ // Match MessageHeader.Opcode
+ if hdr.Opcode != OpcodeControlHardResetClientV3 {
+ return ErrInvalidHeaderOpcode
+ }
+
+ // Parse everything else
+ return msg.FromBytesHeadless(src[OpcodeKeyIDBytesTotal:], hdr)
+}
+
+// FromBytesHeadless fills msg's internal structures from a slice of bytes and uses a pre-filled header.
+func (msg *MessageCrypt2) FromBytesHeadless(src []byte, hdr *MessageHeader) error {
+ // Any MessageCrypt2 is between 343 and 1077 bytes long (without a header).
+ if len(src) < MessageCrypt2BytesMinHL || len(src) > MessageCrypt2BytesMaxHL {
+ return ErrInvalidSourceLength
+ }
+
+ if err := msg.MessageCrypt.FromBytesHeadless(src[:MessageCryptBytesTotalHL], hdr); err != nil {
+ return err
+ }
+
+ return msg.WrappedKey.FromBytes(src[MessageCryptBytesTotalHL:])
+}
+
+// Match returns true if msg's internal structures have valid values.
+func (msg *MessageCrypt2) Match(ignoreTimestamp, ignoreCrypto bool, ad *AuthDigest, sk *StaticKey, cks []*WrappedKey) bool {
+ if !(msg.LocalSessionID > 0 && (msg.ReplayPacketID == 1 || msg.ReplayPacketID == 0x0f000001) &&
+ (ignoreTimestamp || msg.ValidateReplayTimestamp(time.Now()))) {
+ return false
+ }
+
+ if ignoreCrypto {
+ return true
+ }
+
+ if len(cks) > 0 {
+ for _, ck := range cks {
+ if bytes.Equal(ck.HMAC, msg.WrappedKey.HMAC) && bytes.Equal(ck.Encrypted, msg.WrappedKey.Encrypted) {
+ return msg.MessageCrypt.DecryptAndAuthenticate(ad, &ck.StaticKey) &&
+ msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0
+ }
+ }
+ return false
+ }
+
+ return sk == nil || (msg.DecryptAndAuthenticate(ad, sk) && msg.PrevPacketIDsCount == 0 && msg.ThisPacketID == 0)
+}
+
+// ToBytes returns a slice of bytes representing msg's internal structures.
+func (msg *MessageCrypt2) ToBytes() []byte {
+ return slices.Concat(msg.MessageCrypt.ToBytes(), msg.WrappedKey.ToBytes())
+}
+
+// MessageTraitAuth contains fields and implements features related to authentication.
+type MessageTraitAuth struct {
+ // Digest is a pointer to AuthDigest to be used for authentication.
+ Digest *AuthDigest
+ // HMAC is a hash-based messaged authentication code used to verify message integrity.
+ HMAC []byte
+}
+
+// AuthenticateOnClient returns true if msg composed on the client has a valid HMAC.
+func (msg *MessageTraitAuth) AuthenticateOnClient(ad *AuthDigest, sk *StaticKey, xp func() []byte) bool {
+ // No supported digest returns an HMAC of 0 bytes.
+ if len(msg.HMAC) == 0 {
+ return false
+ }
+
+ // Check that there is a valid StaticKey.
+ // A half key may be provided as a server key to validate a WrappedKey.
+ if sk == nil || !(len(sk.KeyBytes) == StaticKeyBytesTotal || len(sk.KeyBytes) == StaticKeyBytesHalf) {
+ return false
+ }
+
+ // Obtain a slice of bytes to compute a digest of.
+ plain := xp()
+
+ // Firstly, try the given digest.
+ if ad != nil {
+ if len(msg.HMAC) == ad.Size && ad.HMACValidateOnClient(sk, plain, msg.HMAC) {
+ msg.Digest = ad
+ return true
+ }
+ return false
+ }
+
+ // Secondly, try the last successful one saved as Digest.
+ if msg.Digest != nil && len(msg.HMAC) == msg.Digest.Size && msg.Digest.HMACValidateOnClient(sk, plain, msg.HMAC) {
+ return true
+ }
+
+ // Finally, try all other supported digests one by one.
+ for _, mad := range AuthDigests {
+ if mad != msg.Digest && len(msg.HMAC) == mad.Size && mad.HMACValidateOnClient(sk, plain, msg.HMAC) {
+ msg.Digest = mad
+ return true
+ }
+ }
+
+ return false
+}
+
+// AuthenticateOnServer returns true if msg composed on the server has a valid HMAC.
+func (msg *MessageTraitAuth) AuthenticateOnServer(ad *AuthDigest, sk *StaticKey, xp func() []byte) bool {
+ // No supported digest returns an HMAC of 0 bytes.
+ if len(msg.HMAC) == 0 {
+ return false
+ }
+
+ // Check that there is a valid StaticKey.
+ if sk == nil || len(sk.KeyBytes) != StaticKeyBytesTotal {
+ return false
+ }
+
+ // Obtain a slice of bytes to compute a digest of.
+ plain := xp()
+
+ // Firstly, try the given digest.
+ if ad != nil {
+ if len(msg.HMAC) == ad.Size && ad.HMACValidateOnServer(sk, plain, msg.HMAC) {
+ msg.Digest = ad
+ return true
+ }
+ return false
+ }
+
+ // Secondly, try the last successful one saved as Digest.
+ if msg.Digest != nil && len(msg.HMAC) == msg.Digest.Size && msg.Digest.HMACValidateOnServer(sk, plain, msg.HMAC) {
+ return true
+ }
+
+ // Finally, try all other supported digests one by one.
+ for _, mad := range AuthDigests {
+ if mad != msg.Digest && len(msg.HMAC) == mad.Size && mad.HMACValidateOnServer(sk, plain, msg.HMAC) {
+ msg.Digest = mad
+ return true
+ }
+ }
+
+ return false
+}
+
+// SignOnClient computes an HMAC for msg composed on the client.
+func (msg *MessageTraitAuth) SignOnClient(ad *AuthDigest, sk *StaticKey, xp func() []byte) error {
+ // Check that there is a valid StaticKey.
+ // A half key may be provided as a server key to sign a WrappedKey.
+ if sk == nil || !(len(sk.KeyBytes) == StaticKeyBytesTotal || len(sk.KeyBytes) == StaticKeyBytesHalf) {
+ return ErrInvalidAuthPrerequisites
+ }
+
+ // Obtain a slice of bytes to compute a digest of.
+ plain := xp()
+
+ // Firstly, try the given digest.
+ if ad != nil {
+ msg.HMAC = ad.HMACGenerateOnClient(sk, plain)
+ }
+
+ // Secondly, try the internal Digest.
+ if msg.Digest != nil {
+ msg.HMAC = msg.Digest.HMACGenerateOnClient(sk, plain)
+ }
+
+ // Finally, use the default digest.
+ msg.Digest, msg.HMAC = AuthDigestDefault, AuthDigestDefault.HMACGenerateOnClient(sk, plain)
+
+ return nil
+}
+
+// SignOnServer computes an HMAC for msg composed on the server.
+func (msg *MessageTraitAuth) SignOnServer(ad *AuthDigest, sk *StaticKey, xp func() []byte) error {
+ // Check that there is a valid StaticKey.
+ // A half key may be provided as a server key to sign a WrappedKey.
+ if sk == nil || !(len(sk.KeyBytes) == StaticKeyBytesTotal || len(sk.KeyBytes) == StaticKeyBytesHalf) {
+ return ErrInvalidAuthPrerequisites
+ }
+
+ // Obtain a slice of bytes to compute a digest of.
+ plain := xp()
+
+ // Firstly, try the given digest.
+ if ad != nil {
+ msg.HMAC = ad.HMACGenerateOnServer(sk, plain)
+ }
+
+ // Secondly, try the internal Digest.
+ if msg.Digest != nil {
+ msg.HMAC = msg.Digest.HMACGenerateOnServer(sk, plain)
+ }
+
+ // Finally, use the default digest.
+ msg.Digest, msg.HMAC = AuthDigestDefault, AuthDigestDefault.HMACGenerateOnServer(sk, plain)
+
+ return nil
+}
+
+// MessageTraitCrypt contains fields and implements features related to en-/decryption.
+type MessageTraitCrypt struct {
+ // Cipher is a pointer to CryptCipher to be used for encryption.
+ Cipher *CryptCipher
+ // Encrypted contains a number of bytes to be decrypted.
+ Encrypted []byte
+}
+
+// DecryptOnClient decrypts msg's encrypted bytes with an encryption key used by the server.
+func (msg *MessageTraitCrypt) DecryptOnClient(sk *StaticKey, ta *MessageTraitAuth, xd func([]byte) error) error {
+ if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size {
+ return ErrInvalidCryptPrerequisites
+ }
+
+ plain := msg.Cipher.DecryptOnClient(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], msg.Encrypted)
+
+ return xd(plain)
+}
+
+// DecryptOnServer decrypts msg's encrypted bytes with an encryption key used by the client.
+func (msg *MessageTraitCrypt) DecryptOnServer(sk *StaticKey, ta *MessageTraitAuth, xd func([]byte) error) error {
+ if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size {
+ return ErrInvalidCryptPrerequisites
+ }
+
+ plain := msg.Cipher.DecryptOnServer(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], msg.Encrypted)
+
+ return xd(plain)
+}
+
+// EncryptOnClient encrypts msg's plain bytes with a decryption key used by the server.
+func (msg *MessageTraitCrypt) EncryptOnClient(sk *StaticKey, ta *MessageTraitAuth, xe func() []byte) error {
+ if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size {
+ return ErrInvalidCryptPrerequisites
+ }
+
+ plain := xe()
+
+ msg.Encrypted = msg.Cipher.EncryptOnClient(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], plain)
+ if len(plain) != len(msg.Encrypted) {
+ return ErrInvalidEncryptedLength
+ }
+
+ return nil
+}
+
+// EncryptOnServer encrypts msg's plain bytes with a decryption key used by the client.
+func (msg *MessageTraitCrypt) EncryptOnServer(sk *StaticKey, ta *MessageTraitAuth, xe func() []byte) error {
+ if sk == nil || msg.Cipher == nil || ta.Digest == nil || len(ta.HMAC) != ta.Digest.Size {
+ return ErrInvalidCryptPrerequisites
+ }
+
+ plain := xe()
+
+ msg.Encrypted = msg.Cipher.EncryptOnServer(sk, ta.HMAC[:min(msg.Cipher.SizeBlock, ta.Digest.Size)], plain)
+ if len(plain) != len(msg.Encrypted) {
+ return ErrInvalidEncryptedLength
+ }
+
+ return nil
+}
+
+// MessageTraitReplay contains fields and implements features related to replay protection.
+type MessageTraitReplay struct {
+ // ReplayPacketID is a 4-byte packet ID used for replay protection.
+ ReplayPacketID uint32
+ // ReplayTimestamp is a 4-byte timestamp used for replay protection.
+ ReplayTimestamp uint32
+}
+
+// ValidateReplayTimestamp returns true if msg has a valid ReplayTimestamp (relative to the provided time).
+func (msg *MessageTraitReplay) ValidateReplayTimestamp(t time.Time) bool {
+ tReplay := time.Unix(int64(msg.ReplayTimestamp), 0)
+ tNowLow := t.UTC().Add(-TimestampValidationInterval)
+ tNowHigh := t.UTC().Add(TimestampValidationInterval)
+ return tReplay.After(tNowLow) && tReplay.Before(tNowHigh)
+}
+
+// MetaData is an optional set of Type and Payload that may be included into WrappedKey.
+type MetaData struct {
+ // Payload contains a number of bytes of variable length corresponding to Type.
+ Payload []byte
+ // Type indicates how to parse Payload correctly. It may equal 0x00 for any user defined data
+ // and 0x01 for a 4-byte timestamp which enables discarding old client keys on the server.
+ Type uint8
+}
+
+// WrappedKey is an authenticated and encrypted client key used to encrypt MessageCrypt in the crypt2 mode.
+type WrappedKey struct {
+ MetaData
+ StaticKey
+ MessageTraitAuth
+ MessageTraitCrypt
+}
+
+// Authenticate returns true if wk's HMAC is valid.
+func (wk *WrappedKey) Authenticate(ad *AuthDigest, sk *StaticKey) bool {
+ return wk.MessageTraitAuth.AuthenticateOnClient(ad, sk, wk.ToBytesAuth)
+}
+
+// DecryptAndAuthenticate decrypts wk's encrypted bytes before calling Authenticate.
+func (wk *WrappedKey) DecryptAndAuthenticate(ad *AuthDigest, sk *StaticKey) bool {
+ if len(wk.Encrypted) < StaticKeyBytesTotal ||
+ len(wk.Encrypted) > WrappedKeyBytesMax-LengthBytesTotal-CryptHMACBytesTotal ||
+ wk.MessageTraitCrypt.DecryptOnClient(sk, &wk.MessageTraitAuth, wk.FromBytesCrypt) != nil {
+ return false
+ }
+ return wk.Authenticate(ad, sk)
+}
+
+// EncryptAndSign encrypts wk's plain bytes before calling Sign.
+func (wk *WrappedKey) EncryptAndSign(ad *AuthDigest, sk *StaticKey) error {
+ if err := wk.MessageTraitCrypt.EncryptOnServer(sk, &wk.MessageTraitAuth, wk.ToBytesCrypt); err != nil {
+ return err
+ }
+ return wk.Sign(ad, sk)
+}
+
+// FromBase64 fills wk's internal structures (including decrypted StaticKey bytes) from a base64 string.
+func (wk *WrappedKey) FromBase64(s string) error {
+ src, err := base64.StdEncoding.DecodeString(s)
+ if err != nil {
+ return err
+ }
+
+ if len(src) < WrappedKeyBytesMin+StaticKeyBytesTotal || len(src) > WrappedKeyBytesMax+StaticKeyBytesTotal {
+ return ErrInvalidSourceLength
+ }
+
+ wk.StaticKey.KeyBytes = src[:StaticKeyBytesTotal]
+
+ return wk.FromBytes(src[StaticKeyBytesTotal:])
+}
+
+// FromBytes fills wk's internal structures from a slice of bytes.
+func (wk *WrappedKey) FromBytes(src []byte) error {
+ // Any WrappedKey has a 2-byte length at the end. Ensure that it has a valid value.
+ if len(src) < WrappedKeyBytesMin || len(src) > WrappedKeyBytesMax ||
+ len(src) != int(BytesOrder.Uint16(src[len(src)-LengthBytesTotal:])) {
+ return ErrInvalidSourceLength
+ }
+
+ // Any WrappedKey has a 32-byte SHA-256 HMAC at the beginning.
+ wk.Digest, wk.HMAC = AuthDigestDefault, src[:CryptHMACBytesTotal]
+
+ // Any WrappedKey has an Encrypted part of at least 256 bytes between HMAC and Length.
+ wk.Cipher, wk.Encrypted = CryptCipherDefault, src[CryptHMACBytesTotal:len(src)-LengthBytesTotal]
+
+ return nil
+}
+
+// FromBytesCrypt fills wk's internal structures from a slice of bytes after decryption.
+func (wk *WrappedKey) FromBytesCrypt(plain []byte) error {
+ if len(plain) != len(wk.Encrypted) {
+ return ErrInvalidPlainLength
+ }
+
+ // Any WrappedKey has a StaticKey at the beginning of the plain text.
+ wk.StaticKey.KeyBytes = plain[:StaticKeyBytesTotal]
+
+ // Any WrappedKey may have a MetaData.Type between StaticKey and MetaData in the plain text.
+ if len(plain) > StaticKeyBytesTotal {
+ wk.MetaData.Type = plain[StaticKeyBytesTotal]
+ }
+
+ // Any KeyMata may have MetaData.Payload at the end of the plain text.
+ if len(plain) > StaticKeyBytesTotal+MetaDataTypeBytesTotal {
+ wk.MetaData.Payload = plain[StaticKeyBytesTotal+MetaDataTypeBytesTotal:]
+ }
+
+ return nil
+}
+
+// FromClientKeyFile fills wk's internal structures (including decrypted StaticKey bytes) from a given file.
+func (wk *WrappedKey) FromClientKeyFile(path string) error {
+ err := wk.StaticKey.FromFile(path, StaticKeyFromFileBase64, 0, wk.StaticKey.FromBase64)
+ if err != nil {
+ return err
+ }
+
+ if len(wk.StaticKey.KeyBytes) < WrappedKeyBytesMin || len(wk.StaticKey.KeyBytes) > WrappedKeyBytesMax {
+ return ErrInvalidStaticKeyFileContents
+ }
+
+ err = wk.FromBytes(wk.StaticKey.KeyBytes[StaticKeyBytesTotal:])
+ if err != nil {
+ return err
+ }
+
+ wk.StaticKey.KeyBytes = wk.StaticKey.KeyBytes[:StaticKeyBytesTotal]
+ return nil
+}
+
+// Sign computes and fills wk's HMAC.
+func (wk *WrappedKey) Sign(ad *AuthDigest, sk *StaticKey) error {
+ return wk.MessageTraitAuth.SignOnClient(ad, sk, wk.ToBytesAuth)
+}
+
+// ToBytes returns a slice of bytes representing wk's internal structures.
+func (wk *WrappedKey) ToBytes() []byte {
+ dst := make([]byte, 0, len(wk.HMAC)+len(wk.Encrypted)+LengthBytesTotal)
+ dst = append(dst, wk.HMAC...)
+ dst = append(dst, wk.Encrypted...)
+ dst = BytesOrder.AppendUint16(dst, uint16(cap(dst)))
+ return dst
+}
+
+// ToBytesAuth returns a slice of bytes representing wk's internal structures without HMAC.
+func (wk *WrappedKey) ToBytesAuth() []byte {
+ if len(wk.MetaData.Payload) > 0 {
+ dst := make([]byte, 0, LengthBytesTotal+len(wk.StaticKey.KeyBytes)+MetaDataTypeBytesTotal+len(wk.MetaData.Payload))
+ dst = BytesOrder.AppendUint16(dst, uint16(cap(dst)+CryptHMACBytesTotal))
+ dst = append(dst, wk.StaticKey.KeyBytes...)
+ dst = append(dst, wk.MetaData.Type)
+ dst = append(dst, wk.MetaData.Payload...)
+ return dst
+ } else {
+ dst := make([]byte, 0, LengthBytesTotal+len(wk.StaticKey.KeyBytes))
+ dst = BytesOrder.AppendUint16(dst, uint16(cap(dst)+CryptHMACBytesTotal))
+ dst = append(dst, wk.StaticKey.KeyBytes...)
+ return dst
+ }
+}
+
+// ToBytesCrypt returns a slice of bytes representing wk's internal structures before encryption.
+func (wk *WrappedKey) ToBytesCrypt() []byte {
+ if len(wk.MetaData.Payload) > 0 {
+ dst := make([]byte, 0, len(wk.StaticKey.KeyBytes)+MetaDataTypeBytesTotal+len(wk.MetaData.Payload))
+ dst = append(dst, wk.StaticKey.KeyBytes...)
+ dst = append(dst, wk.MetaData.Type)
+ dst = append(dst, wk.MetaData.Payload...)
+ return dst
+ } else {
+ return wk.StaticKey.KeyBytes
+ }
+}
+
+var (
+ BytesOrder = binary.BigEndian
+
+ ErrInvalidAuthPrerequisites = errors.New("invalid auth prerequisites")
+ ErrInvalidCryptPrerequisites = errors.New("invalid crypt prerequisites")
+ ErrInvalidEncryptedLength = errors.New("invalid encrypted length")
+ ErrInvalidHeaderOpcode = errors.New("invalid header opcode")
+ ErrInvalidHMACLength = errors.New("invalid HMAC length")
+ ErrInvalidPlainLength = errors.New("invalid plain length")
+ ErrInvalidSourceLength = errors.New("invalid source length")
+ ErrMissingReusableHeader = errors.New("missing reusable header")
+)
+
+const (
+ /*
+ * Irrelevant MessageHeader.Opcode values:
+ *
+ * OpcodeControlHardResetClientV1 uint8 = 1
+ * OpcodeControlHardResetServerV1 uint8 = 2
+ * OpcodeControlSoftResetV1 uint8 = 3
+ * OpcodeControlV1 uint8 = 4
+ * OpcodeAckV1 uint8 = 5
+ * OpcodeDataV1 uint8 = 6
+ * OpcodeControlHardResetServerV2 uint8 = 8
+ * OpcodeDataV2 uint8 = 9
+ * OpcodeControlWrappedClientKeyV1 uint8 = 11
+ */
+
+ OpcodeControlHardResetClientV2 uint8 = 7
+ OpcodeControlHardResetClientV3 uint8 = 10
+
+ KeyIDMask uint8 = 0b1<