diff --git a/analyzer/udp/openvpn.go b/analyzer/udp/openvpn.go new file mode 100644 index 0000000..3853726 --- /dev/null +++ b/analyzer/udp/openvpn.go @@ -0,0 +1,384 @@ +package udp + +import ( + "github.com/apernet/OpenGFW/analyzer" + "github.com/apernet/OpenGFW/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 +} diff --git a/cmd/root.go b/cmd/root.go index 7e54462..4ccfe3a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -92,6 +92,7 @@ var analyzers = []analyzer.Analyzer{ &tcp.TLSAnalyzer{}, &tcp.TrojanAnalyzer{}, &udp.DNSAnalyzer{}, + &udp.OpenVPNAnalyzer{}, &udp.QUICAnalyzer{}, &udp.WireGuardAnalyzer{}, }