Files
udpscope/src/capture.cpp
T

239 lines
7.7 KiB
C++

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 Martino Ferrari
#include "capture.hpp"
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <chrono>
#include <vector>
namespace udpscope {
namespace {
/// The file header is one 64-byte block: fixed fields first, zeroed padding after.
constexpr std::uint64_t kHeaderBytes = 64;
/// `seq` lives at the *end* of a slot, at a fixed offset, because the reader must
/// find it without trusting any other field.
constexpr std::uint64_t kSeqBytes = 8;
void put_u32(std::uint8_t* p, std::uint32_t v) { std::memcpy(p, &v, 4); }
void put_u16(std::uint8_t* p, std::uint16_t v) { std::memcpy(p, &v, 2); }
void put_f64(std::uint8_t* p, double v) { std::memcpy(p, &v, 8); }
std::uint32_t get_u32(const std::uint8_t* p) {
std::uint32_t v;
std::memcpy(&v, p, 4);
return v;
}
double get_f64(const std::uint8_t* p) {
double v;
std::memcpy(&v, p, 8);
return v;
}
double wall_seconds() {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
return double(ts.tv_sec) + double(ts.tv_nsec) * 1e-9;
}
/// `seq` is published with a release store *after* the bytes are down, so a reader
/// that sees a non-zero `seq` sees the whole slot -- the reason no `fsync` is needed
/// for the file to be readable after a crash. Plain __atomic builtins, not
/// `std::atomic_ref`: the target is mmap'd memory the class does not own as an object.
void publish_seq(std::uint8_t* slot, std::uint64_t seq) {
__atomic_store_n(reinterpret_cast<std::uint64_t*>(slot + kSlotBytes - kSeqBytes), seq,
__ATOMIC_RELEASE);
}
std::uint64_t read_seq(const std::uint8_t* slot) {
return __atomic_load_n(reinterpret_cast<const std::uint64_t*>(slot + kSlotBytes - kSeqBytes),
__ATOMIC_ACQUIRE);
}
} // namespace
void PacketCapture::set_error(const char* what) noexcept {
std::snprintf(error_, sizeof(error_), "%s", what);
error_set_.store(true, std::memory_order_release);
}
void PacketCapture::error(char* out, std::size_t cap) const noexcept {
if (cap == 0) return;
out[0] = '\0';
if (!error_set_.load(std::memory_order_acquire)) return;
std::snprintf(out, cap, "%s", error_);
}
bool PacketCapture::open(const std::string& path, std::uint32_t slot_count) {
close();
path_ = path;
slot_count_ = slot_count;
seq_ = 0;
cursor_ = 0;
stored_.store(0, std::memory_order_relaxed);
truncated_.store(0, std::memory_order_relaxed);
error_set_.store(false, std::memory_order_relaxed);
const std::chrono::steady_clock::time_point t0 = std::chrono::steady_clock::now();
// Keep the previous capture one deep: re-enabling must not destroy the capture the
// user enabled *for*. A failure here is reported, and the old previous kept.
std::string prev = path;
const std::size_t slash = prev.rfind('/');
const std::size_t dot = prev.rfind('.');
if (dot != std::string::npos && (slash == std::string::npos || dot > slash)) {
prev.insert(dot, "-prev");
} else {
prev += "-prev";
}
::rename(path.c_str(), prev.c_str());
fd_ = ::open(path.c_str(), O_RDWR | O_CREAT | O_EXCL | O_CLOEXEC, 0600);
if (fd_ < 0) {
set_error(strerror(errno));
return false;
}
// Reserved, not sparse: a store into a page the filesystem cannot back raises
// SIGBUS and kills udpscope outright, and an out-of-space condition must be an
// error at the moment the user clicked, not a crash ten minutes later.
const std::uint64_t total = kHeaderBytes + std::uint64_t(slot_count) * kSlotBytes;
if (::posix_fallocate(fd_, 0, off_t(total)) != 0) {
set_error(strerror(errno));
::close(fd_);
fd_ = -1;
::unlink(path.c_str());
return false;
}
map_ = static_cast<std::uint8_t*>(
::mmap(nullptr, total, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
if (map_ == MAP_FAILED) {
map_ = nullptr;
set_error(strerror(errno));
::close(fd_);
fd_ = -1;
::unlink(path.c_str());
return false;
}
map_bytes_ = total;
std::uint8_t* h = map_;
std::memset(h, 0, kHeaderBytes);
std::memcpy(h, "UDPSCAP\0", 8);
put_u32(h + 8, 1); // version
put_u32(h + 12, slot_count);
put_u32(h + 16, kSlotBytes);
put_u32(h + 20, 0); // cursor: a hint, `seq` is the order
put_f64(h + 24, wall_seconds()); // the epoch recv_time is relative to
put_u32(h + 32, peer_addr_);
put_u16(h + 36, peer_port_);
put_u16(h + 38, 0); // local_port: the client does not expose its own socket
const double ms =
std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count();
open_ms_.store(ms, std::memory_order_relaxed);
enabled_.store(true, std::memory_order_release);
return true;
}
void PacketCapture::close() noexcept {
if (map_ != nullptr) {
::munmap(map_, map_bytes_);
map_ = nullptr;
}
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
map_bytes_ = 0;
enabled_.store(false, std::memory_order_relaxed);
}
void PacketCapture::store(const std::uint8_t* bytes, std::size_t len, double recv_time) noexcept {
if (!enabled_.load(std::memory_order_relaxed) || map_ == nullptr) return;
std::uint8_t* slot = map_ + kHeaderBytes + std::uint64_t(cursor_) * kSlotBytes;
std::uint32_t orig = std::uint32_t(len);
std::size_t fit = len;
if (fit > kMaxDatagramBytes) {
fit = kMaxDatagramBytes;
truncated_.fetch_add(1, std::memory_order_relaxed);
}
put_f64(slot, recv_time);
put_u32(slot + 8, std::uint32_t(fit));
put_u32(slot + 12, orig);
std::memcpy(slot + kRecordHeaderBytes, bytes, fit);
++seq_;
publish_seq(slot, seq_); // last: a non-zero `seq` is what makes a slot complete
cursor_ = (cursor_ + 1) % slot_count_;
put_u32(map_ + 20, cursor_); // a hint for a human in xxd; `seq` is the order
stored_.fetch_add(1, std::memory_order_relaxed);
}
bool read_capture(const std::string& path, CaptureReadStats* out) {
if (out == nullptr) return false;
*out = CaptureReadStats{};
FILE* f = std::fopen(path.c_str(), "rb");
if (f == nullptr) return false;
std::fseek(f, 0, SEEK_END);
const long size = std::ftell(f);
std::fseek(f, 0, SEEK_SET);
out->file_bytes = std::uint64_t(size < 0 ? 0 : size);
std::uint8_t h[kHeaderBytes];
if (std::fread(h, 1, sizeof(h), f) != sizeof(h)) {
std::fclose(f);
return false;
}
out->header_ok = std::memcmp(h, "UDPSCAP\0", 8) == 0 && get_u32(h + 8) == 1;
out->slot_count = get_u32(h + 12);
out->slot_bytes = get_u32(h + 16);
out->cursor = get_u32(h + 20);
out->t0_realtime = get_f64(h + 24);
if (!out->header_ok || out->slot_count == 0 || out->slot_bytes < kRecordHeaderBytes + 8) {
std::fclose(f);
return false;
}
const std::uint32_t payload = out->slot_bytes - kRecordHeaderBytes - kSeqBytes;
std::vector<std::uint8_t> slot(out->slot_bytes);
for (std::uint32_t i = 0; i < out->slot_count; ++i) {
if (std::fseek(f, long(kHeaderBytes + std::uint64_t(i) * out->slot_bytes), SEEK_SET) != 0) {
break;
}
if (std::fread(slot.data(), 1, slot.size(), f) != slot.size()) break;
const std::uint64_t seq = read_seq(slot.data());
const std::uint32_t len = get_u32(slot.data() + 8);
const std::uint32_t orig = get_u32(slot.data() + 12);
if (seq == 0 && len == 0 && orig == 0) {
++out->empty;
continue;
}
if (seq == 0) {
++out->torn; // bytes down, ordinal never published: a capture that died mid-copy
continue;
}
++out->stored;
out->payload += len;
if (orig > payload) ++out->truncated;
if (out->first_seq == 0 || seq < out->first_seq) out->first_seq = seq;
if (seq > out->last_seq) out->last_seq = seq;
}
std::fclose(f);
return true;
}
} // namespace udpscope