First Commit
This commit is contained in:
265
analyzer/udp/dns.go
Normal file
265
analyzer/udp/dns.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer"
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer/utils"
|
||||
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
)
|
||||
|
||||
const (
|
||||
dnsUDPInvalidCountThreshold = 4
|
||||
)
|
||||
|
||||
// DNSAnalyzer is for both DNS over UDP and TCP.
|
||||
var (
|
||||
_ analyzer.UDPAnalyzer = (*DNSAnalyzer)(nil)
|
||||
_ analyzer.TCPAnalyzer = (*DNSAnalyzer)(nil)
|
||||
)
|
||||
|
||||
type DNSAnalyzer struct{}
|
||||
|
||||
func (a *DNSAnalyzer) Name() string {
|
||||
return "dns"
|
||||
}
|
||||
|
||||
func (a *DNSAnalyzer) Limit() int {
|
||||
// DNS is a stateless protocol, with unlimited amount
|
||||
// of back-and-forth exchanges. Don't limit it here.
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *DNSAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
||||
return &dnsUDPStream{logger: logger}
|
||||
}
|
||||
|
||||
func (a *DNSAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
|
||||
s := &dnsTCPStream{logger: logger, reqBuf: &utils.ByteBuffer{}, respBuf: &utils.ByteBuffer{}}
|
||||
s.reqLSM = utils.NewLinearStateMachine(
|
||||
s.getReqMessageLength,
|
||||
s.getReqMessage,
|
||||
)
|
||||
s.respLSM = utils.NewLinearStateMachine(
|
||||
s.getRespMessageLength,
|
||||
s.getRespMessage,
|
||||
)
|
||||
return s
|
||||
}
|
||||
|
||||
type dnsUDPStream struct {
|
||||
logger analyzer.Logger
|
||||
invalidCount int
|
||||
}
|
||||
|
||||
func (s *dnsUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
|
||||
m := parseDNSMessage(data)
|
||||
// To allow non-DNS UDP traffic to get offloaded,
|
||||
// we consider a UDP stream invalid and "done" if
|
||||
// it has more than a certain number of consecutive
|
||||
// packets that are not valid DNS messages.
|
||||
if m == nil {
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= dnsUDPInvalidCountThreshold
|
||||
}
|
||||
s.invalidCount = 0 // Reset invalid count on valid DNS message
|
||||
return &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateReplace,
|
||||
M: m,
|
||||
}, false
|
||||
}
|
||||
|
||||
func (s *dnsUDPStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
return nil
|
||||
}
|
||||
|
||||
type dnsTCPStream struct {
|
||||
logger analyzer.Logger
|
||||
|
||||
reqBuf *utils.ByteBuffer
|
||||
reqMap analyzer.PropMap
|
||||
reqUpdated bool
|
||||
reqLSM *utils.LinearStateMachine
|
||||
reqDone bool
|
||||
|
||||
respBuf *utils.ByteBuffer
|
||||
respMap analyzer.PropMap
|
||||
respUpdated bool
|
||||
respLSM *utils.LinearStateMachine
|
||||
respDone bool
|
||||
|
||||
reqMsgLen int
|
||||
respMsgLen int
|
||||
}
|
||||
|
||||
func (s *dnsTCPStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, done bool) {
|
||||
if skip != 0 {
|
||||
return nil, true
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
var update *analyzer.PropUpdate
|
||||
var cancelled bool
|
||||
if rev {
|
||||
s.respBuf.Append(data)
|
||||
s.respUpdated = false
|
||||
cancelled, s.respDone = s.respLSM.Run()
|
||||
if s.respUpdated {
|
||||
update = &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateReplace,
|
||||
M: s.respMap,
|
||||
}
|
||||
s.respUpdated = false
|
||||
}
|
||||
} else {
|
||||
s.reqBuf.Append(data)
|
||||
s.reqUpdated = false
|
||||
cancelled, s.reqDone = s.reqLSM.Run()
|
||||
if s.reqUpdated {
|
||||
update = &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateReplace,
|
||||
M: s.reqMap,
|
||||
}
|
||||
s.reqUpdated = false
|
||||
}
|
||||
}
|
||||
return update, cancelled || (s.reqDone && s.respDone)
|
||||
}
|
||||
|
||||
func (s *dnsTCPStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
s.reqBuf.Reset()
|
||||
s.respBuf.Reset()
|
||||
s.reqMap = nil
|
||||
s.respMap = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *dnsTCPStream) getReqMessageLength() utils.LSMAction {
|
||||
bs, ok := s.reqBuf.Get(2, true)
|
||||
if !ok {
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
s.reqMsgLen = int(bs[0])<<8 | int(bs[1])
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
|
||||
func (s *dnsTCPStream) getRespMessageLength() utils.LSMAction {
|
||||
bs, ok := s.respBuf.Get(2, true)
|
||||
if !ok {
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
s.respMsgLen = int(bs[0])<<8 | int(bs[1])
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
|
||||
func (s *dnsTCPStream) getReqMessage() utils.LSMAction {
|
||||
bs, ok := s.reqBuf.Get(s.reqMsgLen, true)
|
||||
if !ok {
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
m := parseDNSMessage(bs)
|
||||
if m == nil {
|
||||
// Invalid DNS message
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
s.reqMap = m
|
||||
s.reqUpdated = true
|
||||
return utils.LSMActionReset
|
||||
}
|
||||
|
||||
func (s *dnsTCPStream) getRespMessage() utils.LSMAction {
|
||||
bs, ok := s.respBuf.Get(s.respMsgLen, true)
|
||||
if !ok {
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
m := parseDNSMessage(bs)
|
||||
if m == nil {
|
||||
// Invalid DNS message
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
s.respMap = m
|
||||
s.respUpdated = true
|
||||
return utils.LSMActionReset
|
||||
}
|
||||
|
||||
func parseDNSMessage(msg []byte) analyzer.PropMap {
|
||||
dns := &layers.DNS{}
|
||||
err := dns.DecodeFromBytes(msg, gopacket.NilDecodeFeedback)
|
||||
if err != nil {
|
||||
// Not a DNS packet
|
||||
return nil
|
||||
}
|
||||
m := analyzer.PropMap{
|
||||
"id": dns.ID,
|
||||
"qr": dns.QR,
|
||||
"opcode": dns.OpCode,
|
||||
"aa": dns.AA,
|
||||
"tc": dns.TC,
|
||||
"rd": dns.RD,
|
||||
"ra": dns.RA,
|
||||
"z": dns.Z,
|
||||
"rcode": dns.ResponseCode,
|
||||
}
|
||||
if len(dns.Questions) > 0 {
|
||||
mQuestions := make([]analyzer.PropMap, len(dns.Questions))
|
||||
for i, q := range dns.Questions {
|
||||
mQuestions[i] = analyzer.PropMap{
|
||||
"name": string(q.Name),
|
||||
"type": q.Type,
|
||||
"class": q.Class,
|
||||
}
|
||||
}
|
||||
m["questions"] = mQuestions
|
||||
}
|
||||
if len(dns.Answers) > 0 {
|
||||
mAnswers := make([]analyzer.PropMap, len(dns.Answers))
|
||||
for i, rr := range dns.Answers {
|
||||
mAnswers[i] = dnsRRToPropMap(rr)
|
||||
}
|
||||
m["answers"] = mAnswers
|
||||
}
|
||||
if len(dns.Authorities) > 0 {
|
||||
mAuthorities := make([]analyzer.PropMap, len(dns.Authorities))
|
||||
for i, rr := range dns.Authorities {
|
||||
mAuthorities[i] = dnsRRToPropMap(rr)
|
||||
}
|
||||
m["authorities"] = mAuthorities
|
||||
}
|
||||
if len(dns.Additionals) > 0 {
|
||||
mAdditionals := make([]analyzer.PropMap, len(dns.Additionals))
|
||||
for i, rr := range dns.Additionals {
|
||||
mAdditionals[i] = dnsRRToPropMap(rr)
|
||||
}
|
||||
m["additionals"] = mAdditionals
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func dnsRRToPropMap(rr layers.DNSResourceRecord) analyzer.PropMap {
|
||||
m := analyzer.PropMap{
|
||||
"name": string(rr.Name),
|
||||
"type": rr.Type,
|
||||
"class": rr.Class,
|
||||
"ttl": rr.TTL,
|
||||
}
|
||||
switch rr.Type {
|
||||
// These are not everything, but is
|
||||
// all we decided to support for now.
|
||||
case layers.DNSTypeA:
|
||||
m["a"] = rr.IP.String()
|
||||
case layers.DNSTypeAAAA:
|
||||
m["aaaa"] = rr.IP.String()
|
||||
case layers.DNSTypeNS:
|
||||
m["ns"] = string(rr.NS)
|
||||
case layers.DNSTypeCNAME:
|
||||
m["cname"] = string(rr.CNAME)
|
||||
case layers.DNSTypePTR:
|
||||
m["ptr"] = string(rr.PTR)
|
||||
case layers.DNSTypeTXT:
|
||||
m["txt"] = utils.ByteSlicesToStrings(rr.TXTs)
|
||||
case layers.DNSTypeMX:
|
||||
m["mx"] = string(rr.MX.Name)
|
||||
}
|
||||
return m
|
||||
}
|
||||
31
analyzer/udp/internal/quic/LICENSE
Normal file
31
analyzer/udp/internal/quic/LICENSE
Normal file
@@ -0,0 +1,31 @@
|
||||
Author:: Cuong Manh Le <cuong.manhle.vn@gmail.com>
|
||||
Copyright:: Copyright (c) 2023, Cuong Manh Le
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following
|
||||
disclaimer in the documentation and/or other materials provided
|
||||
with the distribution.
|
||||
|
||||
* Neither the name of the @organization@ nor the names of its
|
||||
contributors may be used to endorse or promote products derived
|
||||
from this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL LE MANH CUONG
|
||||
BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
||||
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
||||
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
|
||||
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
1
analyzer/udp/internal/quic/README.md
Normal file
1
analyzer/udp/internal/quic/README.md
Normal file
@@ -0,0 +1 @@
|
||||
The code here is from https://github.com/cuonglm/quicsni with various modifications.
|
||||
105
analyzer/udp/internal/quic/header.go
Normal file
105
analyzer/udp/internal/quic/header.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
)
|
||||
|
||||
// The Header represents a QUIC header.
|
||||
type Header struct {
|
||||
Type uint8
|
||||
Version uint32
|
||||
SrcConnectionID []byte
|
||||
DestConnectionID []byte
|
||||
Length int64
|
||||
Token []byte
|
||||
}
|
||||
|
||||
// ParseInitialHeader parses the initial packet of a QUIC connection,
|
||||
// return the initial header and number of bytes read so far.
|
||||
func ParseInitialHeader(data []byte) (*Header, int64, error) {
|
||||
br := bytes.NewReader(data)
|
||||
hdr, err := parseLongHeader(br)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
n := int64(len(data) - br.Len())
|
||||
return hdr, n, nil
|
||||
}
|
||||
|
||||
func parseLongHeader(b *bytes.Reader) (*Header, error) {
|
||||
typeByte, err := b.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h := &Header{}
|
||||
ver, err := beUint32(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Version = ver
|
||||
if h.Version != 0 && typeByte&0x40 == 0 {
|
||||
return nil, errors.New("not a QUIC packet")
|
||||
}
|
||||
destConnIDLen, err := b.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.DestConnectionID = make([]byte, int(destConnIDLen))
|
||||
if err := readConnectionID(b, h.DestConnectionID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
srcConnIDLen, err := b.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.SrcConnectionID = make([]byte, int(srcConnIDLen))
|
||||
if err := readConnectionID(b, h.SrcConnectionID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
initialPacketType := byte(0b00)
|
||||
if h.Version == V2 {
|
||||
initialPacketType = 0b01
|
||||
}
|
||||
if (typeByte >> 4 & 0b11) == initialPacketType {
|
||||
tokenLen, err := quicvarint.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tokenLen > uint64(b.Len()) {
|
||||
return nil, io.EOF
|
||||
}
|
||||
h.Token = make([]byte, tokenLen)
|
||||
if _, err := io.ReadFull(b, h.Token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
pl, err := quicvarint.Read(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.Length = int64(pl)
|
||||
return h, err
|
||||
}
|
||||
|
||||
func readConnectionID(r io.Reader, cid []byte) error {
|
||||
_, err := io.ReadFull(r, cid)
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func beUint32(r io.Reader) (uint32, error) {
|
||||
b := make([]byte, 4)
|
||||
if _, err := io.ReadFull(r, b); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return binary.BigEndian.Uint32(b), nil
|
||||
}
|
||||
193
analyzer/udp/internal/quic/packet_protector.go
Normal file
193
analyzer/udp/internal/quic/packet_protector.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
|
||||
"golang.org/x/crypto/chacha20"
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
"golang.org/x/crypto/cryptobyte"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
// NewProtectionKey creates a new ProtectionKey.
|
||||
func NewProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
|
||||
return newProtectionKey(suite, secret, v)
|
||||
}
|
||||
|
||||
// NewInitialProtectionKey is like NewProtectionKey, but the returned protection key
|
||||
// is used for encrypt/decrypt Initial Packet only.
|
||||
//
|
||||
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-initial-secrets
|
||||
func NewInitialProtectionKey(secret []byte, v uint32) (*ProtectionKey, error) {
|
||||
return NewProtectionKey(tls.TLS_AES_128_GCM_SHA256, secret, v)
|
||||
}
|
||||
|
||||
// NewPacketProtector creates a new PacketProtector.
|
||||
func NewPacketProtector(key *ProtectionKey) *PacketProtector {
|
||||
return &PacketProtector{key: key}
|
||||
}
|
||||
|
||||
// PacketProtector is used for protecting a QUIC packet.
|
||||
//
|
||||
// See: https://www.rfc-editor.org/rfc/rfc9001.html#name-packet-protection
|
||||
type PacketProtector struct {
|
||||
key *ProtectionKey
|
||||
}
|
||||
|
||||
// UnProtect decrypts a QUIC packet.
|
||||
func (pp *PacketProtector) UnProtect(packet []byte, pnOffset, pnMax int64) ([]byte, error) {
|
||||
if isLongHeader(packet[0]) && int64(len(packet)) < pnOffset+4+16 {
|
||||
return nil, errors.New("packet with long header is too small")
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-sample
|
||||
sampleOffset := pnOffset + 4
|
||||
sample := packet[sampleOffset : sampleOffset+16]
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9001.html#name-header-protection-applicati
|
||||
mask := pp.key.headerProtection(sample)
|
||||
if isLongHeader(packet[0]) {
|
||||
// Long header: 4 bits masked
|
||||
packet[0] ^= mask[0] & 0x0f
|
||||
} else {
|
||||
// Short header: 5 bits masked
|
||||
packet[0] ^= mask[0] & 0x1f
|
||||
}
|
||||
|
||||
pnLen := packet[0]&0x3 + 1
|
||||
pn := int64(0)
|
||||
for i := uint8(0); i < pnLen; i++ {
|
||||
packet[pnOffset:][i] ^= mask[1+i]
|
||||
pn = (pn << 8) | int64(packet[pnOffset:][i])
|
||||
}
|
||||
pn = decodePacketNumber(pnMax, pn, pnLen)
|
||||
hdr := packet[:pnOffset+int64(pnLen)]
|
||||
payload := packet[pnOffset:][pnLen:]
|
||||
dec, err := pp.key.aead.Open(payload[:0], pp.key.nonce(pn), payload, hdr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decryption failed: %w", err)
|
||||
}
|
||||
return dec, nil
|
||||
}
|
||||
|
||||
// ProtectionKey is the key used to protect a QUIC packet.
|
||||
type ProtectionKey struct {
|
||||
aead cipher.AEAD
|
||||
headerProtection func(sample []byte) (mask []byte)
|
||||
iv []byte
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aead-usage
|
||||
//
|
||||
// "The 62 bits of the reconstructed QUIC packet number in network byte order are
|
||||
// left-padded with zeros to the size of the IV. The exclusive OR of the padded
|
||||
// packet number and the IV forms the AEAD nonce."
|
||||
func (pk *ProtectionKey) nonce(pn int64) []byte {
|
||||
nonce := make([]byte, len(pk.iv))
|
||||
binary.BigEndian.PutUint64(nonce[len(nonce)-8:], uint64(pn))
|
||||
for i := range pk.iv {
|
||||
nonce[i] ^= pk.iv[i]
|
||||
}
|
||||
return nonce
|
||||
}
|
||||
|
||||
func newProtectionKey(suite uint16, secret []byte, v uint32) (*ProtectionKey, error) {
|
||||
switch suite {
|
||||
case tls.TLS_AES_128_GCM_SHA256:
|
||||
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, 16)
|
||||
c, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
aead, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
|
||||
hpKey := hkdfExpandLabel(crypto.SHA256.New, secret, headerProtectionLabel(v), nil, 16)
|
||||
hp, err := aes.NewCipher(hpKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
k := &ProtectionKey{}
|
||||
k.aead = aead
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-aes-based-header-protection
|
||||
k.headerProtection = func(sample []byte) []byte {
|
||||
mask := make([]byte, hp.BlockSize())
|
||||
hp.Encrypt(mask, sample)
|
||||
return mask
|
||||
}
|
||||
k.iv = iv
|
||||
return k, nil
|
||||
case tls.TLS_CHACHA20_POLY1305_SHA256:
|
||||
key := hkdfExpandLabel(crypto.SHA256.New, secret, keyLabel(v), nil, chacha20poly1305.KeySize)
|
||||
aead, err := chacha20poly1305.New(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iv := hkdfExpandLabel(crypto.SHA256.New, secret, ivLabel(v), nil, aead.NonceSize())
|
||||
hpKey := hkdfExpandLabel(sha256.New, secret, headerProtectionLabel(v), nil, chacha20.KeySize)
|
||||
k := &ProtectionKey{}
|
||||
k.aead = aead
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-based-header-prote
|
||||
k.headerProtection = func(sample []byte) []byte {
|
||||
nonce := sample[4:16]
|
||||
c, err := chacha20.NewUnauthenticatedCipher(hpKey, nonce)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.SetCounter(binary.LittleEndian.Uint32(sample[:4]))
|
||||
mask := make([]byte, 5)
|
||||
c.XORKeyStream(mask, mask)
|
||||
return mask
|
||||
}
|
||||
k.iv = iv
|
||||
return k, nil
|
||||
}
|
||||
return nil, errors.New("not supported cipher suite")
|
||||
}
|
||||
|
||||
// decodePacketNumber decode the packet number after header protection removed.
|
||||
//
|
||||
// See: https://datatracker.ietf.org/doc/html/draft-ietf-quic-transport-32#section-appendix.a
|
||||
func decodePacketNumber(largest, truncated int64, nbits uint8) int64 {
|
||||
expected := largest + 1
|
||||
win := int64(1 << (nbits * 8))
|
||||
hwin := win / 2
|
||||
mask := win - 1
|
||||
candidate := (expected &^ mask) | truncated
|
||||
switch {
|
||||
case candidate <= expected-hwin && candidate < (1<<62)-win:
|
||||
return candidate + win
|
||||
case candidate > expected+hwin && candidate >= win:
|
||||
return candidate - win
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
// Copied from crypto/tls/key_schedule.go.
|
||||
func hkdfExpandLabel(hash func() hash.Hash, secret []byte, label string, context []byte, length int) []byte {
|
||||
var hkdfLabel cryptobyte.Builder
|
||||
hkdfLabel.AddUint16(uint16(length))
|
||||
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes([]byte("tls13 "))
|
||||
b.AddBytes([]byte(label))
|
||||
})
|
||||
hkdfLabel.AddUint8LengthPrefixed(func(b *cryptobyte.Builder) {
|
||||
b.AddBytes(context)
|
||||
})
|
||||
out := make([]byte, length)
|
||||
n, err := hkdf.Expand(hash, secret, hkdfLabel.BytesOrPanic()).Read(out)
|
||||
if err != nil || n != length {
|
||||
panic("quic: HKDF-Expand-Label invocation failed unexpectedly")
|
||||
}
|
||||
return out
|
||||
}
|
||||
94
analyzer/udp/internal/quic/packet_protector_test.go
Normal file
94
analyzer/udp/internal/quic/packet_protector_test.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func TestInitialPacketProtector_UnProtect(t *testing.T) {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-server-initial
|
||||
protect := mustHexDecodeString(`
|
||||
c7ff0000200008f067a5502a4262b500 4075fb12ff07823a5d24534d906ce4c7
|
||||
6782a2167e3479c0f7f6395dc2c91676 302fe6d70bb7cbeb117b4ddb7d173498
|
||||
44fd61dae200b8338e1b932976b61d91 e64a02e9e0ee72e3a6f63aba4ceeeec5
|
||||
be2f24f2d86027572943533846caa13e 6f163fb257473d0eda5047360fd4a47e
|
||||
fd8142fafc0f76
|
||||
`)
|
||||
unProtect := mustHexDecodeString(`
|
||||
02000000000600405a020000560303ee fce7f7b37ba1d1632e96677825ddf739
|
||||
88cfc79825df566dc5430b9a045a1200 130100002e00330024001d00209d3c94
|
||||
0d89690b84d08a60993c144eca684d10 81287c834d5311bcf32bb9da1a002b00
|
||||
020304
|
||||
`)
|
||||
|
||||
connID := mustHexDecodeString(`8394c8f03e515708`)
|
||||
|
||||
packet := append([]byte{}, protect...)
|
||||
hdr, offset, err := ParseInitialHeader(packet)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
initialSecret := hkdf.Extract(crypto.SHA256.New, connID, getSalt(hdr.Version))
|
||||
serverSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "server in", []byte{}, crypto.SHA256.Size())
|
||||
key, err := NewInitialProtectionKey(serverSecret, hdr.Version)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pp := NewPacketProtector(key)
|
||||
got, err := pp.UnProtect(protect, offset, 1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, unProtect) {
|
||||
t.Error("UnProtect returns wrong result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketProtectorShortHeader_UnProtect(t *testing.T) {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-chacha20-poly1305-short-hea
|
||||
protect := mustHexDecodeString(`4cfe4189655e5cd55c41f69080575d7999c25a5bfb`)
|
||||
unProtect := mustHexDecodeString(`01`)
|
||||
hdr := mustHexDecodeString(`4200bff4`)
|
||||
|
||||
secret := mustHexDecodeString(`9ac312a7f877468ebe69422748ad00a1 5443f18203a07d6060f688f30f21632b`)
|
||||
k, err := NewProtectionKey(tls.TLS_CHACHA20_POLY1305_SHA256, secret, V1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pnLen := int(hdr[0]&0x03) + 1
|
||||
offset := len(hdr) - pnLen
|
||||
pp := NewPacketProtector(k)
|
||||
got, err := pp.UnProtect(protect, int64(offset), 654360564)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, unProtect) {
|
||||
t.Error("UnProtect returns wrong result")
|
||||
}
|
||||
}
|
||||
|
||||
func mustHexDecodeString(s string) []byte {
|
||||
b, err := hex.DecodeString(normalizeHex(s))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func normalizeHex(s string) string {
|
||||
return strings.Map(func(c rune) rune {
|
||||
if unicode.IsSpace(c) {
|
||||
return -1
|
||||
}
|
||||
return c
|
||||
}, s)
|
||||
}
|
||||
122
analyzer/udp/internal/quic/payload.go
Normal file
122
analyzer/udp/internal/quic/payload.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package quic
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
|
||||
"github.com/quic-go/quic-go/quicvarint"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
func ReadCryptoPayload(packet []byte) ([]byte, error) {
|
||||
hdr, offset, err := ParseInitialHeader(packet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Some sanity checks
|
||||
if hdr.Version != V1 && hdr.Version != V2 {
|
||||
return nil, fmt.Errorf("unsupported version: %x", hdr.Version)
|
||||
}
|
||||
if offset == 0 || hdr.Length == 0 {
|
||||
return nil, errors.New("invalid packet")
|
||||
}
|
||||
|
||||
initialSecret := hkdf.Extract(crypto.SHA256.New, hdr.DestConnectionID, getSalt(hdr.Version))
|
||||
clientSecret := hkdfExpandLabel(crypto.SHA256.New, initialSecret, "client in", []byte{}, crypto.SHA256.Size())
|
||||
key, err := NewInitialProtectionKey(clientSecret, hdr.Version)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewInitialProtectionKey: %w", err)
|
||||
}
|
||||
pp := NewPacketProtector(key)
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-quic-tls-32#name-client-initial
|
||||
//
|
||||
// "The unprotected header includes the connection ID and a 4-byte packet number encoding for a packet number of 2"
|
||||
if int64(len(packet)) < offset+hdr.Length {
|
||||
return nil, fmt.Errorf("packet is too short: %d < %d", len(packet), offset+hdr.Length)
|
||||
}
|
||||
unProtectedPayload, err := pp.UnProtect(packet[:offset+hdr.Length], offset, 2)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frs, err := extractCryptoFrames(bytes.NewReader(unProtectedPayload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data := assembleCryptoFrames(frs)
|
||||
if data == nil {
|
||||
return nil, errors.New("unable to assemble crypto frames")
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
const (
|
||||
paddingFrameType = 0x00
|
||||
pingFrameType = 0x01
|
||||
cryptoFrameType = 0x06
|
||||
)
|
||||
|
||||
type cryptoFrame struct {
|
||||
Offset int64
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func extractCryptoFrames(r *bytes.Reader) ([]cryptoFrame, error) {
|
||||
var frames []cryptoFrame
|
||||
for r.Len() > 0 {
|
||||
typ, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if typ == paddingFrameType || typ == pingFrameType {
|
||||
continue
|
||||
}
|
||||
if typ != cryptoFrameType {
|
||||
return nil, fmt.Errorf("encountered unexpected frame type: %d", typ)
|
||||
}
|
||||
var frame cryptoFrame
|
||||
offset, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frame.Offset = int64(offset)
|
||||
dataLen, err := quicvarint.Read(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frame.Data = make([]byte, dataLen)
|
||||
if _, err := io.ReadFull(r, frame.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
frames = append(frames, frame)
|
||||
}
|
||||
return frames, nil
|
||||
}
|
||||
|
||||
// assembleCryptoFrames assembles multiple crypto frames into a single slice (if possible).
|
||||
// It returns an error if the frames cannot be assembled. This can happen if the frames are not contiguous.
|
||||
func assembleCryptoFrames(frames []cryptoFrame) []byte {
|
||||
if len(frames) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(frames) == 1 {
|
||||
return frames[0].Data
|
||||
}
|
||||
// sort the frames by offset
|
||||
sort.Slice(frames, func(i, j int) bool { return frames[i].Offset < frames[j].Offset })
|
||||
// check if the frames are contiguous
|
||||
for i := 1; i < len(frames); i++ {
|
||||
if frames[i].Offset != frames[i-1].Offset+int64(len(frames[i-1].Data)) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// concatenate the frames
|
||||
data := make([]byte, frames[len(frames)-1].Offset+int64(len(frames[len(frames)-1].Data)))
|
||||
for _, frame := range frames {
|
||||
copy(data[frame.Offset:], frame.Data)
|
||||
}
|
||||
return data
|
||||
}
|
||||
59
analyzer/udp/internal/quic/quic.go
Normal file
59
analyzer/udp/internal/quic/quic.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package quic
|
||||
|
||||
const (
|
||||
V1 uint32 = 0x1
|
||||
V2 uint32 = 0x6b3343cf
|
||||
|
||||
hkdfLabelKeyV1 = "quic key"
|
||||
hkdfLabelKeyV2 = "quicv2 key"
|
||||
hkdfLabelIVV1 = "quic iv"
|
||||
hkdfLabelIVV2 = "quicv2 iv"
|
||||
hkdfLabelHPV1 = "quic hp"
|
||||
hkdfLabelHPV2 = "quicv2 hp"
|
||||
)
|
||||
|
||||
var (
|
||||
quicSaltOld = []byte{0xaf, 0xbf, 0xec, 0x28, 0x99, 0x93, 0xd2, 0x4c, 0x9e, 0x97, 0x86, 0xf1, 0x9c, 0x61, 0x11, 0xe0, 0x43, 0x90, 0xa8, 0x99}
|
||||
// https://www.rfc-editor.org/rfc/rfc9001.html#name-initial-secrets
|
||||
quicSaltV1 = []byte{0x38, 0x76, 0x2c, 0xf7, 0xf5, 0x59, 0x34, 0xb3, 0x4d, 0x17, 0x9a, 0xe6, 0xa4, 0xc8, 0x0c, 0xad, 0xcc, 0xbb, 0x7f, 0x0a}
|
||||
// https://www.ietf.org/archive/id/draft-ietf-quic-v2-10.html#name-initial-salt-2
|
||||
quicSaltV2 = []byte{0x0d, 0xed, 0xe3, 0xde, 0xf7, 0x00, 0xa6, 0xdb, 0x81, 0x93, 0x81, 0xbe, 0x6e, 0x26, 0x9d, 0xcb, 0xf9, 0xbd, 0x2e, 0xd9}
|
||||
)
|
||||
|
||||
// isLongHeader reports whether b is the first byte of a long header packet.
|
||||
func isLongHeader(b byte) bool {
|
||||
return b&0x80 > 0
|
||||
}
|
||||
|
||||
func getSalt(v uint32) []byte {
|
||||
switch v {
|
||||
case V1:
|
||||
return quicSaltV1
|
||||
case V2:
|
||||
return quicSaltV2
|
||||
}
|
||||
return quicSaltOld
|
||||
}
|
||||
|
||||
func keyLabel(v uint32) string {
|
||||
kl := hkdfLabelKeyV1
|
||||
if v == V2 {
|
||||
kl = hkdfLabelKeyV2
|
||||
}
|
||||
return kl
|
||||
}
|
||||
|
||||
func ivLabel(v uint32) string {
|
||||
ivl := hkdfLabelIVV1
|
||||
if v == V2 {
|
||||
ivl = hkdfLabelIVV2
|
||||
}
|
||||
return ivl
|
||||
}
|
||||
|
||||
func headerProtectionLabel(v uint32) string {
|
||||
if v == V2 {
|
||||
return hkdfLabelHPV2
|
||||
}
|
||||
return hkdfLabelHPV1
|
||||
}
|
||||
384
analyzer/udp/openvpn.go
Normal file
384
analyzer/udp/openvpn.go
Normal file
@@ -0,0 +1,384 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer"
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
_ analyzer.UDPAnalyzer = (*OpenVPNAnalyzer)(nil)
|
||||
_ analyzer.TCPAnalyzer = (*OpenVPNAnalyzer)(nil)
|
||||
)
|
||||
|
||||
var (
|
||||
_ analyzer.UDPStream = (*openvpnUDPStream)(nil)
|
||||
_ analyzer.TCPStream = (*openvpnTCPStream)(nil)
|
||||
)
|
||||
|
||||
// Ref paper:
|
||||
// https://www.usenix.org/system/files/sec22fall_xue-diwen.pdf
|
||||
|
||||
// OpenVPN Opcodes definitions from:
|
||||
// https://github.com/OpenVPN/openvpn/blob/master/src/openvpn/ssl_pkt.h
|
||||
const (
|
||||
OpenVPNControlHardResetClientV1 = 1
|
||||
OpenVPNControlHardResetServerV1 = 2
|
||||
OpenVPNControlSoftResetV1 = 3
|
||||
OpenVPNControlV1 = 4
|
||||
OpenVPNAckV1 = 5
|
||||
OpenVPNDataV1 = 6
|
||||
OpenVPNControlHardResetClientV2 = 7
|
||||
OpenVPNControlHardResetServerV2 = 8
|
||||
OpenVPNDataV2 = 9
|
||||
OpenVPNControlHardResetClientV3 = 10
|
||||
OpenVPNControlWkcV1 = 11
|
||||
)
|
||||
|
||||
const (
|
||||
OpenVPNMinPktLen = 6
|
||||
OpenVPNTCPPktDefaultLimit = 256
|
||||
OpenVPNUDPPktDefaultLimit = 256
|
||||
)
|
||||
|
||||
type OpenVPNAnalyzer struct{}
|
||||
|
||||
func (a *OpenVPNAnalyzer) Name() string {
|
||||
return "openvpn"
|
||||
}
|
||||
|
||||
func (a *OpenVPNAnalyzer) Limit() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *OpenVPNAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
||||
return newOpenVPNUDPStream(logger)
|
||||
}
|
||||
|
||||
func (a *OpenVPNAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream {
|
||||
return newOpenVPNTCPStream(logger)
|
||||
}
|
||||
|
||||
type openvpnPkt struct {
|
||||
pktLen uint16 // 16 bits, TCP proto only
|
||||
opcode byte // 5 bits
|
||||
_keyId byte // 3 bits, not used
|
||||
|
||||
// We don't care about the rest of the packet
|
||||
// payload []byte
|
||||
}
|
||||
|
||||
type openvpnStream struct {
|
||||
logger analyzer.Logger
|
||||
|
||||
reqUpdated bool
|
||||
reqLSM *utils.LinearStateMachine
|
||||
reqDone bool
|
||||
|
||||
respUpdated bool
|
||||
respLSM *utils.LinearStateMachine
|
||||
respDone bool
|
||||
|
||||
rxPktCnt int
|
||||
txPktCnt int
|
||||
pktLimit int
|
||||
|
||||
reqPktParse func() (*openvpnPkt, utils.LSMAction)
|
||||
respPktParse func() (*openvpnPkt, utils.LSMAction)
|
||||
|
||||
lastOpcode byte
|
||||
}
|
||||
|
||||
func (o *openvpnStream) parseCtlHardResetClient() utils.LSMAction {
|
||||
pkt, action := o.reqPktParse()
|
||||
if action != utils.LSMActionNext {
|
||||
return action
|
||||
}
|
||||
|
||||
if pkt.opcode != OpenVPNControlHardResetClientV1 &&
|
||||
pkt.opcode != OpenVPNControlHardResetClientV2 &&
|
||||
pkt.opcode != OpenVPNControlHardResetClientV3 {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
o.lastOpcode = pkt.opcode
|
||||
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
|
||||
func (o *openvpnStream) parseCtlHardResetServer() utils.LSMAction {
|
||||
if o.lastOpcode != OpenVPNControlHardResetClientV1 &&
|
||||
o.lastOpcode != OpenVPNControlHardResetClientV2 &&
|
||||
o.lastOpcode != OpenVPNControlHardResetClientV3 {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
|
||||
pkt, action := o.respPktParse()
|
||||
if action != utils.LSMActionNext {
|
||||
return action
|
||||
}
|
||||
|
||||
if pkt.opcode != OpenVPNControlHardResetServerV1 &&
|
||||
pkt.opcode != OpenVPNControlHardResetServerV2 {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
o.lastOpcode = pkt.opcode
|
||||
|
||||
return utils.LSMActionNext
|
||||
}
|
||||
|
||||
func (o *openvpnStream) parseReq() utils.LSMAction {
|
||||
pkt, action := o.reqPktParse()
|
||||
if action != utils.LSMActionNext {
|
||||
return action
|
||||
}
|
||||
|
||||
if pkt.opcode != OpenVPNControlSoftResetV1 &&
|
||||
pkt.opcode != OpenVPNControlV1 &&
|
||||
pkt.opcode != OpenVPNAckV1 &&
|
||||
pkt.opcode != OpenVPNDataV1 &&
|
||||
pkt.opcode != OpenVPNDataV2 &&
|
||||
pkt.opcode != OpenVPNControlWkcV1 {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
|
||||
o.txPktCnt += 1
|
||||
o.reqUpdated = true
|
||||
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
|
||||
func (o *openvpnStream) parseResp() utils.LSMAction {
|
||||
pkt, action := o.respPktParse()
|
||||
if action != utils.LSMActionNext {
|
||||
return action
|
||||
}
|
||||
|
||||
if pkt.opcode != OpenVPNControlSoftResetV1 &&
|
||||
pkt.opcode != OpenVPNControlV1 &&
|
||||
pkt.opcode != OpenVPNAckV1 &&
|
||||
pkt.opcode != OpenVPNDataV1 &&
|
||||
pkt.opcode != OpenVPNDataV2 &&
|
||||
pkt.opcode != OpenVPNControlWkcV1 {
|
||||
return utils.LSMActionCancel
|
||||
}
|
||||
|
||||
o.rxPktCnt += 1
|
||||
o.respUpdated = true
|
||||
|
||||
return utils.LSMActionPause
|
||||
}
|
||||
|
||||
type openvpnUDPStream struct {
|
||||
openvpnStream
|
||||
curPkt []byte
|
||||
// We don't introduce `invalidCount` here to decrease the false positive rate
|
||||
// invalidCount int
|
||||
}
|
||||
|
||||
func newOpenVPNUDPStream(logger analyzer.Logger) *openvpnUDPStream {
|
||||
s := &openvpnUDPStream{
|
||||
openvpnStream: openvpnStream{
|
||||
logger: logger,
|
||||
pktLimit: OpenVPNUDPPktDefaultLimit,
|
||||
},
|
||||
}
|
||||
s.respPktParse = s.parsePkt
|
||||
s.reqPktParse = s.parsePkt
|
||||
s.reqLSM = utils.NewLinearStateMachine(
|
||||
s.parseCtlHardResetClient,
|
||||
s.parseReq,
|
||||
)
|
||||
s.respLSM = utils.NewLinearStateMachine(
|
||||
s.parseCtlHardResetServer,
|
||||
s.parseResp,
|
||||
)
|
||||
return s
|
||||
}
|
||||
|
||||
func (o *openvpnUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, d bool) {
|
||||
if len(data) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
var update *analyzer.PropUpdate
|
||||
var cancelled bool
|
||||
o.curPkt = data
|
||||
if rev {
|
||||
o.respUpdated = false
|
||||
cancelled, o.respDone = o.respLSM.Run()
|
||||
if o.respUpdated {
|
||||
update = &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateReplace,
|
||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
||||
}
|
||||
o.respUpdated = false
|
||||
}
|
||||
} else {
|
||||
o.reqUpdated = false
|
||||
cancelled, o.reqDone = o.reqLSM.Run()
|
||||
if o.reqUpdated {
|
||||
update = &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateReplace,
|
||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
||||
}
|
||||
o.reqUpdated = false
|
||||
}
|
||||
}
|
||||
|
||||
return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit
|
||||
}
|
||||
|
||||
func (o *openvpnUDPStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse OpenVPN UDP packet.
|
||||
func (o *openvpnUDPStream) parsePkt() (p *openvpnPkt, action utils.LSMAction) {
|
||||
if o.curPkt == nil {
|
||||
return nil, utils.LSMActionPause
|
||||
}
|
||||
|
||||
if !OpenVPNCheckForValidOpcode(o.curPkt[0] >> 3) {
|
||||
return nil, utils.LSMActionCancel
|
||||
}
|
||||
|
||||
// Parse packet header
|
||||
p = &openvpnPkt{}
|
||||
p.opcode = o.curPkt[0] >> 3
|
||||
p._keyId = o.curPkt[0] & 0x07
|
||||
|
||||
o.curPkt = nil
|
||||
return p, utils.LSMActionNext
|
||||
}
|
||||
|
||||
type openvpnTCPStream struct {
|
||||
openvpnStream
|
||||
reqBuf *utils.ByteBuffer
|
||||
respBuf *utils.ByteBuffer
|
||||
}
|
||||
|
||||
func newOpenVPNTCPStream(logger analyzer.Logger) *openvpnTCPStream {
|
||||
s := &openvpnTCPStream{
|
||||
openvpnStream: openvpnStream{
|
||||
logger: logger,
|
||||
pktLimit: OpenVPNTCPPktDefaultLimit,
|
||||
},
|
||||
reqBuf: &utils.ByteBuffer{},
|
||||
respBuf: &utils.ByteBuffer{},
|
||||
}
|
||||
s.respPktParse = func() (*openvpnPkt, utils.LSMAction) {
|
||||
return s.parsePkt(true)
|
||||
}
|
||||
s.reqPktParse = func() (*openvpnPkt, utils.LSMAction) {
|
||||
return s.parsePkt(false)
|
||||
}
|
||||
s.reqLSM = utils.NewLinearStateMachine(
|
||||
s.parseCtlHardResetClient,
|
||||
s.parseReq,
|
||||
)
|
||||
s.respLSM = utils.NewLinearStateMachine(
|
||||
s.parseCtlHardResetServer,
|
||||
s.parseResp,
|
||||
)
|
||||
return s
|
||||
}
|
||||
|
||||
func (o *openvpnTCPStream) Feed(rev, start, end bool, skip int, data []byte) (u *analyzer.PropUpdate, d bool) {
|
||||
if skip != 0 {
|
||||
return nil, true
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
var update *analyzer.PropUpdate
|
||||
var cancelled bool
|
||||
if rev {
|
||||
o.respBuf.Append(data)
|
||||
o.respUpdated = false
|
||||
cancelled, o.respDone = o.respLSM.Run()
|
||||
if o.respUpdated {
|
||||
update = &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateReplace,
|
||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
||||
}
|
||||
o.respUpdated = false
|
||||
}
|
||||
} else {
|
||||
o.reqBuf.Append(data)
|
||||
o.reqUpdated = false
|
||||
cancelled, o.reqDone = o.reqLSM.Run()
|
||||
if o.reqUpdated {
|
||||
update = &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateMerge,
|
||||
M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt, "tx_pkt_cnt": o.txPktCnt},
|
||||
}
|
||||
o.reqUpdated = false
|
||||
}
|
||||
}
|
||||
|
||||
return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit
|
||||
}
|
||||
|
||||
func (o *openvpnTCPStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
o.reqBuf.Reset()
|
||||
o.respBuf.Reset()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse OpenVPN TCP packet.
|
||||
func (o *openvpnTCPStream) parsePkt(rev bool) (p *openvpnPkt, action utils.LSMAction) {
|
||||
var buffer *utils.ByteBuffer
|
||||
if rev {
|
||||
buffer = o.respBuf
|
||||
} else {
|
||||
buffer = o.reqBuf
|
||||
}
|
||||
|
||||
// Parse packet length
|
||||
pktLen, ok := buffer.GetUint16(false, false)
|
||||
if !ok {
|
||||
return nil, utils.LSMActionPause
|
||||
}
|
||||
|
||||
if pktLen < OpenVPNMinPktLen {
|
||||
return nil, utils.LSMActionCancel
|
||||
}
|
||||
|
||||
pktOp, ok := buffer.Get(3, false)
|
||||
if !ok {
|
||||
return nil, utils.LSMActionPause
|
||||
}
|
||||
if !OpenVPNCheckForValidOpcode(pktOp[2] >> 3) {
|
||||
return nil, utils.LSMActionCancel
|
||||
}
|
||||
|
||||
pkt, ok := buffer.Get(int(pktLen)+2, true)
|
||||
if !ok {
|
||||
return nil, utils.LSMActionPause
|
||||
}
|
||||
pkt = pkt[2:]
|
||||
|
||||
// Parse packet header
|
||||
p = &openvpnPkt{}
|
||||
p.pktLen = pktLen
|
||||
p.opcode = pkt[0] >> 3
|
||||
p._keyId = pkt[0] & 0x07
|
||||
|
||||
return p, utils.LSMActionNext
|
||||
}
|
||||
|
||||
func OpenVPNCheckForValidOpcode(opcode byte) bool {
|
||||
switch opcode {
|
||||
case OpenVPNControlHardResetClientV1,
|
||||
OpenVPNControlHardResetServerV1,
|
||||
OpenVPNControlSoftResetV1,
|
||||
OpenVPNControlV1,
|
||||
OpenVPNAckV1,
|
||||
OpenVPNDataV1,
|
||||
OpenVPNControlHardResetClientV2,
|
||||
OpenVPNControlHardResetServerV2,
|
||||
OpenVPNDataV2,
|
||||
OpenVPNControlHardResetClientV3,
|
||||
OpenVPNControlWkcV1:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
81
analyzer/udp/quic.go
Normal file
81
analyzer/udp/quic.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer"
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer/internal"
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer/udp/internal/quic"
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
quicInvalidCountThreshold = 4
|
||||
)
|
||||
|
||||
var (
|
||||
_ analyzer.UDPAnalyzer = (*QUICAnalyzer)(nil)
|
||||
_ analyzer.UDPStream = (*quicStream)(nil)
|
||||
)
|
||||
|
||||
type QUICAnalyzer struct{}
|
||||
|
||||
func (a *QUICAnalyzer) Name() string {
|
||||
return "quic"
|
||||
}
|
||||
|
||||
func (a *QUICAnalyzer) Limit() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *QUICAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
||||
return &quicStream{logger: logger}
|
||||
}
|
||||
|
||||
type quicStream struct {
|
||||
logger analyzer.Logger
|
||||
invalidCount int
|
||||
}
|
||||
|
||||
func (s *quicStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
|
||||
// minimal data size: protocol version (2 bytes) + random (32 bytes) +
|
||||
// + session ID (1 byte) + cipher suites (4 bytes) +
|
||||
// + compression methods (2 bytes) + no extensions
|
||||
const minDataSize = 41
|
||||
|
||||
if rev {
|
||||
// We don't support server direction for now
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
|
||||
pl, err := quic.ReadCryptoPayload(data)
|
||||
if err != nil || len(pl) < 4 { // FIXME: isn't length checked inside quic.ReadCryptoPayload? Also, what about error handling?
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
|
||||
if pl[0] != internal.TypeClientHello {
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
|
||||
chLen := int(pl[1])<<16 | int(pl[2])<<8 | int(pl[3])
|
||||
if chLen < minDataSize {
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
|
||||
m := internal.ParseTLSClientHelloMsgData(&utils.ByteBuffer{Buf: pl[4:]})
|
||||
if m == nil {
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= quicInvalidCountThreshold
|
||||
}
|
||||
|
||||
return &analyzer.PropUpdate{
|
||||
Type: analyzer.PropUpdateMerge,
|
||||
M: analyzer.PropMap{"req": m},
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *quicStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
return nil
|
||||
}
|
||||
58
analyzer/udp/quic_test.go
Normal file
58
analyzer/udp/quic_test.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer"
|
||||
)
|
||||
|
||||
func TestQuicStreamParsing_ClientHello(t *testing.T) {
|
||||
// example packet taken from <https://quic.xargs.org/#client-initial-packet/annotated>
|
||||
clientHello := make([]byte, 1200)
|
||||
clientInitial := []byte{
|
||||
0xcd, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05,
|
||||
0x06, 0x07, 0x05, 0x63, 0x5f, 0x63, 0x69, 0x64, 0x00, 0x41, 0x03, 0x98,
|
||||
0x1c, 0x36, 0xa7, 0xed, 0x78, 0x71, 0x6b, 0xe9, 0x71, 0x1b, 0xa4, 0x98,
|
||||
0xb7, 0xed, 0x86, 0x84, 0x43, 0xbb, 0x2e, 0x0c, 0x51, 0x4d, 0x4d, 0x84,
|
||||
0x8e, 0xad, 0xcc, 0x7a, 0x00, 0xd2, 0x5c, 0xe9, 0xf9, 0xaf, 0xa4, 0x83,
|
||||
0x97, 0x80, 0x88, 0xde, 0x83, 0x6b, 0xe6, 0x8c, 0x0b, 0x32, 0xa2, 0x45,
|
||||
0x95, 0xd7, 0x81, 0x3e, 0xa5, 0x41, 0x4a, 0x91, 0x99, 0x32, 0x9a, 0x6d,
|
||||
0x9f, 0x7f, 0x76, 0x0d, 0xd8, 0xbb, 0x24, 0x9b, 0xf3, 0xf5, 0x3d, 0x9a,
|
||||
0x77, 0xfb, 0xb7, 0xb3, 0x95, 0xb8, 0xd6, 0x6d, 0x78, 0x79, 0xa5, 0x1f,
|
||||
0xe5, 0x9e, 0xf9, 0x60, 0x1f, 0x79, 0x99, 0x8e, 0xb3, 0x56, 0x8e, 0x1f,
|
||||
0xdc, 0x78, 0x9f, 0x64, 0x0a, 0xca, 0xb3, 0x85, 0x8a, 0x82, 0xef, 0x29,
|
||||
0x30, 0xfa, 0x5c, 0xe1, 0x4b, 0x5b, 0x9e, 0xa0, 0xbd, 0xb2, 0x9f, 0x45,
|
||||
0x72, 0xda, 0x85, 0xaa, 0x3d, 0xef, 0x39, 0xb7, 0xef, 0xaf, 0xff, 0xa0,
|
||||
0x74, 0xb9, 0x26, 0x70, 0x70, 0xd5, 0x0b, 0x5d, 0x07, 0x84, 0x2e, 0x49,
|
||||
0xbb, 0xa3, 0xbc, 0x78, 0x7f, 0xf2, 0x95, 0xd6, 0xae, 0x3b, 0x51, 0x43,
|
||||
0x05, 0xf1, 0x02, 0xaf, 0xe5, 0xa0, 0x47, 0xb3, 0xfb, 0x4c, 0x99, 0xeb,
|
||||
0x92, 0xa2, 0x74, 0xd2, 0x44, 0xd6, 0x04, 0x92, 0xc0, 0xe2, 0xe6, 0xe2,
|
||||
0x12, 0xce, 0xf0, 0xf9, 0xe3, 0xf6, 0x2e, 0xfd, 0x09, 0x55, 0xe7, 0x1c,
|
||||
0x76, 0x8a, 0xa6, 0xbb, 0x3c, 0xd8, 0x0b, 0xbb, 0x37, 0x55, 0xc8, 0xb7,
|
||||
0xeb, 0xee, 0x32, 0x71, 0x2f, 0x40, 0xf2, 0x24, 0x51, 0x19, 0x48, 0x70,
|
||||
0x21, 0xb4, 0xb8, 0x4e, 0x15, 0x65, 0xe3, 0xca, 0x31, 0x96, 0x7a, 0xc8,
|
||||
0x60, 0x4d, 0x40, 0x32, 0x17, 0x0d, 0xec, 0x28, 0x0a, 0xee, 0xfa, 0x09,
|
||||
0x5d, 0x08, 0xb3, 0xb7, 0x24, 0x1e, 0xf6, 0x64, 0x6a, 0x6c, 0x86, 0xe5,
|
||||
0xc6, 0x2c, 0xe0, 0x8b, 0xe0, 0x99,
|
||||
}
|
||||
copy(clientHello, clientInitial)
|
||||
|
||||
want := analyzer.PropMap{
|
||||
"alpn": []string{"ping/1.0"},
|
||||
"ciphers": []uint16{4865, 4866, 4867},
|
||||
"compression": []uint8{0},
|
||||
"random": []uint8{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31},
|
||||
"session": []uint8{},
|
||||
"sni": "example.ulfheim.net",
|
||||
"supported_versions": []uint16{772},
|
||||
"version": uint16(771),
|
||||
}
|
||||
|
||||
s := quicStream{}
|
||||
u, _ := s.Feed(false, clientHello)
|
||||
got := u.M.Get("req")
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%d B parsed = %v, want %v", len(clientHello), got, want)
|
||||
}
|
||||
}
|
||||
217
analyzer/udp/wireguard.go
Normal file
217
analyzer/udp/wireguard.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"container/ring"
|
||||
"encoding/binary"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"git.difuse.io/Difuse/Mellaris/analyzer"
|
||||
)
|
||||
|
||||
var (
|
||||
_ analyzer.UDPAnalyzer = (*WireGuardAnalyzer)(nil)
|
||||
_ analyzer.UDPStream = (*wireGuardUDPStream)(nil)
|
||||
)
|
||||
|
||||
const (
|
||||
wireguardUDPInvalidCountThreshold = 4
|
||||
wireguardRememberedIndexCount = 6
|
||||
wireguardPropKeyMessageType = "message_type"
|
||||
)
|
||||
|
||||
const (
|
||||
wireguardTypeHandshakeInitiation = 1
|
||||
wireguardTypeHandshakeResponse = 2
|
||||
wireguardTypeData = 4
|
||||
wireguardTypeCookieReply = 3
|
||||
)
|
||||
|
||||
const (
|
||||
wireguardSizeHandshakeInitiation = 148
|
||||
wireguardSizeHandshakeResponse = 92
|
||||
wireguardMinSizePacketData = 32 // 16 bytes header + 16 bytes AEAD overhead
|
||||
wireguardSizePacketCookieReply = 64
|
||||
)
|
||||
|
||||
type WireGuardAnalyzer struct{}
|
||||
|
||||
func (a *WireGuardAnalyzer) Name() string {
|
||||
return "wireguard"
|
||||
}
|
||||
|
||||
func (a *WireGuardAnalyzer) Limit() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (a *WireGuardAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream {
|
||||
return newWireGuardUDPStream(logger)
|
||||
}
|
||||
|
||||
type wireGuardUDPStream struct {
|
||||
logger analyzer.Logger
|
||||
invalidCount int
|
||||
rememberedIndexes *ring.Ring
|
||||
rememberedIndexesLock sync.RWMutex
|
||||
}
|
||||
|
||||
func newWireGuardUDPStream(logger analyzer.Logger) *wireGuardUDPStream {
|
||||
return &wireGuardUDPStream{
|
||||
logger: logger,
|
||||
rememberedIndexes: ring.New(wireguardRememberedIndexCount),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) Feed(rev bool, data []byte) (u *analyzer.PropUpdate, done bool) {
|
||||
m := s.parseWireGuardPacket(rev, data)
|
||||
if m == nil {
|
||||
s.invalidCount++
|
||||
return nil, s.invalidCount >= wireguardUDPInvalidCountThreshold
|
||||
}
|
||||
s.invalidCount = 0 // Reset invalid count on valid WireGuard packet
|
||||
messageType := m[wireguardPropKeyMessageType].(byte)
|
||||
propUpdateType := analyzer.PropUpdateMerge
|
||||
if messageType == wireguardTypeHandshakeInitiation {
|
||||
propUpdateType = analyzer.PropUpdateReplace
|
||||
}
|
||||
return &analyzer.PropUpdate{
|
||||
Type: propUpdateType,
|
||||
M: m,
|
||||
}, false
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) Close(limited bool) *analyzer.PropUpdate {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) parseWireGuardPacket(rev bool, data []byte) analyzer.PropMap {
|
||||
if len(data) < 4 {
|
||||
return nil
|
||||
}
|
||||
if slices.Max(data[1:4]) != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
messageType := data[0]
|
||||
var propKey string
|
||||
var propValue analyzer.PropMap
|
||||
switch messageType {
|
||||
case wireguardTypeHandshakeInitiation:
|
||||
propKey = "handshake_initiation"
|
||||
propValue = s.parseWireGuardHandshakeInitiation(rev, data)
|
||||
case wireguardTypeHandshakeResponse:
|
||||
propKey = "handshake_response"
|
||||
propValue = s.parseWireGuardHandshakeResponse(rev, data)
|
||||
case wireguardTypeData:
|
||||
propKey = "packet_data"
|
||||
propValue = s.parseWireGuardPacketData(rev, data)
|
||||
case wireguardTypeCookieReply:
|
||||
propKey = "packet_cookie_reply"
|
||||
propValue = s.parseWireGuardPacketCookieReply(rev, data)
|
||||
}
|
||||
if propValue == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
m := make(analyzer.PropMap)
|
||||
m[wireguardPropKeyMessageType] = messageType
|
||||
m[propKey] = propValue
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) parseWireGuardHandshakeInitiation(rev bool, data []byte) analyzer.PropMap {
|
||||
if len(data) != wireguardSizeHandshakeInitiation {
|
||||
return nil
|
||||
}
|
||||
m := make(analyzer.PropMap)
|
||||
|
||||
senderIndex := binary.LittleEndian.Uint32(data[4:8])
|
||||
m["sender_index"] = senderIndex
|
||||
s.putSenderIndex(rev, senderIndex)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) parseWireGuardHandshakeResponse(rev bool, data []byte) analyzer.PropMap {
|
||||
if len(data) != wireguardSizeHandshakeResponse {
|
||||
return nil
|
||||
}
|
||||
m := make(analyzer.PropMap)
|
||||
|
||||
senderIndex := binary.LittleEndian.Uint32(data[4:8])
|
||||
m["sender_index"] = senderIndex
|
||||
s.putSenderIndex(rev, senderIndex)
|
||||
|
||||
receiverIndex := binary.LittleEndian.Uint32(data[8:12])
|
||||
m["receiver_index"] = receiverIndex
|
||||
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) parseWireGuardPacketData(rev bool, data []byte) analyzer.PropMap {
|
||||
if len(data) < wireguardMinSizePacketData {
|
||||
return nil
|
||||
}
|
||||
if len(data)%16 != 0 {
|
||||
// WireGuard zero padding the packet to make the length a multiple of 16
|
||||
return nil
|
||||
}
|
||||
m := make(analyzer.PropMap)
|
||||
|
||||
receiverIndex := binary.LittleEndian.Uint32(data[4:8])
|
||||
m["receiver_index"] = receiverIndex
|
||||
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
|
||||
|
||||
m["counter"] = binary.LittleEndian.Uint64(data[8:16])
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) parseWireGuardPacketCookieReply(rev bool, data []byte) analyzer.PropMap {
|
||||
if len(data) != wireguardSizePacketCookieReply {
|
||||
return nil
|
||||
}
|
||||
m := make(analyzer.PropMap)
|
||||
|
||||
receiverIndex := binary.LittleEndian.Uint32(data[4:8])
|
||||
m["receiver_index"] = receiverIndex
|
||||
m["receiver_index_matched"] = s.matchReceiverIndex(rev, receiverIndex)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
type wireGuardIndex struct {
|
||||
SenderIndex uint32
|
||||
Reverse bool
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) putSenderIndex(rev bool, senderIndex uint32) {
|
||||
s.rememberedIndexesLock.Lock()
|
||||
defer s.rememberedIndexesLock.Unlock()
|
||||
|
||||
s.rememberedIndexes.Value = &wireGuardIndex{
|
||||
SenderIndex: senderIndex,
|
||||
Reverse: rev,
|
||||
}
|
||||
s.rememberedIndexes = s.rememberedIndexes.Prev()
|
||||
}
|
||||
|
||||
func (s *wireGuardUDPStream) matchReceiverIndex(rev bool, receiverIndex uint32) bool {
|
||||
s.rememberedIndexesLock.RLock()
|
||||
defer s.rememberedIndexesLock.RUnlock()
|
||||
|
||||
var found bool
|
||||
ris := s.rememberedIndexes
|
||||
for it := ris.Next(); it != ris; it = it.Next() {
|
||||
if it.Value == nil {
|
||||
break
|
||||
}
|
||||
wgidx := it.Value.(*wireGuardIndex)
|
||||
if wgidx.Reverse == !rev && wgidx.SenderIndex == receiverIndex {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
Reference in New Issue
Block a user