您的位置:首页 > 理论基础 > 计算机网络

socket从userspace到kernel的api执行过程(不含tcp/ip协议栈部分)

2016-11-29 17:33 676 查看
glibc版本2.3.6。Kernel版本:4.3

Userspace glibc接口说明

glibc中socket接口定义:(glibc-x.x.x/sysdeps/generic/socket.cx.x.x是版本号)

int __socket (domain, type, protocol)

int domain;

int type;

int protocol;

{

__set_errno (ENOSYS);

return -1;

}

weak_alias (__socket, socket)

stub_warning (socket)

#include <stub-tag.h>

可以在glibc库中做权限检查,这样需要在调用socket接口时到系统服务去检查是否允许当前进程调用socket接口。需要事先配置好调用权限,在系统服务启动时候加载权限配置。gcc编译器扩展为socket定义了别名__socket,__socket的真实定义是在glibc-x.x.x/sysdeps/unix/sysv/linux/i386/socket.S。在i386、arm有对应的socket.S

socket.S关键执行点说明:

1、把socket对应的系统调用号mov到eax寄存器

movl$SYS_ify(socketcall), %eax /* System call number in %eax. */

2、把参数的地址弄到%esp寄存器

lea4(%esp), %ecx

3、产生$0x80软中断,进入内核执行

socket.S 中调用ENTER_KERNEL

4、socket.S文件也有和C文件一致的别名定义,这样编译器就能找到别名的具体实现。

执行0x80软中端后就到达系统调用的总入口system_call()函数,system_call()最终使用汇编call指令(call *sys_call_table(,%eax, 4))根据寄存器%eax中的值执sys_call_table系统调用表102对应的函数指针指向的函数。102系统调用号对应的函数是:sys_socketcall(),glibc中所有socket相关的接口都走这个系统调用接口

sys_call_table系统调用号表定义在:arch/m32r/kernel/syscall_table.S

system_call()汇编接口定义在:arch/x86/kernel/entry_32.S

不同平台会有不同的定义文件

Kernel里面socket相关函数说明:

/net/socket.c里面有sys_socketcall()、sys_socket()、sys_bind()等等一系列socket函数的具体实现:

SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)

SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr,int, addrlen)

SYSCALL_DEFINE2(listen, int, fd, int, backlog)

SYSCALL_DEFINE3(accept, int, fd, struct sockaddr __user *,upeer_sockaddr,int __user*, upeer_addrlen)

SYSCALL_DEFINE3(connect, int, fd, struct sockaddr __user *,uservaddr,int,addrlen)

SYSCALL_DEFINE6(sendto, int, fd, void __user *, buff, size_t, len,unsignedint, flags, struct sockaddr __user *, addr, int,addr_len)

SYSCALL_DEFINE4(send, int, fd, void __user *, buff, size_t, len,unsignedint, flags)

SYSCALL_DEFINE6(recvfrom, int, fd, void __user *, ubuf, size_t,size,unsignedint, flags, struct sockaddr __user *, addr, int __user*, addr_len)

SYSCALL_DEFINE4(recv, int, fd, void __user *, ubuf, size_t, size, unsignedint, flags)

SYSCALL_DEFINE5(setsockopt, int, fd, int, level, int, optname,char __user*, optval, int, optlen)

SYSCALL_DEFINE5(getsockopt, int, fd, int, level, int, optname,char __user*, optval, int __user *, optlen)

/include/linux/syscalls.h里面有上述宏的定义

socket(AF_INET, SOCK_STREAM, IPPROTO_TCP),以下接口都以此种类型的socket来说明

一、socket函数概要说明:

SYSCALL_DEFINE3(socket,int, family, int, type, int, protocol),以下的函数基本上都在socket.c文件里面实现,定义都是SYSCALL_DEFINEx()的格式。SYSCALL_DEFINEx的定义见include/linux/syscalls.h其中的x代表宏封装的函数有几个参数。宏SYSCALL_DEFINE3(socket,
int, family, int, type, int, protocol)封装了socket这个函数,参数是family、type、protocol这三个,参数数据类型都是int。


1、 调用sock_create函数创建structsocket *sock;

retval = sock_create(family, type, protocol, &sock);socket_create函数只是简单的封了一下__sock_create()函数。__sock_create()函数的说明在步骤2的后面。

2、 做socket到文件描述符的映射

retval = sock_map_fd(sock, flags & (O_CLOEXEC |O_NONBLOCK));

sock_map_fd会从当前系统获取一个未使用的文件描述符,执行fd_install把获取到的文件描述符和新创建的socket文件对象绑定,执行完成后内核会把这个文件描述符返回给应用程序。应用层socket()函数的返回值就是这个文件描述符。

__sock_create流程概要说明:

假定应用层调用为:socket(AF_INET,SOCK_STREAM, IPPROTO_TCP),其他参数调用类似。

