Suricata源码阅读笔记:main()

main()函数位于suricata.c文件,其主要流程如下:

1. 定义并初始化程序的全局实例变量。

  • SCInstance类型的suri变量用来保存程序当前的一些状态、标志等上下文环境,通常是用来作为参数传递给各个模块的子函数,因此为了更好的封装性而放到一个结构体变量中,而不是使用零散的长串参数或一堆全局变量。
  • SCInstanceInit函数,顾名思义,即是对suri中各个字段进行初始化。注意,这里对所有字段都进行了显示初始化,因为虽然一个memset清零已经基本达到目的了,但显示地将各个成员设成0/NULL/FALSE对于可读性来说还是有好处的,可以明确地说明各个字段的初始值,且对扩展性也会有好处,例如若后续初始化需要设置一些非0值(如用-1表示无效值),直接更改就好了。

2. 初始化sc_set_caps为FALSE –> 标识是否对主线程进行特权去除(drop privilege),主要是出于安全性考虑。

3. 初始化原子变量engine_stage –> 记录程序当前的运行阶段:SURICATA_INIT、SURICATA_RUNTIME、SURICATA_FINALIZE

4. 初始化日志模块,因为后续的执行流程中将使用日志输出,所以需要最先初始化该模块。

5. 设置当前主线程名字为“Suricata-Main”。线程名字还是挺重要的,至少在gdb调试时info threads可以看到各个线程名,从而可以精确地找到想要查看的线程。另外,在top -H时,也能够显示出线程名字(然而ps -efL时貌似还是只是显示进程名)。

6. 初始化ParserSize模块 –> 使用正则表达式来解析类似“10Mb”这种大小参数,其中正则引擎用的是pcre,因此初始化时就是调用pcre_compile、pcre_study对已经写好的正则表达式进行编译和预处理。

7. 注册各种运行模式。Suricata对“运行模式”这个概念也进行了封装。运行模式存储在runmodes数组中,定义为RunModes runmodes[RUNMODE_USER_MAX]。

  • 首先,数组中每一项(例如runmodes[RUNMODE_PCAP_DEV]),对应一组运行模式,模式组包括(RunModes类型):“IDS+Pcap”模式组、“File+Pcap”模式组、“UnixSocket”模式组等(另外还有其他一些内部模式,如:“列出关键字”模式、“打印版本号”模式等,这些没有存储在runmodes数组中)。
  • 然后,每一个模式组,其中可以包含若干个运行模式(RunMode类型),例如:single、auto、autofp、workers。
  • 运行模式的注册,则是为各个模式组(如RunModeIdsPcapRegister)添加其所支持的运行模式(通过调用RunModeRegisterNewRunMode),并定义改组的默认运行模式,以及非常重要的:注册各个模式下的初始化函数(如RunModeIdsPcapSingle),等后续初始化阶段确定了具体的运行模式后,就会调用这里注册的对应的初始化函数,对该模式下的运行环境进行进一步配置。

8. 初始化引擎模式为IDS模式。引擎模式只有两种:IDS、IPS,初始默认为IDS,而在nfq或ipfw启用时,就会切换成IPS模式,该模式下能够执行“Drop”操作,即拦截数据包。

9. 初始化配置模块,为配置节点树建立root节点。

10. 解析命令行参数。其中,与包捕获相关的选项(如“-i”)都会调用LiveRegisterDevice,以注册一个数据包捕获设备接口(如eth0)。全局的所有已注册的设备接口存储在变量live_devices中,类型为LiveDevice。注意,用多设备同时捕获数据包这个特性在Suricata中目前还只是实验性的。“-v”选项可多次使用,每个v都能将当前日志等级提升一级。

11. 若运行模式为内部模式,则进入该模式执行,完毕后退出程序。

12. FinalizeRunMode,即为运行模式的处理划上句号。主要是设置offline标志、对unknown运行模式进行报错,以及设置全局的run_mode变量。

13. 若运行模式为单元测试模式,则跑(用户通过正则表达式指定的)单元测试,并输出测试结果。

14. 检查当前模式是否与daemon标志冲突。Pcap文件模式及单元测试模式都不能在daemon开启下进行。

