mirror of
https://github.com/AuxXxilium/linux_dsm_epyc7002.git
synced 2024-12-15 07:56:41 +07:00
bpf: selftests add sockmap tests
This generates a set of sockets, attaches BPF programs, and sends some simple traffic using basic send/recv pattern. Additionally, we do a bunch of negative tests to ensure adding/removing socks out of the sockmap fail correctly. Signed-off-by: John Fastabend <john.fastabend@gmail.com> Signed-off-by: David S. Miller <davem@davemloft.net>
This commit is contained in:
parent
41bc94f535
commit
6f6d33f3b3
@ -1744,3 +1744,32 @@ long libbpf_get_error(const void *ptr)
|
||||
return PTR_ERR(ptr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int bpf_prog_load(const char *file, enum bpf_prog_type type,
|
||||
struct bpf_object **pobj, int *prog_fd)
|
||||
{
|
||||
struct bpf_program *prog;
|
||||
struct bpf_object *obj;
|
||||
int err;
|
||||
|
||||
obj = bpf_object__open(file);
|
||||
if (IS_ERR(obj))
|
||||
return -ENOENT;
|
||||
|
||||
prog = bpf_program__next(NULL, obj);
|
||||
if (!prog) {
|
||||
bpf_object__close(obj);
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
bpf_program__set_type(prog, type);
|
||||
err = bpf_object__load(obj);
|
||||
if (err) {
|
||||
bpf_object__close(obj);
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
*pobj = obj;
|
||||
*prog_fd = bpf_program__fd(prog);
|
||||
return 0;
|
||||
}
|
||||
|
@ -243,4 +243,6 @@ int bpf_map__pin(struct bpf_map *map, const char *path);
|
||||
|
||||
long libbpf_get_error(const void *ptr);
|
||||
|
||||
int bpf_prog_load(const char *file, enum bpf_prog_type type,
|
||||
struct bpf_object **pobj, int *prog_fd);
|
||||
#endif
|
||||
|
@ -15,7 +15,7 @@ TEST_GEN_PROGS = test_verifier test_tag test_maps test_lru_map test_lpm_map test
|
||||
test_align
|
||||
|
||||
TEST_GEN_FILES = test_pkt_access.o test_xdp.o test_l4lb.o test_tcp_estats.o test_obj_id.o \
|
||||
test_pkt_md_access.o test_xdp_redirect.o
|
||||
test_pkt_md_access.o test_xdp_redirect.o sockmap_parse_prog.o sockmap_verdict_prog.o
|
||||
|
||||
TEST_PROGS := test_kmod.sh test_xdp_redirect.sh
|
||||
|
||||
|
38
tools/testing/selftests/bpf/sockmap_parse_prog.c
Normal file
38
tools/testing/selftests/bpf/sockmap_parse_prog.c
Normal file
@ -0,0 +1,38 @@
|
||||
#include <linux/bpf.h>
|
||||
#include "bpf_helpers.h"
|
||||
#include "bpf_util.h"
|
||||
#include "bpf_endian.h"
|
||||
|
||||
int _version SEC("version") = 1;
|
||||
|
||||
#define bpf_printk(fmt, ...) \
|
||||
({ \
|
||||
char ____fmt[] = fmt; \
|
||||
bpf_trace_printk(____fmt, sizeof(____fmt), \
|
||||
##__VA_ARGS__); \
|
||||
})
|
||||
|
||||
SEC("sk_skb1")
|
||||
int bpf_prog1(struct __sk_buff *skb)
|
||||
{
|
||||
void *data_end = (void *)(long) skb->data_end;
|
||||
void *data = (void *)(long) skb->data;
|
||||
__u32 lport = skb->local_port;
|
||||
__u32 rport = skb->remote_port;
|
||||
char *d = data;
|
||||
|
||||
if (data + 8 > data_end)
|
||||
return skb->len;
|
||||
|
||||
/* This write/read is a bit pointless but tests the verifier and
|
||||
* strparser handler for read/write pkt data and access into sk
|
||||
* fields.
|
||||
*/
|
||||
d[0] = 1;
|
||||
|
||||
bpf_printk("data[0] = (%u): local_port %i remote %i\n",
|
||||
d[0], lport, bpf_ntohl(rport));
|
||||
return skb->len;
|
||||
}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
48
tools/testing/selftests/bpf/sockmap_verdict_prog.c
Normal file
48
tools/testing/selftests/bpf/sockmap_verdict_prog.c
Normal file
@ -0,0 +1,48 @@
|
||||
#include <linux/bpf.h>
|
||||
#include "bpf_helpers.h"
|
||||
#include "bpf_util.h"
|
||||
#include "bpf_endian.h"
|
||||
|
||||
int _version SEC("version") = 1;
|
||||
|
||||
#define bpf_printk(fmt, ...) \
|
||||
({ \
|
||||
char ____fmt[] = fmt; \
|
||||
bpf_trace_printk(____fmt, sizeof(____fmt), \
|
||||
##__VA_ARGS__); \
|
||||
})
|
||||
|
||||
struct bpf_map_def SEC("maps") sock_map = {
|
||||
.type = BPF_MAP_TYPE_SOCKMAP,
|
||||
.key_size = sizeof(int),
|
||||
.value_size = sizeof(int),
|
||||
.max_entries = 20,
|
||||
};
|
||||
|
||||
SEC("sk_skb2")
|
||||
int bpf_prog2(struct __sk_buff *skb)
|
||||
{
|
||||
void *data_end = (void *)(long) skb->data_end;
|
||||
void *data = (void *)(long) skb->data;
|
||||
__u32 lport = skb->local_port;
|
||||
__u32 rport = skb->remote_port;
|
||||
char *d = data;
|
||||
|
||||
if (data + 8 > data_end)
|
||||
return SK_DROP;
|
||||
|
||||
d[0] = 0xd;
|
||||
d[1] = 0xe;
|
||||
d[2] = 0xa;
|
||||
d[3] = 0xd;
|
||||
d[4] = 0xb;
|
||||
d[5] = 0xe;
|
||||
d[6] = 0xe;
|
||||
d[7] = 0xf;
|
||||
|
||||
bpf_printk("data[0] = (%u): local_port %i remote %i\n",
|
||||
d[0], lport, bpf_ntohl(rport));
|
||||
return bpf_sk_redirect_map(&sock_map, 5, 0);
|
||||
}
|
||||
|
||||
char _license[] SEC("license") = "GPL";
|
@ -22,6 +22,7 @@
|
||||
#include <linux/bpf.h>
|
||||
|
||||
#include <bpf/bpf.h>
|
||||
#include <bpf/libbpf.h>
|
||||
#include "bpf_util.h"
|
||||
|
||||
static int map_flags;
|
||||
@ -453,6 +454,312 @@ static void test_devmap(int task, void *data)
|
||||
close(fd);
|
||||
}
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/select.h>
|
||||
#include <linux/err.h>
|
||||
#define SOCKMAP_PARSE_PROG "./sockmap_parse_prog.o"
|
||||
#define SOCKMAP_VERDICT_PROG "./sockmap_verdict_prog.o"
|
||||
static void test_sockmap(int task, void *data)
|
||||
{
|
||||
int ports[] = {50200, 50201, 50202, 50204};
|
||||
int err, i, fd, sfd[6] = {0xdeadbeef};
|
||||
char buf[] = "hello sockmap user\n";
|
||||
int one = 1, map_fd, s, sc, rc;
|
||||
int parse_prog, verdict_prog;
|
||||
struct bpf_map *bpf_map;
|
||||
struct sockaddr_in addr;
|
||||
struct bpf_object *obj;
|
||||
struct timeval to;
|
||||
__u32 key, value;
|
||||
fd_set w;
|
||||
|
||||
/* Create some sockets to use with sockmap */
|
||||
for (i = 0; i < 2; i++) {
|
||||
sfd[i] = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sfd[i] < 0)
|
||||
goto out;
|
||||
err = setsockopt(sfd[i], SOL_SOCKET, SO_REUSEADDR,
|
||||
(char *)&one, sizeof(one));
|
||||
if (err) {
|
||||
printf("failed to setsockopt\n");
|
||||
goto out;
|
||||
}
|
||||
err = ioctl(sfd[i], FIONBIO, (char *)&one);
|
||||
if (err < 0) {
|
||||
printf("failed to ioctl\n");
|
||||
goto out;
|
||||
}
|
||||
memset(&addr, 0, sizeof(struct sockaddr_in));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
addr.sin_port = htons(ports[i]);
|
||||
err = bind(sfd[i], (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (err < 0) {
|
||||
printf("failed to bind: err %i: %i:%i\n",
|
||||
err, i, sfd[i]);
|
||||
goto out;
|
||||
}
|
||||
err = listen(sfd[i], 32);
|
||||
if (err < 0) {
|
||||
printf("failed to listeen\n");
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
for (i = 2; i < 4; i++) {
|
||||
sfd[i] = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sfd[i] < 0)
|
||||
goto out;
|
||||
err = setsockopt(sfd[i], SOL_SOCKET, SO_REUSEADDR,
|
||||
(char *)&one, sizeof(one));
|
||||
if (err) {
|
||||
printf("set sock opt\n");
|
||||
goto out;
|
||||
}
|
||||
memset(&addr, 0, sizeof(struct sockaddr_in));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
|
||||
addr.sin_port = htons(ports[i - 2]);
|
||||
err = connect(sfd[i], (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (err) {
|
||||
printf("failed to conenct\n");
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (i = 4; i < 6; i++) {
|
||||
sfd[i] = accept(sfd[i - 4], NULL, NULL);
|
||||
if (sfd[i] < 0) {
|
||||
printf("accept failed\n");
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test sockmap with connected sockets */
|
||||
fd = bpf_create_map(BPF_MAP_TYPE_SOCKMAP,
|
||||
sizeof(key), sizeof(value),
|
||||
6, 0);
|
||||
if (fd < 0) {
|
||||
printf("Failed to create sockmap %i\n", fd);
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
/* Nothing attached so these should fail */
|
||||
for (i = 0; i < 6; i++) {
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_ANY);
|
||||
if (!err) {
|
||||
printf("Failed invalid update sockmap '%i:%i'\n",
|
||||
i, sfd[i]);
|
||||
goto out_sockmap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test attaching bad fds */
|
||||
err = __bpf_prog_attach(-1, -2, fd, BPF_CGROUP_SMAP_INGRESS, 0);
|
||||
if (!err) {
|
||||
printf("Failed invalid prog attach\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
/* Load SK_SKB program and Attach */
|
||||
err = bpf_prog_load(SOCKMAP_PARSE_PROG,
|
||||
BPF_PROG_TYPE_SK_SKB, &obj, &parse_prog);
|
||||
if (err) {
|
||||
printf("Failed to load SK_SKB parse prog\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = bpf_prog_load(SOCKMAP_VERDICT_PROG,
|
||||
BPF_PROG_TYPE_SK_SKB, &obj, &verdict_prog);
|
||||
if (err) {
|
||||
printf("Failed to load SK_SKB verdict prog\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
bpf_map = bpf_object__find_map_by_name(obj, "sock_map");
|
||||
if (IS_ERR(bpf_map)) {
|
||||
printf("Failed to load map from verdict prog\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
map_fd = bpf_map__fd(bpf_map);
|
||||
if (map_fd < 0) {
|
||||
printf("Failed to get map fd\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = __bpf_prog_attach(parse_prog, verdict_prog, map_fd,
|
||||
BPF_CGROUP_SMAP_INGRESS, 0);
|
||||
if (err) {
|
||||
printf("Failed bpf prog attach\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
/* Test map update elem */
|
||||
for (i = 0; i < 6; i++) {
|
||||
err = bpf_map_update_elem(map_fd, &i, &sfd[i], BPF_ANY);
|
||||
if (err) {
|
||||
printf("Failed map_fd update sockmap %i '%i:%i'\n",
|
||||
err, i, sfd[i]);
|
||||
goto out_sockmap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test map delete elem and remove send/recv sockets */
|
||||
for (i = 2; i < 4; i++) {
|
||||
err = bpf_map_delete_elem(map_fd, &i);
|
||||
if (err) {
|
||||
printf("Failed delete sockmap %i '%i:%i'\n",
|
||||
err, i, sfd[i]);
|
||||
goto out_sockmap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test map send/recv */
|
||||
sc = send(sfd[2], buf, 10, 0);
|
||||
if (sc < 0) {
|
||||
printf("Failed sockmap send\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
FD_ZERO(&w);
|
||||
FD_SET(sfd[3], &w);
|
||||
to.tv_sec = 1;
|
||||
to.tv_usec = 0;
|
||||
s = select(sfd[3] + 1, &w, NULL, NULL, &to);
|
||||
if (s == -1) {
|
||||
perror("Failed sockmap select()");
|
||||
goto out_sockmap;
|
||||
} else if (!s) {
|
||||
printf("Failed sockmap unexpected timeout\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
if (!FD_ISSET(sfd[3], &w)) {
|
||||
printf("Failed sockmap select/recv\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
rc = recv(sfd[3], buf, sizeof(buf), 0);
|
||||
if (rc < 0) {
|
||||
printf("Failed sockmap recv\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
/* Delete the reset of the elems include some NULL elems */
|
||||
for (i = 0; i < 6; i++) {
|
||||
err = bpf_map_delete_elem(map_fd, &i);
|
||||
if (err && (i == 0 || i == 1 || i >= 4)) {
|
||||
printf("Failed delete sockmap %i '%i:%i'\n",
|
||||
err, i, sfd[i]);
|
||||
goto out_sockmap;
|
||||
} else if (!err && (i == 2 || i == 3)) {
|
||||
printf("Failed null delete sockmap %i '%i:%i'\n",
|
||||
err, i, sfd[i]);
|
||||
goto out_sockmap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test having multiple SMAPs open and active on same fds */
|
||||
err = __bpf_prog_attach(parse_prog, verdict_prog, fd,
|
||||
BPF_CGROUP_SMAP_INGRESS, 0);
|
||||
if (err) {
|
||||
printf("Failed fd bpf prog attach\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
for (i = 0; i < 6; i++) {
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_ANY);
|
||||
if (err) {
|
||||
printf("Failed fd update sockmap %i '%i:%i'\n",
|
||||
err, i, sfd[i]);
|
||||
goto out_sockmap;
|
||||
}
|
||||
}
|
||||
|
||||
/* Test duplicate socket add of NOEXIST, ANY and EXIST */
|
||||
i = 0;
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_NOEXIST);
|
||||
if (!err) {
|
||||
printf("Failed BPF_NOEXIST create\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_ANY);
|
||||
if (err) {
|
||||
printf("Failed sockmap update BPF_ANY\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_EXIST);
|
||||
if (err) {
|
||||
printf("Failed sockmap update BPF_EXIST\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
/* The above were pushing fd into same slot try different slot now */
|
||||
i = 2;
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_NOEXIST);
|
||||
if (!err) {
|
||||
printf("Failed BPF_NOEXIST create\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_ANY);
|
||||
if (err) {
|
||||
printf("Failed sockmap update BPF_ANY\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = bpf_map_update_elem(fd, &i, &sfd[i], BPF_EXIST);
|
||||
if (err) {
|
||||
printf("Failed sockmap update BPF_EXIST\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
/* Try pushing fd into different map, this is not allowed at the
|
||||
* moment. Which programs would we use?
|
||||
*/
|
||||
err = bpf_map_update_elem(map_fd, &i, &sfd[i], BPF_NOEXIST);
|
||||
if (!err) {
|
||||
printf("Failed BPF_NOEXIST create\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = bpf_map_update_elem(map_fd, &i, &sfd[i], BPF_ANY);
|
||||
if (!err) {
|
||||
printf("Failed sockmap update BPF_ANY\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
err = bpf_map_update_elem(map_fd, &i, &sfd[i], BPF_EXIST);
|
||||
if (!err) {
|
||||
printf("Failed sockmap update BPF_EXIST\n");
|
||||
goto out_sockmap;
|
||||
}
|
||||
|
||||
/* Test map close sockets */
|
||||
for (i = 0; i < 6; i++)
|
||||
close(sfd[i]);
|
||||
close(fd);
|
||||
close(map_fd);
|
||||
bpf_object__close(obj);
|
||||
return;
|
||||
out:
|
||||
for (i = 0; i < 6; i++)
|
||||
close(sfd[i]);
|
||||
printf("Failed to create sockmap '%i:%s'!\n", i, strerror(errno));
|
||||
exit(1);
|
||||
out_sockmap:
|
||||
for (i = 0; i < 6; i++)
|
||||
close(sfd[i]);
|
||||
close(fd);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
#define MAP_SIZE (32 * 1024)
|
||||
|
||||
static void test_map_large(void)
|
||||
@ -621,6 +928,7 @@ static void run_all_tests(void)
|
||||
test_arraymap_percpu_many_keys();
|
||||
|
||||
test_devmap(0, NULL);
|
||||
test_sockmap(0, NULL);
|
||||
|
||||
test_map_large();
|
||||
test_map_parallel();
|
||||
|
@ -75,39 +75,6 @@ static struct {
|
||||
__ret; \
|
||||
})
|
||||
|
||||
static int bpf_prog_load(const char *file, enum bpf_prog_type type,
|
||||
struct bpf_object **pobj, int *prog_fd)
|
||||
{
|
||||
struct bpf_program *prog;
|
||||
struct bpf_object *obj;
|
||||
int err;
|
||||
|
||||
obj = bpf_object__open(file);
|
||||
if (IS_ERR(obj)) {
|
||||
error_cnt++;
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
prog = bpf_program__next(NULL, obj);
|
||||
if (!prog) {
|
||||
bpf_object__close(obj);
|
||||
error_cnt++;
|
||||
return -ENOENT;
|
||||
}
|
||||
|
||||
bpf_program__set_type(prog, type);
|
||||
err = bpf_object__load(obj);
|
||||
if (err) {
|
||||
bpf_object__close(obj);
|
||||
error_cnt++;
|
||||
return -EINVAL;
|
||||
}
|
||||
|
||||
*pobj = obj;
|
||||
*prog_fd = bpf_program__fd(prog);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int bpf_find_map(const char *test, struct bpf_object *obj,
|
||||
const char *name)
|
||||
{
|
||||
@ -130,8 +97,10 @@ static void test_pkt_access(void)
|
||||
int err, prog_fd;
|
||||
|
||||
err = bpf_prog_load(file, BPF_PROG_TYPE_SCHED_CLS, &obj, &prog_fd);
|
||||
if (err)
|
||||
if (err) {
|
||||
error_cnt++;
|
||||
return;
|
||||
}
|
||||
|
||||
err = bpf_prog_test_run(prog_fd, 100000, &pkt_v4, sizeof(pkt_v4),
|
||||
NULL, NULL, &retval, &duration);
|
||||
@ -162,8 +131,10 @@ static void test_xdp(void)
|
||||
int err, prog_fd, map_fd;
|
||||
|
||||
err = bpf_prog_load(file, BPF_PROG_TYPE_XDP, &obj, &prog_fd);
|
||||
if (err)
|
||||
if (err) {
|
||||
error_cnt++;
|
||||
return;
|
||||
}
|
||||
|
||||
map_fd = bpf_find_map(__func__, obj, "vip2tnl");
|
||||
if (map_fd < 0)
|
||||
@ -223,8 +194,10 @@ static void test_l4lb(void)
|
||||
u32 *magic = (u32 *)buf;
|
||||
|
||||
err = bpf_prog_load(file, BPF_PROG_TYPE_SCHED_CLS, &obj, &prog_fd);
|
||||
if (err)
|
||||
if (err) {
|
||||
error_cnt++;
|
||||
return;
|
||||
}
|
||||
|
||||
map_fd = bpf_find_map(__func__, obj, "vip_map");
|
||||
if (map_fd < 0)
|
||||
@ -280,8 +253,10 @@ static void test_tcp_estats(void)
|
||||
|
||||
err = bpf_prog_load(file, BPF_PROG_TYPE_TRACEPOINT, &obj, &prog_fd);
|
||||
CHECK(err, "", "err %d errno %d\n", err, errno);
|
||||
if (err)
|
||||
if (err) {
|
||||
error_cnt++;
|
||||
return;
|
||||
}
|
||||
|
||||
bpf_object__close(obj);
|
||||
}
|
||||
@ -336,6 +311,8 @@ static void test_bpf_obj_id(void)
|
||||
/* test_obj_id.o is a dumb prog. It should never fail
|
||||
* to load.
|
||||
*/
|
||||
if (err)
|
||||
error_cnt++;
|
||||
assert(!err);
|
||||
|
||||
/* Check getting prog info */
|
||||
@ -496,8 +473,10 @@ static void test_pkt_md_access(void)
|
||||
int err, prog_fd;
|
||||
|
||||
err = bpf_prog_load(file, BPF_PROG_TYPE_SCHED_CLS, &obj, &prog_fd);
|
||||
if (err)
|
||||
if (err) {
|
||||
error_cnt++;
|
||||
return;
|
||||
}
|
||||
|
||||
err = bpf_prog_test_run(prog_fd, 10, &pkt_v4, sizeof(pkt_v4),
|
||||
NULL, NULL, &retval, &duration);
|
||||
|
Loading…
Reference in New Issue
Block a user