1、 selinux检测

security_socket_create(),这里可以做权限控制

2、 分配新socket

sock =sock_alloc();

3、 构造socket

pf = rcu_dereference(net_families[family]);

net_families[AF_INET]取到inet_family_ops结构。inet_family_ops的定义、注册到net_family都在af_inet.c文件实现。

pf->create(net, sock,protocol, kern);
可以在这里根据网络命名空间做权限控制,需要对struct net结构做扩展,改动比较大。此处的pf->create(),执行的是inet_family_ops结构体中的create函数,也就是inet_create()函数,inet_create会根据socket的类型(SOCK_STREAM、SOCK_DGRAM…..)从inetsw_array中找到匹配的struct
inet_protosw结构体。把匹配中的结构体的.ops成员赋值给步骤2中新alloc出的sock,在bind、listen等处会调用socket的ops函数。把匹配中的结构体中的.prot成员赋值给下面sk_alloc新分配的struct sock结构体成员sk_prot。(Af_inet.c)。注意这里的struct inet_protosw结构的ops和prot成员赋给的对象分别是struct
socket的ops成员和struct sock的sk_prot成员,内核中的socket系列函数会频繁用到这两个成员。

struct inet_protosw 结构体的.prot成员可以是tcp_prot、udp_prot、ping_prot、raw_prot这四个函数。

inet_create()说明:

执行sk_alloc(net, PF_INET, GFP_KERNEL, answer_prot, kern);创建新的sock并把struct netnet赋值给新创建的sock。

执行sock_init_data(sock, sk)把socket赋值给sock。

上述步骤中红色的sock是struct socket结构,蓝色的sock是struct sock结构。

sk_alloc()函数说明:

本函数主要根据struct in
111ca
et_protosw 的成员.prot来创建相应的sock以及构造私有的inet数据,sk_prot_alloc()函数会执行sk = kmalloc(prot->obj_size, priority),此处的prot->obj_size对应于struct proto tcp_prot 的obj_size,也就是sizeof(struct tcp_sock)。

struct tcp_sock的第一个成员是structinet_connection_sock类型的

struct inet_connection_sock 的第一个成员是 structinet_sock类型的

struct inet_sock结构的第一个成员是structsock类型的。

对于一个tcpsocket对象,从地址来看tcp_sock、inet_connection_sock、inet_sock、struct sock四者是在同一个地址的,对地址的指向进行强制类型转换后可以方便的对这四者各自的成员进行操作。

sock_init_data(sock, sk);会把sk_alloc出的struct sock赋值给步骤2中创建的socket。这样就把sock和socket关联上了。从这里也可以看出sturctsocket是一个公共的套接字结构体,而sock是和具体协议相关的套接字数据结构。struct socket结构的抽象层次要高于struct sock。

inet_init()中执行inet_register_protosw()把结构数组inetsw_array中的元素注册到全局链表inetsw,这部分代码在系统启动时加载ip协议栈的时候执行。

每种类型的socket,在inetsw_array数组中都能找到与之对应的ops操作结构。内核把不同类型socket的操作函数接口封装到structproto结构中。

二、bind()函数概要说明:

1、 通过socket文件描述符获取socket

struct socket *sock = sockfd_lookup_light(fd, &err, &fput_needed);

2、 调用sock结构体ops成员的bind函数来绑定地址

此处的ops是structproto_ops inet_stream_ops结构。原因见__sock_create流程概要说明的步骤3。

执行inet_bind()函数还调用ns_capable(net->user_ns,CAP_NET_BIND_SERVICE)来检查能力。这里可以做能力控制。

执行inet_bind()时,先从sock里面取出structsock sk,再执行sk->sk_prot->bind(sk, uaddr, addr_len);本例中sk->sk_prot就是tcp_prot()。

执行最后的bind()函数分两种情况

2.1、RAW的socket直接调sk->sk_prot->bind()

2.2、STREAM、DGRAM的socket单独处理,把地址、端口设置到struct inet_sock变量里面,struct inet_sock指针可以是由struct sock强制类型转换得到(原因见sk_alloc()函数说明)。

三、listen()函数概要说明:

1、 和bind()调用类似。调用structproto_ops inet_stream_ops结构的listen成员函数,也就是inet_listen()。此时只是把socket的状态设置为listen并设置相应参数。

四、 accept()函数概要说明:

1、 在这里会重新分配一个真正和客户端连接的socket。bind创建的socket会把一些字段赋个新创建的socket。

2、 inet_accept()函数会执行sk1->sk_prot->accept()。这里的accept执行的是structproto tcp_prot 结构的accept成员函数,也就是inet_csk_accept。

五、 recv()函数概要说明:

1、 调用sys_recvfrom()

