查看原文
其他

容器时间-中篇-时间OS层拦截

上篇文章介绍了linux内核的时间机制,本篇接着谈这个话题,不过要从内核态中出来,首先介绍下位于用户态的glibc的时间实现,然后介绍下如何在OS层面拦截系统时间欺骗应用,最后会展示一个小demo,演示如何在kubernetes集群里面调整应用时间满足需求。


一、glibc里面的时间获取


linux内核提供了众多的系统调用供用户使用,参考上一篇文章的内核版本,看下3.10.0版本支持的内核调用列表:



在该版本的内核中,共有332个系统调用,其中自然有我们上篇文章谈到的gettimeofday:



在实际的应用开发过程中,不论是偏底层的C语言,还是更接近应用层的java或python等语言,很少有直接调用syscalls的,一般都是一层或多层的封装使得调用方式更为便捷,在标准的linux发行版里面,一般这些封装都基于libc的,无论是glibc,还是在alpine linux发行版里面采用的musl-libc,都对linux内核的syscalls做了最基础的封装,所以接下来的分析会基于glibc源码,版本为对应3.10.0版本内核的2.17的glibc,其下载地址为:http://ftp.gnu.org/gnu/glibc/glibc-2.17.tar.gz,有兴趣的同学可以下载一起学习下。


熟悉glibc的同学知道,其支持的芯片架构极其多,无论是sparc架构,还是power架构,本文中我们针对性地看下x86_64体系下glibc是如何对gettimeofday这个系统调用进行封装的,源码位置在glibc源码/sysdeps/unix/sysv/linux/x86_64/gettimeofday.c:



代码里面可以看到gettimeofday在glibc里面是__gettimeofday的别名,所以真正的实现是在__gettimeofday方法里面,看下这个方法的实现:



从代码里面可以看出,glibc支持vDSO、VSYSCALL以及内联汇编三种方式,前两种是内核用来加速系统调用的方式,这两者的实现和区别这里不再详述,这个以后单开一篇介绍。


这里我们只需要知道,glibc会优先采用vDSO的方式,而在objdump反编译后的结果里有__vdso_gettimeofday这个方法:



如果vdso方式调用失败,则会采用vsyscall的方式,去固定地址拿VSYSCALL_ADDR_vgettimeofday方法:



第三种方式则是采用与其他系统调用相同的方式通过中断方式实现,在glibc源码/sysdeps/unix/sysv/linux/x86_64/sysdep.h文件里面看下INLINE_SYSCALL的实现:



函数里面调用了INTERNAL_SYSCALL方法:



可以看到,glibc通过内联汇编的方式调用syscall指令,从而触发了用户态向内核态的切换,最终调用了gettimeofday这个syscall。


二、时间拦截方案


由上一篇kernel时间机制分析以及上面glibc的时间相关源码分析,基本上我们已经清楚应用中获取时间的整个流程,剩下的就是要回归主题,谈下如何实现拦截系统时间返回任意的时间给应用层使用。


2.1 时间拦截思路


一份代码成为可被执行的应用程序会经过预处理、编译、汇编以及链接四个阶段,前三个阶段完成的是将高级语言翻译成机器语言的任务,在linux系统中代码经历前三个阶段后最终表现为ELF目标文件,而第四个阶段做的则是将前三个阶段生成目标文件、系统库的目标文件以及库文件最终生成可在特定平台运行的可执行文件,这个阶段可以分为动态链接和静态链接:


  •  静态链接的时候,静态库的所有代码都被导入到最终的可执行文件中

  • 动态链接的时候,把动态库的函数以及变量的符号名导入到可执行文件中,仅仅在可执行文件被运行时才把动态库的执行代码加载到内存中

与此对应,就有静态库和动态库两个概念,静态库的好处是对函数库的连接是放在代码编译阶段完成,之后就与函数库再无联系,移植性会好些,弊端是浪费空间和资源,同时一个基础静态库如果出现更新,与之对应的应用程序可能都要随之重新编译。为了解决静态库的这些问题,引入了动态库的概念,因为动态库仅仅在是程序运行阶段才被载入, 如果不同的程序调用相同的动态库,那么在内存中只需要有一份拷贝即可,解决了空间浪费的问题,并且因为动态库与可执行文件是脱离的,静态库的更新时遇到的问题也得到了解决,但是采用动态链接的可执行文件运行时依赖的动态库必须存在,否则会报错,移植性因此没有采用静态链接的应用好。


在linux里面,动态库是以.so命名的,静态库是以.a命名的:



