diff --git a/analyzer/internal/openvpn.go b/analyzer/internal/openvpn.go new file mode 100644 index 0000000..ab1a473 --- /dev/null +++ b/analyzer/internal/openvpn.go @@ -0,0 +1,44 @@ +package internal + +// 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 +) + +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/analyzer/tcp/openvpn.go b/analyzer/tcp/openvpn.go new file mode 100644 index 0000000..ad9d70f --- /dev/null +++ b/analyzer/tcp/openvpn.go @@ -0,0 +1,234 @@ +package tcp + +import ( + "github.com/apernet/OpenGFW/analyzer" + "github.com/apernet/OpenGFW/analyzer/internal" + "github.com/apernet/OpenGFW/analyzer/utils" +) + +var _ analyzer.TCPAnalyzer = (*OpenVpnAnalyzer)(nil) +var _ analyzer.TCPStream = (*openVpnStream)(nil) + +type OpenVpnAnalyzer struct{} + +func (a *OpenVpnAnalyzer) Name() string { + return "openvpn_tcp" +} + +func (a *OpenVpnAnalyzer) Limit() int { + return 0 +} + +func (a *OpenVpnAnalyzer) NewTCP(info analyzer.TCPInfo, logger analyzer.Logger) analyzer.TCPStream { + return newOpenVpnTCPStream(logger) +} + +type openVpnStream struct { + logger analyzer.Logger + + reqBuf *utils.ByteBuffer + reqUpdated bool + reqLSM *utils.LinearStateMachine + reqDone bool + + respBuf *utils.ByteBuffer + respUpdated bool + respLSM *utils.LinearStateMachine + respDone bool + + rxPktCnt int + txPktCnt int + pktLimit int + + lastOpcode byte +} + +type openVpnTcpPkt struct { + pktLen uint16 + opcode byte // 5 bits + _keyId byte // 3 bits, not used + + // We don't care about the rest of the packet + // payload []byte +} + +func newOpenVpnTCPStream(logger analyzer.Logger) *openVpnStream { + s := &openVpnStream{ + logger: logger, + reqBuf: &utils.ByteBuffer{}, + respBuf: &utils.ByteBuffer{}, + pktLimit: internal.OpenVpnTcpPktDefaultLimit, + } + s.reqLSM = utils.NewLinearStateMachine( + s.parseCtlHardResetClient, + s.parseReq, + ) + s.respLSM = utils.NewLinearStateMachine( + s.parseCtlHardResetServer, + s.parseResp, + ) + return s +} + +func (o *openVpnStream) 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.PropUpdateMerge, + M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt}, + } + 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{"tx_pkt_cnt": o.txPktCnt}, + } + o.reqUpdated = false + } + } + + return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit +} + +func (o *openVpnStream) Close(limited bool) *analyzer.PropUpdate { + o.reqBuf.Reset() + o.respBuf.Reset() + return nil +} + +func (o *openVpnStream) parseCtlHardResetClient() utils.LSMAction { + pkt, action := o.parsePkt(false) + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlHardResetClientV1 && + pkt.opcode != internal.OpenVpnControlHardResetClientV2 && + pkt.opcode != internal.OpenVpnControlHardResetClientV3 { + return utils.LSMActionCancel + } + o.lastOpcode = pkt.opcode + + return utils.LSMActionNext +} + +func (o *openVpnStream) parseCtlHardResetServer() utils.LSMAction { + if o.lastOpcode != internal.OpenVpnControlHardResetClientV1 && + o.lastOpcode != internal.OpenVpnControlHardResetClientV2 && + o.lastOpcode != internal.OpenVpnControlHardResetClientV3 { + return utils.LSMActionCancel + } + + pkt, action := o.parsePkt(true) + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlHardResetServerV1 && + pkt.opcode != internal.OpenVpnControlHardResetServerV2 { + return utils.LSMActionCancel + } + o.lastOpcode = pkt.opcode + + return utils.LSMActionNext +} + +func (o *openVpnStream) parseReq() utils.LSMAction { + pkt, action := o.parsePkt(false) + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlSoftResetV1 && + pkt.opcode != internal.OpenVpnControlV1 && + pkt.opcode != internal.OpenVpnAckV1 && + pkt.opcode != internal.OpenVpnDataV1 && + pkt.opcode != internal.OpenVpnDataV2 && + pkt.opcode != internal.OpenVpnControlWkcV1 { + return utils.LSMActionCancel + } + + o.txPktCnt += 1 + o.reqUpdated = true + + return utils.LSMActionPause +} + +func (o *openVpnStream) parseResp() utils.LSMAction { + pkt, action := o.parsePkt(true) + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlSoftResetV1 && + pkt.opcode != internal.OpenVpnControlV1 && + pkt.opcode != internal.OpenVpnAckV1 && + pkt.opcode != internal.OpenVpnDataV1 && + pkt.opcode != internal.OpenVpnDataV2 && + pkt.opcode != internal.OpenVpnControlWkcV1 { + return utils.LSMActionCancel + } + + o.rxPktCnt += 1 + o.respUpdated = true + + return utils.LSMActionPause +} + +// Parse OpenVpn packet header but not consume buffer. +func (o *openVpnStream) parsePkt(rev bool) (p *openVpnTcpPkt, 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 < internal.OpenVpnMinPktLen { + return nil, utils.LSMActionCancel + } + + pktOp, ok := buffer.Get(3, false) + if !ok { + return nil, utils.LSMActionPause + } + if !internal.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 = &openVpnTcpPkt{} + p.pktLen = pktLen + p.opcode = pkt[0] >> 3 + p._keyId = pkt[0] & 0x07 + + return p, utils.LSMActionNext +} diff --git a/analyzer/udp/openvpn.go b/analyzer/udp/openvpn.go new file mode 100644 index 0000000..0c77a0a --- /dev/null +++ b/analyzer/udp/openvpn.go @@ -0,0 +1,205 @@ +package udp + +import ( + "github.com/apernet/OpenGFW/analyzer" + "github.com/apernet/OpenGFW/analyzer/internal" + "github.com/apernet/OpenGFW/analyzer/utils" +) + +var _ analyzer.UDPAnalyzer = (*OpenVpnAnalyzer)(nil) +var _ analyzer.UDPStream = (*openVpnStream)(nil) + +type OpenVpnAnalyzer struct{} + +func (a *OpenVpnAnalyzer) Name() string { + return "openvpn_udp" +} + +func (a *OpenVpnAnalyzer) Limit() int { + return 0 +} + +func (a *OpenVpnAnalyzer) NewUDP(info analyzer.UDPInfo, logger analyzer.Logger) analyzer.UDPStream { + return newOpenVPNTCPStream(logger) +} + +type openVpnStream struct { + logger analyzer.Logger + // We don't introduce `invalidCount` here to decrease the false positive rate + // invalidCount int + + curPkt []byte + + reqUpdated bool + reqLSM *utils.LinearStateMachine + reqDone bool + + respUpdated bool + respLSM *utils.LinearStateMachine + respDone bool + + rxPktCnt int + txPktCnt int + pktLimit int + + lastOpcode byte +} + +type openVpnUdpPkt struct { + opcode byte // 5 bits + _keyId byte // 3 bits, not used + + // We don't care about the rest of the packet + // payload []byte +} + +func newOpenVPNTCPStream(logger analyzer.Logger) *openVpnStream { + s := &openVpnStream{ + logger: logger, + pktLimit: internal.OpenVpnUdpPktDefaultLimit, + } + s.reqLSM = utils.NewLinearStateMachine( + s.parseCtlHardResetClient, + s.parseReq, + ) + s.respLSM = utils.NewLinearStateMachine( + s.parseCtlHardResetServer, + s.parseResp, + ) + return s +} + +func (o *openVpnStream) 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.PropUpdateMerge, + M: analyzer.PropMap{"rx_pkt_cnt": o.rxPktCnt}, + } + o.respUpdated = false + } + } else { + o.reqUpdated = false + cancelled, o.reqDone = o.reqLSM.Run() + if o.reqUpdated { + update = &analyzer.PropUpdate{ + Type: analyzer.PropUpdateMerge, + M: analyzer.PropMap{"tx_pkt_cnt": o.txPktCnt}, + } + o.reqUpdated = false + } + } + + return update, cancelled || (o.reqDone && o.respDone) || o.rxPktCnt+o.txPktCnt > o.pktLimit +} + +func (o *openVpnStream) Close(limited bool) *analyzer.PropUpdate { + return nil +} + +func (o *openVpnStream) parseCtlHardResetClient() utils.LSMAction { + pkt, action := o.parsePkt() + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlHardResetClientV1 && + pkt.opcode != internal.OpenVpnControlHardResetClientV2 && + pkt.opcode != internal.OpenVpnControlHardResetClientV3 { + return utils.LSMActionCancel + } + o.lastOpcode = pkt.opcode + + return utils.LSMActionNext +} + +func (o *openVpnStream) parseCtlHardResetServer() utils.LSMAction { + + if o.lastOpcode != internal.OpenVpnControlHardResetClientV1 && + o.lastOpcode != internal.OpenVpnControlHardResetClientV2 && + o.lastOpcode != internal.OpenVpnControlHardResetClientV3 { + return utils.LSMActionCancel + } + + pkt, action := o.parsePkt() + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlHardResetServerV1 && + pkt.opcode != internal.OpenVpnControlHardResetServerV2 { + return utils.LSMActionCancel + } + o.lastOpcode = pkt.opcode + + return utils.LSMActionNext +} + +func (o *openVpnStream) parseReq() utils.LSMAction { + pkt, action := o.parsePkt() + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlSoftResetV1 && + pkt.opcode != internal.OpenVpnControlV1 && + pkt.opcode != internal.OpenVpnAckV1 && + pkt.opcode != internal.OpenVpnDataV1 && + pkt.opcode != internal.OpenVpnDataV2 && + pkt.opcode != internal.OpenVpnControlWkcV1 { + return utils.LSMActionCancel + } + + o.txPktCnt += 1 + o.reqUpdated = true + + return utils.LSMActionPause +} + +func (o *openVpnStream) parseResp() utils.LSMAction { + pkt, action := o.parsePkt() + if action != utils.LSMActionNext { + return action + } + + if pkt.opcode != internal.OpenVpnControlSoftResetV1 && + pkt.opcode != internal.OpenVpnControlV1 && + pkt.opcode != internal.OpenVpnAckV1 && + pkt.opcode != internal.OpenVpnDataV1 && + pkt.opcode != internal.OpenVpnDataV2 && + pkt.opcode != internal.OpenVpnControlWkcV1 { + return utils.LSMActionCancel + } + + o.rxPktCnt += 1 + o.respUpdated = true + + return utils.LSMActionPause +} + +// Parse OpenVpn packet header but not consume buffer. +func (o *openVpnStream) parsePkt() (p *openVpnUdpPkt, action utils.LSMAction) { + if o.curPkt == nil { + return nil, utils.LSMActionPause + } + + if !internal.OpenVpnCheckForValidOpcode(o.curPkt[0] >> 3) { + return nil, utils.LSMActionCancel + } + + // Parse packet header + p = &openVpnUdpPkt{} + p.opcode = o.curPkt[0] >> 3 + p._keyId = o.curPkt[0] & 0x07 + + o.curPkt = nil + return p, utils.LSMActionNext +} diff --git a/cmd/root.go b/cmd/root.go index 7e54462..dfb5f4c 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -87,11 +87,13 @@ var logFormatMap = map[string]zapcore.EncoderConfig{ var analyzers = []analyzer.Analyzer{ &tcp.FETAnalyzer{}, &tcp.HTTPAnalyzer{}, + &tcp.OpenVpnAnalyzer{}, &tcp.SocksAnalyzer{}, &tcp.SSHAnalyzer{}, &tcp.TLSAnalyzer{}, &tcp.TrojanAnalyzer{}, &udp.DNSAnalyzer{}, + &udp.OpenVpnAnalyzer{}, &udp.QUICAnalyzer{}, &udp.WireGuardAnalyzer{}, }