mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-10-10 04:03:11 +08:00
516 lines
14 KiB
Go
516 lines
14 KiB
Go
package openvpn
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/chacha20poly1305"
|
|
)
|
|
|
|
const (
|
|
DataChannelTagSize = 16
|
|
DataChannelIVSize = 12
|
|
DataChannelCBCIVSize = aes.BlockSize
|
|
dataChannelIVRandBuf = 32 * 1024
|
|
dataChannelReplayWindow = 64
|
|
|
|
PeerIDUnset uint32 = 0xffffff
|
|
)
|
|
|
|
// OpenVPN data-channel ping payload, matching PING_STRING in upstream OpenVPN.
|
|
var openVPNPingPacket = []byte{
|
|
0x2a, 0x18, 0x7b, 0xf3,
|
|
0x64, 0x1e, 0xb4, 0xcb,
|
|
0x07, 0xed, 0x2d, 0x0a,
|
|
0x98, 0x1f, 0xc7, 0x48,
|
|
}
|
|
|
|
func IsPingPacket(packet []byte) bool {
|
|
return bytes.Equal(packet, openVPNPingPacket)
|
|
}
|
|
|
|
type DataChannel struct {
|
|
sendAEAD cipher.AEAD
|
|
recvAEAD cipher.AEAD
|
|
|
|
sendBlock cipher.Block
|
|
recvBlock cipher.Block
|
|
sendHMACKey []byte
|
|
recvHMACKey []byte
|
|
authHash func() hash.Hash
|
|
authSize int
|
|
sendMACPool sync.Pool
|
|
recvMACPool sync.Pool
|
|
|
|
sendImplicitIV [DataChannelIVSize]byte
|
|
recvImplicitIV [DataChannelIVSize]byte
|
|
|
|
keyID uint8
|
|
peerID uint32
|
|
header []byte
|
|
|
|
mu sync.Mutex
|
|
sendPacketID uint32
|
|
// recvEvidence latches true once a data packet labeled with this key ID
|
|
// decrypted successfully. The peer only labels outbound packets with a
|
|
// key whose authentication completed (OpenVPN tls_pre_encrypt /
|
|
// handle_data_channel_packet require KS_AUTH_TRUE), so this is the
|
|
// reliable signal that this epoch has been activated by the peer and can
|
|
// replace the lame-duck for outbound traffic. Stored per-epoch so a
|
|
// back-to-back rekey cannot attribute an older epoch's evidence to a
|
|
// newer key. Guarded by d.mu.
|
|
recvEvidence bool
|
|
recvHighest uint32
|
|
recvWindow uint64
|
|
recvSeen bool
|
|
|
|
randMu sync.Mutex
|
|
randBuf []byte
|
|
randOffset int
|
|
}
|
|
|
|
func NewDataChannel(keys *KeyMaterial, cipherName, authName string, peerID uint32, keyID uint8) (*DataChannel, error) {
|
|
if keys == nil {
|
|
return nil, errors.New("nil openvpn key material")
|
|
}
|
|
if isDataChannelAEAD(cipherName) {
|
|
send, err := newDataChannelAEAD(cipherName, keys.SendCipherKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create send cipher: %w", err)
|
|
}
|
|
recv, err := newDataChannelAEAD(cipherName, keys.RecvCipherKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create recv cipher: %w", err)
|
|
}
|
|
if len(keys.SendHMACKey) < DataChannelIVSize-4 || len(keys.RecvHMACKey) < DataChannelIVSize-4 {
|
|
return nil, errors.New("openvpn implicit IV keys are too short")
|
|
}
|
|
d := &DataChannel{
|
|
sendAEAD: send,
|
|
recvAEAD: recv,
|
|
keyID: keyID & KeyIDMask,
|
|
peerID: peerID,
|
|
header: dataHeader(peerID, keyID),
|
|
}
|
|
copy(d.sendImplicitIV[4:], keys.SendHMACKey[:DataChannelIVSize-4])
|
|
copy(d.recvImplicitIV[4:], keys.RecvHMACKey[:DataChannelIVSize-4])
|
|
return d, nil
|
|
}
|
|
|
|
send, err := newDataChannelCBC(cipherName, keys.SendCipherKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create send cipher: %w", err)
|
|
}
|
|
recv, err := newDataChannelCBC(cipherName, keys.RecvCipherKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create recv cipher: %w", err)
|
|
}
|
|
authHash, authSize, err := newDataChannelAuth(authName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(keys.SendHMACKey) < authSize || len(keys.RecvHMACKey) < authSize {
|
|
return nil, errors.New("openvpn HMAC keys are too short")
|
|
}
|
|
d := &DataChannel{
|
|
sendBlock: send,
|
|
recvBlock: recv,
|
|
sendHMACKey: append([]byte(nil), keys.SendHMACKey[:authSize]...),
|
|
recvHMACKey: append([]byte(nil), keys.RecvHMACKey[:authSize]...),
|
|
authHash: authHash,
|
|
authSize: authSize,
|
|
keyID: keyID & KeyIDMask,
|
|
peerID: peerID,
|
|
header: dataHeader(peerID, keyID),
|
|
}
|
|
d.sendMACPool.New = func() any {
|
|
return hmac.New(d.authHash, d.sendHMACKey)
|
|
}
|
|
d.recvMACPool.New = func() any {
|
|
return hmac.New(d.authHash, d.recvHMACKey)
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func isDataChannelAEAD(cipherName string) bool {
|
|
switch cipherName {
|
|
case CipherAES128GCM, CipherAES192GCM, CipherAES256GCM, CipherChaCha20Poly1305:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func newDataChannelAEAD(cipherName string, key []byte) (cipher.AEAD, error) {
|
|
switch cipherName {
|
|
case CipherAES128GCM, CipherAES192GCM, CipherAES256GCM:
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return cipher.NewGCMWithTagSize(block, DataChannelTagSize)
|
|
case CipherChaCha20Poly1305:
|
|
return chacha20poly1305.New(key)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported openvpn cipher %q", cipherName)
|
|
}
|
|
}
|
|
|
|
func newDataChannelCBC(cipherName string, key []byte) (cipher.Block, error) {
|
|
switch cipherName {
|
|
case CipherAESCBC, CipherAES128CBC, CipherAES192CBC, CipherAES256CBC:
|
|
return aes.NewCipher(key)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported openvpn cipher %q", cipherName)
|
|
}
|
|
}
|
|
|
|
func newDataChannelAuth(authName string) (func() hash.Hash, int, error) {
|
|
switch authName {
|
|
case AuthMD5:
|
|
return md5.New, md5.Size, nil
|
|
case AuthSHA1:
|
|
return sha1.New, sha1.Size, nil
|
|
case AuthSHA256:
|
|
return sha256.New, sha256.Size, nil
|
|
case AuthSHA384:
|
|
return sha512.New384, sha512.Size384, nil
|
|
case AuthSHA512:
|
|
return sha512.New, sha512.Size, nil
|
|
default:
|
|
return nil, 0, fmt.Errorf("unsupported openvpn auth %q", authName)
|
|
}
|
|
}
|
|
|
|
func (d *DataChannel) Encrypt(packet []byte) ([]byte, error) {
|
|
if d == nil {
|
|
return nil, errors.New("nil openvpn data channel")
|
|
}
|
|
|
|
packetID, err := d.nextPacketID()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var encrypted []byte
|
|
if d.sendAEAD != nil {
|
|
encrypted, err = d.encryptAEAD(packet, packetID)
|
|
} else {
|
|
encrypted, err = d.encryptCBC(packet, packetID)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return encrypted, nil
|
|
}
|
|
|
|
func (d *DataChannel) encryptAEAD(packet []byte, packetID uint32) ([]byte, error) {
|
|
header := d.header
|
|
var packetIDBytes [4]byte
|
|
binary.BigEndian.PutUint32(packetIDBytes[:], packetID)
|
|
nonce := d.nonce(packetID, d.sendImplicitIV)
|
|
ad := make([]byte, 0, len(header)+len(packetIDBytes))
|
|
ad = append(ad, header...)
|
|
ad = append(ad, packetIDBytes[:]...)
|
|
sealed := d.sendAEAD.Seal(nil, nonce[:], packet, ad)
|
|
|
|
out := make([]byte, 0, len(header)+4+DataChannelTagSize+len(packet))
|
|
out = append(out, header...)
|
|
out = append(out, packetIDBytes[:]...)
|
|
out = append(out, sealed[len(sealed)-DataChannelTagSize:]...)
|
|
out = append(out, sealed[:len(sealed)-DataChannelTagSize]...)
|
|
return out, nil
|
|
}
|
|
|
|
func (d *DataChannel) encryptCBC(packet []byte, packetID uint32) ([]byte, error) {
|
|
header := d.header
|
|
blockSize := d.sendBlock.BlockSize()
|
|
plainLen := 4 + len(packet)
|
|
padding := blockSize - plainLen%blockSize
|
|
if padding == 0 {
|
|
padding = blockSize
|
|
}
|
|
out := make([]byte, len(header)+d.authSize+DataChannelCBCIVSize+plainLen+padding)
|
|
copy(out, header)
|
|
authenticated := out[len(header)+d.authSize:]
|
|
iv := authenticated[:DataChannelCBCIVSize]
|
|
if err := d.fillCBCIV(iv); err != nil {
|
|
return nil, err
|
|
}
|
|
ciphertext := authenticated[DataChannelCBCIVSize:]
|
|
binary.BigEndian.PutUint32(ciphertext[:4], packetID)
|
|
copy(ciphertext[4:], packet)
|
|
for i := plainLen; i < len(ciphertext); i++ {
|
|
ciphertext[i] = byte(padding)
|
|
}
|
|
cipher.NewCBCEncrypter(d.sendBlock, iv).CryptBlocks(ciphertext, ciphertext)
|
|
d.hmacCopy(&d.sendMACPool, authenticated, out[len(header):])
|
|
return out, nil
|
|
}
|
|
|
|
func (d *DataChannel) Decrypt(packet []byte) ([]byte, error) {
|
|
if d == nil {
|
|
return nil, errors.New("nil openvpn data channel")
|
|
}
|
|
headerSize, err := dataPacketHeaderSize(packet)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if d.recvAEAD != nil {
|
|
return d.decryptAEAD(packet, headerSize)
|
|
}
|
|
return d.decryptCBC(packet, headerSize)
|
|
}
|
|
|
|
func (d *DataChannel) decryptAEAD(packet []byte, headerSize int) ([]byte, error) {
|
|
if len(packet) < 1 {
|
|
return nil, errors.New("empty openvpn data packet")
|
|
}
|
|
if len(packet) < headerSize+4+DataChannelTagSize+1 {
|
|
return nil, errors.New("openvpn data packet too short")
|
|
}
|
|
header := packet[:headerSize]
|
|
packetIDBytes := packet[headerSize : headerSize+4]
|
|
packetID := binary.BigEndian.Uint32(packetIDBytes)
|
|
tag := packet[headerSize+4 : headerSize+4+DataChannelTagSize]
|
|
ciphertext := packet[headerSize+4+DataChannelTagSize:]
|
|
combined := make([]byte, 0, len(ciphertext)+DataChannelTagSize)
|
|
combined = append(combined, ciphertext...)
|
|
combined = append(combined, tag...)
|
|
ad := make([]byte, 0, len(header)+len(packetIDBytes))
|
|
ad = append(ad, header...)
|
|
ad = append(ad, packetIDBytes...)
|
|
|
|
nonce := d.nonce(packetID, d.recvImplicitIV)
|
|
plain, err := d.recvAEAD.Open(nil, nonce[:], combined, ad)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := d.acceptPacketID(packetID); err != nil {
|
|
return nil, err
|
|
}
|
|
return plain, nil
|
|
}
|
|
|
|
func (d *DataChannel) decryptCBC(packet []byte, headerSize int) ([]byte, error) {
|
|
blockSize := d.recvBlock.BlockSize()
|
|
minPacketLen := headerSize + d.authSize + blockSize + blockSize
|
|
if len(packet) < minPacketLen {
|
|
return nil, errors.New("openvpn CBC data packet too short")
|
|
}
|
|
|
|
body := packet[headerSize:]
|
|
tag := body[:d.authSize]
|
|
authenticated := body[d.authSize:]
|
|
var expectedBuf [sha256.Size]byte
|
|
expected := d.hmacAppend(&d.recvMACPool, authenticated, expectedBuf[:0])
|
|
if !hmac.Equal(tag, expected) {
|
|
return nil, errors.New("openvpn CBC data packet HMAC authentication failed")
|
|
}
|
|
|
|
iv := authenticated[:blockSize]
|
|
ciphertext := authenticated[blockSize:]
|
|
if len(ciphertext) == 0 || len(ciphertext)%blockSize != 0 {
|
|
return nil, errors.New("invalid openvpn CBC ciphertext length")
|
|
}
|
|
cipher.NewCBCDecrypter(d.recvBlock, iv).CryptBlocks(ciphertext, ciphertext)
|
|
plain, err := pkcs7Unpad(ciphertext, blockSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(plain) < 4 {
|
|
return nil, errors.New("openvpn CBC plaintext missing packet id")
|
|
}
|
|
|
|
packetID := binary.BigEndian.Uint32(plain[:4])
|
|
if err := d.acceptPacketID(packetID); err != nil {
|
|
return nil, err
|
|
}
|
|
return plain[4:], nil
|
|
}
|
|
|
|
func dataPacketHeaderSize(packet []byte) (int, error) {
|
|
if len(packet) < 1 {
|
|
return 0, errors.New("empty openvpn data packet")
|
|
}
|
|
opcode, _ := parseOpcodeKeyID(packet[0])
|
|
switch opcode {
|
|
case PDataV1:
|
|
return 1, nil
|
|
case PDataV2:
|
|
if len(packet) < 4 {
|
|
return 0, errors.New("openvpn P_DATA_V2 packet missing peer id")
|
|
}
|
|
return 4, nil
|
|
default:
|
|
return 0, fmt.Errorf("not an openvpn data packet: %s", opcode)
|
|
}
|
|
}
|
|
|
|
const dataPacketIDRekeyThreshold = uint32(0xFF000000)
|
|
|
|
var errDataPacketIDExhausted = errors.New("openvpn data packet id reached rekey threshold")
|
|
|
|
func (d *DataChannel) nextPacketID() (uint32, error) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
// OpenVPN starts a soft reset once packet_id_close_to_wrapping reaches
|
|
// this threshold. This client cannot initiate that reset yet, so surface
|
|
// the condition and let the adapter reconnect instead of silently
|
|
// blackholing packets or approaching nonce reuse.
|
|
if d.sendPacketID >= dataPacketIDRekeyThreshold {
|
|
return 0, errDataPacketIDExhausted
|
|
}
|
|
d.sendPacketID++
|
|
return d.sendPacketID, nil
|
|
}
|
|
|
|
// MarkPeerActive records that a packet labeled with this key ID decrypted
|
|
// successfully, i.e. the peer has activated this epoch.
|
|
func (d *DataChannel) MarkPeerActive() {
|
|
d.mu.Lock()
|
|
d.recvEvidence = true
|
|
d.mu.Unlock()
|
|
}
|
|
|
|
// PeerActive reports whether the peer has activated this epoch.
|
|
func (d *DataChannel) PeerActive() bool {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
return d.recvEvidence
|
|
}
|
|
|
|
func (d *DataChannel) acceptPacketID(packetID uint32) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
if !d.recvSeen {
|
|
d.recvHighest = packetID
|
|
d.recvWindow = 1
|
|
d.recvSeen = true
|
|
return nil
|
|
}
|
|
|
|
if packetID > d.recvHighest {
|
|
shift := packetID - d.recvHighest
|
|
if shift >= dataChannelReplayWindow {
|
|
d.recvWindow = 1
|
|
} else {
|
|
d.recvWindow = d.recvWindow<<shift | 1
|
|
}
|
|
d.recvHighest = packetID
|
|
return nil
|
|
}
|
|
|
|
diff := d.recvHighest - packetID
|
|
if diff >= dataChannelReplayWindow {
|
|
return fmt.Errorf("openvpn replayed data packet id %d", packetID)
|
|
}
|
|
mask := uint64(1) << diff
|
|
if d.recvWindow&mask != 0 {
|
|
return fmt.Errorf("openvpn replayed data packet id %d", packetID)
|
|
}
|
|
d.recvWindow |= mask
|
|
return nil
|
|
}
|
|
|
|
func dataHeader(peerID uint32, keyID uint8) []byte {
|
|
if peerID != PeerIDUnset {
|
|
return []byte{
|
|
opcodeKeyID(PDataV2, keyID),
|
|
byte(peerID >> 16),
|
|
byte(peerID >> 8),
|
|
byte(peerID),
|
|
}
|
|
}
|
|
return []byte{opcodeKeyID(PDataV1, keyID)}
|
|
}
|
|
|
|
func (d *DataChannel) nonce(packetID uint32, implicit [DataChannelIVSize]byte) [DataChannelIVSize]byte {
|
|
nonce := implicit
|
|
binary.BigEndian.PutUint32(nonce[:4], binary.BigEndian.Uint32(nonce[:4])^packetID)
|
|
return nonce
|
|
}
|
|
|
|
func (d *DataChannel) fillCBCIV(iv []byte) error {
|
|
d.randMu.Lock()
|
|
defer d.randMu.Unlock()
|
|
|
|
for len(iv) > 0 {
|
|
if d.randOffset == len(d.randBuf) {
|
|
if d.randBuf == nil {
|
|
d.randBuf = make([]byte, dataChannelIVRandBuf)
|
|
}
|
|
if _, err := rand.Read(d.randBuf); err != nil {
|
|
return err
|
|
}
|
|
d.randOffset = 0
|
|
}
|
|
n := copy(iv, d.randBuf[d.randOffset:])
|
|
d.randOffset += n
|
|
iv = iv[n:]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *DataChannel) hmacAppend(pool *sync.Pool, data, dst []byte) []byte {
|
|
mac := pool.Get().(hash.Hash)
|
|
defer pool.Put(mac)
|
|
mac.Reset()
|
|
_, _ = mac.Write(data)
|
|
return mac.Sum(dst)
|
|
}
|
|
|
|
// hmacCopy writes the HMAC of data into dst (which must have enough
|
|
// capacity), returning the number of bytes written. Unlike hmacAppend it
|
|
// does not rely on mac.Sum(dst) appending into dst's backing array — it
|
|
// always writes the tag into dst explicitly.
|
|
func (d *DataChannel) hmacCopy(pool *sync.Pool, data, dst []byte) int {
|
|
mac := pool.Get().(hash.Hash)
|
|
defer pool.Put(mac)
|
|
mac.Reset()
|
|
_, _ = mac.Write(data)
|
|
n := copy(dst, mac.Sum(nil))
|
|
return n
|
|
}
|
|
|
|
func pkcs7Unpad(padded []byte, blockSize int) ([]byte, error) {
|
|
if len(padded) == 0 || len(padded)%blockSize != 0 {
|
|
return nil, errors.New("invalid openvpn CBC padding length")
|
|
}
|
|
padding := int(padded[len(padded)-1])
|
|
if padding == 0 || padding > blockSize || padding > len(padded) {
|
|
return nil, errors.New("invalid openvpn CBC padding")
|
|
}
|
|
for _, b := range padded[len(padded)-padding:] {
|
|
if int(b) != padding {
|
|
return nil, errors.New("invalid openvpn CBC padding")
|
|
}
|
|
}
|
|
return padded[:len(padded)-padding], nil
|
|
}
|
|
|
|
func ParsePeerID(options string) uint32 {
|
|
for _, field := range splitPushOptions(options) {
|
|
if len(field) > len("peer-id ") && field[:len("peer-id ")] == "peer-id " {
|
|
var id uint32
|
|
if _, err := fmt.Sscanf(field, "peer-id %d", &id); err == nil && id <= PeerIDUnset {
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
return PeerIDUnset
|
|
}
|