2、 sys_recvfrom()调用sock_recvmsg()

3、 sock_recvmsg()中的selinux检查通过后调用sock_recvmsg_nosec()

4、 sock_recvmsg_nosec()中调用const struct proto_ops inet_stream_ops 结构体的recvmsg成员函数(inet_recvmsg())。

5、 inet_recvmsg()执行sk->sk_prot->recvmsg()函数调用。执行的是struct prototcp_prot 结构体成员recvmsg,也就是tcp_recvmsg()函数。

6、 执行完上述步骤后,如果有数据sys_recvfrom()就执行move_addr_to_user()把数据拷到应用程序。

六、send()函数概要说明:

send()函数简单的对sendto函数做了个封装

1、 调用sys_sendto()

2、 sys_sendto()中先构造一个接受应用层数据的struct msghdr的结构(说明见段末),设置一些初始值,例如根据是否阻塞设置sock文件的flag标志。

3、 sys_sendto()执行sock_sendmsg()函数,在sock_sendmsg()函数中先执行security_socket_sendmsg()做selinux检查,检测通过后执行sock_sendmsg_nosec()

4、 在sock_sendmsg_nosec()中执行的sock->ops->sendmsg()

sock->ops是const structproto_ops inet_stream_ops结构

sendmsg()是inet_sendmsg()函数

在inet_sendmsg()函数中最后执行的是sk->sk_prot->sendmsg(sk, msg, size)

这里的sk->sk_prot是struct proto tcp_prot

代码最终调到tcp_sendmsg()把数据包发送出去

struct msghdr说明:http://blog.chinaunix.net/uid-22920230-id-3387909.html

sys_sendto构建一个结构体structmsghdr,用于接收来自应用层的数据包,下面是结构体struct msghdr的定义:

struct msghdr {

void *msg_name; /* ptr to socket address structure */

int msg_namelen; /* size of socket address structure */

struct iov_iter msg_iter; /* data */

void *msg_control; /* ancillary data */

__kernel_size_t msg_controllen; /*ancillary data buffer length */

unsigned int msg_flags; /* flagson received message */

struct kiocb *msg_iocb; /* ptr toiocb for async requests */

};

这个结构体的内容可以分为四组:

第一组是msg_name和msg_namelen,记录这个消息的名字,其实就是数据包的目的地址 。msg_name是指向一个结构体structsockaddr的指针,长度为16。结构体struct sockaddr只在进行参数传递时使用,无论是在用户态还是在内核态,我们都把其强制转化为结构体struct sockaddr_in。

strcutsockaddr_in{

sa_family_t sin_family;

unsignedshort int sin_port;

structin_addr sin_addr;

unsignedchar __pad[__SOCK_SIZE__

- sizeof(short int)

-sizeof(unsigned short int)

- sizeof(struct in_addr)];

};

structin_addr{

__u32s_addr;

}

__SOCK_SIZE__的值为16,所以,structsockaddr中真正有用的数据只有8bytes。

在我们的ping的时候,传入到内核的msghdr结构中:

msg.msg_name = { sa_family_t = MY_AF_INET,

sin_port = 0,

sin_addr.s_addr = 172.16.48.1}

msg_msg_namelen= 16

第二组是msg_iov和msg_iovlen,记录这个消息的内容。msg_iov是一个指向结构体structiovec的指针,实际上,确切地说,应该是一个结构体strcut iovec的数组。下面是该结构体的定义:

struct iovec{

void __user *iov_base;

__kernel_size_t iov_len;

}

iov_base指向数据包缓冲区,即参数buff,iov_len是buff的长度。msghdr中允许一次传递多个buff,以数组的形式组织在 msg_iov中,msg_iovlen就记录数组的长度(即有多少个buff)。在ping程序的实例中:

msg.msg_iov = { struct iovec = { iov_base = { icmp头+填充字符'E' },

iov_len = 40 }}

msg.msg_len= 1

第三组是msg_control和msg_controllen,它们可被用于发送任何的控制信息

第四组是msg_flags。其值即为传入的参数flags。raw协议不支持MSG_ OOB向标志,即带外数据。向向内核发送msg 时使用msghdr,netlink socket使用自己的消息头nlmsghdr和自己的消息地址sockaddr_nl:

struct sockaddr_nl {

sa_family_t nl_family;

unsigned short nl_pad;

__u32 nl_pid;

__u32 nl_groups;

};

struct nlmsghdr {

__u32 nlmsg_len; /* Lengthof message */

__u16 nlmsg_type; /* Message type*/

__u16 nlmsg_flags; /* Additional flags */

__u32 nlmsg_seq; /* Sequencenumber */

__u32 nlmsg_pid; /* Sendingprocess PID */

};