我们的时间拦截方案是从动态库的加载为基础的,linux的动态库的加载如果不在编译代码时指定,则会在以下几个路径下寻找并加载:


  • /usr/lib

  • /lib

  • /etc/ld.so.cache

  • LD_LIBRARY_PATH

  • LD_PRELOAD


加载的优先级是依次递增的,也就是LD_PRELOAD最高,它是一个环境变量,在动态库的加载中优先级也是最高的。


时间拦截其实就是对gettimeofday这个方法进行修改,方法是多种多样的,可以去内核里面修改,可以去修改glibc,但是这种修改往往是全局的,肯定是不可行的,所以最优的方案是采用LD_PRELOAD机制,自己实现这个方法并编译成动态库依靠动态库加载的先后顺序来覆盖原始的方法。


2.2 时间拦截落地


既然已经有处理的问题的思路,剩下的就是如何落地了,首先看下社区有没有前辈实现的轮子,发现社区的libfaketime项目(https://github.com/wolfcw/libfaketime.git):



项目2011年已在github上扎根,近3000行代码包含了众多与时间相关的函数:



最关键是包含我们所需要的函数:



于是按照之前思路,编译成动态库,LD_PRELOAD加载,然后查看LD_PRELOAD前后的时间区别:



可以看出,时间拦截的确是生效了,采用LD_PRELOAD覆盖原始的gettimeofday方法后,时间的确是发生了变化,说明思路是可行的,剩下的就是把这个方案优化后应用在kubernetes集群的容器上。


三、kubernetes集群应用的落地


一个问题从有思路,再到思路的验证落地,最终到公司环境中落地还是有较长距离的,如何把这个方案应用到公司的kubernetes环境中,我们需要考虑如下几点:


  • 时间调整的亲和性


时间调整的需求方,一般是来自开发人员和测试人员,他们可能并不理解内核的一些特性,如何将这种方案进行合理的包装,使得他们更容易接受,更容易使用。


  • 多pod时间同步

应用既然已经运行在kubernetes环境上,就说了应用已经完成微服务的改造,这就要求时间的调整在整个应用是一种同步的行为,类似于应用层面的NTP。


  • 多环境时间拦截


应用产品的开发从需求设计到最终交付,必然要历经开发、测试、仿真、生产等多个环境,如何将这种能力在多个环境快速打开以及快速回收做到环境的标准化交付也是需要考虑的一个点。


针对以上三个需求,在应用部署方式上我们采用了sidecar部署的方式,将LD_PRELOAD能力的打开与否放到应用的运行环境中,这样在多个环境里面就可以随时打开或者关闭时间拦截的能力,同时做到环境的标准化交付。



我们制作了多个应用运行环境,来分别激活不同的能力,同时因为标准交付物仅仅是一个war包,保证了交付物的纯净性。


考虑时间调整亲和性以及多pod时间同步的需求,采用configmap作为应用时间的标准,所有微服务应用pod的时间拦截中需要伪造的时间均来自于此:



所有的pod都以volume的形式挂载该configmap:



这样所有微服务应用pod时间同步的需求就得到实现:



可以看出,应用拿到的时间的确是在真实时间的基础上增加了100天,说明时间拦截是成功的。


以上就是容器时间系列文章-中篇的全部内容,主要介绍了如何在OS层面对真实的系统时间进行拦截并且伪造,但是这种时间伪造的方式能否继续上移至应用层呢,答案是肯定的,在下一篇文章即容器时间系列文章的收尾中,将介绍下如何在jvm层面通过jvmti及JNI的方式对时间进行拦截伪造。

文章来源公众号:凯哥侃技术

Kubernetets 培训推荐 


经半年筹备2019年特别推出Kubernetes实战培训课程,课程内容坚持:系统化、实战化、生产可用化,让所有学员能系统化深入学习Kubernetes以及容器相关技术,少走弯路。


课程特色:

1、全网规划最系统的Kubernetes课程,涉及Kubernetes的生产可用HA集群搭建、调度系统、服务发现、服务质量、etcd、Helm、网络、存储、安全、日志监控、CI/CD、私有容器云等;

2、涉及面最广Kubernetes课程,从Docke 到 Kubernetes、Istio,共计33大章节、155个小章节。

线上直播班:了解更多内容;

线下集训班:了解更多内容;

  • 北京:4月12-14日

  • 深圳:4月19-21日

  • 上海:4月26-28日


推荐阅读


点击阅读原文直达报名链接!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存