socket從userspace到kernel的api執行過程(不含tcp/ip協議棧部分)

glibc版本2.3.6Kernel版本: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 inet_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.1IP包,修改其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/12192.168.x.x/16和網絡回送地址
127.0.0.0/8.)的地址作爲源或目的地址。
6阻塞任意源路由包或任何設置了IP選項的包。

可見,阻塞這種目標爲環回地址的包是合理的,然而這件事到底應該有誰來做,這是一個問題,到底應該由防火牆軟件來做呢,還是操作系統協議棧本身來做?Linux是在IP路由模塊中做的,我認爲這樣不是很合理,有時候我真的需要這個功能,總不能重新編譯一下內核吧,我覺得要麼做到Netfilter中,要麼就像rp_filter那樣,做成可以配置的開關。

發佈了5 篇原創文章 · 獲贊 7 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章