This document specifies the architecture for adding network monitoring and enforcement capabilities to AegisBPF. The design extends the existing file access control framework to provide egress/ingress network policy enforcement using LSM socket hooks, maintaining consistency with existing patterns while adding network-specific functionality.
┌─────────────────────────────────────────────────────────────────────────┐
│ USER SPACE (aegisbpf daemon) │
├─────────────────────────────────────────────────────────────────────────┤
│ CLI Parser │ Policy Mgr │ Event Handler │ Metrics │ Network │
│ │ (extended) │ (extended) │ Exporter │ Manager │
└──────────────────────────────┬──────────────────────────────────────────┘
│ libbpf
│
┌────────────────────┴────────────────────┐
│ │
┌─────────┴────────────────────┐ ┌────────────────┴─────────────────────┐
│ BPF Maps (existing+new) │ │ BPF Programs (existing+new) │
│ (pinned in bpffs) │ │ (kernel space) │
├──────────────────────────────┤ ├──────────────────────────────────────┤
│ [EXISTING] │ │ [EXISTING] │
│ deny_inode │ │ LSM: file_open │
│ deny_path │ │ LSM: inode_permission │
│ allow_cgroup │ │ TP: sys_enter_openat │
│ events (ringbuf) │ │ TP: sched_* │
│ block_stats │ │ │
│ ... │ │ [NEW - NETWORK] │
│ │ │ LSM: socket_connect │
│ [NEW - NETWORK] │ │ LSM: socket_bind │
│ deny_ipv4 │ │ LSM: socket_listen │
│ deny_ipv6 │ │ LSM: socket_sendmsg (optional) │
│ deny_port │ │ │
│ deny_cidr_v4 │ │ │
│ deny_cidr_v6 │ │ │
│ net_block_stats │ │ │
│ net_conn_stats │ │ │
└──────────────────────────────┘ └──────────────────────────────────────┘
socket_connect - Egress ControlSEC("lsm/socket_connect")
int BPF_PROG(aegis_socket_connect, struct socket *sock,
struct sockaddr *address, int addrlen)
Purpose: Control outbound connections (TCP connect, UDP sendto with destination)
Decision Flow:
1. Extract address family (AF_INET/AF_INET6)
2. Extract destination IP and port
3. Check cgroup allowlist → ALLOW if matched
4. Check deny_port map → DENY if matched
5. Check deny_ipv4/ipv6 exact match → DENY if matched
6. Check deny_cidr_v4/v6 LPM trie → DENY if matched
7. DEFAULT: ALLOW
socket_bind - Service Exposure ControlSEC("lsm/socket_bind")
int BPF_PROG(aegis_socket_bind, struct socket *sock,
struct sockaddr *address, int addrlen)
Purpose: Control which ports/addresses processes can bind to
Use Cases:
socket_listen - Server Control (Optional)SEC("lsm/socket_listen")
int BPF_PROG(aegis_socket_listen, struct socket *sock, int backlog)
Purpose: Additional control point for server sockets
Extend enum event_type:
enum event_type {
EVENT_EXEC = 1,
EVENT_BLOCK = 2,
// New network events
EVENT_NET_CONNECT_BLOCK = 10,
EVENT_NET_BIND_BLOCK = 11,
EVENT_NET_LISTEN_BLOCK = 12,
};
struct net_block_event {
// Process context (same as file events)
__u32 pid;
__u32 ppid;
__u64 start_time;
__u64 parent_start_time;
__u64 cgid;
char comm[16];
// Network specific
__u8 family; // AF_INET or AF_INET6
__u8 protocol; // IPPROTO_TCP, IPPROTO_UDP
__u16 local_port;
__u16 remote_port;
__u8 direction; // 0=egress, 1=ingress/bind
__u8 _pad;
union {
__be32 ipv4;
__u8 ipv6[16];
} local_addr;
union {
__be32 ipv4;
__u8 ipv6[16];
} remote_addr;
char action[8]; // "AUDIT" or "KILL"
char rule_type[16]; // "ip", "port", "cidr"
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 65536);
__type(key, __be32); // IPv4 address
__type(value, __u8); // flags (reserved)
} deny_ipv4 SEC(".maps");
struct ipv6_key {
__u8 addr[16];
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 65536);
__type(key, struct ipv6_key);
__type(value, __u8);
} deny_ipv6 SEC(".maps");
struct ipv4_lpm_key {
__u32 prefixlen;
__be32 addr;
};
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 16384);
__uint(map_flags, BPF_F_NO_PREALLOC);
__type(key, struct ipv4_lpm_key);
__type(value, __u8);
} deny_cidr_v4 SEC(".maps");
struct ipv6_lpm_key {
__u32 prefixlen;
__u8 addr[16];
};
struct {
__uint(type, BPF_MAP_TYPE_LPM_TRIE);
__uint(max_entries, 16384);
__uint(map_flags, BPF_F_NO_PREALLOC);
__type(key, struct ipv6_lpm_key);
__type(value, __u8);
} deny_cidr_v6 SEC(".maps");
struct port_key {
__u16 port;
__u8 protocol; // IPPROTO_TCP=6, IPPROTO_UDP=17, 0=any
__u8 direction; // 0=egress, 1=bind, 2=both
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 4096);
__type(key, struct port_key);
__type(value, __u8);
} deny_port SEC(".maps");
struct ip_port_key_v4 {
__be32 addr;
__u16 port;
__u8 protocol;
__u8 _pad;
};
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 32768);
__type(key, struct ip_port_key_v4);
__type(value, __u8);
} deny_ip_port_v4 SEC(".maps");
// Per-IP block counts
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 16384);
__type(key, __be32); // IPv4
__type(value, __u64);
} net_ip_stats SEC(".maps");
// Per-port block counts
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_HASH);
__uint(max_entries, 4096);
__type(key, __u16);
__type(value, __u64);
} net_port_stats SEC(".maps");
// Global network block stats
struct net_stats_entry {
__u64 connect_blocks;
__u64 bind_blocks;
__u64 listen_blocks;
__u64 ringbuf_drops;
};
struct {
__uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint(max_entries, 1);
__type(key, __u32);
__type(value, struct net_stats_entry);
} net_block_stats SEC(".maps");
// New pin paths in types.hpp
inline constexpr const char* kDenyIpv4Pin = "/sys/fs/bpf/aegisbpf/deny_ipv4";
inline constexpr const char* kDenyIpv6Pin = "/sys/fs/bpf/aegisbpf/deny_ipv6";
inline constexpr const char* kDenyCidrV4Pin = "/sys/fs/bpf/aegisbpf/deny_cidr_v4";
inline constexpr const char* kDenyCidrV6Pin = "/sys/fs/bpf/aegisbpf/deny_cidr_v6";
inline constexpr const char* kDenyPortPin = "/sys/fs/bpf/aegisbpf/deny_port";
inline constexpr const char* kDenyIpPortV4Pin = "/sys/fs/bpf/aegisbpf/deny_ip_port_v4";
inline constexpr const char* kNetBlockStatsPin = "/sys/fs/bpf/aegisbpf/net_block_stats";
inline constexpr const char* kNetIpStatsPin = "/sys/fs/bpf/aegisbpf/net_ip_stats";
inline constexpr const char* kNetPortStatsPin = "/sys/fs/bpf/aegisbpf/net_port_stats";
version=2
[deny_path]
/etc/shadow
/etc/passwd
[deny_inode]
8388609:131073
[allow_cgroup]
/sys/fs/cgroup/system.slice/docker.service
# ===== NEW NETWORK SECTIONS =====
[deny_ip]
# Single IPs
192.168.1.100
10.0.0.1
2001:db8::1
[deny_cidr]
# CIDR ranges
10.0.0.0/8
192.168.0.0/16
2001:db8::/32
[deny_port]
# port[:protocol[:direction]]
# protocol: tcp, udp, any (default: any)
# direction: egress, bind, both (default: both)
22
3389:tcp:egress
53:udp:egress
[deny_ip_port]
# ip:port[:protocol]
192.168.1.1:443
10.0.0.1:22:tcp
[allow_egress]
# Explicit egress allowlist (if deny-by-default mode)
# Only consulted if network_default=deny
8.8.8.8:53:udp
1.1.1.1:53:udp
// In types.hpp
struct NetworkPolicy {
std::vector<std::string> deny_ips; // Exact IPs
std::vector<std::string> deny_cidrs; // CIDR ranges
std::vector<PortRule> deny_ports; // Port rules
std::vector<IpPortRule> deny_ip_ports; // IP:port combos
std::vector<std::string> allow_egress; // Allowlist (optional)
bool network_enabled = false;
bool default_deny_egress = false; // Future: deny-by-default mode
};
struct PortRule {
uint16_t port;
uint8_t protocol; // 0=any, 6=tcp, 17=udp
uint8_t direction; // 0=egress, 1=bind, 2=both
};
struct IpPortRule {
std::string ip;
uint16_t port;
uint8_t protocol;
};
struct Policy {
int version = 0;
// Existing file rules
std::vector<std::string> deny_paths;
std::vector<InodeId> deny_inodes;
std::vector<std::string> allow_cgroup_paths;
std::vector<uint64_t> allow_cgroup_ids;
// New network rules
NetworkPolicy network;
};
# Network deny management
aegisbpf network deny add --ip 192.168.1.100
aegisbpf network deny add --cidr 10.0.0.0/8
aegisbpf network deny add --port 22 --protocol tcp --direction egress
aegisbpf network deny add --ip-port 192.168.1.1:443
aegisbpf network deny del --ip 192.168.1.100
aegisbpf network deny list
aegisbpf network deny clear
# Network statistics
aegisbpf network stats
aegisbpf network stats --by-ip
aegisbpf network stats --by-port
# Combined status
aegisbpf stats --all # File + network stats
src/
├── network_ops.hpp # Network BPF map operations
├── network_ops.cpp
├── network_policy.hpp # Network policy parsing
├── network_policy.cpp
├── network_events.hpp # Network event handling
├── network_events.cpp
└── network_types.hpp # Network-specific types
class BpfState {
public:
// Existing...
// New network maps
bpf_map* deny_ipv4 = nullptr;
bpf_map* deny_ipv6 = nullptr;
bpf_map* deny_cidr_v4 = nullptr;
bpf_map* deny_cidr_v6 = nullptr;
bpf_map* deny_port = nullptr;
bpf_map* deny_ip_port_v4 = nullptr;
bpf_map* net_block_stats = nullptr;
bpf_map* net_ip_stats = nullptr;
bpf_map* net_port_stats = nullptr;
// Reuse flags
bool deny_ipv4_reused = false;
bool deny_ipv6_reused = false;
// ... etc
};
// In events.cpp
void print_net_block_event(const NetBlockEvent& ev) {
std::ostringstream oss;
oss << "{\"type\":\"net_block\""
<< ",\"pid\":" << ev.pid
<< ",\"ppid\":" << ev.ppid
<< ",\"cgid\":" << ev.cgid
<< ",\"family\":\"" << (ev.family == AF_INET ? "ipv4" : "ipv6") << "\""
<< ",\"protocol\":\"" << protocol_name(ev.protocol) << "\""
<< ",\"remote_ip\":\"" << format_ip(ev) << "\""
<< ",\"remote_port\":" << ntohs(ev.remote_port)
<< ",\"direction\":\"" << (ev.direction ? "bind" : "egress") << "\""
<< ",\"action\":\"" << ev.action << "\""
<< ",\"rule_type\":\"" << ev.rule_type << "\""
<< ",\"comm\":\"" << json_escape(ev.comm) << "\"}";
// ... output to stdout/journald
}
// New metrics
aegisbpf_net_blocks_total{type="connect"}
aegisbpf_net_blocks_total{type="bind"}
aegisbpf_net_blocks_by_ip_total{ip="192.168.1.100"}
aegisbpf_net_blocks_by_port_total{port="22"}
aegisbpf_net_ringbuf_drops_total
aegisbpf_net_rules_total{type="ip"}
aegisbpf_net_rules_total{type="cidr"}
aegisbpf_net_rules_total{type="port"}
deny_ipv4, deny_port mapssocket_connect LSM hook (IPv4 only)socket_bind hook// Fast-path check order in socket_connect:
// 1. Cgroup allowlist (HASH lookup) - skip if trusted
// 2. Bloom filter for IPs (optional, reduces false lookups)
// 3. Exact IP match (HASH)
// 4. Port match (HASH)
// 5. CIDR match (LPM) - most expensive, do last
| Operation | Latency |
|---|---|
| Cgroup allowlist check | ~50-100ns |
| Exact IP lookup | ~50-150ns |
| Port lookup | ~50-100ns |
| CIDR LPM lookup | ~200-500ns |
| Total (worst case) | ~500-900ns |
| Map | Max Entries | Memory |
|---|---|---|
| deny_ipv4 | 65,536 | ~512KB |
| deny_ipv6 | 65,536 | ~1.5MB |
| deny_cidr_v4 | 16,384 | ~256KB |
| deny_cidr_v6 | 16,384 | ~512KB |
| deny_port | 4,096 | ~32KB |
# Test egress blocking
aegisbpf network deny add --ip 1.2.3.4
curl -m 1 http://1.2.3.4 # Should fail/timeout
# Test port blocking
aegisbpf network deny add --port 8080 --direction egress
curl -m 1 http://localhost:8080 # Should fail
# Test cgroup bypass
aegisbpf allow add /sys/fs/cgroup/trusted.slice
# Process in trusted.slice should connect despite rules
::ffff: addresses// bpf/aegis_net.bpf.c (new file or merged into aegis.bpf.c)
SEC("lsm/socket_connect")
int BPF_PROG(aegis_socket_connect, struct socket *sock,
struct sockaddr *address, int addrlen)
{
if (!address)
return 0;
__u16 family = address->sa_family;
if (family != AF_INET && family != AF_INET6)
return 0;
__u64 cgid = bpf_get_current_cgroup_id();
if (is_cgroup_allowed(cgid))
return 0;
__u8 audit = get_effective_audit_mode();
__be32 ipv4 = 0;
__u16 port = 0;
if (family == AF_INET) {
struct sockaddr_in *sin = (struct sockaddr_in *)address;
ipv4 = BPF_CORE_READ(sin, sin_addr.s_addr);
port = BPF_CORE_READ(sin, sin_port);
// Check exact IP
if (bpf_map_lookup_elem(&deny_ipv4, &ipv4))
goto deny;
// Check CIDR
struct ipv4_lpm_key lpm_key = {
.prefixlen = 32,
.addr = ipv4
};
if (bpf_map_lookup_elem(&deny_cidr_v4, &lpm_key))
goto deny;
}
// Check port
struct port_key pk = {
.port = bpf_ntohs(port),
.protocol = 0, // any
.direction = 0 // egress
};
if (bpf_map_lookup_elem(&deny_port, &pk))
goto deny;
return 0;
deny:
increment_net_connect_stats();
emit_net_block_event(cgid, family, ipv4, port, audit);
if (!audit)
bpf_send_signal(SIGKILL);
return audit ? 0 : -EPERM;
}
For existing deployments:
version=2 indicates network supportversion=1 policies work unchanged--network-audit-only flag