阅读:3196回复:1
一个发现,linux可以针对一个socket连接执行vpn连接,真是粒度作的越来越细
一个发现,linux可以针对一个socket连接执行vpn连接,真是粒度作的越来越细
1 简单说明 一般的vpn策略针对是所有外出的数据包进行检查,粒度相对比较粗;而现在Linux已经做到了socket连接执行vpn策略检查,而对其他socket连接没有任何影响。 怎么做呢?很简单,只需要使用setsockopt就可以了,关键字是:IP_IPSEC_POLICY或者IP_XFRM_POLICY。当然通过pf_key/netlink socket也可以做到。 2 简单代码 #include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <linux/xfrm.h> #define IP_XFRM_POLICY 1 int main() { int fd; struct sockaddr_in s_in; struct xfrm_userpolicy_info xp; inet_aton("192.168.13.8", &s_in.sin_addr); s_in.sin_family = AF_INET; s_in.sin_port = htons(555); fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_UDP); if (fd < 0) { perror("socket"); return 1; } if (connect(fd, (struct sockaddr *) &s_in, sizeof(s_in)) < 0) { perror("connect"); return 1; } inet_aton("192.168.13.7", (struct in_addr *) &xp.sel.saddr); xp.sel.prefixlen_s = 32; inet_aton("192.168.13.8", (struct in_addr *) &xp.sel.daddr); xp.sel.prefixlen_d = 32; xp.sel.dport = 0; xp.sel.dport_mask = 0; xp.sel.sport = 0; xp.sel.sport_mask = 0; xp.sel.family = AF_INET; xp.sel.proto = 0; xp.sel.ifindex = 0; xp.sel.user = 0; xp.lft.soft_byte_limit = 10000000; xp.lft.hard_byte_limit = 10000000; xp.lft.soft_packet_limit = 10000000; xp.lft.hard_packet_limit = 10000000; xp.lft.soft_add_expires_seconds = 3600; xp.lft.hard_add_expires_seconds = 3600; xp.lft.soft_use_expires_seconds = 3600; xp.lft.hard_use_expires_seconds = 3600; xp.curlft.bytes = 0; xp.curlft.packets = 0; xp.curlft.add_time = 0; xp.curlft.use_time = 0; xp.priority = 0; xp.index = 0; xp.dir = XFRM_POLICY_FWD; xp.action = XFRM_POLICY_ALLOW; xp.flags = 0; xp.share = XFRM_SHARE_UNIQUE; if (setsockopt(fd, SOL_IP, IP_XFRM_POLICY, &xp, sizeof(xp)) < 0) { perror("setsockopt(IP_XFRM_POLICY)"); return 1; } send(fd, "abrakadabra", 11, 0); close(fd); } 3 patch: 内核sk->policy只有2个大小,在添加策略时没有检查大小,这里存在数组溢出漏洞,因此IP_XFRM_POLICY大小需要仔细设定。 2.6.12以后的内核在pfkey_compile_policy和xfrm_compile_policy中添加了patch。 在pfkey_compile_policy中: if (len < sizeof(struct sadb_x_policy) || pol->sadb_x_policy_len*8 > len || pol->sadb_x_policy_type > IPSEC_POLICY_BYPASS || (!pol->sadb_x_policy_dir || pol->sadb_x_policy_dir > IPSEC_DIR_OUTBOUND)) /* 检查是否超过了边界*/ return NULL; xp = xfrm_policy_alloc(GFP_ATOMIC); 在xfrm_compile_policy中: nr = ((len - sizeof(*p)) / sizeof(*ut)); if (nr > XFRM_MAX_DEPTH) return NULL; if (p->dir > XFRM_POLICY_OUT) /* 检查是否超过了边界*/ return NULL; xp = xfrm_policy_alloc(GFP_KERNEL); |
|
|
沙发#
发布于:2007-05-11 15:45
4 使用sockopt与内核交换数据[转贴]
本文档的Copyleft归yfydz所有,使用GPL发布,可以自由拷贝,转载,转载时请保持文档的完整性,严禁用于任何商业用途。msn: yfydz_no1@hotmail.com 来源:http://yfydz.cublog.cn 1. 前言 打开一个网络socket后可以使用set/getsockopt(2)可实现用户空间与内核的通信,本质和ioctl差不多,区别在于set/getsockopt不用新建设备,直接利用系统已有的socket类型就可以进行,可用setsockopt函数向内核写数据,用getsockopt向内核读数据。 本文内核代码版本为2.6.19.2。 2. 基本过程 首先在内核中要登记相关协议的set/getsockopt的选项命令字和相关的处理函数,然后在用户空间打开该协议的socket后就可以直接调用set/getsockopt来指定命令字执行相关的数据交互操作,常见的TCP、UDP的socket都用这两个系统调用来iptables<->netfilter,ipvsadm<->ip_vs就是这么实现的。 3. set/getsockopt(2) set/getsockopt(2)函数的基本使用格式为: int setsockopt(int sockfd, int proto, int cmd, void *data, int datalen) int getsockopt(int sockfd, int proto, int cmd, void *data, int datalen) 第一个参数是socket描述符;第2个参数proto是sock协议,IP RAW的就用SOL_SOCKET/SOL_IP等,TCP/UDP socket的可用SOL_SOCKET/SOL_IP/SOL_TCP/SOL_UDP等,即高层的socket是都可以使用低层socket的命令字的;第3个参数cmd是操作命令字,由自己定义;第4个参数是数据缓冲区起始位置指针,set操作时是将缓冲区数据写入内核,get的时候是将内核中的数据读入该缓冲区;第5个参数数据长度。 4. 内核实现 内核实现新的sockopt命令字有两类,一类是添加完整的新的协议后引入,一类是在原有协议命令集的基础上增加新的命令字。 sockopt命令字定义没有什么特别之处,就是一个整数,只要对这个协议内部是一个唯一的的即可,不象ioctl的命令字还有一定格式要求。 4.1 完整协议 每个协议都是用struct proto结构(include/net/sock.h)来描述的,Linux内核中缺省定义了三种:TCP、UDP和RAW,所有非TCP、UDP的都用RAW来描述。在net/core/sock.c的sock_get/setsockopt()函数中内核实现了一个所有socket共同的sockopt读写命令集合,在各个协议的内部再单独定义各自协议的独有命令字。 struct proto中有setsockopt和getsocket成员函数,用来定义每个协议的独有相关的命令字。 如对于UDP协议的setsockopt成员函数: static int udp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, int optlen) { // 先判断是否是UDP层,不是的话调IP层的sockopt处理 if (level != SOL_UDP) return ip_setsockopt(sk, level, optname, optval, optlen); // 是UDP级别命令,调用UDP协议本身的sockopt处理 return do_udp_setsockopt(sk, level, optname, optval, optlen); } static int do_udp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, int optlen) { struct udp_sock *up = udp_sk(sk); int val; int err = 0; if(optlen<sizeof(int)) return -EINVAL; if (get_user(val, (int __user *)optval)) return -EFAULT; // 实际UDP独有的命令字就这两个 switch(optname) { case UDP_CORK: if (val != 0) { up->corkflag = 1; } else { up->corkflag = 0; lock_sock(sk); udp_push_pending_frames(sk, up); release_sock(sk); } break; // UDP封装,在IPSEC的NAT-T时使用 case UDP_ENCAP: switch (val) { case 0: case UDP_ENCAP_ESPINUDP: case UDP_ENCAP_ESPINUDP_NON_IKE: up->encap_type = val; break; default: err = -ENOPROTOOPT; break; } break; default: err = -ENOPROTOOPT; break; }; return err; } 所以要实现一个新协议的sockopt控制,只需要类似方法处理即可,定义好struct proto结构后将其注册到系统中即可,对于IP族内协议用inet_register_protosw()函数,其他协议族可类似处理。 4.2 命令扩充 实际使用中单独定义新协议的可能性不是很大,通常只是添加新的命令字即可,对于TCP、UDP的新命令字的添加,需要自己修改内核tcp/udp实现代码,把自己的命令字添加进去后重新编译内核才能生效。 对于IP RAW级别的命令字,netfilter提供了nf_register_sockopt()和nf_unregister_sockopt()来动态登记或取消sockopt命令字,这样可以不用修改内核原来的代码。方法是将netfilter的sockopt操作集合定义为一个链表,要定义新的opt操作就定义一个新的opt操作节点挂接到该链表中,在系统sockopt调用时,会依次查找链表中的命令字,匹配上了就可以成功调用,因此opt的命令字不能和原来IP RAW中定义相同,不过命令字是个32位的数,取值范围很大,只要稍微注意一点是不会冲突的。 netfilter的sock是RAW级别的。 sockopt操作节点结构,结构比较简单明了,就是定义各自命令字的范围空间和相关的处理函数: /* include/linux/netfilter.h */ struct nf_sockopt_ops { // 链表节点 struct list_head list; // 协议族 int pf; /* Non-inclusive ranges: use 0/0/NULL to never get called. */ // set命令的最小值 int set_optmin; // set命令的最大值 int set_optmax; // set函数实现 int (*set)(struct sock *sk, int optval, void __user *user, unsigned int len); int (*compat_set)(struct sock *sk, int optval, void __user *user, unsigned int len); // get命令的最小值 int get_optmin; // get命令的最大值 int get_optmax; // get函数实现 int (*get)(struct sock *sk, int optval, void __user *user, int *len); int (*compat_get)(struct sock *sk, int optval, void __user *user, int *len); /* Number of users inside set() or get(). */ unsigned int use; struct task_struct *cleanup_task; }; opt操作结构登记和撤销函数: /* net/netfilter/nf_sockopt.c */ // nf的sockopt的链表,所有sockopt命令处理都挂接到这个链表 static LIST_HEAD(nf_sockopts); /* Functions to register sockopt ranges (exclusive). */ int nf_register_sockopt(struct nf_sockopt_ops *reg) { struct list_head *i; int ret = 0; // 加锁 if (mutex_lock_interruptible(&nf_sockopt_mutex) != 0) return -EINTR; // 检查当前链表中是否已经挂接了该sockopt操作节点 list_for_each(i, &nf_sockopts) { struct nf_sockopt_ops *ops = (struct nf_sockopt_ops *)i; if (ops->pf == reg->pf && (overlap(ops->set_optmin, ops->set_optmax, reg->set_optmin, reg->set_optmax) || overlap(ops->get_optmin, ops->get_optmax, reg->get_optmin, reg->get_optmax))) { NFDEBUG("nf_sock overlap: %u-%u/%u-%u v %u-%u/%u-%u\n", ops->set_optmin, ops->set_optmax, ops->get_optmin, ops->get_optmax, reg->set_optmin, reg->set_optmax, reg->get_optmin, reg->get_optmax); ret = -EBUSY; goto out; } } // 新节点,添加到opt链表中 list_add(®->list, &nf_sockopts); out: // 解锁 mutex_unlock(&nf_sockopt_mutex); return ret; } EXPORT_SYMBOL(nf_register_sockopt); void nf_unregister_sockopt(struct nf_sockopt_ops *reg) { /* No point being interruptible: we're probably in cleanup_module() */ restart: mutex_lock(&nf_sockopt_mutex); if (reg->use != 0) { // 操作节点还在使用中,阻塞进程直到所有操作完成 /* To be woken by nf_sockopt call... */ /* FIXME: Stuart Young's name appears gratuitously. */ set_current_state(TASK_UNINTERRUPTIBLE); reg->cleanup_task = current; mutex_unlock(&nf_sockopt_mutex); schedule(); goto restart; } // 从链表中删除 list_del(®->list); mutex_unlock(&nf_sockopt_mutex); } EXPORT_SYMBOL(nf_unregister_sockopt); 下面来看一下具体调用流程是如何进行的,首先打开的socket是一个RAW类型的IP socket,对这类socket的setsockopt操作会调用到ip_setsockopt()函数: /* net/ipv4/ip_sockglue.c */ int ip_setsockopt(struct sock *sk, int level, int optname, char __user *optval, int optlen) { int err; if (level != SOL_IP) return -ENOPROTOOPT; // 先按普通IP的sockopt操作执行 err = do_ip_setsockopt(sk, level, optname, optval, optlen); #ifdef CONFIG_NETFILTER // 内核要支持netfilter /* we need to exclude all possible ENOPROTOOPTs except default case */ if (err == -ENOPROTOOPT && optname != IP_HDRINCL && optname != IP_IPSEC_POLICY && optname != IP_XFRM_POLICY #ifdef CONFIG_IP_MROUTE && (optname < MRT_BASE || optname > (MRT_BASE + 10)) #endif ) { // 如果IP中没有这个opt命令字,调用netfilter的sockopt lock_sock(sk); err = nf_setsockopt(sk, PF_INET, optname, optval, optlen); release_sock(sk); } #endif return err; } /* net/netfilter/nf_sockopt.c */ int nf_setsockopt(struct sock *sk, int pf, int val, char __user *opt, int len) { // 实际调用nf_sockopt函数 return nf_sockopt(sk, pf, val, opt, &len, 0); } static int nf_sockopt(struct sock *sk, int pf, int val, char __user *opt, int *len, int get) { struct list_head *i; struct nf_sockopt_ops *ops; int ret; if (mutex_lock_interruptible(&nf_sockopt_mutex) != 0) return -EINTR; // 扫描netfilter的sockopt链表 list_for_each(i, &nf_sockopts) { // 取出opt操作节点 ops = (struct nf_sockopt_ops *)i; // 根据协议,命令字范围判断是否处理该命令字 if (ops->pf == pf) { if (get) { // get操作 if (val >= ops->get_optmin && val < ops->get_optmax) { // opt结构节点使用计数加1 ops->use++; mutex_unlock(&nf_sockopt_mutex); ret = ops->get(sk, val, opt, len); goto out; } } else { // set操作 if (val >= ops->set_optmin && val < ops->set_optmax) { ops->use++; mutex_unlock(&nf_sockopt_mutex); ret = ops->set(sk, val, opt, *len); goto out; } } } } mutex_unlock(&nf_sockopt_mutex); return -ENOPROTOOPT; out: mutex_lock(&nf_sockopt_mutex); // 操作完成,opt结构节点使用减一 ops->use--; if (ops->cleanup_task) wake_up_process(ops->cleanup_task); mutex_unlock(&nf_sockopt_mutex); return ret; } 这样,自己定义的nf的opt节点就可以被遍历到,操作也就有效. 具体实例, ip_vs opt操作节点: /* net/ipv4/ipvs/ip_vs_ctl.c */ static struct nf_sockopt_ops ip_vs_sockopts = { .pf = PF_INET, // 定义set命令字范围 .set_optmin = IP_VS_BASE_CTL, .set_optmax = IP_VS_SO_SET_MAX+1, .set = do_ip_vs_set_ctl, // get命令字范围 .get_optmin = IP_VS_BASE_CTL, .get_optmax = IP_VS_SO_GET_MAX+1, .get = do_ip_vs_get_ctl, }; set/get函数就很简单了,就进行一些合法性检查,然后根据命令字进行相关处理即可: static int do_ip_vs_set_ctl(struct sock *sk, int cmd, void __user *user, unsigned int len) { int ret; unsigned char arg[MAX_ARG_LEN]; struct ip_vs_service_user *usvc; struct ip_vs_service *svc; struct ip_vs_dest_user *udest; // 用户权限检查 if (!capable(CAP_NET_ADMIN)) return -EPERM; // 数据长度检查 if (len != set_arglen[SET_CMDID(cmd)]) { IP_VS_ERR("set_ctl: len %u != %u\n", len, set_arglen[SET_CMDID(cmd)]); return -EINVAL; } // 拷贝数据 if (copy_from_user(arg, user, len) != 0) return -EFAULT; /* increase the module use count */ // ipvs模块使用计数 ip_vs_use_count_inc(); // 加锁 if (mutex_lock_interruptible(&__ip_vs_mutex)) { ret = -ERESTARTSYS; goto out_dec; } // 以下进行具体的命令实现操作: if (cmd == IP_VS_SO_SET_FLUSH) { /* Flush the virtual service */ ret = ip_vs_flush(); goto out_unlock; ...... 5. 用户空间 用户空间的操作很简单,就是用socket(2)打开相关协议类型的socket,直接调用set/getsockopt函数就可以进行操作了. 实例: ipvsadm int ipvs_init(void) { socklen_t len; len = sizeof(ipvs_info); // 打开RAW类型的socket if ((sockfd = socket(AF_INET, SOCK_RAW, IPPROTO_RAW)) == -1) return -1; // 读取ipvs基本信息 if (getsockopt(sockfd, IPPROTO_IP, IP_VS_SO_GET_INFO, (char *)&ipvs_info, &len)) return -1; return 0; } 5. 结论 用setgetsockopt()在用户空间和内核空间传递数据也是常用方法之一,比较简单方便,而且可以在同一个socket中对不同的命令传送不同的数据结构。 新命令字的添加可以按新协议添加,也可以添加到现有的实现中,但没有特别需求的话,netfilter提供的动态登记opt命令字可以动态添加删除sockopt操作命令字,而且不用修改内核原有的程序。 |
|
|