TLS handshake Fatal Alert Close Notify的错误排查 前置知识 浏览器同源安全策略 WebSocket\TCP通讯协议 HTTPS TLS 握手机制 CA证书信任链 strace、dtruss系统调用动态跟踪 keychain安全机制 macOS错误日志体系 背景 前段时间接了一个项目----MOA,一款安装在macOS、Win办公电脑软件,是公司内部安全管理客户端,用来保护员工PC安全的软件。 我之前没有接触过macOS的开发,对objective-C(以下简称OC)语言也不熟悉,我这边的几个研发兄弟也一样,都是做Linux C、C++出身,对于这个陌生的领域,他们倒是不拒绝,很乐意去尝试。 在MOA新的版本中,需要具备与SSO单点登陆系统联动,控制内网SSO的准入功能,研发开发完成后,进行了灰度部署。 在灰度时,遇到一个BUG,浏览器的websocket无法与本地wss服务通讯,遇到tls handshake的错误,导致很多人无法登陆SSO,无法正常办公。 研发兄弟们也都是忙于熟悉OC语言,熟悉macOS的运行机制,忙于做业务功能开发,没时间排查这问题,以至于这BUG导致项目延期两周,影响上线进度。产品经理也在周会上表达了不满,开始对研发的技术能力产生怀疑。。。又到了拯救地球的时刻了,我决定来查一下这BUG。 运行机制介绍 关于这个SSO的通讯机制,工作过程是这样的: 假设:SSO的地址是https://sso.mt.com,MOA监听的地址是localhost.moa.mt.com MOA进程创建HTTP SERVER,监听loopback地址的某端口; 在用户打开浏览器登陆SSO时,SSO的域名会返回一段JS给浏览器; 浏览器执行后,会通过Websocket的方式连接loopback的这个端口,进行通讯; 那么问题来了,这种场景下,除了Websocket还有其他方式吗? 浏览器的同源策略对WS、WSS协议有影响吗? 对浏览器安全比较了解的同学很容易就给出了答案,走Websocket是最合适的方案,同源策略并不对Websocket做限制。 但是,浏览器的当前页面的协议是HTTPS时,Websocket也必须是TLS协议才行,也就是说对应WSS协议。 在macOS上开启Websocket,并且还要支持TLS通讯加密,需要在macOS上做证书私钥的部署,我们RD同学考虑到私钥泄露的风险,决定走自签CA证书的方式,在MOA部署时,将CA证书写入到macOS的Keychain根证书链中。 只不过这一步需要弹出KeyChain Access钥匙串访问的授权密码窗口,让用户授权。 现象 在SSO阻断功能的版本灰度后,收到其他部门的反馈,有好多同事的电脑显示SSO无法与MOA通讯,重启电脑后就好了。一共灰度一百多个人,出问题的就有四五个,而且不知道如何重现这个BUG。这就很让人头疼,我为了重现,尝试可很多办法,安装、卸载、再安装、再卸载,循环往复各种尝试重现,这个BUG终于重现了,但我也不知道具体的重现办法。 从技术上描述这个BUG是这样的,macOS上浏览器的https://sso.mt.com/xxx的Websocket无法连接到本地监听的localhost.moa.mt.com地址,报告网络错误。 对于这种错误,稍微对网络通讯debug的同学,都知道用tcpdump或者wireshark来抓包分析,抓到的数据包截图如下: 从这张通讯图上,你能发现问题出在哪了吗? TLS握手到哪个阶段了?谁先关闭的socket?关闭之前都做了什么通讯? Frame 7: 63 bytes on wire (504 bits), 63 bytes captured (504 bits) on interface 0 Null/Loopback Internet Protocol Version 4, Src: localhost (127.0.0.1), Dst: localhost (127.0.0.1) Transmission Control Protocol, Src Port: sun-sea-port (16161), Dst Port: 63655 (63655), Seq: 1, Ack: 518, Len: 7 Transport Layer Security TLSv1.2 Record Layer: Alert (Level: Fatal, Description: Close Notify) Content Type: Alert (21) Version: TLS 1.2 (0x0303) Length: 2 Alert Message Level: Fatal (2) Description: Close Notify (0) 机智如你,是Server给Client发了Level: Fatal, Description: Close Notify错误后,直接发了FIN包,进行关闭TCP SOCKET。 TLS Fatal Alert Close Notify 什么是TLS handshakeFatal Close Notify错误?第一次见到这个错吧?别担心,我也是。。。为什么在TLS 握手时,Client发了Client Hello后,Server没回Server Hello?而是回了这个错误? 在Openssl的文档SSL_shutdown - shut down a TLS/SSL connection 说明中,找到了这个错误的描述 SSL_shutdown() shuts down an active TLS/SSL connection. It sends the close_notify shutdown alert to the peer. SSL_shutdown() tries to send the close_notify shutdown alert to the peer. Whether the operation succeeds or not, the SSL_SENT_SHUTDOWN flag is set and a currently open session is considered closed and good and will be kept in the session cache for further reuse. Note that SSL_shutdown() must not be called if a previous fatal error has occurred on a connection i.e. if SSL_get_error() has returned SSL_ERROR_SYSCALL or SSL_ERROR_SSL. 从openssl官方文档中的SSL_shutdown函数描述里可以找到close_notify的介绍,当调用SSL_shutdown()函数后,将发送close_notify到对应的peer中。不管是client还是server,都可以发送这个错误。 在SSL_alert_type_string函数的介绍如下: A warning alert is sent, when a non-fatal error condition occurs. The "close notify" alert is sent as a warning alert. Other examples for non-fatal errors are certificate errors ("certificate expired", "unsupported certificate"), for which a warning alert may be sent. (The sending party may however decide to send a fatal error.) The receiving side may cancel the connection on reception of a warning alert on it discretion. Several alert messages must be sent as fatal alert messages as specified by the TLS RFC. A fatal alert always leads to a connection abort. 发送方可以决定是否发送Fatal级别的错误,在我们抓包的数据来看,是Moa Client(也就是说Server端)发送了Fatal级别的错误,原因是可能因为certificate expired、unsupported certificate"等等。但具体的错误,还是需要继续排查确认。 排查 那么,既然知道了TLS handshake的Fatal Close Nofity错误是服务端发起的,那么为什么呢?服务端的进程发生了什么问题?这如何排查?进程不能重启,重启后BUG现象就丢失了。 引用IBM developerworks上的一句话: 有时候,需要了解在应用程序内部正在发生的情况。例如,应用程序可能会运行失败,而又没有显示有帮助的错误消息,或者系统服务没有按照预期的方式运行。在这些情况下,您可能不掌握应用程序源代码,因此无法通过传统的调试过程寻找问题的原因。跟踪提供了一种替代方法。 在进行跟踪时,监视应用程序和操作系统之间的交互,常常会检查应用程序在执行期间调用的操作系统函数。 通过跟踪应用程序,可以查明以下方面的情况: 内存使用量和映射内存的调用 在执行期间打开和关闭的文件 对不同文件的读和写操作 为给定的应用程序装载的库 对于遇到的这个问题,我们要想了解到TLS Close Notify之前,都做了些什么操作,正因为我们程序不能重启,不知道如何重现,所以动态跟踪系统调用的做法最符合我们需求。 Linux strace 在Linux上,我们常用strace来对进程进行系统调用的跟踪探测。比如下面的例子 root@cnxct:~#strace -ff -tt -s 1000 -o 111.log -p 800 00:08:40.417260 futex(0x1ddbf08, FUTEX_WAIT_PRIVATE, 0, NULL) = 0 00:08:43.380903 futex(0x1ddbf20, FUTEX_WAIT_PRIVATE, 2, NULL) = 0 00:08:43.381001 setsockopt(232, SOL_TCP, TCP_NODELAY, [1], 4) = -1 EOPNOTSUPP (Operation not supported) 00:08:43.381053 gettid() = 9757 00:08:43.381077 setsockopt(232, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0 00:08:43.381102 openat(AT_FDCWD, "/dev/urandom", O_RDONLY) = 252 00:08:43.381134 read(252, "K8\232Nl\26\rU\354\312\221\260\20563+v=\367\372\371\317W\f>.\304\221\342B$\375", 32) = 32 00:08:43.381162 close(252) = 0 00:08:43.381603 recvfrom(232, "\22\0\0\0", 4, MSG_DONTWAIT, NULL, NULL) = 4 00:08:43.381629 recvfrom(232, "\3SET NAMES utf8mb4", 18, MSG_DONTWAIT, NULL, NULL) = 18 能看到,strace的结果中,会有当前系统的CPU时间、syscall函数、参数、返回值等,有了系统时间,则会很方便的跟其他调试信息关联。 macOS dtruss 在macOS上,也有想对应的工具,dtruss,之前调试时,用过几次,命令参数、输出结果跟strace差别太大,总是不习惯。 现在遇到问题了,逃不开,只好开始学习这个工具的使用。 参数上 cfc4n@cnxct-mt:~$ dtruss USAGE: dtruss [-acdefholLs] [-t syscall] { -p PID | -n name | command | -W name } -p PID # examine this PID -n name # examine this process name -t syscall # examine this syscall only -W name # wait for a process matching this name -a # print all details -c # print syscall counts -d # print relative times (us) -e # print elapsed times (us) -f # follow children -l # force printing pid/lwpid -o # print on cpu times -s # print stack backtraces -L # don't print pid/lwpid -b bufsize # dynamic variable buf size eg, dtruss df -h # run and examine "df -h" dtruss -p 1871 # examine PID 1871 dtruss -n tar # examine all processes called "tar" dtruss -f test.sh # run test.sh and follow children 从参数上来看, -p PID、-n name都是选择跟踪的目标的 -c、-d、-o等参数是统计对应系统调用执行时占用的CPU时间,在性能分析的场景下很有用。 -s参数是打印系统调用对应的函数调用栈,这个参数对本次bug的排查会很有帮助。 工具参数熟悉后,整理思路开始跟踪吧。 BUG对应的系统调用范围确定 在我们去触发BUG时,程序本身也还在运行,在执行其他业务逻辑。那我们如何确定bug产生的过程对应的系统调用记录范围? 系统调用执行时的时间 前面提到,在Linux平台上,strace的日志会带有系统当前时间,其他的检测软件中也会有记录系统时间,这样关联就比较方便。但macOS平台上,dtruss是没有这个系统当前时间输出的,这样就没法便捷的关联BUG时间段,没法确定syscall日志的范围,很头疼。 比如tcpdump或wireshark捕获的网络通讯包中,也会有系统时间,哪怕没法跟dtruss日志关联,但也还是要抓包捕获,来确认BUG确实产生了。开tcpdump,选择loopback网卡(因为是127.0.0.1这个回环网址,所以是loopback网卡),设定参数port 16161,开启嗅探,同时开始重现BUG。 系统syscall的API入手关联 在wireshark的分析中,确认BUG重现后,着手分析dtruss日志。 从我们研发的经验上考虑,浏览器向MOA发起wss TCP连接时,wss server会去调用accept系统调用来处理,那么我们只要确定在大约的时间段里,找到accept的syscall,就能定位到这个bug产生最开始的入口点。 cfc4n@cnxct-mt:~$ dtruss -p 78158 dtrace: system integrity protection is on, some features will not be available dtrace: failed to initialize dtrace: DTrace requires additional privileges cfc4n@cnxct-mt:~$ please dtruss -p 78158 Password: dtrace: system integrity protection is on, some features will not be available SYSCALL(args) = return workq_kernreturn(0x40, 0x700005CCEB80, 0x1) = 0 Err#-2 kevent_qos(0xFFFFFFFFFFFFFFFF, 0x7FFEE0E63920, 0x1) = 0 0 workq_kernreturn(0x40, 0x700005CCEB80, 0x1) = 0 Err#-2 workq_kernreturn(0x40, 0x700005CCEB80, 0x1) = 0 Err#-2 accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0 fcntl(0x19, 0x4, 0x4) = 0 0 setsockopt(0x19, 0xFFFF, 0x1022) = 0 0 kevent_id(0x6000036A8500, 0x700005CCEB80, 0x0) = 1 0 thread_selfid(0x0, 0x0, 0x0) = 21736566 0 lstat64("/Applications/Firefox.app/Contents/Resources\0", 0x700005D50F70, 0x0) = -1 Err#2 open("/Applications/MOA.app/Contents/Resources/MOA.app/Contents/Resources/server.p12\0", 0x0, 0x1B6) = 26 0 fstat64(0x1A, 0x700005D512B0, 0x0) = 0 0 dtrace: error on enabled probe ID 2174 (ID 168: syscall::read:return): invalid kernel access in action #12 at DIF offset 68 close(0x1A) = 0 0 open("/Users/cfc4n/Library/Keychains/login.keychain-db\0", 0x0, 0x0) = 27 0 __mac_syscall(0x7FFF7833ACD0, 0x52, 0x700005D4F9E0) = 0 0 lstat64("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-gMs51v\0", 0x700005D4FC48, 0x0) = 0 0 fstatfs64(0x1B, 0x700005D4FC48, 0x0) = 0 0 open_dprotected_np(0x600003BFA080, 0xB01, 0xFFFFFFFFFFFFFFFF) = -1 Err#17 open_dprotected_np(0x600003BFA080, 0x901, 0xFFFFFFFFFFFFFFFF) = 28 0 fcntl(0x1B, 0x30, 0x1) = 0 0 fcntl(0x1C, 0x30, 0x1) = 0 0 fcntl(0x1C, 0x4C, 0x1) = 0 0 __mac_syscall(0x7FFF7833ACD0, 0x53, 0x700005D4EFE8) = 0 0 fstat64_extended(0x1C, 0x700005D4F1A8, 0x7FD4A3D4CE10) = 0 0 fchmod_extended(0x1C, 0x1F5, 0x14) = 0 0 fstat64(0x1C, 0x700005D4EF50, 0x0) = 0 0 fchflags(0x1C, 0x0, 0x0) = 0 0 fchown(0x1C, 0x1F5, 0x14) = 0 0 fchmod(0x1C, 0x1A4, 0x0) = 0 0 fsetattrlist(0x1C, 0x700005D4F000, 0x700005D4EFE0) = 0 0 fstat64(0x1B, 0x700005D4FBA0, 0x0) = 0 0 fstat64_extended(0x1C, 0x700005D4FC48, 0x7FD4A3D4CE10) = 0 0 close(0x1B) = 0 0 close(0x1C) = 0 0 utimes("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-gMs51v\0", 0x0, 0x0) = 0 0 rename("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-gMs51v\0", "/Users/cfc4n/Library/Keychains/login.keychain-db\0") = 0 0 flock(0x1A, 0x8, 0x0) = 0 0 close(0x1A) = 0 0 open("/Applications/MOA.app/Contents/Resources/MOA.app/Contents/Info.plist\0", 0x0, 0x1B6) = 26 0 fsync(0x1B, 0x0, 0x0) = 0 0 close(0x1B) = 0 0 lstat64("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-rmdB2z\0", 0x700005D4F988, 0x0) = 0 0 stat64_extended(0x600003BF5D60, 0x700005D4F988, 0x7FD4A3F210C0) = 0 0 chmod_extended(0x600003BF5D60, 0x1F5, 0x14) = 0 0 lstat64_extended(0x600000299F00, 0x7FD4A3F20FC8, 0x7FD4A3F210C0) = 0 0 open("/Users/cfc4n/Library/Keychains/login.keychain-db\0", 0x0, 0x0) = 27 0 workq_kernreturn(0x100, 0x700005D51B80, 0x2) = 0 Err#-2 getentropy(0x700005D51780, 0x20, 0x0) = 0 0 getentropy(0x700005D517D0, 0x40, 0x0) = 0 0 kevent_id(0x6000036BE600, 0x700005C4AD80, 0x1) = 0 0 kevent_id(0x6000036BE600, 0x700005C4AD50, 0x1) = 0 0 dtrace: error on enabled probe ID 2172 (ID 170: syscall::write:return): invalid kernel access in action #12 at DIF offset 68 kevent_id(0x6000036B1F80, 0x700005C4B6F0, 0x1) = 0 0 workq_kernreturn(0x100, 0x700005D51B80, 0x2) = 0 Err#-2 kevent_id(0x6000036A8500, 0x700005C4BB80, 0x1) = 0 0 close(0x19) = 0 0 workq_kernreturn(0x100, 0x700005C4BB80, 0x1) = 0 Err#-2 bsdthread_ctl(0x100, 0x0, 0x2DE13) = 0 0 workq_kernreturn(0x20, 0x0, 0x1) = 0 0 workq_kernreturn(0x100, 0x700005D51B80, 0x1) = 0 Err#-2 (数据有删减)从上面的dtruss日志来看,accept的syscall很显眼accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0,那么,如何来理解这段日志呢? macOS的syscall系统调用也是跟unix的一致,参考BSD System Calls 手册 ACCEPT(2) BSD System Calls Manual NAME accept -- accept a connection on a socket SYNOPSIS #include <sys/socket.h> int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len); RETURN VALUES The call returns -1 on error and the global variable errno is set to indicate the error. If it succeeds, it returns a non-negative integer that is a descriptor for the accepted socket. 从这个syscall的说明来看,参数部分并不重要,重要的是RETURN VALUES的部分,这是TCP SOCKET连接后的所属FD文件描述符,也就是我们BUG重现时,这个网络连接在这个MOA进程中的FD。 BUG出现的对应的accept 找到BUG重现后的FD,那么再找到FD被关闭的地方即可。从dtruss的日志accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0来看,这个FD是25,聪明的你立刻就想到,只要找到close的地方就能确定到BUG出现的syscall日志范围。 BUG出现的对应的close 直接搜索close的系统调用吧,很明显在close(0x19) = 0 0就是,只不过十进制的 25 被显示成十六进制的0x19了。 确定BUG出现的范围 对于这个BUG来说,已经确定了其产生对应的syscall日志范围,那么剩下的事情只要顺着close的地方倒推往上来找。 跟踪 从上面的dtruss结果来看,并没有找到准确的TCP SOCKET write的系统调用,只看到几处dtrace: error on enabled probe ID 2172 (ID 170: syscall::write:return): invalid kernel access in action #12 at DIF offset 68 的错误,字面上理解,无法启用相关进程的syscall:write的探测。那么我们将无法定位到到底是哪里的syscall向TCP SOCKET的FD里写入了Fatal Close Notify的错误。 回到dtruss的参数中,可以看到还有一个参数-s用来打印程序运行的stack backtraces。进行重新重现BUG: cfc4n@cnxct-mt:~$ please dtruss -p 78158 -adfs Password: dtrace: system integrity protection is on, some features will not be available PID/THRD RELATIVE ELAPSD CPU SYSCALL(args) = return 78158/0x1527e26: 11190 88 2 bsdthread_ctl(0x100, 0x0, 0x1335B) = 0 0 libsystem_kernel.dylib`__bsdthread_ctl+0xa libdispatch.dylib`_dispatch_set_priority_and_mach_voucher_slow+0xa0 libdispatch.dylib`_dispatch_set_priority_and_voucher_slow+0xc9 libdispatch.dylib`_dispatch_mach_msg_invoke+0x282 libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_mach_invoke+0x1e1 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 0x10ef9e000 78158/0x1527e26: 11276 106 23 workq_kernreturn(0x20, 0x0, 0x1) = 0 0 libsystem_kernel.dylib`__workq_kernreturn+0xa libdispatch.dylib`_dispatch_root_queue_poke_slow+0xac LaunchServices`LSNotificationReceiver::receiveNotificationFromServer(_xpc_connection_s*, void*)+0x3a3 LaunchServices`___ZN22LSNotificationReceiver23getNotificationListenerEv_block_invoke.10+0x184 libxpc.dylib`_xpc_connection_call_event_handler+0x38 libxpc.dylib`_xpc_connection_mach_event+0x3a5 libdispatch.dylib`_dispatch_client_callout4+0x9 libdispatch.dylib`_dispatch_mach_msg_invoke+0x1b4 libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_mach_invoke+0x1e1 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 0x10ef9e000 ... //很多,有删减 78158/0x152e9e7: 294 101 21 accept(0x4, 0x700005CCE7A0, 0x700005CCE79C) = 25 0 libsystem_kernel.dylib`__accept+0xa itsec-agent`0x000000010edc0023+0x44 libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_continuation_pop+0x19e libdispatch.dylib`_dispatch_source_invoke+0x808 libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 78158/0x152ecde: 1209 37 31 open("/Users/cfc4n/Library/Keychains//.fl34AC2A0A\0", 0x200, 0x124) = 26 0 libsystem_kernel.dylib`__open+0xa Security`Security::DbModifier::modifyDatabase()+0x5d Security`Security::AppleDatabase::dataInsert(Security::DbContext&, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*)+0x54 Security`Security::DatabaseSession::DataInsert(long, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*, cssm_db_unique_record*&)+0xbb Security`cssm_DataInsert(cssm_dl_db_handle, unsigned int, cssm_db_record_attribute_data const*, cssm_data const*, cssm_db_unique_record**)+0x68 Security`CSSM_DL_DataInsert+0x88 Security`SSDatabaseImpl::ssInsert(unsigned int, cssm_db_record_attribute_data const*, cssm_data const*)+0xc5 Security`SSCSPDLSession::makeReferenceKey(SSCSPSession&, unsigned int, Security::CssmKey&, SSDatabase&, unsigned int, Security::CssmData const*)+0xb09 Security`SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x17a Security`non-virtual thunk to SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x21 Security`cssm_UnwrapKey(long, unsigned long long, cssm_context const*, cssm_key const*, cssm_key const*, unsigned int, unsigned int, cssm_data const*, cssm_resource_control_context const*, cssm_key*, cssm_data*, unsigned long long)+0xd0 Security`CSSM_UnwrapKey+0xc7 Security`P12Coder::safeContentsParse(cssm_data const&, SecNssCoder&)+0xd2c Security`impExpPkcs12Import+0x8a0 Security`SecKeychainItemImport+0xd30 Security`SecPKCS12Import+0x100 itsec-agent`0x000000010edd8efe+0x10b itsec-agent`0x000000010ed9f5b8+0x36 itsec-agent`0x000000010edce367+0x39 itsec-agent`0x000000010edce297+0x38 78158/0x152ecde: 15213 33 30 lstat64("/Users/cfc4n/Library/Keychains/login.keychain-db.sb-ad1d361c-3plX5x\0", 0x700005C49A78, 0x0) = 0 0 libsystem_kernel.dylib`lstat$INODE64+0xa Security`Security::DbModifier::commit()+0xda3 Security`Security::AppleDatabase::dataInsert(Security::DbContext&, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*)+0xb1 Security`Security::DatabaseSession::DataInsert(long, unsigned int, cssm_db_record_attribute_data const*, Security::CssmData const*, cssm_db_unique_record*&)+0xbb Security`cssm_DataInsert(cssm_dl_db_handle, unsigned int, cssm_db_record_attribute_data const*, cssm_data const*, cssm_db_unique_record**)+0x68 Security`CSSM_DL_DataInsert+0x88 Security`SSDatabaseImpl::ssInsert(unsigned int, cssm_db_record_attribute_data const*, cssm_data const*)+0xc5 Security`SSCSPDLSession::makeReferenceKey(SSCSPSession&, unsigned int, Security::CssmKey&, SSDatabase&, unsigned int, Security::CssmData const*)+0xb09 Security`SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x17a Security`non-virtual thunk to SSCSPSession::UnwrapKey(unsigned long long, Security::Context const&, Security::CssmKey const*, Security::CssmKey const&, unsigned int, unsigned int, Security::CssmData const*, cssm_resource_control_context const*, Security::CssmKey&, Security::CssmData&, unsigned long long)+0x21 Security`cssm_UnwrapKey(long, unsigned long long, cssm_context const*, cssm_key const*, cssm_key const*, unsigned int, unsigned int, cssm_data const*, cssm_resource_control_context const*, cssm_key*, cssm_data*, unsigned long long)+0xd0 Security`CSSM_UnwrapKey+0xc7 Security`P12Coder::safeContentsParse(cssm_data const&, SecNssCoder&)+0xd2c Security`impExpPkcs12Import+0x8a0 Security`SecKeychainItemImport+0xd30 Security`SecPKCS12Import+0x100 itsec-agent`0x000000010edd8efe+0x10b itsec-agent`0x000000010ed9f5b8+0x36 itsec-agent`0x000000010edce367+0x39 itsec-agent`0x000000010edce297+0x38 ... //有删减 78158/0x152e9e7: 1150 71 4 kevent_id(0x6000036BE600, 0x700005CCDD50, 0x1) = 0 0 libsystem_kernel.dylib`kevent_id+0xa libdispatch.dylib`_dispatch_event_loop_end_ownership+0x2b6 libdispatch.dylib`_dispatch_sync_complete_recurse+0x2c libdispatch.dylib`_dispatch_sync_invoke_and_complete_recurse+0x5b libdispatch.dylib`_dispatch_sync_f_slow+0xc3 libcorecrypto.dylib`ccrng_cryptographic_generate+0x132 libcorecrypto.dylib`ccn_random_bits+0x2b libcorecrypto.dylib`ccec_projectify+0x59 libcorecrypto.dylib`ccec_make_pub_from_priv+0xce libcorecrypto.dylib`ccec_generate_key_internal_fips+0xa0 libcorecrypto.dylib`ccec_sign_internal+0x1c9 libcorecrypto.dylib`ccec_sign+0x57 libcorecrypto.dylib`ccec_pairwise_consistency_check+0x84 libcorecrypto.dylib`ccec_generate_key_fips+0x21 libcoretls.dylib`sslEcdhCreateKey+0x40 libcoretls.dylib`SSLEncodeServerKeyExchange+0x253 libcoretls.dylib`SSLPrepareAndQueueMessage+0x27 libcoretls.dylib`SSLAdvanceHandshake+0xb37 libcoretls.dylib`SSLProcessHandshakeRecordInner+0xea libcoretls.dylib`tls_handshake_process+0x55 dtrace: error on enabled probe ID 2172 (ID 170: syscall::write:return): invalid kernel access in action #13 at DIF offset 68 78158/0x152e9e7: 2915 11 5 kevent_id(0x6000036B1F80, 0x700005CCE6F0, 0x1) = 0 0 libsystem_kernel.dylib`kevent_id+0xa libdispatch.dylib`_dispatch_event_loop_poke+0x205 itsec-agent`0x000000010edc2126+0x2e4 itsec-agent`0x000000010edc9109+0x185 itsec-agent`0x000000010edc8655+0x9d8 itsec-agent`0x000000010edc827e+0x96 libdispatch.dylib`_dispatch_call_block_and_release+0xc libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_lane_serial_drain+0x25a libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 78158/0x152e9e7: 3016 17 13 close(0x19) = 0 0 libsystem_kernel.dylib`close+0xa libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_continuation_pop+0x19e libdispatch.dylib`_dispatch_source_invoke+0x48b libdispatch.dylib`_dispatch_lane_serial_drain+0x10c libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd 找到close(0x19)的地方,往上倒推几个syscall日志,可以定位到真正往TCP SOCKET 写入Fatal Close Notify的地方是在libcoretls.dylib\SSLEncodeServerKeyExchange+0x253`。 定位到服务端的密钥交换过程中,EncodeServer时出现了异常。 回到dtruss的其他日志,能看到在调用Security动态链接库中的impExpPkcs12Import方法里,有大量的/Users/cfc4n/Library/Keychains/login.keychain-db的读写。意味着MOA进程在频繁大量的读写keychain。 除了Keychain的频繁读写,还看到了大量MOA主进程的访问 78158/0x152ecde: 18541 85 31 open("/Applications/MOA.app/Contents/Resources/MOA.app/Contents/MacOS/itsec-agent\0", 0x0, 0x1B6) = 26 0 libsystem_kernel.dylib`__open+0xa Security`Security::OSXVerifier::makeLegacyHash(Security::OSXCode*, unsigned char*)+0x101 Security`Security::OSXVerifier::OSXVerifier(Security::OSXCode*)+0x1b1 Security`Security::KeychainCore::TrustedApplication::TrustedApplication()+0x70 Security`Security::KeychainCore::Access::Access(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)+0x70 Security`P12Coder::safeContentsParse(cssm_data const&, SecNssCoder&)+0xe02 Security`impExpPkcs12Import+0x8a0 Security`SecKeychainItemImport+0xd30 Security`SecPKCS12Import+0x100 itsec-agent`0x000000010edd8efe+0x10b itsec-agent`0x000000010ed9f5b8+0x36 itsec-agent`0x000000010edce367+0x39 itsec-agent`0x000000010edce297+0x38 libdispatch.dylib`_dispatch_call_block_and_release+0xc libdispatch.dylib`_dispatch_client_callout+0x8 libdispatch.dylib`_dispatch_lane_serial_drain+0x25a libdispatch.dylib`_dispatch_lane_invoke+0x181 libdispatch.dylib`_dispatch_workloop_worker_thread+0x256 libsystem_pthread.dylib`_pthread_wqthread+0x1a5 libsystem_pthread.dylib`start_wqthread+0xd CodeSigning是macOS的对二进制程序进行数字签名的机制,以确保进程文件没有被篡改,安全保障机制详情见:About Code Signing ,可以确认的是,MOA进程在每次accept TCP连接时,都进行了主进程的签名校验。难道程序签名校验失败了?因为频繁的重新安装进程,而这个老进程又没退出,但文件内容却被覆盖了? 可以这么怀疑,但还是需要证据证明。 但问题定位到这里,好像卡死了,我不确定Security链接库是否还会写入其他日志,也没有时间去看这个链接库的源码。 根据之前做黑盒分析的经验,决定到系统日志里找找看,macOS的系统日志的进程是console.app。 关联系统日志 在控制台中,搜索MOA的进程名itsec-agent,找BUG重现时的时间点,能看到这些报错信息,而根据dtruss中捕获的Keychains的临时文件名,也能关联得到这些日志。 默认 12:05:49.086566 +0800 itsec-agent caught CssmError: -2147416017 CSSMERR_CSP_ACL_ENTRY_TAG_NOT_FOUND 默认 12:05:49.095362 +0800 itsec-agent CSSM Exception: -2147416032 CSSMERR_CSP_OPERATION_AUTH_DENIED 默认 12:05:53.326473 +0800 itsec-agent 0x6000008ffb80 commited /Users/cfc4n/Library/Keychains/login.keychain-db.sb-dbe9e6c6-5v8ECF to /Users/cfc4n/Library/Keychains/login.keychain-db 默认 12:05:53.331646 +0800 itsec-agent CSSM Exception: -2147413719 CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA 默认 12:06:01.089562 +0800 itsec-agent 0x6040008fb980 commited /Users/cfc4n/Library/Keychains/login.keychain-db.sb-dbe9e6c6-yiHh6n to /Users/cfc4n/Library/Keychains/login.keychain-db 默认 12:06:01.096920 +0800 itsec-agent CSSM Exception: -2147413719 CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA 默认 12:06:01.108208 +0800 itsec-agent 0x6040008fc080 commited /Users/cfc4n/Library/Keychains/login.keychain-db.sb-dbe9e6c6-yrTtXC to /Users/cfc4n/Library/Keychains/login.keychain-db 默认 12:06:01.113267 +0800 itsec-agent CSSM Exception: -2147413719 CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA 默认 12:06:01.146973 +0800 itsec-agent caught CssmError: -2147416017 CSSMERR_CSP_ACL_ENTRY_TAG_NOT_FOUND 默认 12:06:01.156853 +0800 itsec-agent CSSM Exception: -2147416032 CSSMERR_CSP_OPERATION_AUTH_DENIED 默认 12:06:01.160022 +0800 itsec-agent CSSM Exception: -2147416032 CSSMERR_CSP_OPERATION_AUTH_DENIED 这些报错信息,我也是第一次见,在网上稍微搜索相关知识了解,能看到这是macOS的Openssl的类库源码中的报错信息,比如CSSMERR_DL_INVALID_UNIQUE_INDEX_DATA、CSSMERR_CSP_OPERATION_AUTH_DENIED等。 在apple的开发者社区中,看到一个相似的案例: inexplicable CSSMERR_CSP_OPERATION_AUTH_DENIED,里面提到这个错误有很多情况产生 apple的CodeSign keychain证书链中的证书itemid丢失 等等,结合前面dtruss的排查结论,问题方向定位到self-sign自签CA证书上。 定位 其实,这里我的MacBook死机电脑重启了,BUG无法重现,但问题的大体方向几乎定位到了。在前面的结论下,以及搜索到的这些案例,我决定使用下面两种方式吃重现 删除Keychain的自签CA,模拟itemid丢失; 覆盖MOA主进程,让MacOS的CodeSigning机制检测失败; 对于方式一,测试结果如下 MOA继续运行 删除了KeyChain里的自签CA证书 重新访问这个TCP SOCKET 问题并没有复现,但出现了一个相近的错误Description: Certificate Unknown,之后也是服务端发送FIN包后关闭连接。 虽然问题没有复现,但我觉得真相已经很近了,配合CodeSigning的检测问题,需要模拟主进程文件被替换的场景,我又想到一种重现方式 清空删除所有MOA安装包 打开wireshark,抓loopback网卡的port 16161端口 安装老版MOA,按照业务流程走一遍,确保所有功能正常 安装新版MOA,chrome开隐私模式(确保不用上次的COOKIE),登陆SSO MOA弹出申请keychain权限的窗口,点“拒绝” 问题重现可以从wireshark的抓包窗口中确认,看到了Tls handshake时的Fatal Close Notify错误 嗯,问题重现,BUG根本原因找到了。但Keychain的安全机制我还不太清楚,时间关系,暂时放过吧,以后有机会再研究。 解题思路 回顾整个排查过程,我并不清楚OC代码是如何写的,都是黑盒分析,完全是看系统调用syscall来知道这个软件运行的行为,定位BUG问题所在的。 有很多知识是跨平台通用的,比如网络通讯原理、动态跟踪技术、动态调试技术、syscall系统调用、二进制文件执行机制等技术,跟所在的是Windows、Linux还是macOS关系不大。 系统调用syscall跟踪软件在Win上有 Process Monitor ,我在2016年黑盒分析英雄联盟启动器时使用过,之前也没做过Windows的程序研发,只是借助Linux的解题思路找对应得替代软件而已。比如下图: 同样,这次遇到macOS的BUG也是,只要找对应得软件就好,解决问题的思路是通用的。 Linux上有strace,macOS上有dtruss,对于更详细的动态跟踪软件,还有Dtrace、SystemTap等我也在学习中,以后如果有机会,我会继续给大家分享。 通用知识 对于这种BUG的解决,重要的是解题思路,遇到问题该如何处理,如何粗略定位问题所在,从哪里入手?靠猜吗?东一锤子西一榔头的靠运气吗? 就怕你整天TCP协议栈倒背如流,操作系统知识滚瓜烂熟,垃圾回收机制如数家珍,但每次一遇到问题,就一脸懵逼、束手无策,这就很不匹配。 大部分主动学习能力并不强,都是靠事件驱动,比如有BUG产生了,才不得不去学习陌生领域的知识,去解决问题,顺便积累经验。可怕的是有部分人BUG发生就发生了,重启能解决就算,从来不关心根本原因是什么。下次遇到同类型的问题时,依旧是束手无策。 套用道友codedump的一句话知行合一(出自王阳明),珍惜每一次BUG的机会,历练自己。在解决每个问题时,是对你整个知识体系的梳理,对逻辑思维的考验,对技术技能的检查,检验自己、磨练自己、强化自己。 解决方案 WSS的https server监听时,使用的tls证书,要走正规CA签发,避免自签self-sign的CA频繁读写keychain ,避免对keychain的root权限获取。(域名解析到127.0.0.1,拿到私钥的安全风险也很小) MOA升级时,先杀死老版本的所有进程,再进行安装、覆盖、启动。 itsec-agent进程 codesign签名。 本文重点 美团点评信息安全中心招聘,职位包括不限于 iOS/macOS技术专家 Web应用防火墙架构师 JAVA\golang\C++高级工程师 数据安全资深专家 数仓安全架构师 基础安全专家 风险数据挖掘专家 IoT安全专家 移动安全工程师、专家 操作系统安全工程师、专家 更多职位信息,见:同学,我看你骨骼精奇,是干安全/研发的好苗子:https://mp.weixin.qq.com/s/9Y9bAUt51gPvy2ItiFRBCA 。 关注本BLOG微信公众号榫卯江湖,回复JD、JOBS、招聘、工作获取更多职位信息。 相似的案例 inexplicable CSSMERR_CSP_OPERATION_AUTH_DENIED : Certificate-private key combo failure: https://forums.developer.apple.com/thread/99801 Internal error (errSSLInternal) on SSLHandshake:https://forums.developer.apple.com/thread/92193 参考资料 macOS Code Signing In Depth 跟踪 UNIX 应用程序的解决方案 openssl类库的SSL_alert_type_string函数手册 macOS平台Security类库源码的CSSMERRCSP*相关错误 https://docs.apigee.com/api-platform/troubleshoot/runtime/ssl-handshake-failures: Re: inexplicable CSSMERR_CSP_OPERATION_AUTH_DENIED: 扩展知识 http://oliveryang.net/2016/07/linux-perf-tools-tips/ System Integrity Protection – Adding another layer to Apple’s security model SystemTap Beginners Guide 2019-09-09 0 0
你不在意的HTTPS证书吊销机制 缘起 偶刷《长安十二时辰》,午睡时,梦到我穿越到了唐朝,在长安城中的靖安司,做了一天的靖安司司丞。 当徐宾遇害消失的时候我不在司内,当时的情形我不得而知。后来徐宾醒了,据他描述说“通传陆三”是暗桩,险些致徐宾于死地。而擅长大案牍术的高智商人才居然被一个普通通传的几句话骗至险境,实在丢了我的脸。 陆三是通传,熟知靖安司的号令传递系统望楼信号,他是暗桩的消息,传遍整个机构。这让张小敬和姚汝能认为望楼系统已无法完成消息保密传送的功能,其实他们根本不了解这望楼。 整个望楼系统由“传递系统+加密系统”组成,靖安司作为一个军事级别的机构,信息传递绝对是多重加密的。只看懂望楼图案,或者只有密码本都是破译不了密码的,对于通传陆三是暗桩的影响,也只需要更换密码本即可。这些可是我学了RSA非对称加密后设计的望楼系统,早就评估过这些风险了。 即使HTTPS通讯中,丢了密钥也... 嗯?如果HTTPS证书私钥丢了,会怎样?是不是也没法防范这个私钥被利用了? 想到这个问题,我突然从梦中惊醒,去温故一下证书吊销机制。 疑问 HTTPS的证书过期是谁来判断? 证书的合法性又是谁检查的呢? 什么时候触发? 影响性能吗? 如何吊销证书? HTTPS的请求是客户端(浏览器)发起的,他是如何知道证书被吊销的? 验证HTTPS证书的过程是什么样的? HTTPS 通讯过程 大家都清楚,HTTPS的握手是在TCP握手完成后,流程都熟的很,但还是要温故一下: 客户端发送Client Hello,附带支持的加密套件算法、版本、以及随机数Random1. 服务端选择合适的加密套件,返回Server Hello,附带一个随机数Random2. 服务端将自己的证书Certificate发送给客户端。 客户端将自己的证书Certificate发送给服务端(证书登录认证的情况)。 客户端校验证书Certificate Verify。 生成Random3,再用三个Random1,Random2,Random3计算PreMaster Key,并用公钥加密。 Client Cipher Spec,客户端通知服务端,编码改变后的消息,都通过对称加密来进行。 Encrypted HandShake Message 是一个finish消息,表示客户端握手完成,将使用协商好的密钥进行加密,发送给服务端。服务端收到后,对消息进行解密,如果能解密成功,则表明双方是同一个密钥。 对于证书检验这块,相信很多人都不太了解,甚至都不知道会检验什么内容,那么下面我们就来了解一下。 证书完整性验证 使用RAS公钥加密来验证证书上的签名是否合法,如果签名无效,则可认定证书被修改,直接报错。 证书有效性验证 CA在颁发证书时,都为每个证书设定了有效期。包括开始时间与结束时间。系统当前时间不在证书起止时间的话,都认为证书是无效的。 证书吊销状态检测 如果,证书在有效期之内需要丢了怎么办?需要吊销证书了,那么这里就多了一个证书吊销状态的检测。用户将需要吊销的证书通知到CA服务商,CA服务商通知浏览器该证书的撤销状态。 来看一个证书吊销后的浏览器提醒 Chrome返回了NET::ERR_CERT_REVOKED,并且拒绝继续访问,更不提供强制访问的接口,没了继续访问的手动点击链接。 验证发行者 HTTPS数字证书的使用分两个角色 证书发行方issuer,有签名密钥的私钥 证书申请方subject,使用证书公钥进行身份验证的用户 浏览器检查证书的发行者字段与证书路径中上级证书的Suject字段相同。 为了增加安全性,大多数PKI实现还验证发型方的密钥、签名跟当前证书的密钥相同。 但对于信任链来说,根证书自己签发的,也就是说它们的issuer和subject是一样的。 同时,这些CA根证书都是被操作系统、浏览器等直接打入系统的。比如 检查域名(IP)规范 中间CA提供了对域名证书的管理以及颁发颁发的颗粒度度控制。证书的生效范围会限于固定域名、域名范围(包括子域)或者固定IP。 比如下图是https://www.baidu.com的HTTPS证书DNS信息 上图所示,DNS范围包含了多个域名,同时二级以及二级以上域名都支持范围形式。以*通配义字符表示。但*.example.com的二级域名范围就不能包含a.b.example.com这个三级域名。同时,DNS范围也支持IP的,只是IP不支持范围形式,必须把所有IP列表都放入列表中。(PS:百度的证书管理可真乱。) 检查策略约束 法律策略相关检测(略)。 证书的吊销状态检测方式 上面提到了浏览器(客户端)在验证证书合法性时的验证范围,我们暂时只关注证书吊销信息的检测,下面我们仔细来了解一下两种检测机制的实现原理。 Certificate Revocation Lists (CRL) CA会定期更新发布撤销证书列表,Certificate Revocation Lists (以下简称CRL),RFC 5280: Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile。 CRL分布在公共可用的存储库中,浏览器可以在验证证书时获取并查阅CA的最新CRL。 该方法的一个缺陷是撤销的时间粒度限于CRL发布期。只有在计划更新所有当前发布的CRL之后,才会通知浏览器撤销。 各家签名CA厂商的策略不一样,有的是几小时,有的是几天,甚至几周。 2015年,美国几所大学的学生论文中,统计了当时的CA证书吊销情况,如下图: 这个统计可以看出,CA证书厂商的CRL数量不一,大部分是30-50个,而GoDaddy有300多个CRL的地址,同时有近30W个证书是吊销状态的,文件大小平均达到了1M。 证书的CRL信息 CRL信息是CA在颁发证书时,写入在X.509 v的扩展区域的,比如alipay.com的证书信息: 可以看到,其CRL信息为http://crl3.digicert.com/SecureSiteCAG2.crl 以及http://crl4.digicert.com/SecureSiteCAG2.crl CRL 检测流程 可以想象一下,在浏览器去校验证书合法性时,还要去下载一个1M的文件后,再去校验。校验通过后才去连想要访问的网站服务器,这相当浪费时间、效率。 同时,很多浏览器所处网络是有网络访问限制的,可能在一个局域网,比如我们村,或者物理距离非常远,存在严重的网络延迟问题。 Online Certificate Status Protocol (OCSP) 为了解决单个文件大,延迟性高等问题,迎来了新的解决方案Onlie Certificate StatusOCSP(以下简称OCSP)。 在RFC2560X.509 Internet Public Key Infrastructure Online Certificate Status Protocol - OCSP的描述中,浏览器从在线OCSP服务器(也称为OCSP Response Server)请求证书的撤销状态,OCSP Server予以响应。这种方法避免CRL更新延迟问题。同样的,X.509 v3证书的OCSP信息也是存储在拓展信息中,如alipay.com证书那张图的绿色框内部分。 OCSP 检测流程 浏览器在获得Web服务器的公钥证书后,开始验证公钥的合法性,这里会向该公钥的扩展信息中提供的OCSP Server地址发送OCSP Request,获得响应后,确认证书有效,再继续跟Web服务器通信。 OCSP的优点 相对于CRL方式,证书吊销后,CA Server可以立刻将吊销信息发送给浏览器,生效时间快。响应包内容短,不像CRL的响应包,都将近1M以上。 OCSP的缺点 浏览器的每次HTTPS请求创建,都需要连接CA OCSP Server进行验证,有的浏览器所在IP与CA OCSP Server的网络并不是通畅的,比如我们村。而且,OCSP的验证有网络IO,花费了很长的时间,严重影响了浏览器访问服务器的用户体验。 在浏览器发送服务器HTTPS证书序号到CA OCSP Server时,也将暴露了用户的隐私,将用户访问的网址透漏给了CA OCSP Server。 OCSP机制衍生出来的问题 设想一个场景,你是浏览器企业,研发的浏览器在检查证书吊销状态时,得不到OCSP server的响应,你会如何选择? 如果你选择拒绝该证书信息,并且拒绝后续的HTTPS通讯,那么这个方式称之为Hard-fail 如果你选择信任该证书,认为没有被吊销,那么这个方式称之为Soft-fail 如果是hard-fail模式,那浏览器对任何HTTPS服务器访问的先决条件都取决于OCSP Server,这将是一个致命的单点故障,对于具有资深架构设计经验的你来说,你会这么选择吗? 如果是soft-fail模式,取不到OCSP Server的响应就忽略了,那么,要这个机制还有啥意义呢?要你有何用? OCSP Stapling OCSP Stapling的方案是解决了CRL、OCSP的缺点,将通过OCSP Server获取证书吊销状况的过程交给Web 服务器来做,Web 服务器不光可以直接查询OCSP信息,规避网络访问限制、OCSP服务器离用户的物理距离较远等问题,还可以将查询响应缓存起来,给其他浏览器使用。由于OCSP的响应也是具备CA RSA私钥签名的,所以不用担心伪造问题。 解决了访问慢的问题 解决了用户隐私泄露的问题 通讯过程如上,但对于Web服务器在去OCSP Server查询证书状态时,也同样面临无法获取到OCSP Response的问题,在响应给浏览器时,浏览器也还是要面临hard-fail、soft-fail的选择问题,这很让浏览器头大。 OCSP Must-Staple 面对hard-fail、soft-fail的问题,各家浏览器厂商的态度都不一样。同时,不管浏览器如何选择,都不能满足广大域名用户的需求,那么不如把这个选择交给域名用户自己。 为此,OCSP Must-Staple应然而生了,浏览器必须检测OCSP响应。域名证书创建时,自定义设定启用这个选项,将这个信息打入X.509 v3的扩展中,浏览器读取后,强制进行OCSP检测,走hard-fail模式。这个规范被起草在 X.509v3 Extension: OCSP Stapling Required draft-hallambaker-muststaple-00 ,不过,暂未被采纳为RFC标准。 CA厂商支持 目前支持该扩展的证书的CA厂商有Let's Encrypt。 如果使用的是openssl 1.1.0 以前的版本,可以使用11.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05 来指定。RFC 比如生成csr的时候,在openssl.cnf中增加: [v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names 1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05 如果是使用openssl 1.1.0或更高的版本,可以这样设置: [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment subjectAltName = @alt_names tlsfeature = status_request 各平台上浏览器对证书吊销的支持情况 Mac Safari 在Mac OS X 10.7 (Lion)以后,Safari/macOS默认情况下,不检测CRLs、OCSP,走自己的key chain体系。(资料比较少,apple官方也搜不到几条) Windows Internet Explorer Windows Vista系统开始,Internet Explorer 7浏览器内置了CryptoAPI,来支持OCSP方式检测证书吊销情况。检测范围包括issuer发行商的证书、subject服务器的证书。 为什么IE访问HTTPS的网站时,会比别的浏览器慢?你应该已经知道答案了。 Mozilla Firefox 在2010年时,Mozilla Firefox的所有版本都支持OCSP方式检测。在Firefox 3里把OCSP检测机制设定为默认机制。 在以后的版本更新中,Firefox针对中级证书跟域名证书做了不同的策略 中级证书的吊销状态验证 在2015年,Firefox 37开始,针对中级证书的检测,Mozilla也启用了自研的证书吊销状况检查机制OneCRL来代替OCSP机制,目的还是想解决CRL、OCSP机制的缺点。 而中级证书是不能采用OCSP stapling方式,不允许被缓存的。所以... 还有, RFC 6961 defines a mechanism for stapling OCSP responses for CA certificates. Since FIrefox does not rely on OCSP to validate intermediate certificates, we have no plans to implement support for this. Firefox 官方短期内并无支持Multi-staple的打算。 域名证书的吊销状态验证 在Firefox的官方WIKI上,提到针对域名证书的吊销验证分为如下5个方案: Short-Lived Certificates ,默认情况下,针对有效期少于10天的证书,直接跳过验证,认为不安全。可以在security.pki.cert_short_lifetime_in_days参数里配置。 OCSP Stapling,跟RFC规范一样。如果security.OCSP.enabled的值为0,则忽略OCSP response。 OCSP Must-staple,跟RFC规范一样。可以通过设置security.ssl.enable_ocsp_must_staple或security.ssl.enable_ocsp_stapling 参数来禁用。 OCSP,跟RFC规范一样。如果OCSP的响应在2秒(EV证书是10秒)内没返回,则直接忽略。 CRLite chrome 2012年,Chrome禁用了CRLs、OCSP检测: Google Chrome Will No Longer Check for Revoked SSL Certificates Online ,使用了自行设计的证书校验机制 CRLSets 那么,Chrome这么选择的理由是什么呢? 显然,通过上面CRL跟OCSP两种验证方式的比较来看,前者时效性不行,后者影响性能。那么,google Chrome就决定自建证书吊销状态验证系统。 Chrome的安全工程师Adam Langley在他的BLOG上写了一篇文章:《Revocation checking and Chrome's CRL》, 对于Chrome的HTTPS证书验证这块,Adam Langley可是非常有看法的,非常反对使用CRL、OCSP的方式来校验证书吊销状态,连续写了好几篇关于证书吊销状态检测的文章,同时,也在chromium的开发者主页上的安全板块有提到:What's the story with certificate revocation? Chrome's primary mechanism for checking the revocation status of HTTPS certificates is CRLsets. Chrome also supports Online Certificate Status Protocol (OCSP). However, the effectiveness of OCSP is is essentially 0 unless the client fails hard (refuses to connect) if it cannot get a live, valid OCSP response. No browser has OCSP set to hard-fail by default, for good reasons explained by Adam Langley (see [Revocation still doesn't work](No, don't enable revocation checking) and https://www.imperialviolet.org/2014/04/19/revchecking.html). Stapled OCSP with the Must Staple option (hard-fail if a valid OCSP response is not stapled to the certificate) is a much better solution to the revocation problem than non-stapled OCSP. CAs and browsers are working toward that solution (see the Internet-Draft: http://tools.ietf.org/html/draft-hallambaker-tlssecuritypolicy-03). Additionally, non-stapled OCSP poses a privacy problem: in order to check the status of a certificate, the client must query an OCSP responder for the status of the certificate, thus exposing a user's HTTPS browsing history to the responder (a third party). That said, you can use enterprise policies to enable soft-fail OCSP (http://www.chromium.org/administrators/policy-list-3#EnableOnlineRevocationChecks) and hard-fail OCSP for local trust anchors (http://www.chromium.org/administrators/policy-list-3#RequireOnlineRevocationChecksForLocalAnchors). Chrome performs online checking for Extended Validation certificates if it does not already have a non-expired CRLSet entry covering the domain. If Chrome does not get a response, it simply downgrades the security indicator to Domain Validated. See also this bug for more discussion of the user-facing UX: https://crbug.com/361820. 但这也不是完美解决了这个问题,来自An Evaluation of the Effectiveness of Chrome's CRLSets This means that Chrome's CRLSet, which currently lists the serial numbers of 24,206 revoked certificates, reflects only 1.08% of the revoked certificates collected by Websense in one hour. 这篇文章中提到CRLSet的最大问题是包含的证书吊销数据太少,个别情况下占了主流CRL证书吊销信息的2%不到。而且,CRLSets的更新也不是那么及时,chrome为了用户体验,为了性能,为了用户隐私,牺牲了一点点安全性,也是可以理解的。但好像对不起最安全浏览器的称号了。[手动狗头]😂 汇总 如上图,在2015年,Northeastern University、University of Maryland、Duke University等几所大学的研究人员分析了市场上流行的五个浏览器(多个平台、多个版本),把各个浏览器在不同级别证书下的支持情况绘制了表格。(论文在参考文献中给出) 从图中可以看出,我们印象中最安全的浏览器Chrome表现却让你大跌眼镜,在HTTPS的安全性这块,表现最差。上面我们也谈到,chrome为了访问速度隐私等用户体验考虑,忽略了CRL、OCSP等证书吊销状态检测机制,牺牲了TLS这块的安全性支持。 而IE浏览器却出乎我们意料,在HTTPS的安全性这块,支持的非常好,可以说是这几个浏览器中最安全的了,可是...可是他网页打开速度慢啊...😂 截至至今天(2019年7月16日),已经4年过去了,很多浏览器、WebServer的支持情况也有很大的变化,并不能反映出当下互联网的现状,这些数据也作为参考吧。 附:WebServer的支持情况 The Apache web server has supported OCSP stapling since v2.3.3 (ref). The nginx web server has supported OCSP stapling since v1.3.7 (ref). 总结 证书泄露的危害 那么,证书丢了,会对网站安全产生很大影响吗? 回头看下使用该证书的条件 具备证书 具备域名 浏览器访问该服务器 如果几个都具备,那么你就是该网站的官方了。 在安全界,有个攻击手段,叫Man-in-the-middle attack中间人攻击,如果证书被黑客拿到,搭建一个具备相同域名的网站,通过DNS污染的方式使得用户浏览器方位该域名,那么可以成为一个反向代理,把用户的请求解析后,伪造程客户端来跟真实的Web服务器通讯,从而获取双方通信的明文信息,达到攻击的目的。 那这种情况怎么防范?中间人攻击的防御方式是开启客户端对证书的认证,但你的证书私钥又丢了,那能咋办? 通过本文前面章节的了解到,这种情况,也只能去 主动到CA厂商那申请证书吊销了,不管有几个浏览器支持,也得申请,毕竟,这损失能减少一点是一点。 证书泄露了怎么办? 证书泄露了怎么办?从浏览器的支持情况来看,好像及时申请吊销了证书,也不能对丢失的证书起到太大的防范作用,很多浏览器并不是很支持的嘛。 不过,多少也能避免一些损失,毕竟IE浏览器对这块的支持力度还是很大的嘛。 注 本文的参考文献,大部分都是5-6年前的资料,这么多年过去了,互联网技术产品日新月异,里面很多结论早已不符合现状,尤其是浏览器当今对证书吊销状态检测的支持情况。部分内容,仅作为参考,便于读者去了解技术变迁的背景知识。 本文会持续修正文中错别字、排版、错误数据等,建议回到原文阅读。 立个flag 过段时间,把整个X.509 v3的知识点都整理一下,做个内部分享? PEM、CRT、PFX、PKCS#7、PKCS#12、pfx、DER、JKS、KDB、CER、KEY、CSR、CRT、CRL都是什么? ASN.1 又是什么? 立起?🐶🐶🐶 招聘 macOS技术专家 Java高级工程师 详情见https://www.cnxct.com/jobs/ 参考文献 RFC3280 Internet X.509 Public Key Infrastructure Certificate High-reliability OCSP stapling and why it matters Security Certificate Revocation AwarenessThe case for “OCSP Must-Staple” Security Certificate Revocation AwarenessSpecific Implementations Security Sidebar: My Browser Has No Idea Your Certificate Was Just Revoked SSL certificate revocation and how it is broken in practice Revoking Intermediate Certificates: Introducing OneCRL Revocation doesn't work (18 Mar 2011) Revocation checking and Chrome's CRL (05 Feb 2012) No, don't enable revocation checking (19 Apr 2014) Revocation still doesn't work (29 Apr 2014) An End-to-End Measurement of Certificate Revocation in the Web’s PKI 结束 嗯?话说《长安十二时辰》中望楼消息传送机制的加固呢? 嗨,梦都醒了,谁还记得这事啊。 2019-07-16 +1 5 0
保障IDC安全:分布式HIDS集群架构设计 背景 近年来,互联网上安全事件频发,企业信息安全越来越受到重视,而IDC服务器安全又是纵深防御体系中的重要一环。保障IDC安全,常用的是基于主机型入侵检测系统Host-based Intrusion Detection System,即HIDS。在HIDS面对几十万台甚至上百万台规模的IDC环境时,系统架构该如何设计呢?复杂的服务器环境,网络环境,巨大的数据量给我们带来了哪些技术挑战呢? 需求描述 对于HIDS产品,我们安全部门的产品经理提出了以下需求: 满足50W-100W服务器量级的IDC规模。 部署在高并发服务器生产环境,要求Agent低性能低损耗。 广泛的部署兼容性。 偏向应用层和用户态入侵检测(可以和内核态检测部分解耦)。 针对利用主机Agent排查漏洞的最急需场景提供基本的能力,可以实现海量环境下快速查找系统漏洞。 Agent跟Server的配置下发通道安全。 配置信息读取写入需要鉴权。 配置变更历史记录。 Agent插件具备自更新功能。 分析需求 首先,服务器业务进程优先级高,HIDS Agent进程自己可以终止,但不能影响宿主机的主要业务,这是第一要点,那么业务需要具备熔断功能,并具备自我恢复能力。 其次,进程保活、维持心跳、实时获取新指令能力,百万台Agent的全量控制时间一定要短。举个极端的例子,当Agent出现紧急情况,需要全量停止时,那么全量停止的命令下发,需要在1-2分钟内完成,甚至30秒、20秒内完成。这些将会是很大的技术挑战。 还有对配置动态更新,日志级别控制,细分精确控制到每个Agent上的每个HIDS子进程,能自由地控制每个进程的启停,每个Agent的参数,也能精确的感知每台Agent的上线、下线情况。 同时,Agent本身是安全Agent,安全的因素也要考虑进去,包括通信通道的安全性,配置管理的安全性等等。 最后,服务端也要有一致性保障、可用性保障,对于大量Agent的管理,必须能实现任务分摊,并行处理任务,且保证数据的一致性。考虑到公司规模不断地扩大,业务不断地增多,特别是美团和大众点评合并后,面对的各种操作系统问题,产品还要具备良好的兼容性、可维护性等。 总结下来,产品架构要符合以下特性: 集群高可用。 分布式,去中心化。 配置一致性,配置多版本可追溯。 分治与汇总。 兼容部署各种Linux 服务器,只维护一个版本。 节省资源,占用较少的CPU、内存。 精确的熔断限流。 服务器数量规模达到百万级的集群负载能力。 技术难点 在列出产品需要实现的功能点、技术点后,再来分析下遇到的技术挑战,包括不限于以下几点: 资源限制,较小的CPU、内存。 五十万甚至一百万台服务器的Agent处理控制问题。 量级大了后,集群控制带来的控制效率,响应延迟,数据一致性问题。 量级大了后,数据传输对整个服务器内网带来的流量冲击问题。 量级大了后,运行环境更复杂,Agent异常表现的感知问题。 量级大了后,业务日志、程序运行日志的传输、存储问题,被监控业务访问量突增带来监控数据联动突增,对内网带宽,存储集群的爆发压力问题。 我们可以看到,技术难点几乎都是服务器到达一定量级带来的,对于大量的服务,集群分布式是业界常见的解决方案。 架构设计与技术选型 对于管理Agent的服务端来说,要实现高可用、容灾设计,那么一定要做多机房部署,就一定会遇到数据一致性问题。那么数据的存储,就要考虑分布式存储组件。 分布式数据存储中,存在一个定理叫CAP定理: CAP的解释 关于CAP定理,分为以下三点: 一致性(Consistency):分布式数据库的数据保持一致。 可用性(Availability):任何一个节点宕机,其他节点可以继续对外提供服务。 分区容错性(网络分区)Partition Tolerance:一个数据库所在的机器坏了,如硬盘坏了,数据丢失了,可以添加一台机器,然后从其他正常的机器把备份的数据同步过来。 根据定理,分布式系统只能满足三项中的两项而不可能满足全部三项。理解CAP定理的最简单方式是想象两个节点分处分区两侧。允许至少一个节点更新状态会导致数据不一致,即丧失了Consistency。如果为了保证数据一致性,将分区一侧的节点设置为不可用,那么又丧失了Availability。除非两个节点可以互相通信,才能既保证Consistency又保证Availability,这又会导致丧失Partition Tolerance。 参见:CAP Theorem。 CAP的选择 为了容灾上设计,集群节点的部署,会选择的异地多机房,所以 「Partition tolerance」是不可能避免的。那么可选的是 AP 与 CP。 在HIDS集群的场景里,各个Agent对集群持续可用性没有非常强的要求,在短暂时间内,是可以出现异常,出现无法通讯的情况。但最终状态必须要一致,不能存在集群下发关停指令,而出现个别Agent不听从集群控制的情况出现。所以,我们需要一个满足 CP 的产品。 满足CP的产品选择 在开源社区中,比较出名的几款满足CP的产品,比如etcd、ZooKeeper、Consul等。我们需要根据几款产品的特点,根据我们需求来选择符合我们需求的产品。 插一句,网上很多人说Consul是AP产品,这是个错误的描述。既然Consul支持分布式部署,那么一定会出现「网络分区」的问题, 那么一定要支持「Partition tolerance」。另外,在consul的官网上自己也提到了这点 Consul uses a CP architecture, favoring consistency over availability. Consul is opinionated in its usage while Serf is a more flexible and general purpose tool. In CAP terms, Consul uses a CP architecture, favoring consistency over availability. Serf is an AP system and sacrifices consistency for availability. This means Consul cannot operate if the central servers cannot form a quorum while Serf will continue to function under almost all circumstances. etcd、ZooKeeper、Consul对比 借用etcd官网上etcd与ZooKeeper和Consul的比较图。 在我们HIDS Agent的需求中,除了基本的服务发现 、配置同步 、配置多版本控制 、变更通知等基本需求外,我们还有基于产品安全性上的考虑,比如传输通道加密、用户权限控制、角色管理、基于Key的权限设定等,这点 etcd比较符合我们要求。很多大型公司都在使用,比如Kubernetes、AWS、OpenStack、Azure、Google Cloud、Huawei Cloud等,并且etcd的社区支持非常好。基于这几点因素,我们选择etcd作为HIDS的分布式集群管理。 选择etcd 对于etcd在项目中的应用,我们分别使用不同的API接口实现对应的业务需求,按照业务划分如下: Watch机制来实现配置变更下发,任务下发的实时获取机制。 脑裂问题在etcd中不存在,etcd集群的选举,只有投票达到 N/2+1 以上,才会选做Leader,来保证数据一致性。另外一个网络分区的Member节点将无主。 语言亲和性,也是Golang开发的,Client SDK库稳定可用。 Key存储的数据结构支持范围性的Key操作。 User、Role权限设定不同读写权限,来控制Key操作,避免其他客户端修改其他Key的信息。 TLS来保证通道信息传递安全。 Txn分布式事务API配合Compare API来确定主机上线的Key唯一性。 Lease租约机制,过期Key释放,更好的感知主机下线信息。 etcd底层Key的存储为BTree结构,查找时间复杂度为O(㏒n),百万级甚至千万级Key的查找耗时区别不大。 etcd Key的设计 前缀按角色设定: Server配置下发使用 /hids/server/config/{hostname}/master。 Agent注册上线使用 /hids/agent/master/{hostname}。 Plugin配置获取使用 /hids/agent/config/{hostname}/plugin/ID/conf_name。 Server Watch /hids/agent/master/前缀,实现Agent主机上线的瞬间感知。Agent Watch /hids/server/config/{hostname}/来获取配置变更,任务下发。Agent注册的Key带有Lease Id,并启用keepalive,下线后瞬间感知。 (异常下线,会有1/3的keepalive时间延迟) 关于Key的权限,根据不同前缀,设定不同Role权限。赋值给不同的User,来实现对Key的权限控制。 etcd集群管理 在etcd节点容灾考虑,考虑DNS故障时,节点会选择部署在多个城市,多个机房,以我们服务器机房选择来看,在大部分机房都有一个节点,综合承载需求,我们选择了N台服务器部署在个别重要机房,来满足负载、容灾需求。但对于etcd这种分布式一致性强的组件来说,每个写操作都需要N/2+1的节点确认变更,才会将写请求写入数据库中,再同步到各个节点,那么意味着节点越多,需要确认的网络请求越多,耗时越多,反而会影响集群节点性能。这点,我们后续将提升单个服务器性能,以及牺牲部分容灾性来提升集群处理速度。 客户端填写的IP列表,包含域名、IP。IP用来规避DNS故障,域名用来做Member节点更新。最好不要使用Discover方案,避免对内网DNS服务器产生较大压力。 同时,在配置etcd节点的地址时,也要考虑到内网DNS故障的场景,地址填写会混合IP、域名两种形式。IP的地址,便于规避内网DNS故障。 域名形式,便于做个别节点更替或扩容。 我们在设计产品架构时,为了安全性,开启了TLS证书认证,当节点变更时,证书的生成也同样要考虑到上面两种方案的影响,证书里需要包含固定IP,以及DNS域名范围的两种格式。 etcd Cluster节点扩容 节点扩容,官方手册上也有完整的方案,etcd的Client里实现了健康检测与故障迁移,能自动的迁移到节点IP列表中的其他可用IP。也能定时更新etcd Node List,对于etcd Cluster的集群节点变更来说,不存在问题。需要我们注意的是,TLS证书的兼容。 分布式HIDS集群架构图 集群核心组件高可用,所有Agent、Server都依赖集群,都可以无缝扩展,且不影响整个集群的稳定性。即使Server全部宕机,也不影响所有Agent的继续工作。 在以后Server版本升级时,Agent不会中断,也不会带来雪崩式的影响。etcd集群可以做到单节点升级,一直到整个集群升级,各个组件全都解耦。 编程语言选择 考虑到公司服务器量大,业务复杂,需求环境多变,操作系统可能包括各种Linux以及Windows等。为了保证系统的兼容性,我们选择了Golang作为开发语言,它具备以下特点:可以静态编译,直接通过syscall来运行,不依赖libc,兼容性高,可以在所有Linux上执行,部署便捷。 静态编译语言,能将简单的错误在编译前就发现。 具备良好的GC机制,占用系统资源少,开发成本低。 容器化的很多产品都是Golang编写,比如Kubernetes、Docker等。 etcd项目也是Golang编写,类库、测试用例可以直接用,SDK支持快速。 良好的CSP并发模型支持,高效的协程调度机制。产品架构大方向 HIDS产品研发完成后,部署的服务都运行着各种业务的服务器,业务的重要性排在第一,我们产品的功能排在后面。为此,确定了几个产品的大方向: 高可用,数据一致,可横向扩展。 容灾性好,能应对机房级的网络故障。 兼容性好,只维护一个版本的Agent。 依赖低,不依赖任何动态链接库。 侵入性低,不做Hook,不做系统类库更改。 熔断降级可靠,宁可自己挂掉,也不影响业务 。产品实现 篇幅限制,仅讨论框架设计、熔断限流、监控告警、自我恢复以及产品实现上的主进程与进程监控。 框架设计 hids-framework 如上图,在框架的设计上,封装常用类库,抽象化定义Interface,剥离etcd Client,全局化Logger,抽象化App的启动、退出方法。使得各模块(以下简称App)只需要实现自己的业务即可,可以方便快捷的进行逻辑编写,无需关心底层实现、配置来源、重试次数、熔断方案等等。 沙箱隔离 考虑到子进程不能无限的增长下去,那么必然有一个进程包含多个模块的功能,各App之间既能使用公用底层组件(Logger、etcd Client等),又能让彼此之间互不影响,这里进行了沙箱化处理,各个属性对象仅在各App的sandbox里生效。同样能实现了App进程的性能熔断,停止所有的业务逻辑功能,但又能具有基本的自我恢复功能。 IConfig 对各App的配置抽象化处理,实现IConfig的共有方法接口,用于对配置的函数调用,比如Check的检测方法,检测配置合法性,检测配置的最大值、最小值范围,规避使用人员配置不在合理范围内的情况,从而避免带来的风险。 框架底层用Reflect来处理JSON配置,解析读取填写的配置项,跟Config对象对比,填充到对应Struct的属性上,允许JSON配置里只填写变化的配置,没填写的配置项,则使用Config对应Struct的默认配置。便于灵活处理配置信息。 Timer、Clock调度 在业务数据产生时,很多地方需要记录时间,时间的获取也会产生很多系统调用。尤其是在每秒钟产生成千上万个事件,这些事件都需要调用获取时间接口,进行clock_gettime等系统调用,会大大增加系统CPU负载。 而很多事件产生时间的准确性要求不高,精确到秒,或者几百个毫秒即可,那么框架里实现了一个颗粒度符合需求的(比如100ms、200ms、或者1s等)间隔时间更新的时钟,即满足事件对时间的需求,又减少了系统调用。 同样,在有些Ticker场景中,Ticker的间隔颗粒要求不高时,也可以合并成一个Ticker,减少对CPU时钟的调用。 Catcher 在多协程场景下,会用到很多协程来处理程序,对于个别协程的panic错误,上层线程要有一个良好的捕获机制,能将协程错误抛出去,并能恢复运行,不要让进程崩溃退出,提高程序的稳定性。 抽象接口 框架底层抽象化封装Sandbox的Init、Run、Shutdown接口,规范各App的对外接口,让App的初始化、运行、停止等操作都标准化。App的模块业务逻辑,不需要关注PID文件管理,不关注与集群通讯,不关心与父进程通讯等通用操作,只需要实现自己的业务逻辑即可。App与框架的统一控制,采用Context包以及Sync.Cond等条件锁作为同步控制条件,来同步App与框架的生命周期,同步多协程之间同步,并实现App的安全退出,保证数据不丢失。 限流 网络IO 限制数据上报速度。 队列存储数据任务列表。 大于队列长度数据丢弃。 丢弃数据总数计数。 计数信息作为心跳状态数据上报到日志中心,用于数据对账。 磁盘IO 程序运行日志,对日志级别划分,参考 /usr/include/sys/syslog.h: LOG_EMERG LOG_ALERT LOG_CRIT LOG_ERR LOG_WARNING LOG_NOTICE LOG_INFO LOG_DEBUG 在代码编写时,根据需求选用级别。级别越低日志量越大,重要程度越低,越不需要发送至日志中心,写入本地磁盘。那么在异常情况排查时,方便参考。 日志文件大小控制,分2个文件,每个文件不超过固定大小,比如20M、50M等。并且,对两个文件进行来回写,避免日志写满磁盘的情况。 IRetry 为了加强Agent的鲁棒性,不能因为某些RPC动作失败后导致整体功能不可用,一般会有重试功能。Agent跟etcd Cluster也是TCP长连接(HTTP2),当节点重启更换或网络卡顿等异常时,Agent会重连,那么重连的频率控制,不能是死循环般的重试。假设服务器内网交换机因内网流量较大产生抖动,触发了Agent重连机制,不断的重连又加重了交换机的负担,造成雪崩效应,这种设计必须要避免。 在每次重试后,需要做一定的回退机制,常见的指数级回退,比如如下设计,在规避雪崩场景下,又能保障Agent的鲁棒性,设定最大重试间隔,也避免了Agent失控的问题。 事件拆分 百万台IDC规模的Agent部署,在任务执行、集群通讯或对宿主机产生资源影响时,务必要错峰进行,根据每台主机的唯一特征取模,拆分执行,避免造成雪崩效应。 监控告警 古时候,行军打仗时,提倡「兵马未动,粮草先行」,无疑是冷兵器时代决定胜负走向的重要因素。做产品也是,尤其是大型产品,要对自己运行状况有详细的掌控,做好监控告警,才能确保产品的成功。 对于etcd集群的监控,组件本身提供了Metrics数据输出接口,官方推荐了Prometheus来采集数据,使用Grafana来做聚合计算、图标绘制,我们做了Alert的接口开发,对接了公司的告警系统,实现IM、短信、电话告警。 Agent数量感知,依赖Watch数字,实时准确感知。 如下图,来自产品刚开始灰度时的某一时刻截图,Active Streams(即etcd Watch的Key数量)即为对应Agent数量,每次灰度的产品数量。因为该操作,是Agent直接与集群通讯,并且每个Agent只Watch一个Key。且集群数据具备唯一性、一致性,远比心跳日志的处理要准确的多。 etcd-Grafana-Watcher-Monitor etcd集群Members之间健康状况监控 etcd-Grafana-GC-Heap-Objects 用于监控管理etcd集群的状况,包括Member节点之间数据同步,Leader选举次数,投票发起次数,各节点的内存申请状况,GC情况等,对集群的健康状况做全面掌控。 程序运行状态监控告警 agent-mem-es agent-cpu-es 全量监控Agent的资源占用情况,统计每天使用最大CPU\内存的主机Agent,确定问题的影响范围,及时做策略调整,避免影响到业务服务的运行。并在后续版本上逐步做调整优化。 百万台服务器,日志告警量非常大,这个级别的告警信息的筛选、聚合是必不可少的。减少无用告警,让研发运维人员疲于奔命,也避免无用告警导致研发人员放松了警惕,前期忽略个例告警,先解决主要矛盾。 告警信息分级,告警信息细分ID。 根据告警级别过滤,根据告警ID聚合告警,来发现同类型错误。 根据告警信息的所在机房、项目组、产品线等维度来聚合告警,来发现同类型错误。 数据采集告警 单机数据数据大小、总量的历史数据对比告警。 按机房、项目组、产品线等维度的大小、总量等维度的历史数据对比告警。 数据采集大小、总量的对账功能,判断经过一系列处理流程的日志是否丢失的监控告警。熔断针对单机Agent使用资源大小的阈值熔断,CPU使用率,连续N次触发大于等于5%,则进行保护性熔断,退出所有业务逻辑,以保护主机的业务程序优先。 Master进程进入空闲状态,等待第二次时间Ticker到来,决定是否恢复运行。 各个App基于业务层面的监控熔断策略。灰度管理 在前面的配置管理中的etcd Key设计里,已经细分到每个主机(即每个Agent)一个Key。那么,服务端的管理,只要区分该主机所属机房、环境、群组、产品线即可,那么,我们的管理Agent的颗粒度可以精确到每个主机,也就是支持任意纬度的灰度发布管理与命令下发。 数据上报通道 组件名为 log_agent ,是公司内部统一日志上报组件,会部署在每一台VM、Docker上。主机上所有业务均可将日志发送至该组件。 log_agent会将日志上报到Kafka集群中,经过处理后,落入Hive集群中。(细节不在本篇讨论范围) 主进程 主进程实现跟etcd集群通信,管理整个Agent的配置下发与命令下发;管理各个子模块的启动与停止;管理各个子模块的CPU、内存占用情况,对资源超标进行进行熔断处理,让出资源,保证业务进程的运行。 插件化管理其他模块,多进程模式,便于提高产品灵活性,可更简便的更新启动子模块,不会因为个别模块插件的功能、BUG导致整个Agent崩溃。 进程监控 方案选择 我们在研发这产品时,做了很多关于linux进程创建监控的调研,不限于安全产品,大约有下面三种技术方案:方案Docker兼容性开发难度数据准确性系统侵入性cn_proc不支持Docker一般存在内核拿到的PID,在/proc/下丢失的情况无Audit不支持Docker一般同cn_proc弱,但依赖AuditdHook定制高精确强 对于公司的所有服务器来说,几十万台都是已经在运行的服务器,新上的任何产品,都尽量避免对服务器有影响,更何况是所有服务器都要部署的Agent。 意味着我们在选择系统侵入性来说,优先选择最小侵入性的方案。 对于Netlink的方案原理,可以参考这张图(来自:kernel-proc-connector-and-containers) process-connector。 系统侵入性比较 cn_proc跟Autid在「系统侵入性」和「数据准确性」来说,cn_proc方案更好,而且使用CPU、内存等资源情况,更可控。 Hook的方案,对系统侵入性太高了,尤其是这种最底层做HOOK syscall的做法,万一测试不充分,在特定环境下,有一定的概率会出现Bug,而在百万IDC的规模下,这将成为大面积事件,可能会造成重大事故。 兼容性上比较 cn_proc不兼容Docker,这个可以在宿主机上部署来解决。 Hook的方案,需要针对每种Linux的发行版做定制,维护成本较高,且不符合长远目标(收购外部公司时遇到各式各样操作系统问题) 数据准确性比较 在大量PID创建的场景,比如Docker的宿主机上,内核返回PID时,因为PID返回非常多非常快,很多进程启动后,立刻消失了,另外一个线程都还没去读取/proc/,进程都丢失了,场景常出现在Bash执行某些命令。 最终,我们选择Linux Kernel Netlink接口的cn_proc指令作为我们进程监控方案,借助对Bash命令的收集,作为该方案的补充。当然,仍然存在丢数据的情况,但我们为了系统稳定性,产品侵入性低等业务需求,牺牲了一些安全性上的保障。 对于Docker的场景,采用宿主机运行,捕获数据,关联到Docker容器,上报到日志中心的做法来实现。 遇到的问题 内核Netlink发送数据卡住 内核返回数据太快,用户态ParseNetlinkMessage解析读取太慢,导致用户态网络Buff占满,内核不再发送数据给用户态,进程空闲。对于这个问题,我们在用户态做了队列控制,确保解析时间的问题不会影响到内核发送数据。对于队列的长度,我们做了定值限制,生产速度大于消费速度的话,可以丢弃一些数据,来保证业务正常运行,并且来控制进程的内存增长问题。 疑似“内存泄露”问题 在一台Docker的宿主机上,运行了50个Docker实例,每个Docker都运行了复杂的业务场景,频繁的创建进程,在最初的产品实现上,启动时大约10M内存占用,一天后达到200M的情况。 经过我们Debug分析发现,在ParseNetlinkMessage处理内核发出的消息时,PID频繁创建带来内存频繁申请,对象频繁实例化,占用大量内存。同时,在Golang GC时,扫描、清理动作带来大量CPU消耗。在代码中,发现对于linux/connector.h里的struct cb_msg、linux/cn_proc.h里的struct proc_event结构体频繁创建,带来内存申请等问题,以及Golang的GC特性,内存申请后,不会在GC时立刻归还操作系统,而是在后台任务里,逐渐的归还到操作系统,见:debug.FreeOSMemory FreeOSMemory forces a garbage collection followed by an attempt to return as much memory to the operating system as possible. (Even if this is not called, the runtime gradually returns memory to the operating system in a background task.) 但在这个业务场景里,大量频繁的创建PID,频繁的申请内存,创建对象,那么申请速度远远大于释放速度,自然内存就一直堆积。 从文档中可以看出,FreeOSMemory的方法可以将内存归还给操作系统,但我们并没有采用这种方案,因为它治标不治本,没法解决内存频繁申请频繁创建的问题,也不能降低CPU使用率。 为了解决这个问题,我们采用了sync.Pool的内置对象池方式,来复用回收对象,避免对象频繁创建,减少内存占用情况,在针对几个频繁创建的对象做对象池化后,同样的测试环境,内存稳定控制在15M左右。 大量对象的复用,也减少了对象的数量,同样的,在Golang GC运行时,也减少了对象的扫描数量、回收数量,降低了CPU使用率。 项目进展 在产品的研发过程中,也遇到了一些问题,比如:etcd Client Lease Keepalive的Bug。 Agent进程资源限制的Cgroup触发几次内核Bug。 Docker宿主机上瞬时大量进程创建的性能问题。 网络监控模块在处理Nginx反向代理时,动辄几十万TCP链接的网络数据获取压力。 个别进程打开了10W以上的fd。 方法一定比困难多,但方法不是拍脑袋想出来的,一定要深入探索问题的根本原因,找到系统性的修复方法,具备高可用、高性能、监控告警、熔断限流等功能后,对于出现的问题,能够提前发现,将故障影响最小化,提前做处理。在应对产品运营过程中遇到的各种问题时,逢山开路,遇水搭桥,都可以从容的应对。 经过我们一年的努力,已经部署了除了个别特殊业务线之外的其他所有服务器,数量达几十万台,产品稳定运行。在数据完整性、准确性上,还有待提高,在精细化运营上,需要多做改进。 本篇更多的是研发角度上软件架构上的设计,关于安全事件分析、数据建模、运营策略等方面的经验和技巧,未来将会由其他同学进行分享,敬请期待。 总结 我们在研发这款产品过程中,也看到了网上开源了几款同类产品,也了解了他们的设计思路,发现很多产品都是把主要方向放在了单个模块的实现上,而忽略了产品架构上的重要性。 比如,有的产品使用了syscall hook这种侵入性高的方案来保障数据完整性,使得对系统侵入性非常高,Hook代码的稳定性,也严重影响了操作系统内核的稳定。同时,Hook代码也缺少了监控熔断的措施,在几十万服务器规模的场景下部署,潜在的风险可能让安全部门无法接受,甚至是致命的。 这种设计,可能在服务器量级小时,对于出现的问题多花点时间也能逐个进行维护,但应对几十万甚至上百万台服务器时,对维护成本、稳定性、监控熔断等都是很大的技术挑战。同时,在研发上,也很难实现产品的快速迭代,而这种方式带来的影响,几乎都会导致内核宕机之类致命问题。这种事故,使用服务器的业务方很难进行接受,势必会影响产品的研发速度、推进速度;影响同事(SRE运维等)对产品的信心,进而对后续产品的推进带来很大的阻力。 以上是笔者站在研发角度,从可用性、可靠性、可控性、监控熔断等角度做的架构设计与框架设计,分享的产品研发思路。 笔者认为大规模的服务器安全防护产品,首先需要考虑的是架构的稳定性、监控告警的实时性、熔断限流的准确性等因素,其次再考虑安全数据的完整性、检测方案的可靠性、检测模型的精确性等因素。 九层之台,起于累土。只有打好基础,才能运筹帷幄,决胜千里之外。 参考资料https://en.wikipedia.org/wiki/CAP_theorem https://www.consul.io/intro/vs/serf.html https://golang.org/src/runtime/debug/garbage.go?h=FreeOSMemory#L99 https://www.ibm.com/developerworks/cn/linux/l-connector/ https://www.kernel.org/doc/ https://coreos.com/etcd/docs/latest/作者简介 陈驰,美团点评技术专家,2017年加入美团,十年以上互联网产品研发经验,专注于分布式系统架构设计,目前主要从事安全防御产品研发工作。 关于美团安全 美团安全部的大多数核心人员,拥有多年互联网以及安全领域实践经验,很多同学参与过大型互联网公司的安全体系建设,其中也不乏全球化安全运营人才,具备百万级IDC规模攻防对抗的经验。安全部也不乏CVE“挖掘圣手”,有受邀在Black Hat等国际顶级会议发言的讲者,当然还有很多漂亮的运营妹子。 目前,美团安全部涉及的技术包括渗透测试、Web防护、二进制安全、内核安全、分布式开发、大数据分析、安全算法等等,同时还有全球合规与隐私保护等策略制定。我们正在建设一套百万级IDC规模、数十万终端接入的移动办公网络自适应安全体系,这套体系构建于零信任架构之上,横跨多种云基础设施,包括网络层、虚拟化/容器层、Server 软件层(内核态/用户态)、语言虚拟机层(JVM/JS V8)、Web应用层、数据访问层等,并能够基于“大数据+机器学习”技术构建全自动的安全事件感知系统,努力打造成业界最前沿的内置式安全架构和纵深防御体系。 随着美团的高速发展,业务复杂度不断提升,安全部门面临更多的机遇和挑战。我们希望将更多代表业界最佳实践的安全项目落地,同时为更多的安全从业者提供一个广阔的发展平台,并提供更多在安全新兴领域不断探索的机会。 招聘信息 美团安全部正在招募Web&二进制攻防、后台&系统开发、机器学习&算法等各路小伙伴。如果你想加入我们,欢迎简历请发至邮箱zhaoyan17@meituan.com 具体职位信息可参考这里:https://mp.weixin.qq.com/s/ynEq5LqQ2uBcEaHCu7Tsiw 美团安全应急响应中心MTSRC主页:security.meituan.com 首发我司『美团技术团队』微信公众号 [微信公众号]保障IDC安全:分布式HIDS集群架构设计 [官方博客]保障IDC安全:分布式HIDS集群架构设计 2019-01-18 +4 8 0
openresty的unescape_uri函数处理百分号后面字符的小特性 我们的 WAF (Web Application Firewall)是搭建在 nginx 上,使用 lua modules 来实现的。也就是 openresty 的 luajit 功能来实现WEB 攻击判断。处理收到的请求时,一般会进行unescape_uri 处理后,再走规则匹配。 在离线分析的机器学习识别结果里,发现有这么一条漏报/aa%20a?openId=%%3Cscript%3E%,这个问题却出乎我们意料,为此,开始排查产生该现象的原因。 问题描述 源字符串: openresty的unescape_uri函数处理后结果 期望的结果: 问题现象: 转义不对;丢了百分号。 如何重现 通过浏览器,构造一个包含如上特征的 uri,按回车提交到 nginx?浏览器会自动把字符串转码吗?转码是按照什么规范来的?IE9、IE10、chrome、safari是分别如何处理的? 习惯用postman的人,知道postman是否会escape uri呢? 如果你忽略了这一点,那么会对你的测试造成很大的影响,耽误时间经历,让你在重试阶段都对问题产生了怀疑。 从HTTP协议角度考虑,只要确保通过协议发送到 http server 的请求中,uri 是符合我们需要的即可。至于浏览器的 escape做法,可以忽略,直接构造 http 协议来重现。 重现时,通过浏览器 提交「问题字符串」,在 nginx 层 输出 unescape 后的结果。 注意浏览器是否对 URI 进行了 URI_ENCODE,不同浏览器执行的 RFC 标准也不一样。 最根本的方式是,跳过浏览器,直接构造 http request 包,确保源字符串完整发送只 nginx。 重现该问题,开始排查 nginx 对 uri decode 的实现方式。 分析问题: waf的处理方式: 使用ngx.unescape_uri函数 确认处理 uri 的原始函数ngx.unescape_uri 该函数在 nginx lua modules 模块中,在src/ngx_http_lua_string.c 的64、65行: 如上代码,可知ngx.unescape_uri函数是ngx_http_lua_ngx_unescape_uri实现,对于真正 decode 字符串功能,是使用了src/ngx_http_lua_util.c文件内的ngx_http_lua_unescape_uri函数做 decode,该函数代码如下: 代码阅读: 对 uri 的字符串按照从左往右挨个处理 初始化 state 为 sw_usual,当state 为sw_usual时 若第一个字符是「?」,则把「?」赋值给 dst 字符串,然后直接返回 若第一个字符串是「+」,则替换成空格,赋值给 dst 字符串,然后跳出流程,走到下一个字符判断。(nginx 函数ngx_unescape_uri是没这个处理的) 若第一个字符是「%」,则跳过当前字符,对src 源字符串的下一个字符判断,走到sw_quoted流程处理下一个字符。 sw_quoted判断当前字符是符合[0-9a-f],则进入下一个字符的处理。 若不在[0-9a-f]范围内,则赋值给 dst 字符串,跳出当前流程处理下一个字符 sw_quoted_second 处理两个字符串,转换,赋值到 dst 中 结合案例分析: 结合案例中的源字符串%%3Cscript%3E%来看, 第一个字符% 走到上面第5条,不会被赋值到dst中,会继续判断下一个字符 第二个字符也是% ,走到上面第7条,直接被赋值 dst 中 第3、4个字符是普通字符,直接赋值到 dst 中。 第11个字符%走到上面第5条,跳过,继续判断下一个字符 第12、13个字符走到上面第8条,正确unescape转码。 最后一个字符% ,走到上面第5条,跳过,进行下一个字符判断 下一个字符没了,结束 所以,%%3Cscript%3E% 转换为 %3Cscript> ,这里的% 是源字符串的第二个百分号,第一个丢了(因为后面的字符也是%,不符合[0-9a-z]规则);最后一个% 也丢了,因为后面没字符了,while 跳出循环,结束了。 总结问题: 百分号后面还是百分号问题的修复 百分号是最后一个字符串的修复 修复问题: 测试代码: 备注: nginx 底层的 ngx_unescape_uri 函数(/src/core/ngx_string.c)实现部分,跟 lua modules 实现不太一样,openresty 里多了对「+」转空格的处理。() 疑问: nginx的做法是按照哪个 RFC 标准转换的? lua modules的做法符合 RFC 标准吗? uri_decode在其他语言里的是如何实现的? 解答: 翻阅 nginx 官方文档,并没找到 uri unescape的规范文档,只是代码注释里看到了是RFC3986 符合 RFC 规范,但RFC3986 规范并没提到百分号%后面不符合的字符该如何处理,也没提到单独的百分号该如何处理。 你自己去试试? 结论 nginx 的这个处理,并不是 bug,RFC3986 规范里只是规定了百分号%后面的字符哪些字符,转换成哪种格式,并没有规定不符合规范的字符改如何处理,没规定是丢弃还是保留。所以,这只是 nginx 的一个特性feature,特性feature,特性feature,并不是BUG,不是 BUG,不是 BUG。 参考: Uniform Resource Identifier (URI): Generic Syntax 2018-09-20 1 0
etcd clientv3的lease keepalive租约频繁续期bug 排查小记 背景: etcd 3.2.23 golang 1.9.1 centos 6 etcd cluster集群节点7台,节点member之间 https 通讯,与 client 之间也是 https 通讯 测试环境 2W 台主机,每台都要安装这个 agent,每个 agent注册一个包含lease的key,key的 ttl 设置为10s 设置keepalive 产品等着发布上线,持续遇到集群性能问题,长时间找不到原因,感觉我要崩溃了 集群性能问题不解决,线上更多的服务器更是没法部署,领导不认可,不满意,我就要被开除了,我就要辞职了。。。 Etcd github官方提了issue,官方没时间帮我们看(理解官方的苦衷,issue 太多,很多都是使用者的问题)... 问题现象: 带有lease id的 key 无故丢失 leader节点 大部分时间 TIME-WAIT数量非常高 leader 节点的 CLOSE-WAIT 有时也非常高 leader 的 CPU 使用率持续在80-90%的样子 leader 节点的内存使用率持续上升,疑似内存泄露的症状 监控图如下: etcd leader的 etcd 进程入手: 先分析「疑似内存溢出」问题,在 etcd 里,已经有pprof的信息,golang的tool里也有直接读取 http pprof 性能分析的工具,只是不支持 https 的。需要将 pprof 文件下载到本地,再从本地读取 结果如下: 可以看到大量 math 相关的包函数占用了大量的 cpu,而我们程序的业务中,并不涉及数学计算相关业务,也不会引用 math 包。唯一可能性就是在 https 的 tls 证书在做 RSA 算法校验时,用到了大量数学计算。我把 pprof 导出问 svg 图确认了一下,确实是 tls 的证书计算占用了大量 CPU。 但是 https 的链接,不应该是建立1次后就不再建立了吗?为什么会有这么大量的 cpu 计算呢。难道 TIMEWAIT 这么高跟这个有关系吗?回到 TIMEWAIT 的问题上来,此刻,leader 节点是 xxxx-etcdserver-xxx02,且TIME_WAIT非常高 leader 节点上的time-wait来源,都是来自各个member节点,平均每个节点将近几万个 TIMEWAIT,相隔几秒再次查询,还是5万多个。而各个 member 节点上,查到的是跟 leader 节点的2380端口 保持1千多个 ESTABLISHED 状态的连接,也确实是频繁的创建链接。为什么member节点频繁的创建链接呢?tcpdump抓点TCP 通讯包看一下: 如上抓包的图,发现了什么? 120140 2018-06-22 20:37:30.219 10.72.198.234 74 10.20.127.136 TCP 12902 → etcd-server(2380) [SYN] Seq=0 Win=14600 Len=0 MSS=1460 SACK_PERM=1 TSval=3008487334 TSecr=0 WS=128 120141 2018-06-22 20:37:30.219 10.20.127.136 74 10.72.198.234 TCP etcd-server(2380) → 12902 [SYN, ACK] Seq=0 Ack=1 Win=14480 Len=0 MSS=1460 SACK_PERM=1 TSval=2796204056 TSecr=3008487334 WS=128 120362 2018-06-22 20:37:30.242 10.72.198.234 66 10.20.127.136 TCP 12902 → etcd-server(2380) [ACK] Seq=1 Ack=1 Win=14720 Len=0 TSval=3008487358 TSecr=2796204056 120368 2018-06-22 20:37:30.242 10.72.198.234 204 10.20.127.136 TCP 12902 → etcd-server(2380) [PSH, ACK] Seq=1 Ack=1 Win=14720 Len=138 TSval=3008487358 TSecr=2796204056 120369 2018-06-22 20:37:30.242 10.20.127.136 66 10.72.198.234 TCP etcd-server(2380) → 12902 [ACK] Seq=1 Ack=139 Win=15616 Len=0 TSval=2796204080 TSecr=3008487358 174076 2018-06-22 20:37:40.242 10.72.198.234 66 10.20.127.136 TCP 12902 → etcd-server(2380) [FIN, ACK] Seq=139 Ack=1 Win=14720 Len=0 TSval=3008497358 TSecr=2796204080 174377 2018-06-22 20:37:40.282 10.20.127.136 66 10.72.198.234 TCP etcd-server(2380) → 12902 [ACK] Seq=1 Ack=140 Win=15616 Len=0 TSval=2796214120 TSecr=3008497358 从这些数据包中,你能看出什么问题?想一想,打开这张图,想5分钟再往下看。。。 120140是10.72.198.234 的 member 节点作为客户端,向10.20.127.136的2380的 leader 节点发起TCP 连接请求。 120141是 leader 回的握手确认包。120362是 member 发的 ACK 握手完成确认包。这些都很简单明了。 120368是 member 向 leader 发的数据包,120369是 leader 回 member 收到前面数据包的 ack。嗯,这些也很好明白。 174076是什么? member向 leader 发的FIN 关闭 TCP 连接的数据包,为什么?为什么member要关闭连接? 再抽查几个数据包看看,从 “专家信息”里预览的1000多个“SYN”里找.... 可以看出,都是 member主动向 leader 发 FIN 关闭包,这是为什么呢? 仅仅是看出来这些吗? 有没有注意到 member 发的消息包,收到 leader 回的 ack 之后,距离 member 主动发的 FIN 之间的时间间隔?都是10s ?为什么是10s? leader 回了 ack,但没主动向 member 发“FIN,ACK”,为什么没回呢?若没回,那么 leader 上的这个 TCP 五元组的状态是什么?是的,是“CLOSE-WAIT”,难道这就是造成leader上大量 CLOSE-WAIT 的原因?先怀疑一下,继续往下看... 如上所说,member 向 leader 发了一个PSH数据包,发来什么内容呢?为什么 leader 没回信息? payload的总长度是138,完全看不出来是什么数据包?不,应该还是有眼熟的,看前3个字节,0x16,0x03,0x01,如果对 HTTPS 熟悉的话,会联想到这是 TLS 的magic number,0x16对应十进制的22 即“Client Hello 的 Handshake”,后面的0x31,0x01是 TLS 的版本号。再配合我们的 member 跟 leader 之间确实是 https 通讯,可以确定这个地方是 tls 的client Hello握手包。也就是说配合前面发现的 10s 的间隔,可以看出 member 与 leader 之间的 TLS 握手并未完成,leader 并没有回 Server Hello... 整个TCP 包比较短,没有发现其他有用的信息... 去掉 HTTPS,减轻 cpu 负担: 回到之前的 CPU pprof 情况来看,是 TLS 相关证书计算,导致 CPU 使用率较高,但大量的 TCP 连接的发起,才是导致大量 TLS 计算的原因,导致内存飙升,消息积压的原因。不过,我们还是尝试去掉 member 与 leader 时间的 tls 证书校验,尝试减轻 leader 的CPU 使用。但结果影响不大,CPU 使用率依旧很高,不过,这也在意料之中,毕竟不是问题根本原因,查看pprof 如下 CPU 使用率较高的,是 runtime.futex 跟系统调用的 syscall.Syscall,以及runtime下的包,heapBitsForObject、scanobject、greyobject是 golang GC 时的运行时函数,意味着GC 已经占用很大的 CPU 了。 配合memory pprof来看下 如上,可以看出,大约65%的内存都用在了recvBuffer.put 这里的 put 方法是将收到的recvMsg写入到b.backlog这个 slice 中的,func (b recvBuffer) put(r recvMsg)跟 (b recvBuffer) load()两个函数都对b.backlog有读写操作,也就是说,这个slice 写入、读取时,都会进行加锁,严重占用 CPU 以及影响性能,增大系统负载。而且若 (b recvBuffer) load()处理速度跟不上func (b recvBuffer) put(r recvMsg)写入速度,意味着这个slice会持续增长,造成看似内存溢出的现象。。。 那问题来了,func (b recvBuffer) put(r recvMsg)为什么有这么大的调用量,为什么在频繁写入...为什么呢??? 定位到内存溢出的疑似原因: grpc/server.go func (s Server) serveStreams(st transport.ServerTransport) grpc/transport/handler_server.go 文件284行 func (ht serverHandlerTransport) HandleStreams 函数中的334行 然后再走到了,func (b *recvBuffer) put(r recvMsg)函数里,也就是说,这里的消息slice堆积,是因为接收到了大量http request。那接下来就是要找到这些http request 是来自谁了? 阅读 etcd 源码: 配合 member 之间大量的 TCP TIME-WAIT 状态链接,排查方向改到 member 节点上。 要查找 etcd member 建立 TCP/HTTP 链接的地方, 在源码里,还是非常多的,一时间没找到更好的bug 定位方式,只要从etcd 源码去逐步分析排查。 etcdmain/main.go 39行 startEtcdOrProxyV2() etcdmain/etcd.go 103行 startEtcd() etcdmain/etcd.go 186行 embed.StartEtcd(cfg) embed/etcd.go 107行 startPeerListeners() ,用 rafthttp.NewListener 创建了一个Listener。用于 etcd member 之间通讯。依赖github.con/coreos/etcd/pkg/transport包 embed/etcd.go 110行 startClientListeners() 用 net.Listen 创建了一个Listener。用于 client 跟 member 之间通讯。keepalive 使用github.con/coreos/etcd/pkg/transport包 157行 etcdserver.NewServer() 对本地配置读取,数据库目录读取,设置mvcc多版本控制函数,设置 lease 租约续期函数等等 166行 e.servePeers() 进行对 Linster 启动 Serve 接受请求 317行 etcdhttp.NewPeerHandler(e.Server) 设置HTTP Handler,里面函数如下: 如上,etcd 节点之间的通讯,设定了这几个Handler "/" --> http.NotFound "/raft"、"/raft/" --> raftHandler "/members" --> mh "/leases/internal" 、 "/leases" --> raftHandler "/version" --> versionHandler 也就是说,member 之间通讯的大量通讯只会是这些 handler 中间的一个,除非源码找漏了。。。 从这个开源产品功能特性上来看,member 之间需要大量同步的,应该就是数据了,应该把线索放到raftHandler上吗?头大.... raftHandler是s.RaftHandler() 返回的, 值是 EtcdServer.r.transport.Handler()返回值。 EtcdServer.r是在etcdserver/server.go的414行赋值的 etcdserver/server.go 的511行 EtcdServer.r.transport = tr进行赋值,类型为rafthttp.Transport ,其Handler 如下代码: 这里又有几个 Handler ,而且都是 raft 处理相关的,代码读起来比较吃力,很难理解作者意图, 打印http.dial堆栈: 要想从这里查找 TCP 连接的发起代码,恐怕是难上加难,所以,我换了个思路,直接在 TCP 连接 Dail 的地方,判断 addr 是 etcd leader 的地址,打印调用栈。使用 runtime/debug下的debug.PrintStack()函数输出。同时打印了连接完成后的remote.addr,用于校对。不过,这改动是 golang 的系统 runtime 类库,(改了系统库,记得改回来)。要小心谨慎,在http/transport.go 882行 增加如下 重新编译 etcd server,替换到集群中的某一个 member 节点上。 开始压测这个节点,重现大量 TIME-WAIT 的现象。现象重现后,在日志里也拿到了运行堆栈 在压测时,我用 netstat 抽样打印几次当前 member 上的 TCP 连接信息数据,也是用于校对日志输出的数据。 在压测的日志数据里,每建立一个 TCP 连接,都打印了这些堆栈,并且打印了远程 IP 的地址、端口。跟前面抽样打印的 TCP 数据核对一下,发现连接 leader 2380端口的几个 TCP 连接五元组跟 上面打印的remote.addr的五元组一致。 也就是说,是这个函数创建了大量的从 member 到 leader 之间的 TCP 连接。好了,如果找到大量 TCP 连接发起的函数了,又找到了该函数的调用栈了,那岂不是找到问题了? 然而,事情并没有这么简单,在上面的调用栈日志里,只能定位到coreos/etcd/pkg/transport.(*rwTimeoutDialer).Dial,并不能打印出所有调用堆栈,为什么? 找到 etcd member 10s 后主动断开 TCP 连接的原因: 在系统类库里的net/http/transport.go的Transport.dial方法中,如果该Transport自定义了Dial方法,则调用自定义的...这个自定义的Dial方法是在github.com/coreos/etcd/pkg/transport/transport.go里定义的,如下: 在代码dial := func(net, addr string) (net.Conn, error) ... 这里,自定义了一个 Dial 方法,赋值到http.Transport.Dial 函数属性上,并且设定TLS 的超时时间是 TLSHandshakeTimeout: 10 time.Second,这也就是之前抓包时,看到的10s 后,member 节点主动断开连接的原因----超过 TLS 设定的timeout时间了。 函数NewTransport的调用地方,只在NewTimeoutTransport这里找到,如下: 也就是说,找到NewTimeoutTransport调用着,就能确定发起大量 TCP 的代码...但之前的堆栈打印,并没有打印到这些函数调用,只好再想其他办法。 在NewTimeoutTransport的代码里,有一句对 HTTP Keepalive的最大持有空闲长连接数字的设置tr.MaxIdleConnsPerHost = 1024,这个参数决定 golang 的 http client 包对 server 发起 http 请求时,能持有的最大空闲 TCP 连接数量,在 etcd 的代码里,被设置为1024了,也就是说,etcd member 跟 etcd leader 之间空闲时,持有TCP 连接数可以是1024个,便于下次复用。(插一句,有人说,http 是最好的 RPC,这里我是不认同的,光从TCP 连接复用上,就有非常大的差异,在http 1.1 TCP 链接 「另类」复用,http 1.1 的keepalive属性也是仅当前一个请求响应后,这个链接才能继续复用,请求响应串行化。而在 http2里,是不需要等待上一个请求返回的,才有真正意义上的多路复用见:Streams and Multiplexing , 如果你做过 rpc 的功能实现,你就知道 rpc 在 tcp 上是如何实现请求响应的一一对应的,你就能很快明白http2\http1.1的 TCP 链接复用的区别了。) 排查 etcd member 跟 etcd leader 之间的通讯: 回到正题,对于代码这里的定位,我尝试去阅读代码来协助定位,但 etcd 的源码太复杂了,我读到 raft 协议节点之间数据同步这块,卡住了。开始从其他路子尝试解决,之前的 cpu 占用较高问题,我们在 member 节点之间去掉了 tls 证书验证,那么我们在 member 上做 tcp 抓包,抓2380端口数据包,看看数据发送的都是什么内容。 数据包中,可以看出 member 向 leader 上发送了大量的 /leases的 POST 请求。根据这个 uri,找到发起 HTTP 请求的代码,在etcdserver/v3_server.go的252行 阅读这段代码,可以看到函数调用leasehttp.RenewHTTP将请求发送到leader.PeerURLs,即发送到 leader 节点上。继续跟进leasehttp.RenewHTTP, 可以看出,该请求使用 golang 标准包的http.Client.Do来将请求发送出去。http.Client.Transport使用参数传入的rt http.RoundTripper。 定位到10s TLS timeout 的相关函数调用: 回到上一层代码 s.peerRt参数来自*EtcdServer.peerRt,在etcdserver/server.go的429行被赋值peerRt: prt prt变量在该文件的288行被赋值prt, err := rafthttp.NewRoundTripper(cfg.PeerTLSInfo, cfg.peerDialTimeout()) rafthttp.NewRoundTripper函数调用rafthttp.NewTimeoutTransport 没错,rafthttp.NewTimeoutTransport调用了rafthttp.NewTransport函数,也就是之前堆栈打印时,发现大量 TCP 链接创建使用的这个函数,也就是10s TLS timeout 的地方。 这里可以关联上了,确认大量发起 TCP 连接的代码调用关系,证明方向正确,剩下的就是顺藤摸瓜,找到发起大量请求的原因即可。 回到LeaseRenew函数调用的地方,调用func (s *EtcdServer) LeaseRenew函数代码见github.com/coreos/etcd/etcdserver/api/v3rpc/lease.go的88行附近 71行 func (ls *LeaseServer) LeaseKeepAlive函数调用func (ls *LeaseServer) leaseKeepAlive() etcdserver/etcdserverpb/rpc.pb.go的3417行func _Lease_LeaseKeepAlive_Handler(srv interface{}, stream grpc.ServerStream)调用了func (ls *LeaseServer) LeaseKeepAlive函数 etcdserver/etcdserverpb/rpc.pb.go的3480行将函数_Lease_LeaseKeepAlive_Handler设置为 StreamName: "LeaseKeepAlive"的 Handler,在通过*grpc.Server的RegisterService方法,在启动时,注册到监听器中 leaseKeepAlive函数是一个事件循环,从stream.Recv()拿到数据,则进行发送到 leader 节点上 stream.Recv()函数是google.golang.org/grpc/stream.go中的函数,这个不是 etcd 官方自己的代码,而是另外一个开源项目,意味着我还需要再把这块代码再阅读阅读,理解理解。 通过漫长的阅读,可以看到这里收到的消息都是来自 client 发送来的消息,其实可以从变量命名上看到端倪,比如 client 跟 member 之间通讯处理的变量都包含stream字样,而member到 leader 或 member 节点之间都带有peer字样,其实,client 跟 etcd 节点之间通讯用的是 http2传输协议,消息编码用的是grpc+protobuf;而etcd member节点之间是http1.1传输,消息编码也是 protobuf。 etcd lease 租约续期的时机: 就是说,这里的大量lease 租约续期的请求,来自 client 的请求,在我的案例中,我把主机 lease 的 TTL 设置为10S,这块会有异常吗?client 的 lease 续期是谁来发送?client 自己吗?多久发送一次?过期前的1秒? 在 etcd 的 client 类库中,对于租约续期的代码在clientv3/lease.go的474行 nextKeepAlive变量是 lease TTL 的三分之一,用于下次续期时的触发时刻,在后面的sendKeepAliveLoop函数续期时,回每隔500ms 进行一次循环,检测是否续期。也就是说,我们场景下,10s 的 TTL 会每隔3S 一次续期。3W 客户端的话,每秒1W 的写请求,而之前我们压测结果是集群节点可以有4W 以上的 (官方压测数据)qps,不至于这点都撑不住。这是个疑点。不过,前面的分析判断,大量的续约续期导致大量 TCP 链接的建立,大量 TCP 链接的建立,导致 leader 节点上大量对象分配,大量内存申请,golang gc 时,消耗大量 CPU,那这个问题也应该要优化一下,为此,我们把 TTL 设定为600S,即10分钟,也就是说,会在200秒后进行一次续期。 扩大lease TTL,尝试优化集群负载: 修复后的版本,我们在线下发布了6000多台,观察效果,果然,在发布后,内存有明显的下降,CPU 指标也有,但是在1个小时后,指标又上升了。。。 问题依旧: 这是为什么呢?中间大约1个小时的负载指标下降是因为发布过程带来的假象吗?上次变更的 lease 有效果吗?可我更改时,是测试过的,测试报告都有呢----agent 的 etcd lease租约时长 调整 ,TTL 续期是200S 一次了,但这是为什么?为了确认这个问题,我在其中一台 member 上抓包确认 上面确实是大量的 lease 包,那么每个 lease id 之间的发送间隔是多少?是我们预期的200秒吗?或者这些 lease 包是同一个 lease id 吗?一共有多少个 lease id 呢? 分析 tcpdump 的pcapng文件: 之前从 etcd 的源码分析可以看出,etcd node 节点之间的通讯消息编码也是 protobuf 的,我们需要把 http response body 的字节流复制出来,用 protobuf 的对应结果体解码即可。可是从 2018-07-05 16:38:31.015981 到 2018-07-05 16:43:20.420401 之间,间隔5分钟之间,一共103万个数据包,手动复制,实在没法统计。借用pcap包,对整个 tcpdump抓包的pcapng文件,分析, 过滤响应类型是application/protobuf的,取 http response body内容,并对其做 pb.LeaseKeepAliveRequest protobuf的结构体解析,计算 lease 总数与唯一数 运行结果: lease id 总数数: 77550, lease id 唯一数:1382。 这些结果说明,相同的 lease id 在频繁发送续约请求。再次写程序,把这些 lease id 取出来,到 etcd 集群上查看 TTL 剩余时间是否是预期的400-600秒之间,但结果却总是599秒,以其中一个 lease id 为例,每隔1秒去抓取 TTL 信息,但每次结果 TTL 都是599秒, grantTTL 是600,意味着 etcd member 持续不断的为这些 lease id 不停的续期。 我们之前排查结果是, member 的续期是来自 client 的 lease keepalive 请求,那么方向得放到 client 上。 抽查其中一台 client,抓包分析,也确实是在频繁发送/etcdserverpb.Lease/LeaseKeepAlive 请求,这就很奇怪,跟之前我的测试结果完全不一样,明明测试时,是每个200S 一起续期请求,为什么这里变得这么频繁,每秒都发起。 回到 etcd clientv3的代码上: 现象很奇怪,排查这个问题,只能再次从源码层面入手,方向也放到 /etcdserverpb.Lease/LeaseKeepAlive的地方,函数sendKeepAliveLoop这里开始循环处理判断是否需要续期租约 如上代码,每隔 500毫秒一次循环,遍历l.keepAlives map,判断每个LeaseID的nextKeepAlive是否到了,client 决定是否发起LeaseKeepAliveRequest。这个nextKeepAlive的赋值是在clientv3/lease.go的recvKeepAlive函数内,第473行 是的,这块代码之前看过,之前关注的只是 TTL 时间的设定,但仔细看看这段代码ka.nextKeepAlive = nextKeepAlive,这里做下一次lease 续期的赋值,判断方法是select case,判断条件是ch <- karesp,在 golang 里,这种写法表示该channle能写入的话,则进入case里面的流程,对ka.nextKeepAlive进行赋值;否则不赋值。。。否则,不赋值。。。否则,不赋值。。。 那么,这么 channel 长度多少呢?会不会写入的速度大于读取导致没写进去,导致下一次的续期时间没赋值成功呢?继续追查,在clientv3/lease.go的222行KeepAlive函数里 ch := make(chan *LeaseKeepAliveResponse, leaseResponseChSize)进行chan 的申请,长度leaseResponseChSize是常量,值是16。chs是个slice,长度1,只有这一个。那么,接下来,去确认这个 ch 读取的代码吧,可是我读完整个 lease.go也没找到哪里读取这个 channel 的代码,除了几处 close 的回收操作。那问题来了,会不会是这里把 ch channel 写满了,就无法再写入进去,导致ka.nextKeepAlive = nextKeepAlive流程走不到,导致ka.nextKeepAlive一直是上次的过去的续期时间,导致频繁进行续期呢? 找到疑点,每秒2次 lease keepalive 的请求: 开始测试,在 ch channel 写入的 default 分支里,打印“无法写入”等字样,并在for 后面打印这个ka的 TTL 等信息,用于核对。log.Println(fmt.Sprintf("karesp.TTL:%d, nextKeepAlive:%s,ka.nextKeepAlive:%s, ka.deadline:%s",karesp.TTL, nextKeepAlive.String(),ka.nextKeepAlive.String(), ka.deadline.String())) 改动后,重新编译代码,上传至某台 client 上 ,关掉 DEBUG 模式,观察日志输出。 文件见 /xxxx/xxx/core/nohup.out.1 ,在 2018/07/05 21:10:42 启动程序,经过1个多小时的观察,果然在2018/07/05 22:04:05时,lease 续约续期的请求从200秒变成了每秒2次。同时到 etcd 集群上查询响应 lease id,发现 TTL 均为599,grantTTL 为600,则可说明 是客户端在频繁重新发送租约请求。 第一个版本没打印ka.nextKeepAlive ,调整后,重新打印,为了让等待时间变短些,我把600s 改成60,便于观察。得到如下日志 2018/07/05 22:40:02 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:40:02 karesp.TTL:60, nextKeepAlive:2018-07-05 22:40:22.811626819 +0800 CST m=+40.135091508,ka.nextKeepAlive:2018-07-05 22:40:22.811626819 +0800 CST m=+40.135091508, ka.deadline:2018-07-05 22:41:02.811627601 +0800 CST m=+80.135092217 2018/07/05 22:40:22 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:40:22 karesp.TTL:60, nextKeepAlive:2018-07-05 22:40:42.922698966 +0800 CST m=+60.246163715,ka.nextKeepAlive:2018-07-05 22:40:42.922698966 +0800 CST m=+60.246163715, ka.deadline:2018-07-05 22:41:22.922699917 +0800 CST m=+100.246164470 2018/07/05 22:40:43 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:40:43 karesp.TTL:60, nextKeepAlive:2018-07-05 22:41:03.330494852 +0800 CST m=+80.653959932,ka.nextKeepAlive:2018-07-05 22:41:03.330494852 +0800 CST m=+80.653959932, ka.deadline:2018-07-05 22:41:43.330496602 +0800 CST m=+120.653961489 2018/07/05 22:41:03 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:41:03 karesp.TTL:60, nextKeepAlive:2018-07-05 22:41:23.341112484 +0800 CST m=+100.664577164,ka.nextKeepAlive:2018-07-05 22:41:23.341112484 +0800 CST m=+100.664577164, ka.deadline:2018-07-05 22:42:03.341113662 +0800 CST m=+140.664578212 2018/07/05 22:41:23 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:41:23 karesp.TTL:60, nextKeepAlive:2018-07-05 22:41:43.350264635 +0800 CST m=+120.673729313,ka.nextKeepAlive:2018-07-05 22:41:43.350264635 +0800 CST m=+120.673729313, ka.deadline:2018-07-05 22:42:23.350265498 +0800 CST m=+160.673730050 2018/07/05 22:41:43 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:41:43 karesp.TTL:60, nextKeepAlive:2018-07-05 22:42:03.572888276 +0800 CST m=+140.896353032,ka.nextKeepAlive:2018-07-05 22:42:03.572888276 +0800 CST m=+140.896353032, ka.deadline:2018-07-05 22:42:43.572889271 +0800 CST m=+180.896353840 2018/07/05 22:42:03 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:42:03 karesp.TTL:60, nextKeepAlive:2018-07-05 22:42:23.86879775 +0800 CST m=+161.192262488,ka.nextKeepAlive:2018-07-05 22:42:23.86879775 +0800 CST m=+161.192262488, ka.deadline:2018-07-05 22:43:03.868798687 +0800 CST m=+201.192263243 2018/07/05 22:42:23 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:42:23 karesp.TTL:60, nextKeepAlive:2018-07-05 22:42:43.877613966 +0800 CST m=+181.201079020,ka.nextKeepAlive:2018-07-05 22:42:43.877613966 +0800 CST m=+181.201079020, ka.deadline:2018-07-05 22:43:23.877615637 +0800 CST m=+221.201080417 2018/07/05 22:42:43 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:42:43 karesp.TTL:60, nextKeepAlive:2018-07-05 22:43:03.884576518 +0800 CST m=+201.208041122,ka.nextKeepAlive:2018-07-05 22:43:03.884576518 +0800 CST m=+201.208041122, ka.deadline:2018-07-05 22:43:43.884577361 +0800 CST m=+241.208041904 2018/07/05 22:43:03 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:43:03 karesp.TTL:60, nextKeepAlive:2018-07-05 22:43:23.893274193 +0800 CST m=+221.216738996,ka.nextKeepAlive:2018-07-05 22:43:23.893274193 +0800 CST m=+221.216738996, ka.deadline:2018-07-05 22:44:03.893275148 +0800 CST m=+261.216739702 2018/07/05 22:43:23 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:43:24 karesp.TTL:60, nextKeepAlive:2018-07-05 22:43:44.058766258 +0800 CST m=+241.382230998,ka.nextKeepAlive:2018-07-05 22:43:44.058766258 +0800 CST m=+241.382230998, ka.deadline:2018-07-05 22:44:24.058767168 +0800 CST m=+281.382231711 2018/07/05 22:43:44 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:43:44 karesp.TTL:60, nextKeepAlive:2018-07-05 22:44:04.413012991 +0800 CST m=+261.736477737,ka.nextKeepAlive:2018-07-05 22:44:04.413012991 +0800 CST m=+261.736477737, ka.deadline:2018-07-05 22:44:44.41301386 +0800 CST m=+301.736478391 2018/07/05 22:44:04 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:44:04 karesp.TTL:60, nextKeepAlive:2018-07-05 22:44:24.420796492 +0800 CST m=+281.744261106,ka.nextKeepAlive:2018-07-05 22:44:24.420796492 +0800 CST m=+281.744261106, ka.deadline:2018-07-05 22:45:04.420797607 +0800 CST m=+321.744262161 2018/07/05 22:44:24 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:44:24 karesp.TTL:60, nextKeepAlive:2018-07-05 22:44:44.428145652 +0800 CST m=+301.751610589,ka.nextKeepAlive:2018-07-05 22:44:44.428145652 +0800 CST m=+301.751610589, ka.deadline:2018-07-05 22:45:24.428147239 +0800 CST m=+341.751612018 2018/07/05 22:44:44 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:44:44 karesp.TTL:60, nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961,ka.nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961, ka.deadline:2018-07-05 22:45:44.436152168 +0800 CST m=+361.759616723 2018/07/05 22:45:04 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:45:04 无法写入 2018/07/05 22:45:04 karesp.TTL:60, nextKeepAlive:2018-07-05 22:45:24.441563525 +0800 CST m=+341.765028110,ka.nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961, ka.deadline:2018-07-05 22:46:04.44156426 +0800 CST m=+381.765028789 2018/07/05 22:45:04 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:45:04 无法写入 2018/07/05 22:45:04 karesp.TTL:60, nextKeepAlive:2018-07-05 22:45:24.941897369 +0800 CST m=+342.265361977,ka.nextKeepAlive:2018-07-05 22:45:04.436151288 +0800 CST m=+321.759615961, ka.deadline:2018-07-05 22:46:04.941898136 +0800 CST m=+382.265362694 2018/07/05 22:45:05 lease Id KeepAliveRequest:5904610818195269641 2018/07/05 22:45:05 无法写入 再16次之后,即第17次,开始恢复每秒2次的租约请求。。。所以,也验证了此 bug。 也就是说 不管grant的租约TTL设置多久,最终都会变成0.5秒发送lease 续期一次!!!!! 也就是说 不管grant的租约TTL设置多久,最终都会变成0.5秒发送lease 续期一次!!!!! 也就是说 不管grant的租约TTL设置多久,最终都会变成0.5秒发送lease 续期一次!!!!! 之前的134版本灰度灰度后,中间CPU 降低了大约1个小时的样子,这是为什么呢? 解释 cpu 降低1小时的疑点: 我们这么计算, TTL 600,续约续约是200, ka.chs的 数组长度是1,第一个chan <- LeasesKeepAliveResponse的长度是16 ,全部写满的时间是20016/60 = 53分钟。 也非常契合 falcon 上的 cpu 信息截图 另外再从我加日志的第一次测试时间来算: 2018/07/05 22:04:05 - 2018/07/05 21:10:42 约为53分钟。也就是说,将近1小时,跟 falcon 的统计吻合的。。。 找到真凶: 定问题了,就是修复方式了,我并没看到 ch channel 的使用的地方,或许我没理解作者的用意,我觉得,不管 ch channel 是否写入,都必须将下一次nextKeepAlive的时间赋值为最新时间。将ka.nextKeepAlive = nextKeepAlive移到for select外面即可。 不过,我们产品急于推广,保险起见,牺牲了 lease 的需求,去掉了这个功能。发布到服务器后,负载情况有非常大的改善,CPU 从40%-50%降低到2%-5%左右。 回顾总结: 回顾etcd 集群遇到的所有现象,配合这个问题,所有问题都好解释了,那么,我按照事情发生发展的顺序总结一下原因: etcd clientv3的lease keepalive 类库有 bug,不管 ttl 设置多久,都会在续期16次后,变为0.5s 一次 agent 部署到大量服务器上后(2W 台),每个 agent 持有一个 lease id agent(相对于 etcd 来说,是client)连接到 etcd member 上,将lease 续期请求每秒2次发送到 member 上 etcd member 跟 leader 在续期 lease 上是使用 http 1.1,TCP 连接在上一个请求没响应之前,没法复用,几乎都是新创建的 TCP 连接 etcd leader 节点每秒收到 2W * 2 个 TCP 连接请求 早期 etcd node 之间是 https 通讯,TCP 建立后,会进行 TLS 握手,TLS 校验是 RSA 的数学计算,会消耗大量 CPU,导致 leader 节点 CPU 飙升 etcd leader 使用 grpc 类库监听的 http server,每一个 client 连接过来时,都会创建对象、结构体、channel用来通讯,占用更多内存 etcd leader 收到 etcd member转发来的 lease keepalive请求, 放到一个slice里(实际上这块是grpc做的) etcd leader 对存放recvMsg的b.backlog 写入速度大于消费速度,导致 slice 越来越大,占用更多内存,导致内存不停上升,导致内存溢出假象 etcd leader 处理请求的速度小于写入速度,导致请求堆积,超过 lease id 的 TTL,导致 lease id 判断过期,删除 TTL,也解释了 hids server 的 key 莫名其妙消失的现象 etcd member 大量新建 TCP 请求到 etcd leader,leader 处理后就关闭连接,导致 leader 侧会出现大量 TIME-WAIT 状态的链接。 etcd leader 处理 member lease 的请求太慢,超过 TLS 的10s 后,member 触发 TLS 超时,主动断开连接,但 leader 仍处理不过来,TCP 连接变为 CLOSE-WAIT 状态 最终,稻草压死骆驼。 该 bug 已反馈至 etcd 官方 clientv3 sdk will send /etcdserverpb.Lease/LeaseKeepAlive twice every second for lease id after over time 困扰我1个多月的 bug,不会忘记这个时刻 关于 etcd 过载保护的想法: etcd lease 的 keepalive 放到2/3的时间点去做是不是更合适? grpc 的过载保护是不是也要做下,比如接受 client 请求这里从数组改用定长队列,超过丢弃,来保证集群可用。 etcd 节点之间通讯改用 http2是不是更合适 声明 本文可转载,但不得删除文中 招聘信息,以及QQ 群信息。 原文来自:www.cnxct.com/etcd-lease-keepalive-debug-note/ 招聘[上海/北京]安全研发专家 月薪 40k-60k 上海市/ 5-10 年/本科/研发 招聘[上海/北京]美团点评--50-80K 招 Java 高级技术专家 简历请发至 cfc4n[{at}]cnxct.com ,谢谢。 对你的etcd 产品是否影响: 使用etcd clientv3(golang 的客户端类库) ,所有v3版本的客户端(包括3.0.x,3.1.x,3.2.x,3.3.x,3.4.x)。 使用 lease 租约,并设置 keepalive client 量比较大能看出来效果,几百台的话,对性能影响不大。 官方已修复 coreos etcd在2018年7月24日已经修复该 bug,涉及etcd client v3的3个版本效率非常高。详情如下 https://github.com/coreos/etcd/blob/master/CHANGELOG-3.1.md https://github.com/coreos/etcd/blob/master/CHANGELOG-3.2.md https://github.com/coreos/etcd/blob/master/CHANGELOG-3.3.md Fix lease keepalive interval updates when response queue is full. If <-chan *clientv3LeaseKeepAliveResponse from clientv3.Lease.KeepAlive was never consumed or channel is full, client was sending keepalive request every 500ms instead of expected rate of every "TTL / 3" duration. 2018-07-18 +4 8 0
golang语言编译的二进制可执行文件为什么比 C 语言大 最近一位朋友问我“为什么同样的hello world 入门程序”为什么golang编译出来的二进制文件,比 C 大,而且大很多。我做了个测试,来分析这个问题。C 语言的hello world程序: golang 语言的hello world程序: 编译,查看生成文件大小 正如这位朋友所说c.out是12K,而 go_fmt.out是1.5M,差距奇大无比....为什么呢? 这两个二进制可执行文件文件里,都包含了什么? 众所周知,linux 上的二进制可执行文件是 ELF Executable and Linkable Format 可执行和可链接格式 如上图,ELF 文件分为如下: ELF文件的组成:ELF header 程序头:描述段信息 Section头:链接与重定位需要的数据 程序头与Section头需要的数据.text .data 在 Linux 上, 查看elf格式构成可以使用readelf ELF Header:头的信息 ELF 头的长度都是一样的,不会带来总体体积的变化。区别是个别字节的值不一样,比如Entry point address 程序入口点的值不一样等。 接下来是 程序头:,也就是 section部分(在linker连接器的角度是section部分或者装载器角度的segment) 可以看到c.out里引用了一个动态链接库libc.so.6,再看下go_fmt.out的情况 c.out的执行,依赖了libc.so.6, libc.so.6肯定需要ld.so的,看下依赖情况, 也就是说,C的程序默认使用了libc.so动态链接库,go 的程序,默认进行了静态编译,不依赖任何动态链接库。所以体积变大了。 那么,只是这一个原因吗? 我在 golang 的官方文档里找到如下的解释: Why is my trivial program such a large binary? The linker in the gc tool chain creates statically-linked binaries by default. All Go binaries therefore include the Go run-time, along with the run-time type information necessary to support dynamic type checks, reflection, and even panic-time stack traces. A simple C "hello, world" program compiled and linked statically using gcc on Linux is around 750 kB, including an implementation of printf. An equivalent Go program using fmt.Printf is around 1.5 MB, but that includes more powerful run-time support and type information. 将c的程序也使用静态编译试试。。。 可以看到,使用静态编译生成的二进制文件c_static.out为888K,仍然比 GO 写的小了一半,这到底是为什么呢?到底是哪里多了? 在ELF 可执行文件里,就需要以程序编译链接的角度来分析了,对于一个 ELF 文件的分析,上面部分分析过 ELF header部分,以及 dynamic section的情况了。再以看一下剩余的section信息。 ELF中的section主要提供给Linker使用, 而segment提供给Loader用,Linker需要关心.text, .rel.text, .data, .rodata等等,关键是Linker需要做relocation。而Loader只需要知道这个段的Read、Write、Execute的属性。 再去看go_fmt.out里都包含了什么,为了方便校对,写了一个程序来对比 对比一下两个文件的section段信息 发现go_fmt.out多了好多.debug_*开头的 section,这是用于 debug 的段信息。再次编译,去除这些信息,同时也把 C 静态编译的二进制也去除符号表和重定位信息。 如上结果,go_fmt_sw.out为1012K,c_static_gs.out为820K,还大了近200KB。到底是哪里大的呢? 刚刚的两个elf 文件的section对比中,还有一个比较特殊的go_fmt.out中 有一个名字叫.gopclntab的段,类型是SHT_PROGBITS程序段,大小为 282414字节,也就是275K,在c_static.out里并没有这个段的,也没有.gosymtab这个段。二者不一样,section段名字有规范标准吗? 其实,对于linker链接器来说,会关心段(section)的名字,但对loader加载器来说,并不关心名字,只关心这个段(segment)的权限,是否可执行,所在的偏移地址,用于函数的执行。 那.gopclntab段包含了什么内容呢?我写了一个程序分析了这个段的内容,程序代码如下: 编译后执行 如上可以看到,有很多函数是以fmt.(*pp)、strconv.*、sync.*、reflect.*、unicode.*等开头的,后面对应的函数名,也与 golang 的包里对应的包中函数名一致。。。用 IDA来确认一遍 ,果然在 .gopclntab 段里有很多 reflect.*开头的函数。 这就很奇怪了,golang 编译时,默认把 runtime 包编译进来就好了,应该不会把strconv\sync\reflect\unicode等包包含进来啊。程序中,只写了一句fmt.Println(),莫非是fmt包import了其他几个包导致的?回去搜了下代码,果然... 嗯,应该是这里问题,改用 go 的内置函数print试试。 编译后,对比大小 看如上结果,go_print_sw.out 变成了 624K , c_static_gs.out为820K,不光没比C的静态编译的大,还比它小呢。。。 不过呢,这也不能说明什么问题,只是因为其包含的函数内容不一样。 好了,至此已经知道为什么 golang 编译的文件比 C 的大了,因为 go 语言是静态编译的,而 C 的编译(比如 gcc编译器)都是动态链接库形式编译的。所以,导致了 go 编译的文件稍微大的问题。其次,跟其他语言比较字符串输出的话,用print内置函数就好了,就不要使用fmt包下的函数来比较了,因为 fmt 包引入了好多其他的包。。。这也增加编译后的二进制文件的体积。 其实呢,golang 的编译(不涉及 cgo 编译的前提下)默认使用了静态编译,不依赖任何动态链接库,这样可以任意部署到各种运行环境,不用担心依赖库的版本问题。只是体积大一点而已,存储时占用了一点磁盘,运行时,多占用了一点内存。早期动态链接库的产生,是因为早期的系统的内存资源十分宝贵,由于内存紧张的问题在早期的系统中显得更加突出,因此人们首先想到的是要解决内存使用效率不高这一问题,于是便提出了动态装入的思想。也就产生了动态链接库。在现在的计算机里,操作系统的硬盘内存更大了,尤其是服务器,32G、64G 的内存都是最基本的。可以不用为了节省几百 KB 或者1M,几 M 的内存而大大费周折了。而 golang 就采用这种做法,可以避免各种 so 动态链接库依赖的问题,这点是非常值得称赞的。 另外,.gopclntab 跟 .gosymtab段的讨论,以及其他建议 https://groups.google.com/forum/#!topic/golang-dev/EJhwBrITcpA https://groups.google.com/forum/#!msg/golang-dev/EJhwBrITcpA/EMHeWpriGZUJ 如果想要ELF二进制程序变小的方法,还可以使用 linux 的strip,以及 UPX 来压缩 ELF 文件。 推荐一本书《程序员的自我修养-链接、装载与库》链接 参考: 《程序的链接和装入及Linux下动态链接的实现》 《Reversing GO binaries like a pro》 2017-09-21 +10 14 0
golang游戏项目中使用 tls、https 与etcd v3服务通讯 在全球同服的游戏架构中,网络结构一般设计成集群的形式,用户随便在哪个服务器节点上都可以玩,也就要求每个节点的环境都是一致的,节点之间是可以自由下线上线的,用户是可以在每个节点自由注册的...等等,各种需求的出现,会需要一款能主动发现节点上线线,能统计全局所有服务器负载、状态,能实时下发配置等信息到所有节点的这么一款软件,那么就用到 ETCD 了。 etcd 作为 高可用分布式配置同步的软件,一般只对内网服务,偶尔也会暴露到外网。不管是内网,还是外网,都需要安全认证这一步,而其默认是有以及用户名、密码的认证方式,只是需要对每一个索引做权限设定。etcd 官方也后 TSL 的认证,同时也只是 HTTPS 形式传输数据,以确保哪怕在内网传输,也保证数据安全性。 ETCD server的配置的下载地址:https://github.com/coreos/etcd/releases/download/v3.2.7/etcd-v3.2.7-linux-amd64.tar.gz ,解压后,可看到其版本信息 如果不需要证书,不需要鉴权就可以访问,则可以直接使用了。。。 对于需要确保加密传输,需要权限认证的服务,那么还是需要证书来保证的,关于证书生成,,借助工具 cfssl 为了确保游戏集群管理的 ETCD 的高可用,一般都会有多个 ETCD 服务同时运行组成一个 ETCD 的集群。 ETCD 才调用 RAFT 的算法进行主节点选举,日志同步等等。ETCD 节点之间的通讯,也需要证书的认证。为了便于以后的etcd节点变更,一般都会以域名形式来提供服务,故需要解析几个 etcd 的域名,我这里以etcd1.cnxct.com、etcd2.cnxct.com,同时解析到对应服务器上。我这里的例子是解析到本机。 创建证书临时存放的目录(为了看起来好区别) 查看一下生成的文件列表: 在这里的列表中,分为client.pem公钥,client-key.pem私钥。以及 ca.pem 公钥,以及几个 CSR证书请求文件;ca-key.pem 为 自制 ca 私钥文件,需要妥善保管,不能外泄。 证书生成好了,验证一下证书的有效性 如上,目测证书有效。将证书复制到 etcd 的配置目录 开始配置etcd的配置文件 我在从官方下载下来的二进制包中 ,并没有配置文件的例子文件,而且网上搜到的,几乎都是早起etcd 2 时代的 linux 上conf 文件常见配置格式(就是那种井号注释的那种)。在 etcd 3,都搞成了 yaml 格式了。 在网上用 google、baidu 搜了好久,压根找不到yaml的例子。后来在 github 官网找到了 yaml 的例子地址 , https://github.com/coreos/etcd/blob/master/etcd.conf.yml.sample ,这个强烈建议 etcd 项目组将这个配置的例子放到打包的二进制压缩包里。 这里稍微说一下,在3.1.9时, listen-client-urls 跟 listen-client-urls 两个参数,还是可以直接使用域名来配置的。 3.2.x 就不支持了,只能填正确的 IP 来配置。 initial-advertise-peer-urls跟advertise-client-urls两个参数可继续使用域名形式的地址。 测试一下 CA 证书在 etcd 服务器上是否正常工作 同样,测试 client 证书在 etcd 的服务上是否正常使用 如上结果,可以看出,两个证书均正常使用。 开始在 Golang 的 client 中使用证书文件: etcd 本身就是用 golang 语言编写的,项目托管在 etcd 的 github地址,客户端类库在client目录,以及 clientv3下,前一个适用与etcd v2的版本,后一个适用与etcd v3的版本,在 golang 的包里,就是github.com/coreos/etcd/clientv3。在我的项目中,我使用的是v3版本的客户端,使用证书,建立链接的伪代码如下 在游戏集群中,游戏服务器的节点在整个集群中是唯一的,每个节点都拥有一个唯一的节点 ID,而这个唯一 ID 在 ETCD 里的注册,需要用到CAS原子性的操作,在 V2的客户但里,是Set方法时,Options的PrevExist参数是 false 来实现。 在 V3里,是Txn来开启事务,实现 CAS 操作 在游戏服务器节点启动后,把自己注册到 ETCD 后,将从 ETCD 加载自己节点所属的配置信息,比如若是 gateway 节点的话,加载自己将转发到逻辑服务器的节点列表,加载客户端版本过滤信息等等。伪代码如下: 同样,在游戏集群中,经常会遇到配置的调整,在以前的方法中,都是挨个到服务器上修改配置再重新加载,或者高级一点的,自己研发 GMtools 推送消息到游戏的服务进程等等。我们在使用 etcd 时,使用 etcd 的 watch 功能,监控某个 KEY 的变动,若有变化,则立刻刷新到进程中的配置环境上,进行配置变更。当然,有些 DB 链接的 DSN,这些不会跟着改的。这种需求可以使用this.kapi.Watch(context.TODO(),keyName。 对于整个集群的配置管理,一般在 gmtools 里做统一配置管理,将配置写入到 ETCD,再有游戏服务器节点启动时加载,或者游戏服务器节点 watch 监控,遇到更新时也自动同步到其环境中。事例如下: 综上所述,对 etcd server 开启了 https 的加密传输,开启了 tls 的证书认证。可放心的在内网或者公网来传输数据,修改下发数据,确保不会恶意或无意的修改污染了。。。 附golang Test 的截图: Coreos Etcd分布式集群 讨论 QQ 群: 2017-09-15 1 0
Clash Royale皇室战争的黑盒技术分析 今年开春,Clash Royale《皇室战争》在国内上架,又是SuperCell公司的大作,想必会非常火。在去年年底我层玩过封测版本,掉线比较严重,但仍没影响到我对它的好奇,我很想知道它的网络同步是如何实现的,战斗那块是帧同步吗?全是客户端计算的吗?这些疑问一直困扰着我,这次在国内上线了,网络状况也好,调试起来会更会舒心,更方便。 为此,我在工作之余,开始对Clash Royale《皇室战争》进行了黑盒分析,洋洋洒洒写了80多页的PPT,用于团队内部分享。顺便也发在博客上,让博客看上去有点生机。 由于我逆向基础差,对clash royale的libg.so分析零零散散的将近两周,一直没能搞定sc文件内容的编码规则,没搞定通讯的消息包结构编码规则,直到团队内部分享日期逼近,才决定暂停分析,尽快收尾,先完成分享,以后再继续。 而我又不想就这样放弃,在分享前后的几天里,我内心一直纠结这事,放不下来,抽空就想去再调试调试,为此落下了工作,计划中的功能也没给其他兄弟们做个好开头,没专心把主体框架搭建好,直到另外一个兄弟简介的提醒,列出大纲框架,我才意识到已经耽误很大的进度了,羞愧难当,今天(周六)本没打算去公司加班的,又把周五背回来的电脑又背回去,乖乖的写点东西,才让我内心平静下来。现在想想,今日凌晨3点多的恶梦或许就是因为愧疚吧。现在这里做个了结,不在工作时间去分析这个问题,不能再耽误工作进度。 分享里,更多的是介绍思路,我本身因为逆向功底原因,未能实现协议的解密,是借助coc-proxy这个项目来完成的。同样,关于SC文件解码部分,也是参考COC的SC文件拼接规则进行的。感谢github社区的技术资料。 PDF如下,由于内容较多(127页),加载时间可能较长,请耐心等待...另外,本PDF会持续更新,增加内容以及修正错误,请来本站阅读最新版本。 希望自己可以在空闲时间里把遗漏的知识点补充的扎实点。 2016年5月17日,公开了公司内部版(公司内部应该修复完毕),127页,10M左右,包含公司内两款手游的解密过程,以及手机游戏客户端的资源、协议、加密密钥保护方案,常见反外挂方案等。希望可以帮助到同样做游戏的朋友。 下载地址: clash royale皇室战争静态资源解密过程与常见手游客户端保护方案 2016-04-10 +2 6 0
密码保护:记一次证书考试的经历 因为各种原因,觉得考个证书备用,万一下次有啥事故,把我这个无证工程师抓进去了呢。 2015年年底,报了浦东软件园的某JAVA证书。在读书时,我比较叛逆,经常不屑教育制度的规定,抱着及格就好的心态,直到我毕业后工作,深知基础知识的薄弱对学习新知识点的影响是极其的大,更深知各种证书对户籍政策的重要性,对生活的重要性。面对这次新的考试学习,还是抱着努力考好的心态。我做了将近10年的程序开发,虽然没写过JAVA代码,但很多知识点还是存在共性的,面对这次考试,认真复习几点,也觉得考试内容很简单,都答对也并不难。再说,工作这么多年来,越来越喜欢追根问底,越来越追求尽善尽美。再者,每次周末上课,都跟儿子解释说我去上学,要考试的,也跟儿子玩笑似的说要靠满分,给儿子做榜样。虽然久而久之,满分的想法越来越深。 考试前几天,特意的认真复习着,考完试也觉得没答错任何题目。 直到昨天考试分数出来....选择题70,代码编写62,及格,可TMD及格不是我想要的,匪夷所思的结果,我根本不信。我奔着满分来的,你却给我个及格? 这样的结果让我,遂跟班主任老师提了这事,有劳她帮我咨询一下却收到这样的答复: 嗯?我没提交?我放在桌面了?我做了10年的开发,连需求文档都读错了?我放错文件位置了?我不知道该放哪? 抱着对考生成绩负责的意愿,首席考评员再次到该考生考试工位上单独判分,发现成果物提交内容未放入指定文件夹内,全部放置在桌面上。 原则上该生操作题只能判为0分,在首席考评员协同考评员讨论后,同意对该生放置在桌面上的成果物进行判分,最终给出操作题62分的合格分。 注:调用监控资料发现该考生的确没有在最终提交的确认环节让在场考评员进行成果物提交正确与否的确认。 唉呀呀,这技能好呀,接连甩锅,还扣个帽子......通篇一本正经的胡说八道,我从未见过如此厚颜无耻之人...... 老师,这锅我不背 尊敬的周老师,你好 我是考生陈X,给您添麻烦了。 我考试当天提交答案时,问了监考老师,提交过程、方式对不对,监考老师站我旁边看了下,说可以了,你可以走了。但我发现界面还在显示上传中,不要离开的字样,我没有走,我坐在那里看着界面,大约5-10分钟,界面显示我提交完成了。之后,我才走开。 我做了大约10年的程序开发,对于试题的要求,还是理解的,会将答案放到该放的位置,这点我确信一定没有任何错误,不会出现老师邮件中提到的“成果物提交内容未放入指定文件夹内”这种情况。而且,在下午的上机选择题考试中,我在答题完成之后,还特意去D盘看了下,我的答案压缩包还在D盘没有被清空。(选择题界面是全屏的,无法切换窗口,我说下步骤吧,尽管考题界面全屏覆盖,ALT+TAB等按键无法切换窗口,但屏幕最上面有个上课系统,忘记软件的名字了。上课的系统有个「问号」按钮,可以打开帮助文件,帮助文件是CHM的,CHM可以打开Internet选项,internet选项可以打开缓存存放目录,然后,就可以打开 文件夹浏览的窗口了,然后就可以看到D盘的答案目录了),这点我确认我的答案压缩包肯定在指定的位置,所以,对于老师给的答复,我不能认同,也请老师再次帮我确认一下,到底是什么原因,是不是我的答案丢失了?亦或者到底是谁丢的?当场的监考老师明确告诉我,让我可以走了,那为什么又说我没穿到指定位置呢? 为什么我下午的考试,扔发现我的答案在指定位置?烦请老师帮我查下,谢谢。 可又能如何呢?老师,这锅我背了 我是上海浦东软件园国家职业技能鉴定所负责人XXX,根据国家职业资格鉴定考试核分内容及规定原则: 1. 核对漏登、漏判、错登、错统分数的情况,不涉及评分标准和评判尺度; 2. 核分原则上对于主观题科目、客观题科目考试(计算机读卡阅卷)仅以下两种情况可以申请核分:参加考试但分数为零分的或参加考试但反映为缺考的; 3. 考生不可以任何理由查看试卷; 望理解上述国家职业资格鉴定相关内容及规定。 另:我鉴定所秉承对考生负责、对国家职业技能鉴定考评员工作的负责,关于考生陈X(身份证号:XXXXXXXXX)对于成绩质疑一事经过调查,结论如下: 上海浦东软件园国家职业技能鉴定所(第二十七所)核实后证明国家职业技能鉴定考评员根据该考生整体答题情况给出评判分数为真实、可靠数据,不存在故意或严重疏忽。 这解释了我的疑问了吗?通篇未提啊,这位老师是如此的机智。而我被这文字背后的气势镇住了,我怂了,可我又能如何呢?再说,如此坚持下去,也太浪费我时间了,我还要加班敲代码呢。。。老师,这锅我背了...工头又喊我搬砖了,不写了,搬砖去了... “及格就好”也曾经是我追求过的标准,但当我认真去做某事时,我想要的是完美,不是及格,而这结果却让我大跌眼镜,太令我失望了。 人生中,谁还没遇到点操蛋的事呢....我一介草民,又能怎样?不如分享出来,大家一起乐呵乐呵... 我太较真咯?该无所谓咯?我幼小的心灵就这样被伤害了,我该如何面对今后的考试呢?以什么样的态度呢?我该如何要求我自己呢?及格就好?会不会又遇到类似这次“看老师心情打分”的情况呢? 2016-04-07 +1 5 0