diff options
| author | tongzongzhen <[email protected]> | 2024-08-27 18:22:25 +0800 |
|---|---|---|
| committer | tongzongzhen <[email protected]> | 2024-08-27 18:22:25 +0800 |
| commit | c1bef60e76175c7e4662506c53502fbd8cb0139b (patch) | |
| tree | 4384709c1d1a4136a9245ab59e05fdbbc65fcfa4 | |
| parent | 5abcfe283a84bb280f71b623d3049382a28d88d8 (diff) | |
init dummy_ebpf
| -rw-r--r-- | .clang-format | 2 | ||||
| -rw-r--r-- | dummy_ebpf/CMakeLists.txt | 10 | ||||
| -rw-r--r-- | dummy_ebpf/include/config.h | 16 | ||||
| -rw-r--r-- | dummy_ebpf/include/device.h | 22 | ||||
| -rw-r--r-- | dummy_ebpf/include/tap.h | 7 | ||||
| -rw-r--r-- | dummy_ebpf/include/xdp_prog_user.h | 3 | ||||
| -rw-r--r-- | dummy_ebpf/send.py | 15 | ||||
| -rw-r--r-- | dummy_ebpf/src/CMakeLists.txt | 29 | ||||
| -rw-r--r-- | dummy_ebpf/src/config.c | 31 | ||||
| -rw-r--r-- | dummy_ebpf/src/device.c | 163 | ||||
| -rw-r--r-- | dummy_ebpf/src/main.c | 30 | ||||
| -rw-r--r-- | dummy_ebpf/src/tap.c | 166 | ||||
| -rw-r--r-- | dummy_ebpf/src/xdp_prog_kernel.c | 49 | ||||
| -rw-r--r-- | dummy_ebpf/src/xdp_prog_user.c | 62 |
14 files changed, 605 insertions, 0 deletions
diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..07d9df6 --- /dev/null +++ b/.clang-format @@ -0,0 +1,2 @@ +BasedOnStyle: LLVM +ColumnLimit: 100
\ No newline at end of file diff --git a/dummy_ebpf/CMakeLists.txt b/dummy_ebpf/CMakeLists.txt new file mode 100644 index 0000000..b0f1f08 --- /dev/null +++ b/dummy_ebpf/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.10) + +project(dummy_redirect) + +find_package(PkgConfig) +pkg_check_modules(NL3 REQUIRED libnl-3.0 libnl-genl-3.0 libnl-route-3.0) + +include_directories(include) + +add_subdirectory(src)
\ No newline at end of file diff --git a/dummy_ebpf/include/config.h b/dummy_ebpf/include/config.h new file mode 100644 index 0000000..d45e6f1 --- /dev/null +++ b/dummy_ebpf/include/config.h @@ -0,0 +1,16 @@ +#pragma once +#include <linux/limits.h> + +#include "device.h" + +#define PROG_NAME_MAXSIZE 32 + +struct config { + int nr_device; + struct device_desc devices[DEVICE_MAX_NUM]; + char open_filename[PATH_MAX]; + char prog_name[PROG_NAME_MAXSIZE]; +}; + +void global_config_init(); +const struct config *global_config_get();
\ No newline at end of file diff --git a/dummy_ebpf/include/device.h b/dummy_ebpf/include/device.h new file mode 100644 index 0000000..dc977ab --- /dev/null +++ b/dummy_ebpf/include/device.h @@ -0,0 +1,22 @@ +#pragma once +#include <net/if.h> +#include <stdint.h> + +#define DEVICE_MAX_NUM 10 + +struct device_desc { + uint8_t key; + char device_type[20]; + char device_name[IF_NAMESIZE]; +}; + +const struct device { + int nr_device; + int device_fds[DEVICE_MAX_NUM]; + int device_index[DEVICE_MAX_NUM]; + struct device_desc device_descs[DEVICE_MAX_NUM]; +}; + +void device_init(); +const struct device *device_inst_get(); +int xdp_device_index_get();
\ No newline at end of file diff --git a/dummy_ebpf/include/tap.h b/dummy_ebpf/include/tap.h new file mode 100644 index 0000000..d0016ca --- /dev/null +++ b/dummy_ebpf/include/tap.h @@ -0,0 +1,7 @@ +#pragma once + +#include <linux/if_tun.h> + +int tap_alloc(const char *ifname, int flags); +int tap_set_mac(const unsigned char *interface_name, const unsigned char *str_macaddr); +int tap_set_ip(const unsigned char *interface_name, const unsigned char *ipaddr);
\ No newline at end of file diff --git a/dummy_ebpf/include/xdp_prog_user.h b/dummy_ebpf/include/xdp_prog_user.h new file mode 100644 index 0000000..a423483 --- /dev/null +++ b/dummy_ebpf/include/xdp_prog_user.h @@ -0,0 +1,3 @@ +#pragma once + +struct xdp_program *load_bpf_and_xdp_attach();
\ No newline at end of file diff --git a/dummy_ebpf/send.py b/dummy_ebpf/send.py new file mode 100644 index 0000000..fb24d15 --- /dev/null +++ b/dummy_ebpf/send.py @@ -0,0 +1,15 @@ +from scapy.all import * + +def arp_test(): + # 构建一个 ARP 请求数据包 + arp_request = Ether(dst="ff:ff:ff:ff:ff:ff") / ARP(pdst="192.168.1.1") + + # 在数据包负载后面追加一个不属于这个数据包的字符 + extra_data = 'a' + arp_request = arp_request / Raw(load=extra_data) + + # 发送 ARP 请求到 dummy0 接口 + sendp(arp_request, iface="dummy0") + +if __name__ == "__main__": + arp_test() diff --git a/dummy_ebpf/src/CMakeLists.txt b/dummy_ebpf/src/CMakeLists.txt new file mode 100644 index 0000000..0dd48f6 --- /dev/null +++ b/dummy_ebpf/src/CMakeLists.txt @@ -0,0 +1,29 @@ +find_package(PkgConfig) +pkg_check_modules(LIBBPF REQUIRED libbpf) +pkg_check_modules(LIBXDP REQUIRED libxdp) + +find_path(ASM_TYPES_H_PATH NAMES asm/types.h PATHS /usr/include/x86_64-linux-gnu) +if(ASM_TYPES_H_PATH) + message(STATUS "Found asm/types.h at ${ASM_TYPES_H_PATH}") + include_directories(${ASM_TYPES_H_PATH}) +else() + message(FATAL_ERROR "asm/types.h not found") +endif() + +set(BPF_C_FILE ${CMAKE_CURRENT_SOURCE_DIR}/xdp_prog_kernel.c) +set(BPF_O_FILE ${CMAKE_CURRENT_BINARY_DIR}/xdp_prog_kernel.o) +add_custom_command(OUTPUT ${BPF_O_FILE} + COMMAND clang -g -O2 -target bpf -D__x86_64__ -I${ASM_TYPES_H_PATH} -c ${BPF_C_FILE} -o ${BPF_O_FILE} + COMMAND_EXPAND_LISTS + VERBATIM + DEPENDS ${BPF_C_FILE} + COMMENT "[clang] Building BPF file: ${BPF_C_FILE}") + +add_custom_target(generate_bpf_obj ALL + DEPENDS ${BPF_O_FILE} +) + +set(SOURCE_FILES main.c config.c device.c tap.c xdp_prog_user.c) +add_executable(main ${SOURCE_FILES}) +target_include_directories(main PRIVATE ${NL3_INCLUDE_DIRS}) +target_link_libraries(main PRIVATE ${NL3_LIBRARIES} ${LIBBPF_LIBRARIES} ${LIBXDP_LIBRARIES})
\ No newline at end of file diff --git a/dummy_ebpf/src/config.c b/dummy_ebpf/src/config.c new file mode 100644 index 0000000..915f14a --- /dev/null +++ b/dummy_ebpf/src/config.c @@ -0,0 +1,31 @@ +#include <stdio.h> +#include <string.h> + +#include "config.h" + +static struct config global_conf = {}; + +void global_config_init() { + // There are three devices in the configuration by default + global_conf.nr_device = 3; + + struct device_desc *device_ref = &global_conf.devices[0]; + snprintf(device_ref->device_type, sizeof(device_ref->device_type), "%s", "dummy"); + snprintf(device_ref->device_name, sizeof(device_ref->device_name), "%s", "dummy0"); + + device_ref = &global_conf.devices[1]; + device_ref->key = '0'; + snprintf(device_ref->device_type, sizeof(device_ref->device_type), "%s", "tap"); + snprintf(device_ref->device_name, sizeof(device_ref->device_name), "%s", "tap0"); + + device_ref = &global_conf.devices[2]; + device_ref->key = '1'; + snprintf(device_ref->device_type, sizeof(device_ref->device_type), "%s", "tap"); + snprintf(device_ref->device_name, sizeof(device_ref->device_name), "%s", "tap1"); + + // The file where ebpf is located, and the ebpf program name + snprintf(global_conf.open_filename, sizeof(global_conf.open_filename), "s", "xdp_prog_kernel.o"); + snprintf(global_conf.prog_name, sizeof(global_conf.prog_name), "s", "xdp_redirect_map_func"); +} + +const struct config *global_config_get() { return &global_conf; }
\ No newline at end of file diff --git a/dummy_ebpf/src/device.c b/dummy_ebpf/src/device.c new file mode 100644 index 0000000..65c6926 --- /dev/null +++ b/dummy_ebpf/src/device.c @@ -0,0 +1,163 @@ +#include "device.h" +#include "config.h" +#include "tap.h" + +#include <assert.h> +#include <linux/if.h> +#include <linux/rtnetlink.h> +#include <netlink/errno.h> +#include <netlink/msg.h> +#include <netlink/netlink.h> +#include <netlink/route/link.h> +#include <netlink/socket.h> + +struct device device_inst; + +static int netlink_add(const char *iftype, const char *ifname) { + struct rtnl_link *link = rtnl_link_alloc(); + rtnl_link_set_type(link, iftype); + rtnl_link_set_name(link, ifname); + + struct nl_sock *sk = nl_socket_alloc(); + nl_connect(sk, NETLINK_ROUTE); + + return rtnl_link_add(sk, link, NLM_F_CREATE | NLM_F_EXCL); +} + +static int netlink_change(const char *ifname, unsigned int flags) { + // modify from: https://github.com/thom311/libnl/blob/main/tests/test-loopback-up-down.c + struct nl_sock *sk; + struct rtnl_link *link, *change; + struct nl_cache *cache; + int err = 0; + + sk = nl_socket_alloc(); + if ((err = nl_connect(sk, NETLINK_ROUTE)) < 0) { + nl_perror(err, "Unable to connect socket"); + return err; + } + + if ((err = rtnl_link_alloc_cache(sk, AF_UNSPEC, &cache)) < 0) { + nl_perror(err, "Unable to allocate cache"); + goto out; + } + + if (!(link = rtnl_link_get_by_name(cache, ifname))) { + fprintf(stderr, "Interface not found\n"); + err = 1; + goto out; + } + + /* exit if the loopback interface is already activated */ + /* After calling tap_alloc(), the tap state is already UP */ + err = rtnl_link_get_flags(link); + if ((err & IFF_UP)) { + err = 0; + goto out; + } + + change = rtnl_link_alloc(); + rtnl_link_set_flags(change, flags); + if ((err = rtnl_link_change(sk, link, change, 0)) < 0) { + nl_perror(err, "Unable to activate lo"); + goto out; + } + + err = 0; + +out: + nl_socket_free(sk); + return err; +} + +void device_create() { + int ret = 0; + memset(&device_inst, 0, sizeof(struct device)); + const struct config *conf = global_config_get(); + + for (int i = 0; i < conf->nr_device; i++) { + const struct device_desc *src_device_ref = &conf->devices[i]; + struct device_desc *dst_device_ref = &device_inst.device_descs[i]; + + dst_device_ref->key = src_device_ref->key; + snprintf(dst_device_ref->device_name, sizeof(dst_device_ref->device_name), "%s", + src_device_ref->device_name); + snprintf(dst_device_ref->device_type, sizeof(dst_device_ref->device_type), "%s", + src_device_ref->device_type); + + if (strcmp(dst_device_ref->device_type, "dummy") == 0) { + ret = netlink_add(dst_device_ref->device_type, dst_device_ref->device_name); + if (ret != 0 && ret != -NLE_EXIST) { + fprintf(stderr, "%s\n", nl_geterror(ret)); + goto failure; + } + } else if (strcmp(dst_device_ref->device_type, "tap") == 0) { + ret = tap_alloc(dst_device_ref->device_name, IFF_TAP | IFF_NO_PI); + if (ret <= 0) { + perror("tap_alloc failed"); + goto failure; + } + device_inst.device_fds[i] = ret; + } else { + fprintf(stderr, "Currently, creation of this type of network card is not supported:%s", + dst_device_ref->device_type); + } + } + + device_inst.nr_device = conf->nr_device; + +end: + return; + +failure: + exit(EXIT_FAILURE); +} + +void device_index_get() { + int ifindex = 0; + for (unsigned int i = 0; i < device_inst.nr_device; i++) { + const char *dev_name = device_inst.device_descs[i].device_name; + + ifindex = if_nametoindex(dev_name); + if (ifindex == 0) { + fprintf(stderr, "Failed to get %s index.", dev_name); + goto failure; + } + + device_inst.device_index[i] = ifindex; + } + +failure: + exit(EXIT_FAILURE); +} + +void device_open() { + int ret = 0; + for (unsigned int i = 0; i < device_inst.nr_device; i++) { + const char *dev_name = device_inst.device_descs[i].device_name; + ret = netlink_change(dev_name, IFF_UP); + if (ret != 0) { + fprintf(stderr, "Failed to start the device: %s", dev_name); + exit(EXIT_FAILURE); + } + } +} + +void device_init() { + device_create(); + device_index_get(); + device_open(); +} + +const struct device *device_inst_get() { return &device_inst; } + +int xdp_device_index_get() { + for (unsigned int i = 0; i < device_inst.nr_device; i++) { + const char *device_type = device_inst.device_descs[i].device_type; + if (strcmp(device_type, "dummy") == 0) { + return device_inst.device_index[i]; + } + } + + return 0; +}
\ No newline at end of file diff --git a/dummy_ebpf/src/main.c b/dummy_ebpf/src/main.c new file mode 100644 index 0000000..981c80d --- /dev/null +++ b/dummy_ebpf/src/main.c @@ -0,0 +1,30 @@ +#include <argp.h> +#include <net/if.h> +#include <signal.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <time.h> +#include <unistd.h> + +#include "config.h" +#include "device.h" +#include "xdp_prog_user.h" + +static volatile bool exiting = false; +static void sig_handler(int sig) { exiting = true; } + +int main(int argc, char *argv[]) { + // init config + global_config_init(); + + // create and open device + device_init(); + + /* Cleaner handling of Ctrl-C */ + signal(SIGINT, sig_handler); + signal(SIGTERM, sig_handler); + + // load ebpf prog + load_bpf_and_xdp_attach(); +}
\ No newline at end of file diff --git a/dummy_ebpf/src/tap.c b/dummy_ebpf/src/tap.c new file mode 100644 index 0000000..0c3fcd2 --- /dev/null +++ b/dummy_ebpf/src/tap.c @@ -0,0 +1,166 @@ +// from: https://blog.csdn.net/xxb249/article/details/86690067 + +#include "tap.h" + +#include <arpa/inet.h> +#include <fcntl.h> +#include <linux/if.h> +#include <linux/if_tun.h> +#include <netinet/in.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/ioctl.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +int tap_alloc(const char *ifname, int flags) { + struct ifreq ifr; + int fd, err; + char *clonedev = "/dev/net/tun"; + + if ((fd = open(clonedev, O_RDWR)) < 0) { + return fd; + } + + memset(&ifr, 0, sizeof(ifr)); + ifr.ifr_flags = flags; + strcpy(ifr.ifr_name, ifname); + if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) { + close(fd); + return err; + } + /* 进程退出 tap0不消失 如果想删除则设置为0 */ + if (ioctl(fd, TUNSETPERSIST, 1) < 0) { + perror("enabling TUNSETPERSIST"); + exit(1); + } + return fd; +} + +int tap_set_mac(const unsigned char *interface_name, const unsigned char *str_macaddr) { + int ret; + int sock_fd; + struct ifreq ifr; + unsigned int mac2bit[6]; + + if (interface_name == NULL || str_macaddr == NULL) { + return -1; + } + + // 提取mac格式 + sscanf((char *)str_macaddr, "%02X:%02X:%02X:%02X:%02X:%02X", (unsigned int *)&mac2bit[0], + (unsigned int *)&mac2bit[1], (unsigned int *)&mac2bit[2], (unsigned int *)&mac2bit[3], + (unsigned int *)&mac2bit[4], (unsigned int *)&mac2bit[5]); + + sock_fd = socket(PF_INET, SOCK_DGRAM, 0); + if (sock_fd < 0) { + return -2; + } + + sprintf(ifr.ifr_ifrn.ifrn_name, "%s", interface_name); + ifr.ifr_ifru.ifru_hwaddr.sa_family = 1; + ifr.ifr_ifru.ifru_hwaddr.sa_data[0] = mac2bit[0]; + ifr.ifr_ifru.ifru_hwaddr.sa_data[1] = mac2bit[1]; + ifr.ifr_ifru.ifru_hwaddr.sa_data[2] = mac2bit[2]; + ifr.ifr_ifru.ifru_hwaddr.sa_data[3] = mac2bit[3]; + ifr.ifr_ifru.ifru_hwaddr.sa_data[4] = mac2bit[4]; + ifr.ifr_ifru.ifru_hwaddr.sa_data[5] = mac2bit[5]; + + ret = ioctl(sock_fd, SIOCSIFHWADDR, &ifr); + if (ret != 0) { + return -4; + } + close(sock_fd); + return 0; +} + +int tap_set_ip(const unsigned char *interface_name, const unsigned char *ipaddr) { + int err; + int ret; + int socket_fd; + struct ifreq ifr; + struct sockaddr_in sin; + + if (interface_name == NULL || ipaddr == NULL) { + return -1; + } + + socket_fd = socket(AF_INET, SOCK_DGRAM, 0); + if (socket_fd < 0) { + printf("Create Socket Failed.\n"); + return -2; + } + // 指定网卡名称且up + sprintf(ifr.ifr_name, "%s", interface_name); + /* 获得接口的标志 */ + if ((err = ioctl(socket_fd, SIOCGIFFLAGS, (void *)&ifr)) < 0) { + perror("ioctl SIOCGIFADDR"); + close(socket_fd); + return -3; + } + ifr.ifr_flags |= IFF_UP; + ret = ioctl(socket_fd, SIOCSIFFLAGS, &ifr); + if (ret != 0) { + printf("Up Device %s Failed.\n", interface_name); + close(socket_fd); + return -3; + } + // 设置ip + memset(&sin, 0, sizeof(struct sockaddr_in)); + sin.sin_family = AF_INET; + inet_pton(AF_INET, ipaddr, &sin.sin_addr.s_addr); + memcpy(&ifr.ifr_addr, &sin, sizeof(struct sockaddr)); + ret = ioctl(socket_fd, SIOCSIFADDR, &ifr); + if (ret != 0) { + printf("Set Ipaddr For Device %s Failed.\n", interface_name); + close(socket_fd); + return -4; + } + + // 设置mask + sin.sin_family = AF_INET; + inet_pton(AF_INET, "255.255.255.0", &sin.sin_addr.s_addr); + memcpy(&ifr.ifr_netmask, &sin, sizeof(struct sockaddr)); + ret = ioctl(socket_fd, SIOCSIFNETMASK, &ifr); + + if (ret != 0) { + printf("Set NetMask For Device %s Failed.\n", interface_name); + close(socket_fd); + return -5; + } + close(socket_fd); + return 0; +} + +#if 0 +int main() { + int tun_fd, nread; + char buffer[1500]; + + /* Flags: IFF_TUN - TUN device (no Ethernet headers) + * IFF_TAP - TAP device + * IFF_NO_PI - Do not provide packet information + */ + tun_fd = tun_alloc("tap0", IFF_TAP | IFF_NO_PI); + + if (tun_fd < 0) { + perror("Allocating interface"); + exit(1); + } + tap_set_mac("tap0", "08:00:11:22:33:44"); + tap_set_ip("tap0", "192.168.62.10"); + while (1) { + nread = read(tun_fd, buffer, sizeof(buffer)); + if (nread < 0) { + perror("Reading from interface"); + close(tun_fd); + exit(1); + } + printf("Read %d bytes [%s] from tun/tap device\n", nread, buffer); + } + return 0; +} +#endif
\ No newline at end of file diff --git a/dummy_ebpf/src/xdp_prog_kernel.c b/dummy_ebpf/src/xdp_prog_kernel.c new file mode 100644 index 0000000..3cd20f4 --- /dev/null +++ b/dummy_ebpf/src/xdp_prog_kernel.c @@ -0,0 +1,49 @@ +// clang-format off +#include <linux/types.h> +// clang-format on +#include <bpf/bpf_endian.h> +#include <bpf/bpf_helpers.h> +#include <linux/bpf.h> +#include <linux/in.h> + +// modify from: +// https://github.com/xdp-project/xdp-tutorial/blob/master/packet03-redirecting/xdp_prog_kern.c + +struct { + __uint(type, BPF_MAP_TYPE_DEVMAP); + __type(key, __u8); + __type(value, int); + __uint(max_entries, 256); +} tx_port SEC(".maps"); + +SEC("xdp_redirect_map") +int xdp_redirect_map_func(struct xdp_md *ctx) { + int ret = 0; + int action = XDP_PASS; + void *data = (void *)(long)ctx->data; + void *data_end = (void *)(long)ctx->data_end; + + // According to the last character, determine which interface to forward the data packet + unsigned char *last_byte = (unsigned char *)(data_end - 1); + __u8 key = *last_byte; + bpf_printk("key: %d", key); + + // Remove the last character + ret = bpf_xdp_adjust_tail(ctx, -1); + if (ret < 0) { + bpf_printk("bpf_xdp_adjust_tail failed. return code:%d", ret); + action = XDP_DROP; + } + + // Redirect the packet to the endpoint referenced by map at index key. + ret = bpf_redirect_map(&tx_port, key, 0); + if (ret != XDP_REDIRECT) { + bpf_printk("bpf_redirect_map failed. return code:%d", ret); + action = XDP_DROP; + } + +out: + return action; +} + +char _license[] SEC("license") = "GPL";
\ No newline at end of file diff --git a/dummy_ebpf/src/xdp_prog_user.c b/dummy_ebpf/src/xdp_prog_user.c new file mode 100644 index 0000000..559a65e --- /dev/null +++ b/dummy_ebpf/src/xdp_prog_user.c @@ -0,0 +1,62 @@ +#include <errno.h> +#include <error.h> +#include <stdio.h> +#include <stdlib.h> + +#include <xdp/libxdp.h> + +#include "config.h" +#include "device.h" +#include "xdp_prog_user.h" + +struct xdp_program *load_bpf_and_xdp_attach() { + /* In next assignment this will be moved into ../common/ */ + int prog_fd = -1; + int err; + struct bpf_object *obj = NULL; + + const struct config *conf = global_config_get(); + int ifindex = xdp_device_index_get(); + + // Clear previous prog + struct xdp_multiprog *mp = xdp_multiprog__get_from_ifindex(ifindex); + err = libxdp_get_error(mp); + if (!err) { + err = xdp_multiprog__detach(mp); + if (err != 0) { + perror("xdp_multiprog__detach failed."); + exit(EXIT_FAILURE); + } + } + + obj = bpf_object__open(conf->open_filename); + if (obj == NULL) { + perror("bpf_object__open failed"); + exit(EXIT_FAILURE); + } + + struct xdp_program_opts prog_opts = {}; + prog_opts.sz = sizeof(struct xdp_program_opts); + prog_opts.obj = obj; + prog_opts.prog_name = conf->prog_name; + + struct xdp_program *prog = xdp_program__create(&prog_opts); + if (prog == NULL) { + perror("xdp_program__create failed"); + exit(EXIT_FAILURE); + } + + err = xdp_program__attach(prog, ifindex, XDP_MODE_UNSPEC, 0); + if (err != 0) { + perror("xdp_program__attach failed"); + exit(EXIT_FAILURE); + } + + prog_fd = xdp_program__fd(prog); + if (prog_fd < 0) { + fprintf(stderr, "ERR: xdp_program__fd failed: %s\n", strerror(errno)); + exit(EXIT_FAILURE); + } + + return prog; +}
\ No newline at end of file |