15. 初始化全局变量。包括:数据包队列trans_q、数据队列data_queues(干嘛的?)、对应的mutex和cond、建立小写字母表。

16. 初始化时间。包括:获取当前时间所用的spin lock,以及设置时区(调用tzset()即可)。

17. 为快速模式匹配注册关键字。调用SupportFastPatternForSigMatchList函数,按照优先级大小插入到sm_fp_support_smlist_list链表中。

18. 若用户未在输入参数中指定配置文件,则使用默认配置文件(/etc/suricata/suricata.yaml)。

19. 调用LoadYamlConfig读取Yaml格式配置文件。Yaml格式解析是通过libyaml库来完成的,解析的结果存储在配置节点树(见conf.c)中。对include机制的支持:在第一遍调用ConfYamlLoadFile载入主配置文件后,将在当前配置节点树中搜寻“include”节点,并对其每个子节点的值(即通过include语句所指定的子配置文件路径),同样调用ConfYamlLoadFile进行载入。

20. 再次初始化日志模块。这次,程序将会根据配置文件中日志输出配置(logging.outputs)填充SCLogInitData类型的结构体,调用SCLogInitLogModule重新初始化日志模块。

21. 打印版本信息。这是Suricata启动开始后第一条打印信息

22. 打印当前机器的CPU/核个数,这些信息是通过sysconf系统函数获取的。

23. 若运行模式为DUMP_CONFIG,则调用ConfDump打印出当前的所有配置信息。ConfDump通过递归调用ConfNodeDump函数实现对配置节点树的DFS(深度优先遍历)。

24. 执行PostConfLoadedSetup,即运行那些需要在配置载入完成后就立马执行的函数。这里面涉及的流程和函数非常多:

  • MpmTableSetup:设置多模式匹配表,该表中每一项就是一个实现了某种多模式匹配算法(如WuManber、AC)的匹配器。以注册AC匹配器为例,MpmTableSetup会调用MpmACRegister函数实现AC注册,函数内部其实只是填充mpm_table中对应AC的那一项(mpm_table[MPM_AC])的各个字段,如:匹配器名称("ac")、初始化函数(SCACInitCtx)、增加模式函数(SCACAddPatternCS)、实际的搜索执行函数(SCACSearch)。
  • 设置rule_reload标志。如果配置文件中对应选项打开,则会设置该标志,表示可以进行“规则热重载”,即能够在程序运行时载入或替换规则集。
  • AppLayerDetectProtoThreadInit:初始化应用层协议检测模块。其中,AlpProtoInit函数初始化该模块所用到的多模式匹配器,RegisterAppLayerParsers函数注册各种应用层协议的解析器(如RegisterHTPParsers函数对应HTTP协议),而AlpProtoFinalizeGlobal函数完成一些收尾工作,包括调用匹配器的预处理(Prepare)函数、建立模式ID和规则签名之间的映射等。
  • AppLayerParsersInitPostProcess:这个函数内部建立了一个解析器之间的映射,还不太懂其用途。
  • 设置并验证日志存储目录是否存在。若配置文件中未指定,则使用默认目录,linux下默认为/var/log/suricata。
  • 获取与包捕获相关的一些配置参数,目前包括:max-pending-packets、default-packet-size。
  • 设置host_mode(主机模式),两种模式:router和sniffer-only,而如果设置为“auto”,则会进行自动选择:IPS模式下运行为router,否则为sniffer-only。
  • SCHInfoLoadFromConfig:从配置文件中载入host os policy(主机OS策略)信息。网络入侵通常是针对某些特定OS的漏洞,因此如果能够获取部署环境中主机的OS信息,肯定对入侵检测大有裨益。具体这些信息是怎么使用的,暂时也还不清楚。
  • DefragInit:初始化IP分片重组模块。
  • SigTableSetup:初始化检测引擎,主要是注册检测引擎所支持的规则格式(跟Snort规则基本一致)中的关键字,比如sid、priority、msg、within、distance等等。
  • TmqhSetup:初始化queue handler(队列处理函数),这个是衔接线程模块数据包队列之间的桥梁,目前共有5类handler:simple, nfq, packetpool, flow, ringbuffer。每类handler内部都有一个InHandlerOutHandler,一个用于从上一级队列中获取数据包,另一个用于处理完毕后将数据包送入下一级队列。
  • StorageInit:初始化存储模块,这个模块可以用来临时存储一些数据,数据类型目前有两种:host、flow。具体在何种场景下用,目前未知。
  • CIDRInit:初始化CIDR掩码数组,cidrs[i]对应前i位为1的掩码。
  • SigParsePrepare:为规则签名解析器的正则表达式进行编译(pcre_compile)和预处理(pcre_study)。
  • SCPerfInitCounterApi:初始化性能计数器模块。这个模块实现了累加计数器(例如统计收到的数据包个数、字节数)、平均值计数器(统计平均包长、处理时间)、最大计数器(最大包长、处理时间)、基于时间间隔的计数器(当前流量速率)等,默认输出到日志目录下的stats.log文件。
  • 几个Profiling模块的初始化函数。Profiling模块提供内建的模块性能分析功能,可以用来分析模块性能、各种锁的实际使用情况(竞争时间)、规则的性能等。
  • SCReputationInitCtx:初始化IP声望模块。IP声望数据在内部是以Radix tree的形式存储的,但目前还不知道数据源是从哪来的,而且也没看到这个模块的函数在哪调用。
  • SCProtoNameInit:读取/etc/protocols文件,建立IP层所承载的上层协议号和协议名的映射(如6-> ”TCP”,17-> ”UDP“)。
  • TagInitCtx、ThresholdInit:与规则中的tag、threshould关键字的实现相关,这里用到了Storage模块,调用HostStorageRegister和FlowStorageRegister注册了几个(与流/主机绑定的?)存储区域。
  • DetectAddressTestConfVars、DetectPortTestConfVars:检查配置文件中"vars"选项下所预定义的一些IP地址(如局域网地址块)、端口变量(如HTTP端口号)是否符合格式要求。
  • RegisterAllModules:这是个非常重要的函数!里面注册了Suricata所支持的所有线程模块(Thread Module)。以pcap相关模块为例,TmModuleReceivePcapRegister函数注册了Pcap捕获模块,而TmModuleDecodePcapRegister函数注册了Pcap数据包解码模块。所谓注册,就是在tmm_modules模块数组中对应的那项中填充TmModule结构的所有字段,这些字段包括:模块名字、线程初始化函数、包处理或包获取函数、线程退出清理函数、一些标志位等等。
  • AppLayerHtpNeedFileInspection:设置suricata内部模块与libhtp(HTTP处理库)对接关系的函数,具体细节暂时不管。
  • DetectEngineRegisterAppInspectionEngines:名字都这么长了,肯定很复杂。。。暂时不管。
  • 若设置了rule_reload标志,则注册相应的信号处理函数(目前设置的函数都是些提示函数,没有做实际重载)。这里用的是比较惯用的SIGUSR2信号来触发rule reload。
  • StorageFinalize:关闭storage模块的注册,为已注册的storage实际分配存储空间。
  • TmModuleRunInit:调用之前注册的线程模块的初始化函数进行初始化。

25. 检查是否进入Daemon模式。若需要进入Daemon模式,则会检测pidfile是否已经存在(daemon下只能有一个实例运行),然后进行Daemonize,最后创建一个pidfile。Daemonize的主要思路是:fork->子进程调用setsid创建一个新的session,关闭stdin、stdout、stderr,并告诉父进程 –> 父进程等待子进程通知,然后退出 –> 子进程继续执行。

26. 初始化信号handler。首先为SIGINT(ctrl-c触发)和SIGTERM(不带参数kill时触发)这两个常规退出信号分别注册handler,对SIGINT的处理是设置程序的状态标志为STOP,即让程序优雅地退出;而对SIGTERM是设置为KILL,即强杀。接着,程序会忽略SIGPIPE(这个信号通常是在Socket通信时向已关闭的连接另一端发送数据时收到)和SIGSYS(当进程尝试执行一个不存在的系统调用时收到)信号,以加强程序的容错性和健壮性。

27. 获取配置文件中指定的Suricata运行时的usergroup,如果命令行中没有指定的话。然后,将指定的user和group通过getpwuid、getpwnam、getgrnam等函数转换为uid和gid,为后续的实际设置uid和gid做准备。注意,这段代码也是在InitSignalHandler中执行的,不知道为什么放这里,跟信号有关系么。。。

28. 初始化Packet pool,即预分配一些Packet结构体,分配的数目由之前配置的max_pending_packets确定,而数据包的数据大小由default_packet_size确定(一个包的总占用空间为default_packet_size+sizeof(Packet))。在调用PacketGetFromAlloc新建并初始化一个数据包后,再调用PacketPoolStorePacket将该数据包存入ringbuffer。Suricata中用于数据包池的Ring Buffer类型为RingBuffer16,即容量为2^16=65536(但为什么max_pending_packets的最大值被限定为65534呢?)。

29. 初始化Host engine。这货好像跟之前的host类型的storage有关系,具体怎么用后面再看看吧。

30. 初始化Flow engine。跟前面的host engine类似,不过这个的用处就很明显了,就是用来表示一条TCP/UDP/ICMP/SCTP流的,程序当前所记录的所有流便组成了流表,在flow引擎中,流表为flow_hash这个全局变量,其类型为FlowBucket *,而FlowBucket中则能够存储一个Flow链表,典型的一张chained hash Table。在初始化函数FlowInitConfig中,首先会使用配置文件信息填充flow_config,然后会按照配置中的hash_size为流表实际分配内存,接着按照prealloc进行流的预分配(FlowAlloc->FlowEnqueue,存储在flow_spare_q这个FlowQueue类型的队列中),最后调用FlowInitFlowProto为流表所用于的各种流协议进行配置,主要是设置timeout时间。

31. 初始化Decect engine。若配置文件中未指定mpm(多模式匹配器),则默认使用AC,即使用mpm_table中AC那一项。SRepInit函数(与前面的SCReputationInitCtx不同!)会初始化检测引擎中域reputaion相关信息,即从配置文件中指定的文件中读取声望数据。其余配置比较复杂,暂不关注。

32. 读取和解析classification.config和reference.config,这两个文件用于支持规则格式中的classification(规则分类)和refercence(规则参考资料)字段。

33. 设置规则的动作优先级顺序,默认为Pass->Drop->Reject->Alert。举例来说,若有一条Pass规则和Drop规则都匹配到了某个数据库,则会优先应用Pass规则。

34. 初始化Magic模块。Magic模块只是对libmagic库进行了一层封装,通过文件中的magic字段来检测文件的类型(如”PDF-1.3“对应PDF文件)。

35. 设置是否延迟检测。若delayed-detect为yes,则系统将在载入规则集之前就开始处理数据包,这样能够在IPS模式下将少系统的down time(宕机时间)。

36. 如果没有设置延迟检测,就调用LoadSignatures载入规则集

37. 如果设置了live_reload,则重新注册用于规则重载的SIGUSR2信号处理函数(这次是设置为真正的重载处理函数)。放在这里是为了防止在初次载入规则集时就被触发重载。

38. 初始化ASN.1解码模块。Wikipedia:ASN.1(Abstract Syntax Notation One) 是一套标准,是描述数据的示、编码、传输、解码的灵活的记法。应用层协议如X.400(email)、X.500和LDAP(目录服务)、H.323(VoIP)和SNMP使用 ASN.1 描述它们交互的协议数据单元。

39. 处理CoreDump相关配置。Linux下可用prctl函数获取和设置进程dumpable状态,设置corefile大小则是通过通用的setrlimit函数。

40. 调用gettimeofday保存当前时间,存储在suri->start_time中,作为系统的启动时间。

41. 去除主线程的权限。这个是通过libcap-ng实现的,首先调用capng_clear清空所有权限,然后根据运行模式添加一些必要权限(主要是为了抓包),最后调用capng_change_id设置新的uid和gid。主线程的权限应该会被新建的子线程继承,因此只需要在主线程设置即可。

42. 初始化所有Output模块。这些模块之前在线程模块注册函数里已经都注册了,这里会根据配置文件再进行配置和初始化,最后把当前配置下启用了的output模块放到RunModeOutputs链表中。

43. 若当前抓包模式下未指定设备接口(通过-i <dev>或--pcap=<dev>等方式),则解析配置文件中指定的Interface,并调用LiveRegisterDevice对其进行注册。

44. 若当前的模式为CONF_TEST,即测试配置文件是否有效,则现在就可以退出了。这也说明,程序运行到这里,配置工作已经基本完成了。

45. 初始化运行模式。首先,根据配置文件和程序中的默认值来配置运行模式(single、auto这些),而运行模式类型(PCAP_DEV、PCAPFILE这些)也在之前已经确定了,因此运行模式已经固定下来,可以从runmodes表中获取到特定的RunMode了,接着就调用RunMode中的RunModeFunc,进入当前运行模式的初始化函数。以PCAP_DEV类型下的autofp模式为例,该模式的初始化函数为:RunModeIdsPcapAutoFp。这个函数的执行流程为:

  • 调用RunModeInitialize进行通用的运行模式初始化,目前主要是设置CPU affinity和threading_detect_ratio。
  • 调用RunModeSetLiveCaptureAutoFp设置该模式下的模块组合
    • 确实参数:接口个数nlive、线程个数thread_max(由用户指定,或CPU个数决定)。
    • RunmodeAutoFpCreatePickupQueuesString:创建一个包含thread_max个接收队列名字的字符串,如"pickup1,pickup2,pickup3"。
    • ParsePcapConfig:解析pcap接口相关配置,如buffer-size、bpf-filter、promisc等。
    • PcapConfigGeThreadsCount:获取pcap接口配置中指定的threads(抓包线程个数,默认为1),保存到threads_count变量。
    • 创造threads_count个抓包线程
      • TmThreadCreatePacketHandler函数专门用于创建包处理线程,函数内部会调用通用的TmThreadCreate创建线程,并将线程类型设置为TVT_PPT
      • 线程名字为"RxPcap"+接口名+i,如“RxPcapeth01”。
      • inq、inqh都设置为"packetpool",表示将从数据包池(而不是某个数据包队列)中获取包。
      • outqh设置为"flow",表示使用之前注册的flow类型的queue handler作为线程的输出队列处理器,这个类型可以保证同一条flow的包都会输出给同一个queue,具体的包调度策略取决于autop-scheduler指定的算法。
      • outq设置为前面所设置的接收队列名字符串,而之前的flow类型handler的TmqhOutputFlowSetupCtx函数将会解析队列名字符串,并创建出相应个数(threads_max)的队列。
      • slots函数设置为"pktacqloop",表示这个线程的插槽类型为pktacqloop,这样在TmThreadSetSlots函数中就会将线程执行函数(tm_func)设置为针对该插槽类型的TmThreadsSlotPktAcqLoop函数。最终线程在被pthread_create执行时传入的回调函数就是这个线程执行函数。
      • TmSlotSetFuncAppend:将“ReceivePcap"和"DecodePcap"这两个线程模块嵌入到前面创建的每个抓包线程的插槽中去。
      • TmThreadSetCPU:设置线程的CPU相关参数。
      • TmThreadSpawn:按照之前所填充好的ThreadVars生成实际的线程,并将该线程添加到全局线程数组tv_root中去。
    • 创造thread_max个检测线程
      • 线程名字为"Detect"+i,每个线程都有与一个输入队列绑定,即inq设置为"pickup"+i 队列。
      • inqh设置为"flow",即使用flow类型(与前面的抓包线程相匹配)的queue handler作为线程的输入队列处理器。
      • outq、outqh都设置为"packetpool",表示这个线程的包处理完后会直接回收到packet pool中去。
      • slots函数设置为"varslot",表示这个线程的插槽类型为varslot,对应的执行函数为TmThreadsSlotVar。
      • 接着,跟上面类似,把"StreamTcp"(用于TCP会话跟踪、重组等)、"Detect"(调用检测引擎进行实际的入侵检测)和"RespondReject"(用于通过主动应答来拒绝连接)这三个线程模块嵌入进去。不过,这里在插入“Detect”模块时,调用的是TmSlotSetFuncAppendDelayed,用于支持delayed-detect功能。
      • SetupOutputs:由于这组检测线程是处理数据包的完结之处,因此这里需要把输出模块也嵌入到这些线程中去,方式也是通过TmSlotSetFuncAppend函数,对象是RunModeOutputs中存储的输出模块。