tcp socket 的连接发送数据包、断开连接等操作最终都是调用的struct proto tcp_prot里面的相关函数。不同协议的socket有不同的struct proto与之对应。具体见af_inet.c 中的struct inet_protosw inetsw_array定义。

对socket的控制可以在应用层做,也可以到内核做。selinux、能力的钩子已经放到现有的socket流程中了,所以对现有的钩子函数扩充来处理是比较规范的。

七、附录一个和本地socket相关的防火墙问题( 原文地址:http://www.linuxidc.com/Linux/2012-06/63520.htm)

对于本地的socket,127.0.x.x(kernel直接取ip地址的前两个字节来判断)的数据包在协议栈里被处理掉了,不会到iptables里处理。

如有下面的iptables规则:

iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT--to-destination 127.0.0.1:1234

你觉得会成功吗?试一下就知道,不会成功。这是为什么呢?奇怪的是,不但没有返回包,即使本地没有1234这个端口在监听,也不会发送reset,这说明数据包根本就没有到达传输层,通过forward统计信息来看,它也没有经过forward,那么数据包到哪里去了呢?执行conntrack
-E,发现没有任何新的conntrack生成的事件,并且/proc/net/ip_conntrack中也没有任何甚至是转化前的到达80端口的连接。
通过IP安全规则,得知实际上是不允许外部进来的包以loopback地址为目标的,否则攻击就太容易了,比方说我在局域网上放一个目标地址为127.0.0.1的IP包,修改其MAC目标地址为全1,这样所有的机器都将可以收到这样的包,因此路由模块负责丢弃这样的数据包。从表象上上,数据包被丢弃了,如果从统计数据来看,执行rtstat命令,将会看到下面一列:

|rt_cache|

|in_marti|

| an_dst|

| 34|


这说明34个这样的包被丢弃了,在哪里被丢了呢?肯定在路由模块,毕竟这是IP的策略,查看源码,在ip_route_input_slow里面发现:
1. if (ipv4_is_lbcast(daddr) || ipv4_is_zeronet(daddr) ||
2. ipv4_is_loopback(daddr))
3. goto martian_destination;
4. ...
5. martian_destination:
6. RT_CACHE_STAT_INC(in_martian_dst);
我们看到,这种情况的目的地址是一个“火星地址”,被丢弃了,同时还记录了一条统计信息。这说明,环回地址只能通过本机来访问,在本机出去的包经过ip_route_output后,其dst字段即路由结果已经被设置,如果是访问127.0.0.1,那么将不会再到ip_route_input,详见ip_rcv_finish:
1. if (skb_dst(skb) == NULL) {
2. int err = ip_route_input(skb, iph->daddr, iph->saddr, iph->tos,
3. skb->dev);
4. ...
5. }
理解了数据包在哪里被丢弃了之后,对于出现的现象就很好理解了。为何当发起到80端口的访问且途径设置了前面iptables规则的机器时/proc/net/ip_conntrack中没有任何关于端口80的连接呢?这是因为该连接的数据包既没有被forward到网卡,也没有被input到本地,因此该连接始终没有被confirm,而我们知道ip_conntrack机制只有在confirm时才会将连接加入到hash链表中并且报告事件。

本文描述的是一个关于漏洞的问题,经典的《专家们公认的20个最危险的安全漏洞》一文中有下面的论述:

G5 – 没有过滤地址不正确的包

G5.1描述

IP地址欺诈是黑客经常用来隐藏自己踪迹的一种手段。例如常见的smurf攻击就利用了路由的特性向数以千记的机器发出了一串数据包。每一个数据包都假冒了一个受害主机的IP地址作为源地址,于是上千台主机会同时向这个受害的主机返回数据包,导致该主机或网络崩溃。对流进和流出你网络的数据进行过滤可以提供一种高层的保护。过滤规则如下:

1.任何进入你网络的数据包不能把你网络内部的地址作为源地址。

2.任何进入你网络的数据包必须把你网络内部的地址作为目的地址。

3.任何离开你网络的数据包必须把你网络内部的地址作为源地址。

4.任何离开你网络的数据包不能把你网络内部的地址作为目的地址。

5.任何进入或离开你网络的数据包不能把一个私有地址(private address)或在RFC1918中

列出的属于保留空间(包括10.x.x.x/8,172.16.x.x/12或192.168.x.x/16和网络回送地址

127.0.0.0/8.)的地址作为源或目的地址。

6.阻塞任意源路由包或任何设置了IP选项的包。


可见,阻塞这种目标为环回地址的包是合理的,然而这件事到底应该有谁来做,这是一个问题,到底应该由防火墙软件来做呢,还是操作系统协议栈本身来做?Linux是在IP路由模块中做的,我认为这样不是很合理,有时候我真的需要这个功能,总不能重新编译一下内核吧,我觉得要么做到Netfilter中,要么就像rp_filter那样,做成可以配置的开关。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: