wtmp日志读取

简介: wtmp日志读取

wtmp日志介绍

之前遇到一个AIX服务器登录不上,但是能ping通的事情。一开始我怀疑是sshd服务坏掉了,但是使用telnet也无法登录。好在这台机器所在的机房就在我隔壁,于是外接显示器,直接上机操作。好在直接通过物理介质还是能登录得上去的。

上去一看,好家伙,直接提示根目录磁盘不足了。于是就查跟目录下都有哪些东西占用了比较大的空间。

不看不知道,一看吓一跳,根目录下一共3.2G/var/log下一个wtmp文件就占了2.8G

那么这个wtmp是啥?能不能删除呢?查了一下资料,才知道这个wtmp是系统记录登录信息的一个日志文件。

不过这个日志文件并不是文本格式,并不能直接查看,而是二进制格式,需要借助一些其他手段。

既然只是记录登录信息的日志,那删了也无妨,于是问题解决。

过了一段时间,另外一台HPUX机器也出现了同样的问题,也是wtmp文件把磁盘占满导致无法远程连接。于是我痛定思痛,决心好好研究一下这个wtmp日志。

刚动这个念头,契机就来了。

正好有一个客户希望可以采集这种二进制的wtmp文件。于是就趁此机会好好研究了一把这个日志。

/var/log目录下,记录登录信息的日志一共有几类:

  • /var/log/utmp 当前正在登录的用户,相当于who命令的输出
  • /var/log/btmp 记录登录失败的信息,可以使用lastb命令查看
  • /var/log/wtmp 记录当前正在登录和历史登录系统的用户信息,可以使用last命令查看

使用last命令读取

我们先使用last命令看看读出来的wtmp是什么样的内容:

从上图可知,last命令读出的wtmp文件,其内容主要是:

第一列: 用户名

第二列:终端位置

第三列:登录IP或者内核

第四列:开始时间

第五列:结束时间

第六列:持续时间

因此,我们需要有一个程序,能将wtmp日志解析成上述的格式,才是最终的目标。

使用Go语言读取

auditbeat是elastic开源的一款go语言编写的采集器。其中就有涉及到采集wtmp文件的相关实现。

它首先定义了一个utmp的结构体:

type utmpC struct {
  Type UtType
  // Alignment
  _ [2]byte
  Pid      int32
  Device   [UT_LINESIZE]byte
  Terminal [4]byte
  Username [UT_NAMESIZE]byte
  Hostname [UT_HOSTSIZE]byte
  ExitStatusTermination int16
  ExitStatusExit        int16
  SessionID int32
  TimeSeconds      int32
  TimeMicroseconds int32
  IP [4]int32
  Unused [20]byte
}
type Utmp struct {
  UtType   UtType
  UtPid    int
  UtLine   string
  UtUser   string
  UtHost   string
  UtTv     time.Time
  UtAddrV6 [4]uint32
}

然后使用ReadNextUtmp函数来遍历wtmp文件:

func ReadNextUtmp(r io.Reader) (*Utmp, error) {
  utmpC := new(utmpC)
  err := binary.Read(r, byteOrder, utmpC)
  if err != nil {
    return nil, err
  }
  return newUtmp(utmpC), nil
}

newUtmp就是将utmpC转换为utmp格式的一个转换函数。utmpCwtmp存储登录信息的内部二进制格式。

调用逻辑如下:

func readNewInFile(utmpPath string) error{
    f, err := os.Open(utmpPath)
    if err != nil {
      return fmt.Errorf("error opening file %v: %w", utmpFile.Path, err)
    }
    for {
      utmp, err := ReadNextUtmp(f)
      if err != nil && err != io.EOF {
        return fmt.Errorf("error reading entry in UTMP file %v: %w", utmpFile.Path, err)
      }
      if utmp != nil {
        r.log.Debugf("utmp: (ut_type=%d, ut_pid=%d, ut_line=%v, ut_user=%v, ut_host=%v, ut_tv.tv_sec=%v, ut_addr_v6=%v)",
          utmp.UtType, utmp.UtPid, utmp.UtLine, utmp.UtUser, utmp.UtHost, utmp.UtTv, utmp.UtAddrV6)
      } else {
        // Eventually, we have read all UTMP records in the file.
        break
      }
    }
  }
  return nil
}

当然原始代码比这个复杂,我在这里做了一些精简,原始代码里还有一些判断文件滚动的逻辑。具体代码在utmp_c.goutmp.go,感兴趣的可以参考。

使用C语言实现

C语言是提供了utmp相关的系统实现的,这些接口在utmp.h中,主要的接口包含以下这些:

