You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
601 lines
22 KiB
601 lines
22 KiB
#!/usr/bin/python
|
|
#
|
|
# Copyright 2016 The Android Open Source Project
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import ctypes
|
|
import errno
|
|
import os
|
|
import socket
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
import bpf
|
|
from bpf import BPF_ADD
|
|
from bpf import BPF_AND
|
|
from bpf import BPF_CGROUP_INET_EGRESS
|
|
from bpf import BPF_CGROUP_INET_INGRESS
|
|
from bpf import BPF_CGROUP_INET_SOCK_CREATE
|
|
from bpf import BPF_DW
|
|
from bpf import BPF_F_RDONLY
|
|
from bpf import BPF_F_WRONLY
|
|
from bpf import BPF_FUNC_get_current_uid_gid
|
|
from bpf import BPF_FUNC_get_socket_cookie
|
|
from bpf import BPF_FUNC_get_socket_uid
|
|
from bpf import BPF_FUNC_ktime_get_boot_ns
|
|
from bpf import BPF_FUNC_ktime_get_ns
|
|
from bpf import BPF_FUNC_map_lookup_elem
|
|
from bpf import BPF_FUNC_map_update_elem
|
|
from bpf import BPF_FUNC_skb_change_head
|
|
from bpf import BPF_JNE
|
|
from bpf import BPF_MAP_TYPE_HASH
|
|
from bpf import BPF_PROG_TYPE_CGROUP_SKB
|
|
from bpf import BPF_PROG_TYPE_CGROUP_SOCK
|
|
from bpf import BPF_PROG_TYPE_SCHED_CLS
|
|
from bpf import BPF_PROG_TYPE_SOCKET_FILTER
|
|
from bpf import BPF_REG_0
|
|
from bpf import BPF_REG_1
|
|
from bpf import BPF_REG_10
|
|
from bpf import BPF_REG_2
|
|
from bpf import BPF_REG_3
|
|
from bpf import BPF_REG_4
|
|
from bpf import BPF_REG_6
|
|
from bpf import BPF_REG_7
|
|
from bpf import BPF_STX
|
|
from bpf import BPF_W
|
|
from bpf import BPF_XADD
|
|
from bpf import BpfAlu64Imm
|
|
from bpf import BpfExitInsn
|
|
from bpf import BpfFuncCall
|
|
from bpf import BpfJumpImm
|
|
from bpf import BpfLdxMem
|
|
from bpf import BpfLoadMapFd
|
|
from bpf import BpfMov64Imm
|
|
from bpf import BpfMov64Reg
|
|
from bpf import BpfProgAttach
|
|
from bpf import BpfProgAttachSocket
|
|
from bpf import BpfProgDetach
|
|
from bpf import BpfProgLoad
|
|
from bpf import BpfRawInsn
|
|
from bpf import BpfStMem
|
|
from bpf import BpfStxMem
|
|
from bpf import CreateMap
|
|
from bpf import DeleteMap
|
|
from bpf import GetFirstKey
|
|
from bpf import GetNextKey
|
|
from bpf import LookupMap
|
|
from bpf import UpdateMap
|
|
import csocket
|
|
import net_test
|
|
from net_test import LINUX_VERSION
|
|
import sock_diag
|
|
|
|
libc = ctypes.CDLL(ctypes.util.find_library("c"), use_errno=True)
|
|
|
|
HAVE_EBPF_ACCOUNTING = bpf.HAVE_EBPF_4_9
|
|
HAVE_EBPF_SOCKET = bpf.HAVE_EBPF_4_14
|
|
|
|
# bpf_ktime_get_ns() was made non-GPL requiring in 5.8 and at the same time
|
|
# bpf_ktime_get_boot_ns() was added, both of these changes were backported to
|
|
# Android Common Kernel in 4.14.221, 4.19.175, 5.4.97.
|
|
# As such we require 4.14.222+ 4.19.176+ 5.4.98+ 5.8.0+,
|
|
# but since we only really care about LTS releases:
|
|
HAVE_EBPF_KTIME_GET_NS_APACHE2 = (
|
|
((LINUX_VERSION > (4, 14, 221)) and (LINUX_VERSION < (4, 19, 0))) or
|
|
((LINUX_VERSION > (4, 19, 175)) and (LINUX_VERSION < (5, 4, 0))) or
|
|
(LINUX_VERSION > (5, 4, 97))
|
|
)
|
|
HAVE_EBPF_KTIME_GET_BOOT_NS = HAVE_EBPF_KTIME_GET_NS_APACHE2
|
|
|
|
KEY_SIZE = 8
|
|
VALUE_SIZE = 4
|
|
TOTAL_ENTRIES = 20
|
|
TEST_UID = 54321
|
|
TEST_GID = 12345
|
|
# Offset to store the map key in stack register REG10
|
|
key_offset = -8
|
|
# Offset to store the map value in stack register REG10
|
|
value_offset = -16
|
|
|
|
|
|
# Debug usage only.
|
|
def PrintMapInfo(map_fd):
|
|
# A random key that the map does not contain.
|
|
key = 10086
|
|
while 1:
|
|
try:
|
|
next_key = GetNextKey(map_fd, key).value
|
|
value = LookupMap(map_fd, next_key)
|
|
print(repr(next_key) + " : " + repr(value.value)) # pylint: disable=superfluous-parens
|
|
key = next_key
|
|
except socket.error:
|
|
print("no value") # pylint: disable=superfluous-parens
|
|
break
|
|
|
|
|
|
# A dummy loopback function that causes a socket to send traffic to itself.
|
|
def SocketUDPLoopBack(packet_count, version, prog_fd):
|
|
family = {4: socket.AF_INET, 6: socket.AF_INET6}[version]
|
|
sock = socket.socket(family, socket.SOCK_DGRAM, 0)
|
|
if prog_fd is not None:
|
|
BpfProgAttachSocket(sock.fileno(), prog_fd)
|
|
net_test.SetNonBlocking(sock)
|
|
addr = {4: "127.0.0.1", 6: "::1"}[version]
|
|
sock.bind((addr, 0))
|
|
addr = sock.getsockname()
|
|
sockaddr = csocket.Sockaddr(addr)
|
|
for _ in range(packet_count):
|
|
sock.sendto("foo", addr)
|
|
data, retaddr = csocket.Recvfrom(sock, 4096, 0)
|
|
assert "foo" == data
|
|
assert sockaddr == retaddr
|
|
return sock
|
|
|
|
|
|
# The main code block for eBPF packet counting program. It takes a preloaded
|
|
# key from BPF_REG_0 and use it to look up the bpf map, if the element does not
|
|
# exist in the map yet, the program will update the map with a new <key, 1>
|
|
# pair. Otherwise it will jump to next code block to handle it.
|
|
# REG0: regiter storing return value from helper function and the final return
|
|
# value of eBPF program.
|
|
# REG1 - REG5: temporary register used for storing values and load parameters
|
|
# into eBPF helper function. After calling helper function, the value for these
|
|
# registers will be reset.
|
|
# REG6 - REG9: registers store values that will not be cleared when calling
|
|
# eBPF helper function.
|
|
# REG10: A stack stores values need to be accessed by the address. Program can
|
|
# retrieve the address of a value by specifying the position of the value in
|
|
# the stack.
|
|
def BpfFuncCountPacketInit(map_fd):
|
|
key_pos = BPF_REG_7
|
|
return [
|
|
# Get a preloaded key from BPF_REG_0 and store it at BPF_REG_7
|
|
BpfMov64Reg(key_pos, BPF_REG_10),
|
|
BpfAlu64Imm(BPF_ADD, key_pos, key_offset),
|
|
# Load map fd and look up the key in the map
|
|
BpfLoadMapFd(map_fd, BPF_REG_1),
|
|
BpfMov64Reg(BPF_REG_2, key_pos),
|
|
BpfFuncCall(BPF_FUNC_map_lookup_elem),
|
|
# if the map element already exist, jump out of this
|
|
# code block and let next part to handle it
|
|
BpfJumpImm(BPF_AND, BPF_REG_0, 0, 10),
|
|
BpfLoadMapFd(map_fd, BPF_REG_1),
|
|
BpfMov64Reg(BPF_REG_2, key_pos),
|
|
# Initial a new <key, value> pair with value equal to 1 and update to map
|
|
BpfStMem(BPF_W, BPF_REG_10, value_offset, 1),
|
|
BpfMov64Reg(BPF_REG_3, BPF_REG_10),
|
|
BpfAlu64Imm(BPF_ADD, BPF_REG_3, value_offset),
|
|
BpfMov64Imm(BPF_REG_4, 0),
|
|
BpfFuncCall(BPF_FUNC_map_update_elem)
|
|
]
|
|
|
|
|
|
INS_BPF_EXIT_BLOCK = [
|
|
BpfMov64Imm(BPF_REG_0, 0),
|
|
BpfExitInsn()
|
|
]
|
|
|
|
# Bpf instruction for cgroup bpf filter to accept a packet and exit.
|
|
INS_CGROUP_ACCEPT = [
|
|
# Set return value to 1 and exit.
|
|
BpfMov64Imm(BPF_REG_0, 1),
|
|
BpfExitInsn()
|
|
]
|
|
|
|
# Bpf instruction for socket bpf filter to accept a packet and exit.
|
|
INS_SK_FILTER_ACCEPT = [
|
|
# Precondition: BPF_REG_6 = sk_buff context
|
|
# Load the packet length from BPF_REG_6 and store it in BPF_REG_0 as the
|
|
# return value.
|
|
BpfLdxMem(BPF_W, BPF_REG_0, BPF_REG_6, 0),
|
|
BpfExitInsn()
|
|
]
|
|
|
|
# Update a existing map element with +1.
|
|
INS_PACK_COUNT_UPDATE = [
|
|
# Precondition: BPF_REG_0 = Value retrieved from BPF maps
|
|
# Add one to the corresponding eBPF value field for a specific eBPF key.
|
|
BpfMov64Reg(BPF_REG_2, BPF_REG_0),
|
|
BpfMov64Imm(BPF_REG_1, 1),
|
|
BpfRawInsn(BPF_STX | BPF_XADD | BPF_W, BPF_REG_2, BPF_REG_1, 0, 0),
|
|
]
|
|
|
|
INS_BPF_PARAM_STORE = [
|
|
BpfStxMem(BPF_DW, BPF_REG_10, BPF_REG_0, key_offset),
|
|
]
|
|
|
|
|
|
@unittest.skipUnless(HAVE_EBPF_ACCOUNTING,
|
|
"BPF helper function is not fully supported")
|
|
class BpfTest(net_test.NetworkTest):
|
|
|
|
def setUp(self):
|
|
super(BpfTest, self).setUp()
|
|
self.map_fd = -1
|
|
self.prog_fd = -1
|
|
self.sock = None
|
|
|
|
def tearDown(self):
|
|
if self.prog_fd >= 0:
|
|
os.close(self.prog_fd)
|
|
if self.map_fd >= 0:
|
|
os.close(self.map_fd)
|
|
if self.sock:
|
|
self.sock.close()
|
|
super(BpfTest, self).tearDown()
|
|
|
|
def testCreateMap(self):
|
|
key, value = 1, 1
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES)
|
|
UpdateMap(self.map_fd, key, value)
|
|
self.assertEqual(value, LookupMap(self.map_fd, key).value)
|
|
DeleteMap(self.map_fd, key)
|
|
self.assertRaisesErrno(errno.ENOENT, LookupMap, self.map_fd, key)
|
|
|
|
def CheckAllMapEntry(self, nonexistent_key, total_entries, value):
|
|
count = 0
|
|
key = nonexistent_key
|
|
while True:
|
|
if count == total_entries:
|
|
self.assertRaisesErrno(errno.ENOENT, GetNextKey, self.map_fd, key)
|
|
break
|
|
else:
|
|
result = GetNextKey(self.map_fd, key)
|
|
key = result.value
|
|
self.assertGreaterEqual(key, 0)
|
|
self.assertEqual(value, LookupMap(self.map_fd, key).value)
|
|
count += 1
|
|
|
|
def testIterateMap(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES)
|
|
value = 1024
|
|
for key in range(0, TOTAL_ENTRIES):
|
|
UpdateMap(self.map_fd, key, value)
|
|
for key in range(0, TOTAL_ENTRIES):
|
|
self.assertEqual(value, LookupMap(self.map_fd, key).value)
|
|
self.assertRaisesErrno(errno.ENOENT, LookupMap, self.map_fd, 101)
|
|
nonexistent_key = -1
|
|
self.CheckAllMapEntry(nonexistent_key, TOTAL_ENTRIES, value)
|
|
|
|
def testFindFirstMapKey(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES)
|
|
value = 1024
|
|
for key in range(0, TOTAL_ENTRIES):
|
|
UpdateMap(self.map_fd, key, value)
|
|
first_key = GetFirstKey(self.map_fd)
|
|
key = first_key.value
|
|
self.CheckAllMapEntry(key, TOTAL_ENTRIES - 1, value)
|
|
|
|
def testRdOnlyMap(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES, map_flags=BPF_F_RDONLY)
|
|
value = 1024
|
|
key = 1
|
|
self.assertRaisesErrno(errno.EPERM, UpdateMap, self.map_fd, key, value)
|
|
self.assertRaisesErrno(errno.ENOENT, LookupMap, self.map_fd, key)
|
|
|
|
def testWrOnlyMap(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES, map_flags=BPF_F_WRONLY)
|
|
value = 1024
|
|
key = 1
|
|
UpdateMap(self.map_fd, key, value)
|
|
self.assertRaisesErrno(errno.EPERM, LookupMap, self.map_fd, key)
|
|
|
|
def testProgLoad(self):
|
|
# Move skb to BPF_REG_6 for further usage
|
|
instructions = [
|
|
BpfMov64Reg(BPF_REG_6, BPF_REG_1)
|
|
]
|
|
instructions += INS_SK_FILTER_ACCEPT
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SOCKET_FILTER, instructions)
|
|
SocketUDPLoopBack(1, 4, self.prog_fd)
|
|
SocketUDPLoopBack(1, 6, self.prog_fd)
|
|
|
|
def testPacketBlock(self):
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SOCKET_FILTER, INS_BPF_EXIT_BLOCK)
|
|
self.assertRaisesErrno(errno.EAGAIN, SocketUDPLoopBack, 1, 4, self.prog_fd)
|
|
self.assertRaisesErrno(errno.EAGAIN, SocketUDPLoopBack, 1, 6, self.prog_fd)
|
|
|
|
def testPacketCount(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES)
|
|
key = 0xf0f0
|
|
# Set up instruction block with key loaded at BPF_REG_0.
|
|
instructions = [
|
|
BpfMov64Reg(BPF_REG_6, BPF_REG_1),
|
|
BpfMov64Imm(BPF_REG_0, key)
|
|
]
|
|
# Concatenate the generic packet count bpf program to it.
|
|
instructions += (INS_BPF_PARAM_STORE + BpfFuncCountPacketInit(self.map_fd)
|
|
+ INS_SK_FILTER_ACCEPT + INS_PACK_COUNT_UPDATE
|
|
+ INS_SK_FILTER_ACCEPT)
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SOCKET_FILTER, instructions)
|
|
packet_count = 10
|
|
SocketUDPLoopBack(packet_count, 4, self.prog_fd)
|
|
SocketUDPLoopBack(packet_count, 6, self.prog_fd)
|
|
self.assertEqual(packet_count * 2, LookupMap(self.map_fd, key).value)
|
|
|
|
##############################################################################
|
|
#
|
|
# Test for presence of kernel patch:
|
|
#
|
|
# ANDROID: net: bpf: Allow TC programs to call BPF_FUNC_skb_change_head
|
|
#
|
|
# 4.14: https://android-review.googlesource.com/c/kernel/common/+/1237789
|
|
# commit fe82848d9c1c887d2a84d3738c13e644d01b6d6f
|
|
#
|
|
# 4.19: https://android-review.googlesource.com/c/kernel/common/+/1237788
|
|
# commit 6e04d94ab72435b45c413daff63520fd724e260e
|
|
#
|
|
# 5.4: https://android-review.googlesource.com/c/kernel/common/+/1237787
|
|
# commit d730995e7bc5b4c10cc176235b704a274e6ec16f
|
|
#
|
|
# Upstream in Linux v5.8:
|
|
# net: bpf: Allow TC programs to call BPF_FUNC_skb_change_head
|
|
# commit 6f3f65d80dac8f2bafce2213005821fccdce194c
|
|
#
|
|
@unittest.skipUnless(bpf.HAVE_EBPF_4_14,
|
|
"no bpf_skb_change_head() support for pre-4.14 kernels")
|
|
def testSkbChangeHead(self):
|
|
# long bpf_skb_change_head(struct sk_buff *skb, u32 len, u64 flags)
|
|
instructions = [
|
|
BpfMov64Imm(BPF_REG_2, 14), # u32 len
|
|
BpfMov64Imm(BPF_REG_3, 0), # u64 flags
|
|
BpfFuncCall(BPF_FUNC_skb_change_head),
|
|
] + INS_BPF_EXIT_BLOCK
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SCHED_CLS, instructions,
|
|
b"Apache 2.0")
|
|
# No exceptions? Good.
|
|
|
|
def testKtimeGetNsGPL(self):
|
|
instructions = [BpfFuncCall(BPF_FUNC_ktime_get_ns)] + INS_BPF_EXIT_BLOCK
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SCHED_CLS, instructions)
|
|
# No exceptions? Good.
|
|
|
|
##############################################################################
|
|
#
|
|
# Test for presence of kernel patch:
|
|
#
|
|
# UPSTREAM: net: bpf: Make bpf_ktime_get_ns() available to non GPL programs
|
|
#
|
|
# 4.14: https://android-review.googlesource.com/c/kernel/common/+/1585269
|
|
# commit cbb4c73f9eab8f3c8ac29175d45c99ccba382e15
|
|
#
|
|
# 4.19: https://android-review.googlesource.com/c/kernel/common/+/1355243
|
|
# commit 272e21ccc9a92feeee80aff0587410a314b73c5b
|
|
#
|
|
# 5.4: https://android-review.googlesource.com/c/kernel/common/+/1355422
|
|
# commit 45217b91eaaa3a563247c4f470f4cb785de6b1c6
|
|
#
|
|
@unittest.skipUnless(HAVE_EBPF_KTIME_GET_NS_APACHE2,
|
|
"no bpf_ktime_get_ns() support for non-GPL programs")
|
|
def testKtimeGetNsApache2(self):
|
|
instructions = [BpfFuncCall(BPF_FUNC_ktime_get_ns)] + INS_BPF_EXIT_BLOCK
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SCHED_CLS, instructions,
|
|
b"Apache 2.0")
|
|
# No exceptions? Good.
|
|
|
|
##############################################################################
|
|
#
|
|
# Test for presence of kernel patch:
|
|
#
|
|
# BACKPORT: bpf: add bpf_ktime_get_boot_ns()
|
|
#
|
|
# 4.14: https://android-review.googlesource.com/c/kernel/common/+/1585587
|
|
# commit 34073d7a8ee47ca908b56e9a1d14ca0615fdfc09
|
|
#
|
|
# 4.19: https://android-review.googlesource.com/c/kernel/common/+/1585606
|
|
# commit 4812ec50935dfe59ba9f48a572e278dd0b02af68
|
|
#
|
|
# 5.4: https://android-review.googlesource.com/c/kernel/common/+/1585252
|
|
# commit 57b3f4830fb66a6038c4c1c66ca2e138fe8be231
|
|
#
|
|
@unittest.skipUnless(HAVE_EBPF_KTIME_GET_BOOT_NS,
|
|
"no bpf_ktime_get_boot_ns() support")
|
|
def testKtimeGetBootNs(self):
|
|
instructions = [
|
|
BpfFuncCall(BPF_FUNC_ktime_get_boot_ns),
|
|
] + INS_BPF_EXIT_BLOCK
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SCHED_CLS, instructions,
|
|
b"Apache 2.0")
|
|
# No exceptions? Good.
|
|
|
|
def testGetSocketCookie(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES)
|
|
# Move skb to REG6 for further usage, call helper function to get socket
|
|
# cookie of current skb and return the cookie at REG0 for next code block
|
|
instructions = [
|
|
BpfMov64Reg(BPF_REG_6, BPF_REG_1),
|
|
BpfFuncCall(BPF_FUNC_get_socket_cookie)
|
|
]
|
|
instructions += (INS_BPF_PARAM_STORE + BpfFuncCountPacketInit(self.map_fd)
|
|
+ INS_SK_FILTER_ACCEPT + INS_PACK_COUNT_UPDATE
|
|
+ INS_SK_FILTER_ACCEPT)
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SOCKET_FILTER, instructions)
|
|
packet_count = 10
|
|
def PacketCountByCookie(version):
|
|
self.sock = SocketUDPLoopBack(packet_count, version, self.prog_fd)
|
|
cookie = sock_diag.SockDiag.GetSocketCookie(self.sock)
|
|
self.assertEqual(packet_count, LookupMap(self.map_fd, cookie).value)
|
|
self.sock.close()
|
|
PacketCountByCookie(4)
|
|
PacketCountByCookie(6)
|
|
|
|
def testGetSocketUid(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES)
|
|
# Set up the instruction with uid at BPF_REG_0.
|
|
instructions = [
|
|
BpfMov64Reg(BPF_REG_6, BPF_REG_1),
|
|
BpfFuncCall(BPF_FUNC_get_socket_uid)
|
|
]
|
|
# Concatenate the generic packet count bpf program to it.
|
|
instructions += (INS_BPF_PARAM_STORE + BpfFuncCountPacketInit(self.map_fd)
|
|
+ INS_SK_FILTER_ACCEPT + INS_PACK_COUNT_UPDATE
|
|
+ INS_SK_FILTER_ACCEPT)
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_SOCKET_FILTER, instructions)
|
|
packet_count = 10
|
|
uid = TEST_UID
|
|
with net_test.RunAsUid(uid):
|
|
self.assertRaisesErrno(errno.ENOENT, LookupMap, self.map_fd, uid)
|
|
SocketUDPLoopBack(packet_count, 4, self.prog_fd)
|
|
self.assertEqual(packet_count, LookupMap(self.map_fd, uid).value)
|
|
DeleteMap(self.map_fd, uid)
|
|
SocketUDPLoopBack(packet_count, 6, self.prog_fd)
|
|
self.assertEqual(packet_count, LookupMap(self.map_fd, uid).value)
|
|
|
|
|
|
@unittest.skipUnless(HAVE_EBPF_ACCOUNTING,
|
|
"Cgroup BPF is not fully supported")
|
|
class BpfCgroupTest(net_test.NetworkTest):
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
super(BpfCgroupTest, cls).setUpClass()
|
|
cls._cg_dir = tempfile.mkdtemp(prefix="cg_bpf-")
|
|
cmd = "mount -t cgroup2 cg_bpf %s" % cls._cg_dir
|
|
try:
|
|
subprocess.check_call(cmd.split())
|
|
except subprocess.CalledProcessError:
|
|
# If an exception is thrown in setUpClass, the test fails and
|
|
# tearDownClass is not called.
|
|
os.rmdir(cls._cg_dir)
|
|
raise
|
|
cls._cg_fd = os.open(cls._cg_dir, os.O_DIRECTORY | os.O_RDONLY)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
os.close(cls._cg_fd)
|
|
subprocess.call(("umount %s" % cls._cg_dir).split())
|
|
os.rmdir(cls._cg_dir)
|
|
super(BpfCgroupTest, cls).tearDownClass()
|
|
|
|
def setUp(self):
|
|
super(BpfCgroupTest, self).setUp()
|
|
self.prog_fd = -1
|
|
self.map_fd = -1
|
|
|
|
def tearDown(self):
|
|
if self.prog_fd >= 0:
|
|
os.close(self.prog_fd)
|
|
if self.map_fd >= 0:
|
|
os.close(self.map_fd)
|
|
try:
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_EGRESS)
|
|
except socket.error:
|
|
pass
|
|
try:
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_INGRESS)
|
|
except socket.error:
|
|
pass
|
|
try:
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_SOCK_CREATE)
|
|
except socket.error:
|
|
pass
|
|
super(BpfCgroupTest, self).tearDown()
|
|
|
|
def testCgroupBpfAttach(self):
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SKB, INS_BPF_EXIT_BLOCK)
|
|
BpfProgAttach(self.prog_fd, self._cg_fd, BPF_CGROUP_INET_INGRESS)
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_INGRESS)
|
|
|
|
def testCgroupIngress(self):
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SKB, INS_BPF_EXIT_BLOCK)
|
|
BpfProgAttach(self.prog_fd, self._cg_fd, BPF_CGROUP_INET_INGRESS)
|
|
self.assertRaisesErrno(errno.EAGAIN, SocketUDPLoopBack, 1, 4, None)
|
|
self.assertRaisesErrno(errno.EAGAIN, SocketUDPLoopBack, 1, 6, None)
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_INGRESS)
|
|
SocketUDPLoopBack(1, 4, None)
|
|
SocketUDPLoopBack(1, 6, None)
|
|
|
|
def testCgroupEgress(self):
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SKB, INS_BPF_EXIT_BLOCK)
|
|
BpfProgAttach(self.prog_fd, self._cg_fd, BPF_CGROUP_INET_EGRESS)
|
|
self.assertRaisesErrno(errno.EPERM, SocketUDPLoopBack, 1, 4, None)
|
|
self.assertRaisesErrno(errno.EPERM, SocketUDPLoopBack, 1, 6, None)
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_EGRESS)
|
|
SocketUDPLoopBack(1, 4, None)
|
|
SocketUDPLoopBack(1, 6, None)
|
|
|
|
def testCgroupBpfUid(self):
|
|
self.map_fd = CreateMap(BPF_MAP_TYPE_HASH, KEY_SIZE, VALUE_SIZE,
|
|
TOTAL_ENTRIES)
|
|
# Similar to the program used in testGetSocketUid.
|
|
instructions = [
|
|
BpfMov64Reg(BPF_REG_6, BPF_REG_1),
|
|
BpfFuncCall(BPF_FUNC_get_socket_uid)
|
|
]
|
|
instructions += (INS_BPF_PARAM_STORE + BpfFuncCountPacketInit(self.map_fd)
|
|
+ INS_CGROUP_ACCEPT + INS_PACK_COUNT_UPDATE
|
|
+ INS_CGROUP_ACCEPT)
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SKB, instructions)
|
|
BpfProgAttach(self.prog_fd, self._cg_fd, BPF_CGROUP_INET_INGRESS)
|
|
packet_count = 20
|
|
uid = TEST_UID
|
|
with net_test.RunAsUid(uid):
|
|
self.assertRaisesErrno(errno.ENOENT, LookupMap, self.map_fd, uid)
|
|
SocketUDPLoopBack(packet_count, 4, None)
|
|
self.assertEqual(packet_count, LookupMap(self.map_fd, uid).value)
|
|
DeleteMap(self.map_fd, uid)
|
|
SocketUDPLoopBack(packet_count, 6, None)
|
|
self.assertEqual(packet_count, LookupMap(self.map_fd, uid).value)
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_INGRESS)
|
|
|
|
def checkSocketCreate(self, family, socktype, success):
|
|
try:
|
|
sock = socket.socket(family, socktype, 0)
|
|
sock.close()
|
|
except socket.error as e:
|
|
if success:
|
|
self.fail("Failed to create socket family=%d type=%d err=%s" %
|
|
(family, socktype, os.strerror(e.errno)))
|
|
return
|
|
if not success:
|
|
self.fail("unexpected socket family=%d type=%d created, should be blocked"
|
|
% (family, socktype))
|
|
|
|
def trySocketCreate(self, success):
|
|
for family in [socket.AF_INET, socket.AF_INET6]:
|
|
for socktype in [socket.SOCK_DGRAM, socket.SOCK_STREAM]:
|
|
self.checkSocketCreate(family, socktype, success)
|
|
|
|
@unittest.skipUnless(HAVE_EBPF_SOCKET,
|
|
"Cgroup BPF socket is not supported")
|
|
def testCgroupSocketCreateBlock(self):
|
|
instructions = [
|
|
BpfFuncCall(BPF_FUNC_get_current_uid_gid),
|
|
BpfAlu64Imm(BPF_AND, BPF_REG_0, 0xfffffff),
|
|
BpfJumpImm(BPF_JNE, BPF_REG_0, TEST_UID, 2),
|
|
]
|
|
instructions += INS_BPF_EXIT_BLOCK + INS_CGROUP_ACCEPT
|
|
self.prog_fd = BpfProgLoad(BPF_PROG_TYPE_CGROUP_SOCK, instructions)
|
|
BpfProgAttach(self.prog_fd, self._cg_fd, BPF_CGROUP_INET_SOCK_CREATE)
|
|
with net_test.RunAsUid(TEST_UID):
|
|
# Socket creation with target uid should fail
|
|
self.trySocketCreate(False)
|
|
# Socket create with different uid should success
|
|
self.trySocketCreate(True)
|
|
BpfProgDetach(self._cg_fd, BPF_CGROUP_INET_SOCK_CREATE)
|
|
with net_test.RunAsUid(TEST_UID):
|
|
self.trySocketCreate(True)
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|