46. 若unix-command为enable状态,则创建Unix-socket命令线程,可与suricata客户端使用JSON格式信息进行通信。命令线程的创建是通过TmThreadCreateCmdThread函数,创建的线程类型为TVT_CMD。线程执行函数为UnixManagerThread

47. 创建Flow管理线程,用于对流表进行超时删除处理。管理线程创建是通过TmThreadCreateMgmtThread函数,类型为TVT_MGMT,执行函数为FlowManagerThread

48. 初始化Stream TCP模块。其中调用了StreamTcpReassembleInit函数进行重组模块初始化。

49. 创建性能计数相关线程,包括一个定期对各计数器进行同步的唤醒线程(SCPerfWakeupThread),和一个定期输出计数值的管理线程(SCPerfMgmtThread)。

50. 检查数据包队列的状态是否有效:每个数据包队列都应该至少有一个reader和一个writer。在前面线程绑定inq时会增加其reader_cnt,绑定outq时会增加其writer_cnt。

51. 等待子线程初始化完成。检查是否初始化完成的方式是遍历tv_root,调用TmThreadsCheckFlag检查子线程的状态标志。

52. 更新engine_stage为SURICATA_RUNTIME,即程序已经初始化完成,进入运转状态。这里的更新用的是原子CAS操作,防止并发更新导致状态不一致(但目前没在代码中只到到主线程有更新engine_stage操作,不存在并发更新)。

53. 让目前处于paused状态的线程继续执行。在TmThreadCreate中,线程的初始状态设置为了PAUSE,因此初始化完成后就会等待主线程调用TmThreadContinue让其继续。从这以后,各线程就开始正式执行其主流程了。

54. 若设置了delayed_detect,则现在开始调用LoadSignatures加载规则集,激活检测线程,并注册rule_reload信号处理函数。这里,激活检测线程是通过调用TmThreadActivateDummySlot函数,这个函数会将之前注册的slot中的slotFunc替换为实际操作函数,而不是原先在delayed_detect情况下设置的什么都不做的TmDummyFunc。

55. 进入死循环。若受到引擎退出信号(SURICATA_KILL或SURICATA_STOP),则退出循环,执行后续退出操作,否则就调用TmThreadCheckThreadState检查各线程的状态,决定是否进行结束线程、重启线程、终止程序等操作,然后usleep一会儿(1s),继续循环。

56. 接着,程序就进入到了退出阶段,首先会更新engine_stage为SURICATA_DEINIT,然后依次关闭Unix-socket线程、Flow管理线程。

57. 停止包含抓包或解码线程模块的线程。这个是通过TmThreadDisableThreadsWithTMS实现,里面会检查每个线程的slots里嵌入的线程模块的flags中是否包含指定的flag(这里是TM_FLAG_RECEIVE_TM或TM_FLAG_DECODE_TM),一个线程模块的flags在注册时就已经指定了。关闭是通过向线程发送KILL信号(设置线程变量的THV_KILL标志)实现,收到该信号的线程会进入RUNNING_DONE状态,然后等待主线程下一步发出DEINIT信号。

58. 强制对仍有未处理的分段的流进行重组。

59. 打印进程运行的总时间(elapsed time)。

60. 在rule_reload开启下,首先同样调用TmThreadDisableThreadsWithTMS停止检测线程。特别地,该函数对于inq不为"packetpool"的线程(即该线程从一个PakcetQueue中获取数据包),会等到inq中的数据包都处理完毕再关闭这个线程。然后,检测是否reload正在进行,如果是则等待其完成,即不去打断它。

61. 杀死所有子线程。杀死线程的函数为TmThreadKillThread,这个函数会同时向子线程发出KILL和DEINIT信号,然后等待子线程进入CLOSED状态,之后,再调用线程的清理函数(InShutdownHandler)以及其对应的ouqh的清理函数(OutHandlerCtxFree),最后调用pthread_join等待子线程退出。

62. 执行一大堆清理函数:清理性能计数模块、关闭Flow engine、清理StreamTCP、关闭Host engine、清理HTP模块并打印状态、移除PID文件、关闭检测引擎、清理应用层识别模块、清理Tag环境、关闭所有输出模块,etc…

63. 调用exit以engine_retval为退出状态终止程序

转载于:https://my.oschina.net/openadrian/blog/184621

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值