//这个函数相当于上面的ReadNextUtmp,每次获取一条登录信息,如果读到了文件末尾,则返回NULL
//第一次使用该函数会打开文件,文件读完之后可以使用endutent()来关闭文件
struct utmp *getutent(void);  
//从 utmp 文件中的读写位置逐一往后搜索参数 ut 指定的记录
// 如果ut->ut_type 为RUN_LVL, BOOT_TIME, NEW_TIME, OLD_TIME 其中之一则查找与ut->ut_type 相符的记录
// 若ut->ut_type为INIT_PROCESS, LOGIN_PROCESS, USER_PROCESS 或DEAD_PROCESS 其中之一, 则查找与ut->ut_id相符的记录
struct utmp *getutid(struct utmp *ut); 
//从utmp 文件的读写位置逐一往后搜索ut_type 为USER_PROCESS 或LOGIN_PROCESS 的记录, 而且ut_line 和ut->ut_line 相符
struct utmp *getutline(struct utmp *ut);
//将一个struct utmp结构体写进文件utmp中, 也就是手动写入登录信息
struct utmp *pututline(struct utmp *ut);
//打开文件utmp,并且将文件指针指向文件的最开始,相当于fseek到文件开始位置
void setutent(void);
//关闭文件utmp
void endutent(void);
//设定utmp文件所在的路径,默认的路径为宏 _PATH_UTMP,利用该函数,可以控制读哪个文件
int utmpname(const char *file);

上面这些接口中反复出现的结构体struct utmp,其实和上文中go语言实现里的utmpC是一个东西,只不过这里是C语言的定义方式,其结构体如下:

/* The structure describing an entry in the user accounting database.  */
struct utmp
{
  short int ut_type;        /* Type of login.  */
  pid_t ut_pid;         /* Process ID of login process.  */
  char ut_line[UT_LINESIZE];    /* Devicename.  */
  char ut_id[4];        /* Inittab ID.  */
  char ut_user[UT_NAMESIZE];    /* Username.  */
  char ut_host[UT_HOSTSIZE];    /* Hostname for remote login.  */
  struct exit_status ut_exit;   /* Exit status of a process marked
                   as DEAD_PROCESS.  */
/* The ut_session and ut_tv fields must be the same size when compiled
   32- and 64-bit.  This allows data files and shared memory to be
   shared between 32- and 64-bit applications.  */
#ifdef __WORDSIZE_TIME64_COMPAT32
  int32_t ut_session;       /* Session ID, used for windowing.  */
  struct
  {
    int32_t tv_sec;     /* Seconds.  */
    int32_t tv_usec;        /* Microseconds.  */
  } ut_tv;          /* Time entry was made.  */
#else
  long int ut_session;      /* Session ID, used for windowing.  */
  struct timeval ut_tv;     /* Time entry was made.  */
#endif
  int32_t ut_addr_v6[4];    /* Internet address of remote host.  */
  char __unused[20];        /* Reserved for future use.  */
};

这里需要说明的是,ut_type解析出来是数字,它其实是一个enum,对应关系如下:

#define EMPTY       0   /* No valid user accounting information.  */
#define RUN_LVL     1   /* The system's runlevel.  */
#define BOOT_TIME   2   /* Time of system boot.  */
#define NEW_TIME    3   /* Time after system clock changed.  */
#define OLD_TIME    4   /* Time when system clock changed.  */
#define INIT_PROCESS    5   /* Process spawned by the init process.  */
#define LOGIN_PROCESS   6   /* Session leader of a logged in user.  */
#define USER_PROCESS    7   /* Normal process.  */
#define DEAD_PROCESS    8   /* Terminated process.  */
#define ACCOUNTING  9
/* Old Linux name for the EMPTY type.  */
#define UT_UNKNOWN  EMPTY

有了以上知识储备,就可以使用C语言获取wtmp文件内容了:

#include <utmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h> 
char *ntop(int32_t ip_addr)
{
    int addr_1 = ip_addr % 256;        
    ip_addr = ip_addr / 256;
        int addr_2 = ip_addr % 256;         
        ip_addr  = ip_addr / 256;
        int addr_3 = ip_addr % 256;        
        ip_addr  = ip_addr  / 256;
        int addr_4 = ip_addr % 256;
        char ip[16] = {0};
        sprintf(ip, "%d.%d.%d.%d", addr_1, addr_2, addr_3, addr_4);
        return ip;
}
int main(){
        utmpname("/var/log/wtmp");
        setutent();
        while(1){
                struct utmp *ut = getutent();
                if (ut == NULL){
                        break;
                }
                printf("{\"ut_type\":%d, \"ut_pid\":%d, \"ut_line\":\"%s\", \"ut_id\": \"%s\", \"ut_user\": \"%s\", \"ut_host\":\"%s\",\"ut_exit\":{\"e_termination\":%d, \"e_exit\":%d},\"ut_tv\":%d, \"ut_session\":%d, \"ut_addr6\":\"%s\"}\n", 
                                ut->ut_type, ut->ut_pid, ut->ut_line, ut->ut_id, ut->ut_user, ut->ut_host, ut->ut_exit.e_termination, ut->ut_exit.e_exit, ut->ut_tv.tv_sec + ut->ut_tv.tv_usec / 1000,ut->ut_session, ntop(ut->ut_addr_v6[0]));
        }
        endutent();
        return 0;
}

