/* * ksyslog: In-kernel syslog receiver * Copyright(C) 2013 Atzm WATANABE All rights reserved * Distributed under the GPL */ #include #include #include #include #include #include #include #include #include #include #include "compat.h" #include "ksyslog.h" static struct socket *ksyslog_rcv_sk; static struct workqueue_struct *ksyslog_wq; static struct ksyslog_queue __percpu *ksyslog_queue; #ifdef CONFIG_PROC_FS static struct proc_dir_entry *ksyslog_procdir; static struct proc_dir_entry *ksyslog_proc_size; static struct proc_dir_entry *ksyslog_proc_stats; #endif static char *ksyslog_host = "0.0.0.0"; static ushort ksyslog_port = 514; static char *ksyslog_path = "/var/log/ksyslog.log"; static ulong ksyslog_queue_size_max = 4096; module_param(ksyslog_host, charp, 0444); module_param(ksyslog_port, ushort, 0444); module_param(ksyslog_path, charp, 0644); module_param(ksyslog_queue_size_max, ulong, 0644); static int ksyslog_queue_init(void (*handler)(struct work_struct *)) { int cpu; struct ksyslog_queue *q; ksyslog_queue = alloc_percpu(struct ksyslog_queue); if (unlikely(!ksyslog_queue)) return -ENOMEM; for_each_possible_cpu(cpu) { q = per_cpu_ptr(ksyslog_queue, cpu); INIT_LIST_HEAD(&q->head); INIT_WORK(&q->work, handler); spin_lock_init(&q->lock); atomic64_set(&q->size, 0); ksyslog_stats_zero(&q->write_stats); ksyslog_stats_zero(&q->drop_stats); ksyslog_stats_zero(&q->discard_stats); } return 0; } static void ksyslog_queue_uninit(void) { if (likely(ksyslog_queue)) free_percpu(ksyslog_queue); ksyslog_queue = NULL; } static int ksyslog_close(struct file *file) { int err; mm_segment_t oldfs; oldfs = get_fs(); set_fs(get_ds()); err = filp_close(file, NULL); set_fs(oldfs); return err; } static struct file * ksyslog_open(const char *path) { struct file *file; struct path ppath; mm_segment_t oldfs; oldfs = get_fs(); set_fs(get_ds()); if (unlikely(kern_path(path, LOOKUP_OPEN|LOOKUP_FOLLOW, &ppath))) file = filp_open(path, O_CREAT|O_WRONLY|O_APPEND|O_LARGEFILE, 0600); else file = filp_open(path, O_WRONLY|O_APPEND|O_LARGEFILE, 0); if (unlikely(IS_ERR(file))) goto out; compat_fsnotify_open(file); if (unlikely(S_ISDIR(file->f_path.dentry->d_inode->i_mode))) { ksyslog_close(file); file = ERR_PTR(-EISDIR); goto out; } if (unlikely(file->f_pos < 0)) { ksyslog_close(file); file = ERR_PTR(-EIO); goto out; } out: set_fs(oldfs); return file; } static int ksyslog_write(struct file *file, const char *buf, const size_t length) { int err; mm_segment_t oldfs; oldfs = get_fs(); set_fs(get_ds()); err = vfs_write(file, (__force void __user *)buf, length, &file->f_pos); set_fs(oldfs); return err; } static void ksyslog_drop_warning(const struct ksyslog_entry *entry) { net_warn_ratelimited("ksyslog: dropped: %llu %s.%s %u.%u.%u.%u %.*s\n", timeval_to_ns(&entry->tv) / 1000 / 1000 / 1000, ksyslog_facility_str(entry->facility), ksyslog_severity_str(entry->severity), entry->saddr.addr8[0], entry->saddr.addr8[1], entry->saddr.addr8[2], entry->saddr.addr8[3], (int)entry->length, entry->data); } static struct ksyslog_entry * ksyslog_entry_create(const struct sk_buff *skb, const struct iphdr *iph, const struct udphdr *udph) { struct ksyslog_entry *entry; unsigned int priority, facility, severity, month, day, hour, minute, second; unsigned char *start, month_s[4]; struct tm tm; int length, i; if (sscanf(skb->data, "<%3u>%3s %2u %2u:%2u:%2u ", &priority, month_s, &day, &hour, &minute, &second) != 6) return ERR_PTR(-EINVAL); start = memchr(skb->data, '>', 5); if (!start) return ERR_PTR(-EINVAL); start++; facility = priority >> 3; severity = priority & 7; if (facility >= __KSYSLOG_F_MAX) return ERR_PTR(-EINVAL); if (severity >= __KSYSLOG_S_MAX) return ERR_PTR(-EINVAL); month = ksyslog_month_num(month_s); if (!month) return ERR_PTR(-EINVAL); if (day > 31) return ERR_PTR(-EINVAL); if (hour > 23) return ERR_PTR(-EINVAL); if (minute > 59) return ERR_PTR(-EINVAL); if (second > 59) return ERR_PTR(-EINVAL); entry = kzalloc(sizeof(*entry), GFP_ATOMIC); if (unlikely(!entry)) return ERR_PTR(-ENOMEM); length = skb->len - (start - skb->data); entry->data = kzalloc(length, GFP_ATOMIC); if (unlikely(!entry->data)) { kfree(entry); return ERR_PTR(-ENOMEM); } if (skb->tstamp.tv64) entry->tv = ktime_to_timeval(skb->tstamp); else do_gettimeofday(&entry->tv); time_to_tm(entry->tv.tv_sec, 0, &tm); entry->time = mktime(tm.tm_year + 1900, month, day, hour, minute, second); entry->priority = priority; entry->facility = facility; entry->severity = severity; entry->daddr.addr32 = iph->daddr; entry->saddr.addr32 = iph->saddr; entry->dport = udph->dest; entry->sport = udph->source; entry->length = length; memcpy(entry->data, start, length); for (i = 0; i < length; i++) if (unlikely(entry->data[i] == '\n')) entry->data[i] = ' '; return entry; } static void ksyslog_entry_free(struct rcu_head *head) { struct ksyslog_entry *entry = container_of(head, struct ksyslog_entry, rcu); kfree(entry->data); kfree(entry); } static int ksyslog_entry_add(struct ksyslog_queue *queue, struct ksyslog_entry *entry) { if (unlikely(atomic64_read(&queue->size) >= ksyslog_queue_size_max)) return -ENOBUFS; list_add_tail_rcu(&entry->list, &queue->head); WARN_ON(atomic64_inc_return(&queue->size) > ksyslog_queue_size_max); return 0; } static void ksyslog_entry_del(struct ksyslog_queue *queue, struct ksyslog_entry *entry, bool free) { WARN_ON(atomic64_dec_return(&queue->size) < 0); list_del_rcu(&entry->list); if (free) call_rcu(&entry->rcu, ksyslog_entry_free); } static void ksyslog_entry_destroy(struct ksyslog_queue *queue) { struct ksyslog_entry *entry, *next; list_for_each_entry_safe(entry, next, &queue->head, list) ksyslog_entry_del(queue, entry, true); } static int ksyslog_entry_format(char **buf, const struct ksyslog_entry *entry) { *buf = kzalloc(54 + entry->length + 2, GFP_ATOMIC); if (unlikely(!*buf)) return -ENOMEM; return sprintf(*buf, "%llu %s.%s %u.%u.%u.%u %.*s\n", timeval_to_ns(&entry->tv) / 1000 / 1000 / 1000, ksyslog_facility_str(entry->facility), ksyslog_severity_str(entry->severity), entry->saddr.addr8[0], entry->saddr.addr8[1], entry->saddr.addr8[2], entry->saddr.addr8[3], (int)entry->length, entry->data); } static bool ksyslog_entry_write(struct file *file, struct ksyslog_entry *entry) { int length; char *buf; length = ksyslog_entry_format(&buf, entry); if (unlikely(length < 0)) return false; if (unlikely(ksyslog_write(file, buf, length) != length)) { kfree(buf); return false; } kfree(buf); return true; } static void ksyslog_work_handler(struct work_struct *work) { struct file *file; struct ksyslog_entry *entry; struct ksyslog_queue *q; q = container_of(work, struct ksyslog_queue, work); file = ksyslog_open(ksyslog_path); if (unlikely(IS_ERR(file))) return; spin_lock_bh(&q->lock); entry = list_first_or_null_rcu(&q->head, struct ksyslog_entry, list); if (unlikely(!entry)) { spin_unlock_bh(&q->lock); goto out; } ksyslog_entry_del(q, entry, false); spin_unlock_bh(&q->lock); if (likely(ksyslog_entry_write(file, entry))) { ksyslog_stats_add(&q->write_stats, entry->length); } else { ksyslog_stats_add(&q->drop_stats, entry->length); ksyslog_drop_warning(entry); } call_rcu(&entry->rcu, ksyslog_entry_free); out: ksyslog_close(file); if (atomic64_read(&q->size) > 0) queue_work(ksyslog_wq, work); } static int ksyslog_rcv(struct sock *sk, struct sk_buff *skb) { int err; struct iphdr *iph; struct udphdr *udph; struct ksyslog_entry *entry; struct ksyslog_queue *q; q = per_cpu_ptr(ksyslog_queue, smp_processor_id()); if (unlikely(skb_linearize(skb))) { ksyslog_stats_add(&q->drop_stats, skb->len); goto out; } iph = ip_hdr(skb); udph = udp_hdr(skb); if (unlikely(!skb_pull(skb, sizeof(*udph)))) { ksyslog_stats_add(&q->drop_stats, skb->len); goto out; } entry = ksyslog_entry_create(skb, iph, udph); if (unlikely(IS_ERR(entry))) { if (PTR_ERR(entry) == -EINVAL) { ksyslog_stats_add(&q->discard_stats, skb->len); goto out; } ksyslog_stats_add(&q->drop_stats, skb->len); goto out; } spin_lock_bh(&q->lock); err = ksyslog_entry_add(q, entry); spin_unlock_bh(&q->lock); if (unlikely(err)) { ksyslog_stats_add(&q->drop_stats, entry->length); ksyslog_drop_warning(entry); ksyslog_entry_free(&entry->rcu); goto out; } queue_work(ksyslog_wq, &q->work); out: consume_skb(skb); return 0; } #ifdef CONFIG_PROC_FS static int ksyslog_size_seq_show(struct seq_file *seq, void *v) { int cpu; struct ksyslog_queue *q; seq_puts(seq, "{\n"); for_each_possible_cpu(cpu) { q = per_cpu_ptr(ksyslog_queue, cpu); seq_printf(seq, " \"%u\": \"%lu\",\n", cpu, atomic64_read(&q->size)); } seq_puts(seq, "}\n"); return 0; } static int ksyslog_size_seq_open(struct inode *inode, struct file *file) { return single_open(file, ksyslog_size_seq_show, PDE_DATA(inode)); } static int ksyslog_stats_seq_show(struct seq_file *seq, void *v) { int cpu; struct ksyslog_queue *q; seq_puts(seq, "{\n"); for_each_possible_cpu(cpu) { q = per_cpu_ptr(ksyslog_queue, cpu); seq_printf(seq, " \"%u\": {\n", cpu); seq_puts(seq, " \"write\": {\n"); seq_printf(seq, " \"bytes\": \"%lu\",\n", atomic64_read(&q->write_stats.bytes)); seq_printf(seq, " \"packets\": \"%lu\",\n", atomic64_read(&q->write_stats.packets)); seq_puts(seq, " },\n"); seq_puts(seq, " \"drop\": {\n"); seq_printf(seq, " \"bytes\": \"%lu\",\n", atomic64_read(&q->drop_stats.bytes)); seq_printf(seq, " \"packets\": \"%lu\",\n", atomic64_read(&q->drop_stats.packets)); seq_puts(seq, " },\n"); seq_puts(seq, " \"discard\": {\n"); seq_printf(seq, " \"bytes\": \"%lu\",\n", atomic64_read(&q->discard_stats.bytes)); seq_printf(seq, " \"packets\": \"%lu\",\n", atomic64_read(&q->discard_stats.packets)); seq_puts(seq, " },\n"); seq_puts(seq, " },\n"); } seq_puts(seq, "}\n"); return 0; } static int ksyslog_stats_seq_open(struct inode *inode, struct file *file) { return single_open(file, ksyslog_stats_seq_show, PDE_DATA(inode)); } static struct file_operations ksyslog_size_fops = { .owner = THIS_MODULE, .open = ksyslog_size_seq_open, .read = seq_read, .llseek = seq_lseek, .release = single_release, }; static struct file_operations ksyslog_stats_fops = { .owner = THIS_MODULE, .open = ksyslog_stats_seq_open, .read = seq_read, .llseek = seq_lseek, .release = single_release, }; static void ksyslog_proc_destroy(void) { if (ksyslog_proc_size) remove_proc_entry("size", ksyslog_procdir); ksyslog_proc_size = NULL; if (ksyslog_proc_stats) remove_proc_entry("stats", ksyslog_procdir); ksyslog_proc_stats = NULL; if (ksyslog_procdir) remove_proc_entry("ksyslog", NULL); ksyslog_procdir = NULL; } static int ksyslog_proc_init(void) { ksyslog_procdir = proc_mkdir("ksyslog", NULL); if (!ksyslog_procdir) { pr_err("ksyslog: proc_mkdir failed\n"); goto err; } ksyslog_proc_size = proc_create("size", S_IRUGO, ksyslog_procdir, &ksyslog_size_fops); if (!ksyslog_proc_size) { pr_err("ksyslog: proc_create(size) failed\n"); goto err; } ksyslog_proc_stats = proc_create("stats", S_IRUGO, ksyslog_procdir, &ksyslog_stats_fops); if (!ksyslog_proc_stats) { pr_err("ksyslog: proc_create(stats) failed\n"); goto err; } return 0; err: ksyslog_proc_destroy(); return -ENOMEM; } #endif static void ksyslog_finish(void) { int cpu; if (ksyslog_rcv_sk) sock_release(ksyslog_rcv_sk); ksyslog_rcv_sk = NULL; if (ksyslog_wq) destroy_workqueue(ksyslog_wq); ksyslog_wq = NULL; #ifdef CONFIG_PROC_FS ksyslog_proc_destroy(); #endif for_each_possible_cpu(cpu) ksyslog_entry_destroy(per_cpu_ptr(ksyslog_queue, cpu)); rcu_barrier(); ksyslog_queue_uninit(); } static int __init ksyslog_init(void) { int err; struct sockaddr_in sin; err = ksyslog_queue_init(ksyslog_work_handler); if (err) goto err; #ifdef CONFIG_PROC_FS err = ksyslog_proc_init(); if (err) goto err; #endif ksyslog_wq = create_workqueue("ksyslog"); if (!ksyslog_wq) { pr_err("ksyslog: create_workqueue failed\n"); err = -ENOMEM; goto err; } err = sock_create(AF_INET, SOCK_DGRAM, 0, &ksyslog_rcv_sk); if (err) { pr_err("ksyslog: sock_create failed\n"); goto err; } sin.sin_family = AF_INET; sin.sin_addr.s_addr = in_aton(ksyslog_host); sin.sin_port = htons(ksyslog_port); err = kernel_bind(ksyslog_rcv_sk, (struct sockaddr *)&sin, sizeof(struct sockaddr_in)); if (err) { pr_err("ksyslog: kernel_bind failed\n"); goto err; } udp_sk(ksyslog_rcv_sk->sk)->encap_type = UDP_ENCAP_KSYSLOG; udp_sk(ksyslog_rcv_sk->sk)->encap_rcv = ksyslog_rcv; udp_encap_enable(); return 0; err: ksyslog_finish(); return err; } static void __exit ksyslog_exit(void) { ksyslog_finish(); } module_init(ksyslog_init); module_exit(ksyslog_exit); MODULE_AUTHOR("Atzm WATANABE"); MODULE_DESCRIPTION("In-kernel syslog receiver"); MODULE_LICENSE("GPL");