zoukankan      html  css  js  c++  java
  • Linux内核基于Netfilter的内核级包过滤防火墙实现

    测试内核版本:Linux Kernel 2.6.35----Linux Kernel 3.2.1

    原创作品,转载请标明http://blog.csdn.net/yming0221/article/details/7572382

    更多请查看专栏http://blog.csdn.net/column/details/linux-kernel-net.html

    作者:闫明


    知识基础:本防火墙的开发基于对Linux内核网络栈有个良好的概念,本人对网络栈的分析是基于早期版本(Linux 1.2.13),在明确了网络栈架构的前提下,上升一步分析高级版本内核中的Netfilter防火墙实现原理,然后进行模块或内核编程,开发一款基于包过滤的个人防火墙。

    包过滤防火墙:包过滤防火墙是用一个软件查看所流经的数据包的包头(header),由此决定整个包的命运。它可能会决定丢弃(DROP)这个包,可能会接受(ACCEPT)这个包(让这个包通过),也可能执行其它更复杂的动作。工作于网络层,能对IP数据报进行首部检查。例如:IP源地址,目的地址,源端口和目的端口等。

    本防火墙的包过滤功能如下:  

    * 拒绝来自某主机或某网段的所有连接。
      * 允许来自某主机或某网段的所有连接。
      * 拒绝来自某主机或某网段的指定端口的连接。
      * 允许来自某主机或某网段的指定端口的连接。
      * 拒绝发去某主机或某网段的所有连接。
      * 允许发去某主机或某网段的所有连接。
      * 拒绝发去某主机或某网段的指定端口的连接。
      * 允许发去某主机或某网段的指定端口的连接。

    Netfilter框架是Linux内核分析和过滤特定协议数据包处理框架,为其他模块动态参与网络层数据包处理提供了方便的途径。

    该防火墙的总体结构如下:


    本防火墙的简单功能就是检查数据包是否符合过滤的条件,如果不符合就舍弃(Drop),否则就接受(Accept),这里定义八个链表头结点

    1. struct ip_node  ip_allowed_in_node_head;/*允许的远程主机或网络IP地址头节点*/  
    2. struct ip_node  ip_denied_in_node_head;/*拒绝的远程主机或网络IP地址头节点*/  
    3. struct ip_node  ip_allowed_out_node_head;/*允许的本地主机或网络IP地址头节点*/  
    4. struct ip_node  ip_denied_out_node_head;/*拒绝的本地主机或网络IP地址头节点*/  
    5.   
    6. struct port_node port_allowed_in_node_head;/*允许的远程主机或网络传输层端口号头节点*/  
    7. struct port_node port_denied_in_node_head;/*拒绝的远程主机或网络传输层端口号头节点*/  
    8. struct port_node port_allowed_out_node_head;/*允许的本地主机或网络传输层端口号头节点*/  
    9. struct port_node port_denied_out_node_head;/*拒绝的本地主机或网络传输层端口号头节点*/  

    用于保存配置文件中的地址或端口信息。

    定义两个钩子函数hook_func_in和hook_func_out,分别将其挂载到INET协议族的入口NF_INET_LOCAL_IN和出口NF_INET_LOCAL_OUT:

    1. static struct nf_hook_ops my_netfilter[] =  
    2. {  
    3.     {  
    4.         .hook       =hook_func_in,  
    5.         .owner      =THIS_MODULE,  
    6.         .pf     =PF_INET,  
    7.         .hooknum    =NF_INET_LOCAL_IN,  
    8.         .priority   =100  
    9.     },  
    10.     {  
    11.         .hook       =hook_func_out,  
    12.         .owner      =THIS_MODULE,  
    13.         .pf     =PF_INET,  
    14.         .hooknum    =NF_INET_LOCAL_OUT,  
    15.         .priority   =100  
    16.     }  
    17. };  


    说明一下自己定义的一些宏和引用的头文件:

    1. #ifndef MODULE  
    2. #define MODULE  
    3. #endif  
    4.   
    5. #ifndef __KERNEL__  
    6. #define __KERNEL__  
    7. #endif  
    8. //#define NET_DOWN  
    9. #define MY_FIREWALL_DEBUG  
    10.   
    11. #include <asm/system.h>  
    12. #include <linux/module.h>  
    13. #include <linux/types.h>  
    14. #include <linux/kernel.h>  
    15. #include <linux/string.h>  
    16.   
    17. #include <linux/net.h>  
    18. #include <linux/socket.h>  
    19. #include <linux/sockios.h>  
    20. #include <linux/in.h>  
    21. #include <linux/inet.h>  
    22.   
    23.   
    24. #include <net/ip.h>  
    25. #include <net/protocol.h>  
    26. #include <linux/skbuff.h>  
    27. #include <net/sock.h>  
    28. #include <net/icmp.h>  
    29. #include <net/raw.h>  
    30. #include <net/checksum.h>  
    31. #include <linux/netfilter_ipv4.h>  
    32. #include <linux/tcp.h>  
    33. #include <linux/udp.h>  
    34. #include <linux/igmp.h>  
    35.   
    36. #include <linux/fs.h>  
    37. #include <linux/mm.h>  
    38. #include <asm/uaccess.h>  
    39.   
    40. #define YES 1  
    41. #define NO 0  
    42.   
    43. #define IP_MAX_LEN 20  
    44. #define PORT_MAX_LEN 20  
    45.   
    46. #define ALLOWED_IP_IN 0  
    47. #define DENIED_IP_IN 1  
    48. #define ALLOWED_IP_OUT 2  
    49. #define DENIED_IP_OUT 3  
    50.   
    51. #define ALLOWED_PORT_IN 0  
    52. #define DENIED_PORT_IN 1  
    53. #define ALLOWED_PORT_OUT 2  
    54. #define DENIED_PORT_OUT 3  
    55.   
    56. #define ALLOWED_IN_IP_CONF_FILE_DIR "/etc/my_firewall/ip_allowed_in"  
    57. #define DENIED_IN_IP_CONF_FILE_DIR "/etc/my_firewall/ip_denied_in"  
    58. #define ALLOWED_IN_PORT_CONF_FILE_DIR "/etc/my_firewall/port_allowed_in"  
    59. #define DENIED_IN_PORT_CONF_FILE_DIR "/etc/my_firewall/port_denied_in"  
    60.   
    61. #define ALLOWED_OUT_IP_CONF_FILE_DIR "/etc/my_firewall/ip_allowed_out"  
    62. #define DENIED_OUT_IP_CONF_FILE_DIR "/etc/my_firewall/ip_denied_out"  
    63. #define ALLOWED_OUT_PORT_CONF_FILE_DIR "/etc/my_firewall/port_allowed_out"  
    64. #define DENIED_OUT_PORT_CONF_FILE_DIR "/etc/my_firewall/port_denied_out"  
    65.   
    66. //DEFINE FOR WORK_MODE  
    67.   
    68. /*不工作状态,默认*/  
    69. #define MODE_FREE 0  
    70. /*允许来自某主机或某网段的所有连接*/  
    71. #define MODE_IP_ONLY_ALLOWED_IN 1  
    72.   
    73. /*拒绝来自某主机或某网段的所有连接*/  
    74. #define MODE_IP_ONLY_DENIED_IN 2  
    75.   
    76. /*允许来自某主机或某网段指定端口的连接*/  
    77. #define MODE_IP_PORT_ALLOWED_IN 3  
    78.   
    79. /*拒绝来自某主机或某网段的指定端口的连接*/  
    80. #define MODE_IP_PORT_DENIED_IN 4  
    81.   
    82. /*允许本地主机或本地网络与其他主机或网络的所有连接*/  
    83. #define MODE_IP_ONLY_ALLOWED_OUT 5  
    84.   
    85. /*拒绝本地主机或本地网络与其他主机或网络的所有连接*/  
    86. #define MODE_IP_ONLY_DENIED_OUT 6  
    87.   
    88. /*允许本地主机或网络与其他主机或其他网络的指定端口的连接*/  
    89. #define MODE_IP_PORT_ALLOWED_OUT 7  
    90.   
    91. /*拒绝本地主机或网络与其他主机或其他网络的指定端口的连接*/  
    92. #define MODE_IP_PORT_DENIED_OUT 8  


    下面是防火墙模块的初始化函数:

    1. int init_firewall()  
    2. {  
    3.     (&ip_allowed_in_node_head)->next = NULL;  
    4.     (&ip_denied_in_node_head)->next = NULL;  
    5.     (&port_allowed_in_node_head)->next = NULL;  
    6.     (&port_denied_in_node_head)->next = NULL;  
    7.   
    8.     (&ip_allowed_out_node_head)->next = NULL;  
    9.     (&ip_denied_out_node_head)->next = NULL;  
    10.     (&port_allowed_out_node_head)->next = NULL;  
    11.     (&port_denied_out_node_head)->next = NULL;  
    12.   
    13.     switch(work_mode)  
    14.     {  
    15.         case MODE_IP_ONLY_ALLOWED_IN:  
    16.             open_ip_cfg_file(ALLOWED_IN_IP_CONF_FILE_DIR,ALLOWED_IP_IN);  
    17.             break;  
    18.         case MODE_IP_ONLY_DENIED_IN:  
    19.             open_ip_cfg_file(DENIED_IN_IP_CONF_FILE_DIR,DENIED_IP_IN);  
    20.             break;  
    21.         case MODE_IP_PORT_ALLOWED_IN:  
    22.             open_port_cfg_file(ALLOWED_IN_PORT_CONF_FILE_DIR,ALLOWED_PORT_IN);  
    23.             open_ip_cfg_file(ALLOWED_IN_IP_CONF_FILE_DIR,ALLOWED_IP_IN);  
    24.             break;  
    25.         case MODE_IP_PORT_DENIED_IN:  
    26.             open_port_cfg_file(DENIED_IN_PORT_CONF_FILE_DIR,DENIED_PORT_IN);  
    27.             open_ip_cfg_file(ALLOWED_IN_IP_CONF_FILE_DIR,ALLOWED_IP_IN);  
    28.             break;  
    29.         case MODE_IP_ONLY_ALLOWED_OUT:  
    30.             open_ip_cfg_file(ALLOWED_OUT_IP_CONF_FILE_DIR,ALLOWED_IP_OUT);  
    31.             break;  
    32.         case MODE_IP_ONLY_DENIED_OUT:  
    33.             open_ip_cfg_file(DENIED_OUT_IP_CONF_FILE_DIR,DENIED_IP_OUT);  
    34.             break;  
    35.         case MODE_IP_PORT_ALLOWED_OUT:  
    36.             open_port_cfg_file(ALLOWED_OUT_PORT_CONF_FILE_DIR,ALLOWED_PORT_OUT);  
    37.             open_ip_cfg_file(ALLOWED_OUT_IP_CONF_FILE_DIR,ALLOWED_IP_OUT);  
    38.             break;  
    39.         case MODE_IP_PORT_DENIED_OUT:  
    40.             open_port_cfg_file(DENIED_OUT_PORT_CONF_FILE_DIR,DENIED_PORT_OUT);  
    41.             open_ip_cfg_file(ALLOWED_OUT_IP_CONF_FILE_DIR,ALLOWED_IP_OUT);  
    42.             break;  
    43.         default:break;  
    44.     }  
    45.       
    46.       
    47.       
    48.     //open_port_cfg_file(DENIED_PORT_CONF_FILE,DENIED_PORT);  
    49.     nf_register_hook(&my_netfilter[0]);  
    50.     nf_register_hook(&my_netfilter[1]);  
    51.     printk("INIT my firewall OK!\n");  
    52.     return 0;  
    53. }  

    先从文件读取配置文件,加载到内核,然后注册钩子操作结构my_netfilter[0],my_netfilter[1]

    下图是Netfilter的IPV4下的结构

    上述的两个函数挂载位置NF_INET_LOCAL_IN和NF_INET_LOCAL_OUT,分别处理从本机发出和到达本机的数据包。

    下面就是挂载到这两个挂载点的钩子函数,用于数据包的检查

    1. static unsigned int hook_func_in(unsigned int hook,  
    2.                 struct sk_buff *skb,  
    3.                 const struct net_device *in,  
    4.                 const struct net_device *out,  
    5.                 int (*okfn)(struct sk_buff *))  
    6. {  
    7. #ifdef NET_DOWN  
    8.     return NF_DROP;  
    9. #else  
    10.     struct iphdr *iph = ip_hdr(skb);  
    11.     __be32 saddr = ntohl(iph->saddr);  
    12.       
    13.       
    14.     __be16 sport = 0,dport = 0;  
    15.     if(trans_port(iph,skb,&sport,&dport) == NO && \  
    16.         work_mode == MODE_IP_PORT_ALLOWED_IN || \  
    17.             work_mode == MODE_IP_PORT_DENIED_IN)  
    18.         return NF_ACCEPT;  
    19.   
    20. #ifdef MY_FIREWALL_DEBUG  
    21.     __be32 daddr = ntohl(iph->daddr);  
    22.     printk("saddr= %u : %u  daddr= %u : %d\n",saddr,sport,daddr,dport);  
    23. #endif  
    24.   
    25.     switch(work_mode)  
    26.     {  
    27.         case MODE_FREE:  
    28.             return NF_ACCEPT;  
    29.         case MODE_IP_ONLY_ALLOWED_IN:  
    30.             if(ip_in_cfg_file(saddr,&ip_allowed_in_node_head))  
    31.                 return NF_ACCEPT;  
    32.             else return NF_DROP;  
    33.             break;  
    34.         case MODE_IP_ONLY_DENIED_IN:  
    35.             if(ip_in_cfg_file(saddr,&ip_denied_in_node_head))  
    36.                 return NF_DROP;  
    37.             else return NF_ACCEPT;  
    38.             break;  
    39.         case MODE_IP_PORT_ALLOWED_IN:  
    40.             if(ip_in_cfg_file(saddr,&ip_allowed_in_node_head) && \  
    41.                 port_in_cfg_file(sport,&port_allowed_in_node_head))  
    42.                 return NF_ACCEPT;  
    43.             else return NF_DROP;  
    44.             break;  
    45.         case MODE_IP_PORT_DENIED_IN:  
    46.             if(ip_in_cfg_file(saddr,&ip_allowed_in_node_head) && \  
    47.                 !port_in_cfg_file(sport,&port_denied_in_node_head))  
    48.                 return NF_ACCEPT;  
    49.             else return NF_DROP;  
    50.             break;  
    51.         default:  
    52.             return NF_DROP;  
    53.             break;  
    54.     }  
    55. #endif  
    56.   
    57. }  
    58.   
    59. static unsigned int hook_func_out(unsigned int hook,  
    60.                 struct sk_buff *skb,  
    61.                 const struct net_device *in,  
    62.                 const struct net_device *out,  
    63.                 int (*okfn)(struct sk_buff *))  
    64. {  
    65. #ifdef NET_DOWN  
    66.     return NF_DROP;  
    67. #else  
    68.     struct iphdr *iph = ip_hdr(skb);  
    69.       
    70.     __be32 daddr = ntohl(iph->daddr);  
    71.       
    72.     __be16 sport = 0,dport = 0;  
    73.   
    74.     if(trans_port(iph,skb,&sport,&dport) == NO && \  
    75.         work_mode == MODE_IP_PORT_ALLOWED_OUT && \  
    76.             work_mode == MODE_IP_PORT_DENIED_OUT)  
    77.         return NF_ACCEPT;  
    78.   
    79. #ifdef MY_FIREWALL_DEBUG  
    80.     __be32 saddr = ntohl(iph->saddr);  
    81.     printk("saddr= %u : %u  daddr= %u : %d\n",saddr,sport,daddr,dport);  
    82. #endif  
    83.   
    84.     switch(work_mode)  
    85.     {  
    86.         case MODE_FREE:  
    87.             return NF_ACCEPT;  
    88.         case MODE_IP_ONLY_ALLOWED_OUT:  
    89.             if(ip_in_cfg_file(daddr,&ip_allowed_out_node_head))  
    90.                 return NF_ACCEPT;  
    91.             else return NF_DROP;  
    92.             break;  
    93.         case MODE_IP_ONLY_DENIED_OUT:  
    94.             if(ip_in_cfg_file(daddr,&ip_denied_out_node_head))  
    95.                 return NF_DROP;  
    96.             else return NF_ACCEPT;  
    97.             break;  
    98.         case MODE_IP_PORT_ALLOWED_OUT:  
    99.             if(ip_in_cfg_file(daddr,&ip_allowed_out_node_head) && \  
    100.                 port_in_cfg_file(dport,&port_allowed_out_node_head))  
    101.                 return NF_ACCEPT;  
    102.             else return NF_DROP;  
    103.             break;  
    104.         case MODE_IP_PORT_DENIED_OUT:  
    105.             if(ip_in_cfg_file(daddr,&ip_allowed_out_node_head) && \  
    106.                 !port_in_cfg_file(dport,&port_denied_out_node_head))  
    107.                 return NF_ACCEPT;  
    108.             else return NF_DROP;  
    109.             break;  
    110.         default:  
    111.             return NF_DROP;  
    112.             break;  
    113.     }  
    114. #endif  
    115.   
    116. }  

    下面是打开文件并读取信息的函数,这里以打开IP地址的配置文件为例

    1. static int open_ip_cfg_file(char * file_dir,int flag)  
    2. {  
    3.     struct file * filp = NULL;  
    4.       
    5.     char str[IP_MAX_LEN];  
    6.     int i = 0;  
    7.   
    8.     if((filp = filp_open(file_dir,O_RDONLY,0)) < 0)   
    9.         return NO;  
    10.   
    11.     mm_segment_t fs;  
    12.     fs = get_fs();  
    13.     set_fs(KERNEL_DS);  
    14.   
    15.   
    16.     struct ip_node * work = NULL;  
    17.   
    18.     while((filp->f_op->read(filp,&str[i],1,&filp->f_pos)) == 1)  
    19.     {  
    20.         if(str[i] == '\n')//next line  
    21.         {  
    22.             str[i] = '\0';//the end of a string  
    23.             i = 0;  
    24. #ifdef MY_FIREWALL_DEBUG  
    25.             printk("%s\n",str);  
    26. #endif  
    27.             work = (struct ip_node *)kmalloc(sizeof(ip_allowed_in_node_head),GFP_ATOMIC);  
    28.           
    29.               
    30.             if( ip_to_unsigned(str,&(work->ip_start),&(work->ip_end)) == 0 )  
    31.                 return NO;  
    32.   
    33.             switch(flag)  
    34.             {  
    35.                 case ALLOWED_IP_IN:  
    36.                     work->next = (&ip_allowed_in_node_head)->next;  
    37.                     (&ip_allowed_in_node_head)->next = work;//head insert  
    38.                     break;  
    39.                 case DENIED_IP_IN:  
    40.                     work->next = (&ip_denied_in_node_head)->next;  
    41.                     (&ip_denied_in_node_head)->next = work;//head insert  
    42.                     break;  
    43.                 case ALLOWED_IP_OUT:  
    44.                     work->next = (&ip_allowed_out_node_head)->next;  
    45.                     (&ip_allowed_out_node_head)->next = work;//head insert  
    46.                     break;  
    47.                 case DENIED_IP_OUT:  
    48.                     work->next = (&ip_denied_out_node_head)->next;  
    49.                     (&ip_denied_out_node_head)->next = work;//head insert  
    50.                     break;  
    51.                 default:break;  
    52.             }  
    53.                   
    54.             filp->f_op->read(filp,&str[0],1,&filp->f_pos);//eat the '\r'  
    55.         }  
    56.           
    57.   
    58.         if(i > IP_MAX_LEN) return NO;  
    59.         i++;  
    60.           
    61.     }  
    62.     return YES;  
    63. }  

    这里配置文件中不仅支持具体的IP地址,还支持IP地址网段,例如

    192.168.1.1

    192.168.1.*

    192.168.*.*

    192.*.*.*

    下面是处理函数

    1. /************************************************ 
    2. str:The IP Address like 192.168.1.1 
    3. start:The pointer to the start IP 
    4. end:The pointer to the end IP 
    5. ***********************************************/  
    6. static int ip_to_unsigned(const char * str,unsigned int * start,unsigned int * end)  
    7. {  
    8.     char cache[4][4];  
    9.     /*split the IP address*/  
    10.     int i;  
    11.     int k = 0;  
    12.     int j = 0;  
    13.     for(i = 0;str[i] != '\0';i++)  
    14.     {  
    15.         cache[j][k] = str[i];  
    16.         if(str[i] == '.')  
    17.         {  
    18.             cache[j][k] = '\0';  
    19.             k = 0;  
    20.             j++;  
    21.               
    22.         }  
    23.         else k++;  
    24.         if(j > 3) return NO;  
    25.     }  
    26.     cache[3][k] = '\0';  
    27.   
    28.     short int a[4];  
    29.     for(i = 0;i < 4;i++)  
    30.     {  
    31.         if(cache[i][0] != '*')  
    32.         {  
    33.             a[i] = (short)simple_strtol(cache[i],NULL,0);  
    34.             if(a[i] < 0 || a[i] > 255) return NO;  
    35.         }  
    36.         else  
    37.         {  
    38.             break;  
    39.         }  
    40.     }  
    41.   
    42.     switch(i)  
    43.     {  
    44.         case 4:/*Specific IP Address eg.  192.168.1.1   */  
    45.             *start = *end = (a[0]<<24) + (a[1]<<16) + (a[2]<<8 )+a[3];  
    46.             break;  
    47.         case 3:/*  eg. 192.168.1.*   */  
    48.             *start = (a[0]<<24) + (a[1]<<16) + (a[2]<<8);  
    49.             *end = *start + (1<<8) - 1;  
    50.             break;  
    51.         case 2:/*  eg. 192.168.*.*   */  
    52.             *start = (a[0]<<24) + (a[1]<<16);  
    53.             *end = *start + (1<<16) - 1;  
    54.             break;  
    55.         case 1:/*  eg. 192.*.*.*    */  
    56.             *start = (a[0]<<24);  
    57.             *end = *start + (1<<24) - 1;  
    58.             break;  
    59.         default:  
    60.             *start = 0;  
    61.             *end = (1<<32) - 1;  
    62.             break;  
    63.     }  
    64.   
    65.     return  YES;  
    66. }  

    模块的移除函数

    1. void remove_firewall()  
    2. {  
    3.     free_ip_list(&ip_allowed_in_node_head);  
    4.     free_ip_list(&ip_denied_in_node_head);  
    5.   
    6.     free_ip_list(&ip_allowed_out_node_head);  
    7.     free_ip_list(&ip_denied_out_node_head);  
    8.   
    9.     free_port_list(&port_allowed_in_node_head);  
    10.     free_port_list(&port_denied_in_node_head);  
    11.   
    12.     free_port_list(&port_allowed_out_node_head);  
    13.     free_port_list(&port_denied_out_node_head);  
    14.   
    15.     nf_unregister_hook(&my_netfilter[0]);  
    16.     nf_unregister_hook(&my_netfilter[1]);  
    17.     printk("CLEAN up my firewall OK!\n");  
    18. }  

    1. MODULE_LICENSE("Dual BSD/GPL");  
    2. MODULE_AUTHOR("yming0221@gmail.com");  
    该防火墙支持命令参数设置,根据参数设置防火墙的工作模式,只需定义和声明

    1. static int work_mode = 0;//default mode  
    2. module_param(work_mode,int,S_IRUGO);  

    目前测试,防火墙正常工作。

    可以看到数据包能到达网络层,但由于防火墙启用的相应检查规则,浏览器等应用层软件无法联网,数据包被丢弃。



  • 相关阅读:
    Atitit 代码的导航 1.1.代码的层次导航 语句 函数方法 类 包 1.2.4.4. 代码可视化 流程图 一个方法内,多个代码行的关系图 语句to方法 2 1.3.4.5. 类图 类结构
    Atitit 知识与学科的分类 杜威十进分类法 图书分类法已经采用二十二个大类 目录 1.1. 类知识的积累是一个从少到多的过程 1 1.2. 杜威十进分类法(Dewey Decimal Class
    Atitit refact art 重构的艺术 目录 1. Concept 1 1.1. Bp 1 2. Prob 2 3. Tool 2 1.Concept 1. legacy code遗留代
    Atitit 知识点 文章 框架 结构 大纲 attilax 总结 艾提拉总结 技术掌握文档总结的 v5 s420.docx 1.1. Preface前言 序言 1 2. 技术流程了解》》选型(标准
    Atitit 研发体系 codelib 代码库的建设 目录 1. 概念与组成 2 1.1. Java代码 2 1.2. Js代码 2 1.3. H5 代码 js+css+htm+txt 2 1.4.
    Atitit 方法运行器methodRunnerV3 方法虚拟机 vm 新特性 java cp C:\0wkspc\methodRunner\bin Djava.ext.dirs="
    Atitit ati擅长领域总结 目录 1.1. 要点::文化 教育 祭祀(spec ,bp ??) 2 1.2. 项目>提取共同特点》》产品》》内部产品+tool》》sdk》》spec》》准则
    Atitit webshell java 实现 命令行输出读取问题总结 1.1. 读取组赛 或者读取了一部分。。使用cmd /c 模式,强制关闭刷新缓冲区 1 1.2. 乱码解决 1 1.3. /h
    Atiitt 兼容性提升的艺术 attilax总结 目录 1. 兼容性产生的原因 2 1.1. Api变化 2 1.2. 需求的资源不满足 2 2. 兼容性的分类 2 2.1. Web方面的兼容性
    Atitit 格式转换的艺术 以excel转换txt为例
  • 原文地址:https://www.cnblogs.com/hehehaha/p/6332907.html
Copyright © 2011-2022 走看看