以上程序运行结果如下所示:

[root@ck94 wtmp]# ./a.out 
{"ut_type":7, "ut_pid":41857, "ut_line":"pts/84", "ut_id": "s/84root", "ut_user": "root", "ut_host":"10.2.1.24","ut_exit":{"e_termination":0, "e_exit":0},"ut_tv":1678526541, "ut_session":0, "ut_addr6":"10.2.1.24"}
{"ut_type":7, "ut_pid":49088, "ut_line":"pts/100", "ut_id": "/100root", "ut_user": "root", "ut_host":"10.2.1.24","ut_exit":{"e_termination":0, "e_exit":0},"ut_tv":1678526356, "ut_session":0, "ut_addr6":"10.2.1.24"}
{"ut_type":7, "ut_pid":41020, "ut_line":"pts/101", "ut_id": "/101root", "ut_user": "root", "ut_host":"ck08","ut_exit":{"e_termination":0, "e_exit":0},"ut_tv":1678526996, "ut_session":0, "ut_addr6":"192.168.110.8"}
{"ut_type":8, "ut_pid":41018, "ut_line":"pts/101", "ut_id": "", "ut_user": "", "ut_host":"","ut_exit":{"e_termination":0, "e_exit":0},"ut_tv":1678527030, "ut_session":0, "ut_addr6":"0.0.0.0"}
{"ut_type":7, "ut_pid":41068, "ut_line":"pts/101", "ut_id": "/101root", "ut_user": "root", "ut_host":"ck08","ut_exit":{"e_termination":0, "e_exit":0},"ut_tv":1678527173, "ut_session":0, "ut_addr6":"192.168.110.8"}
{"ut_type":8, "ut_pid":41062, "ut_line":"pts/101", "ut_id": "", "ut_user": "", "ut_host":"","ut_exit":{"e_termination":0, "e_exit":0},"ut_tv":1678527208, "ut_session":0, "ut_addr6":"0.0.0.0"}
...

以上使用系统函数获取,看似方便,实则有一个不好的地方,那就是采集wtmp的程序肯定是要长期运行采集的,可是万一采集程序因为某种原因停止了运行,当下次重新启动时,如何断点续采,而不是从头开始?

最好的方式还是采取auditbeat中一样的视线方式,直接从文件读取,并记录下读到的offset,当下次读取时,直接fseekoffset的位置,于是代码如下:

#include <utmp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h> 
char *ntop(int32_t ip_addr)
{
    int addr_1 = ip_addr % 256;        
    ip_addr = ip_addr / 256;
        int addr_2 = ip_addr % 256;         
        ip_addr  = ip_addr / 256;
        int addr_3 = ip_addr % 256;        
        ip_addr  = ip_addr  / 256;
        int addr_4 = ip_addr % 256;
        char ip[16] = {0};
        sprintf(ip, "%d.%d.%d.%d", addr_1, addr_2, addr_3, addr_4);
        return ip;
}
int main(){
        FILE *fp = fopen("/var/log/wtmp", "rb");
        int chunk_size = sizeof(struct utmp);
        void *chunk = calloc(1, chunk_size);
        while(1){
                int rbytes = fread(chunk, chunk_size, 1, fp);
                if (rbytes == 0){
                        break;
                }
                struct utmp *ut = NULL;
                ut = (struct utmp*)chunk;
                printf("{\"ut_type\":%d, \"ut_pid\":%d, \"ut_line\":\"%s\", \"ut_id\": \"%s\", \"ut_user\": \"%s\", \"ut_host\":\"%s\",\"ut_exit\":{\"e_termination\":%d, \"e_exit\":%d},\"ut_tv\":%d, \"ut_session\":%d, \"ut_addr6\":\"%s\"}\n", 
                                ut->ut_type, ut->ut_pid, ut->ut_line, ut->ut_id, ut->ut_user, ut->ut_host, ut->ut_exit.e_termination, ut->ut_exit.e_exit, ut->ut_tv.tv_sec + ut->ut_tv.tv_usec / 1000,ut->ut_session, ntop(ut->ut_addr_v6[0]));
        }
        free(chunk);
        return 0;
}

以上代码可以得到同样的运行结果,这里就不演示了。


推荐一个零声学院免费教程,个人觉得老师讲得不错,分享给大家:[Linux,Nginx,ZeroMQ,MySQL,Redis,

fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,

TCP/IP,协程,DPDK等技术内容,点击立即学习: C/C++Linux服务器开发/高级架构师

相关实践学习
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
【涂鸦即艺术】基于云应用开发平台CAP部署AI实时生图绘板
目录
相关文章
|
安全 Unix Linux
`/var/log/wtmp` 和 `/var/run/utmp`日志详解
`/var/log/wtmp` 和 `/var/run/utmp` 是Unix/Linux系统中记录用户登录信息的关键文件。`wtmp` 文件存储所有登录和注销事件,供 `last` 命令显示登录历史,而 `utmp` 文件实时更新,记录当前登录用户信息,可由 `who` 或 `w` 命令解析展示。两者皆为root用户访问,系统重启可能清空,且常受安全措施保护,用于系统管理和安全审计。
1583 1
|
XML 安全 Java
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
本文介绍了Java日志框架的基本概念和使用方法,重点讨论了SLF4J、Log4j、Logback和Log4j2之间的关系及其性能对比。SLF4J作为一个日志抽象层,允许开发者使用统一的日志接口,而Log4j、Logback和Log4j2则是具体的日志实现框架。Log4j2在性能上优于Logback,推荐在新项目中使用。文章还详细说明了如何在Spring Boot项目中配置Log4j2和Logback,以及如何使用Lombok简化日志记录。最后,提供了一些日志配置的最佳实践,包括滚动日志、统一日志格式和提高日志性能的方法。
4858 32
【日志框架整合】Slf4j、Log4j、Log4j2、Logback配置模板
|
监控 安全 Apache
什么是Apache日志?为什么Apache日志分析很重要?
Apache是全球广泛使用的Web服务器软件,支持超过30%的活跃网站。它通过接收和处理HTTP请求,与后端服务器通信,返回响应并记录日志,确保网页请求的快速准确处理。Apache日志分为访问日志和错误日志,对提升用户体验、保障安全及优化性能至关重要。EventLog Analyzer等工具可有效管理和分析这些日志,增强Web服务的安全性和可靠性。
579 9
|
监控 容灾 算法
阿里云 SLS 多云日志接入最佳实践:链路、成本与高可用性优化
本文探讨了如何高效、经济且可靠地将海外应用与基础设施日志统一采集至阿里云日志服务(SLS),解决全球化业务扩展中的关键挑战。重点介绍了高性能日志采集Agent(iLogtail/LoongCollector)在海外场景的应用,推荐使用LoongCollector以获得更优的稳定性和网络容错能力。同时分析了多种网络接入方案,包括公网直连、全球加速优化、阿里云内网及专线/CEN/VPN接入等,并提供了成本优化策略和多目标发送配置指导,帮助企业构建稳定、低成本、高可用的全球日志系统。
1151 54
|
XML JSON Java
Logback 与 log4j2 性能对比:谁才是日志框架的性能王者?
【10月更文挑战第5天】在Java开发中,日志框架是不可或缺的工具,它们帮助我们记录系统运行时的信息、警告和错误,对于开发人员来说至关重要。在众多日志框架中,Logback和log4j2以其卓越的性能和丰富的功能脱颖而出,成为开发者们的首选。本文将深入探讨Logback与log4j2在性能方面的对比,通过详细的分析和实例,帮助大家理解两者之间的性能差异,以便在实际项目中做出更明智的选择。
1623 3
|
存储 缓存 关系型数据库
图解MySQL【日志】——Redo Log
Redo Log(重做日志)是数据库中用于记录数据页修改的物理日志,确保事务的持久性和一致性。其主要作用包括崩溃恢复、提高性能和保证事务一致性。Redo Log 通过先写日志的方式,在内存中缓存修改操作,并在适当时候刷入磁盘,减少随机写入带来的性能损耗。WAL(Write-Ahead Logging)技术的核心思想是先将修改操作记录到日志文件中,再择机写入磁盘,从而实现高效且安全的数据持久化。Redo Log 的持久化过程涉及 Redo Log Buffer 和不同刷盘时机的控制参数(如 `innodb_flush_log_at_trx_commit`),以平衡性能与数据安全性。
864 5
图解MySQL【日志】——Redo Log
|
监控 Java 应用服务中间件
Tomcat log日志解析
理解和解析Tomcat日志文件对于诊断和解决Web应用中的问题至关重要。通过分析 `catalina.out`、`localhost.log`、`localhost_access_log.*.txt`、`manager.log`和 `host-manager.log`等日志文件,可以快速定位和解决问题,确保Tomcat服务器的稳定运行。掌握这些日志解析技巧,可以显著提高运维和开发效率。
1607 13
|
缓存 Java 编译器