Tuesday, December 18, 2007

08年前大陆面临的八大危机 - 郎咸平

郎咸平:08年前大陆面临的八大危机 2007-12-10 11:46:37

时间:2007年11月16日下午 地点:上海衡山路小红楼  
 
现场:中国日报五强联盟2007总裁年会   以下为郎咸平教授现场演讲内容,由郎咸平财经工作室编辑整理   
 
 郎咸平:今天做这个演讲和以往不一样,今天特意准备了一份提纲,打破了传统。原因是,我很怕媒体报道这么重大问题的时候给我写错,所以把正确的提纲发给 每个人,给你们一个提纲而提出我的看法。在一个小时的时间内我按照我们议程的要求谈一谈大家最关切的话题,也就是2007年我们的经济到底是怎么回事。在 此我提出中国经济的八大危机,在我谈这个话题之前我认为这个话题对我们传媒业是非常重要的。我可以这么讲,传媒在做报道的时候有没有把握住一个中心思想, 我们对国家政策的推行是一个什么样的态度。与其做一个纯粹的报道,不如集思广益尽采各家说法,这就是一个客观媒体所要达到的目标。为了配合我这个讲话,我 今天提出自己的一家之言,我想对我们中国经济的现状提出一个整合性的结论。今天对于大家所关切的宏观调控做个课题,我对于今天政府所推行的各项政策我有一 个总结性的发言。我想利用一个中医的知识来解答,那就是我们今天的中国经济就像得了肺炎一样,按中医的理论,治疗肺炎要用大凉,如果我们真用大凉很可能把 这个病人治死掉,因为他体质是虚弱的,最好的方法先温补、固本,体质提上去之后最后才能用大凉来治疗肺炎。我们今天所有的宏观调控政策包括金融政策本身, 为什么解决不了泡沫的问题,通货膨胀的问题,主要原因就是我前面讲的他在用大凉治疗肺炎。而我提出的方案是先固本,为了我阐释我的想法,我要根据八大危机 一一来讨论。      

第一大危机是宏观调控的目标我认为是错误的,我们目前看到的股市楼市泡沫是什么原因呢?难道是政府讲的流动性过剩造成的吗?我认为是错的!      

今天的中国整体情况非常复杂,不是一个简单的流动性过剩,我认为最有冲击力的原因是由于近几年来整个国家投资营商环境的急速恶化,因此我们企业家把应该投 资而不投资的钱挤压出来形成虚拟资金打入股市打入楼市。这是我认为最重要的第一笔资金——虚拟资金。第二笔资金就是在目前大面积腐败之下的腐败款大量进入 楼市和股市,第三笔资金就是各位熟知的国际热钱,第四个资金才是我们老百姓的储蓄款。目前所有宏观调控的政策所针对的资金基本上是第三项和第四项。我举个 例子,外国人不得买房的规定是针对第三项资金,第二套房贷的问题是针对第四项资金,我不能说针对第三第四项是无效的,但是你的目标是错的。我们所有的政策 有没有针对第一项,那就是因为投资营商环境的急速恶化挤压出虚拟资金大量进入股市和楼市呢?我们有没有针对腐败款进入楼市股市的现象进行调控呢?目前并没 有对这两大资金进行调控,所以我们宏观调控的力道是不足,因为你所针对的资金方向是错误的。这就是我所提出的第一个危机。

第二大危机就是我们如何做调控?这个调控给我们目前经济带来了严重的金融风险,也就是大幅提高了金融风险。      

我想用日本90年代的泡沫现象和中国现在的泡沫现象做个比较。各位都应该知道“广场协议”这回事。英、德、法、美、日曾经签订过“广场协议”,“广场协 议” 要求日币升值。当时日本政府签下了协议之后那就很清楚的落入了美国圈套。道理很简单,当日本政府签下字,说明日币即将升值。那么全世界国际炒家就会去买日 币,因为他赌日币升值。大家千万别把汇率当作经济现象,汇率不是教科书上所定义的货币之间的价格,没那么简单,汇率是各国政府达到政治目的的手段!当日本 签下了“广场协议”:由于日币低估造成大幅贸易顺差,那么下一步必然是日元升值。日本政府签字的当日就昭告全世界的国际炒家日币要升值了,所以必然结果是 国际热钱大量进入日本。结果日币升值了!各位以为货币像一般的商品一样吗?价格提高之后可以平衡供需吗?错,那是一般的货品的价格,可是汇率不是,当汇率 价格一上升,国际炒家一看,哇!真的升值了,再买,更多的钱流入日本,再逼迫日币升值,更多热钱流入日本,再逼迫日币升值,几年下来日币升值了一倍,几乎 摧毁日本的经济。但是各位请注意有个现象是我们所忽略的,那就是在日币不断升值的过程中,美国财政部通过各种管道压迫日本降低利率以及放宽信贷,由于降低 利率放宽信贷的结果,造成流动性泛滥,日本各大商社很高兴向银行借钱,因为借钱容易,利息低了,可以迅速做大做强,所以造成日本经济的表面繁荣。这个表面 繁荣现象反映在股市就是股市泡沫,反映在楼市就是楼市泡沫,反映在购买日常用品就是通货膨胀。所以日币的升值、股市楼市泡沫和通货膨胀本身的原因是在美国 的压力之下造成的流动性过剩所造成的。那么最近看过很多媒体的报道,把这个问题说的比较正确的,因此认为我们中国政府针对目前的人民币汇率升值、股市泡 沫、楼市泡沫和通货膨胀应该和日本当时的政策是反其道而行之,也就是提高利率紧缩信贷。在这里清楚的告诉各位,他们都错了。因为日本的问题是流动性过剩, 但是今天中国的问题不是。中国是什么问题呢?按照我前面所讲的,造成泡沫的原因基本是由于投资营商环境的急速恶化造成的,因而挤压出大量的虚拟资金大量进 入股市楼市形成泡沫。由于原因不同,政府的宏观调控政策必须不同,但是很不幸的,我们的政策是提高利率紧缩信用,造成什么结果呢?它是进一步打击了已经恶 化的投资营商环境。利率已经提高了6%以上了,最近几天还在提高利率。那么各位设身处地的为我们的企业家想一想,你认为他在这种场合之下还愿意投资吗?中 国哪几个制造业能创造出这么高的利润,银行利率的提升逼得我们制造业放弃投资,不做了,把应该投资而不投资的钱拿出来形成虚拟资金炒股炒楼去了,所以为什 么宏观调控下股价越涨楼价越涨的原因就是因为宏观调控的目标是错的。因为和当时日本情况是不一样的。我们今天不是因为流动性过剩导致的泡沫现象,而是因为 投资营商环境的急速恶化。当然我也不否认今天中国流动性过剩的现象,但是中国流动性过剩不是股市楼市泡沫的主要原因,而真正的驱动因素,是因为我国的投资 营商环境的急速恶化所导致的。那么没有认清楚经济问题、经济弊端,贸然提高利率、紧缩货币,各位知道什么结果吗?随着利率的不断上升使得我国的金融风险迅 速积累,一旦到了不可承受的那天,中国就产生了重大危机。那么回顾一下当时的日本是怎么回事,当时日本的各大商社由于低利率大幅借款,而日本企业由于高负 债经营迅速积累的大量的金融风险到了一个不可承受的阶段产生了泡沫而崩溃,所以日本经济的危机来自金融风险,也就是负债积累所产生的金融风险。今天中国的 经济危机呢?一部分来自于不当的政策使得利率不断调升货币不断紧缩,产生了金融风险而给我国的经济带来重大的危机。而这也是为什么在07年年底我以一个所 谓的中立学者的身份我要做出以下的结论,那就是我完全支持中共中央宏观调控的思想,因为我认为宏观调控是必要的,但是我反对我们执行层面所推行的金融政 策,因为他的目标是错的。      
第三大危机就是从90年代中期之后我们各地地方政府以GDP为纲的理念,造成一个中国特有的二元经济现象。      

请问各位最近看过媒体的报道,媒体报道说我们今天中国经济是过热的,可是我请各位来宾你们再去问问你们的企业家,他们的日子好过吗?他们的日子是一天比一 天难过。不论他是谁,不论他从事什么行业,不论他在哪,他今年的日子肯定比去年难过,明年也肯定比今年难过,这是一定的。既然日子那么难过,那么经济怎么 可能过热呢?但是我们的指标指示经济过热,怎么回事呢?那就是我的理论,中国经济是个二元经济现象,中国经济是同时过热同时过冷,那些部门过热呢?那就是 我们地方政府以GDP为纲的理念之下,他投资建设的有关部门是过热的,比如钢铁、水泥、房地产是过热的。什么部门是过冷的呢?大部分的民营企业是过冷的。 这种二元经济现象可以说是全世界绝无仅有的现象,甚至可能只有在中国才可能出现,而造成这个现象的原因就是十几年来我们的地方政府以GDP为纲的理念大力 推动地方建设的结果。那么这十几年来我们老百姓的医疗、看不起病、上不起学、住不起房、退不起休怎么办呢?在十七大之前是不太被关注的吧。哪一个地方政府 不是开膛破肚修桥铺路的,美其名曰搞地方建设,实际上是为了升官发财的。对干部的考核都是以GDP为考核的标准,但是更严重的,在这种考核标准之下,地方 干部推动建设的结果是使得我们经济产生这种特殊的二元经济现象,就是和建设有关的部门基本上是过热的、和建设无关的民营企业基本上是过冷的。那么这种二元 经济现象和我们股票市场的二八经济现象非常类似。股票市场为什么有二八现象啊?什么是大盘股呢?你发现大盘股和我们过热部门是相关的,比如说过去的房地 产、钢铁、水泥还有其他的融资银行都是过热的,所以股票市场的二八现象和我们的二元经济基本上是相互对应的。那么在这种特殊的二元经济之下,一定会产生两 个必然的结果:   

第一个结果就是宏观调控进一步失效,第二个结果一定产生通货膨胀。那么我一一来说明。   

为什么宏观调控会失效,过热的部门为什么过热,他不是经济性的原因,而是政治性的原因,它是推动GDP为纲的心态导致过热部门过热。那么宏观调控是什么目 的?是用经济手段来控制过热,但是过热是政治原因,你用经济手段来调控就有问题。问题在哪里?举个例子,当中央银行提高利率为0.25%个百分点,地方政 府会因为提高利率而不搞建设吗?那照搞,不会停的,照样开膛破肚修桥铺路不会停的,为什么?为了提高GDP!提高利率的结果是他照样向银行借钱不会停的, 那么过冷部门就遭殃了,过冷部门基本上是民营企业的,而大部分民营企业的资金来源是地下金融,俗称黑市,黑市金融的利率是非常有效率、非常敏感,按照我个 人研究结果显示,他的调幅是官方利率的4倍,也就是央行提高0.25%之后,黑市利率提高4倍,提高4倍的结果是进一步打击了已经萧条的民营企业部门。按 照我前面的理论来看,你打击了他们投资兴趣,他们做什么呢?在这种逐渐恶化的投资营商环境之下他就不想投资了,而会挤压出更多的虚拟资金进入过热部门炒股 炒楼。过冷部门由于资金流出,结果更冷,过热部门由于接受了他的资金变得更热。所以越宏观调控中国的二元经济现象越突出,而这也是为什么我们越宏观调控我 们的经济数据显示中国经济更热,这就是过热的原因。是一部分部门过热而不是全体过热,你继续按照这个目标进行宏观调控的话,二元经济会更严重。   

第二是一定会产生通货膨胀。那么2006年年底农产品丰收,按照经济理论,当然这些理论在中国是不能用的,都是反的!按照经济理论农产品丰收粮油价格应该 下跌对吧,可到了一月,粮油价上升,那就是二元经济造成的。怎么造成的呢?各位再回忆一下我刚才讲的资金大量从过冷部门转到过热部门,所有过冷部门缺资 金,缺资金怎么办呢?政府印钞票,印钞票的结果造成全面通货膨胀。那么这个时候流动性过剩问题就出来了,使得一月的粮油价格上涨,上涨的幅度高达20%, 到了五月份猪肉价格上涨26%,蛋上涨37%,一直到我们今天的十一月份,这个涨幅从我们的20%涨到60%,那么最近还是涨幅很高。也就是说你们吃饭的 时候呢,咬一口肉吃一口饭要多付50%的钱,他不像洗衣机,电视机那样,看旧的就可以了。吃饭每天要吃3次,这个涨幅是不得了的。按照目前的统计,你们买 菜钱呢现在应该是上涨50%了吧。那么这么严重的通货膨胀而且是民生必须品是怎么来的,基本上是我讲的二元经济与虚拟资金的原因。以猪肉为例,我们谈一谈 猪肉价格为什么上涨?有两个原因,第一个是猪瘟,在07年的今天竟然会产生猪瘟的现象,可见地方政府是如何的失职,这个控制猪瘟不能靠农民的。这一定是政 府行为,现在的地方政府都在抓建设,这种疾病的控制他已经不太在乎了,而这也是为什么在07年会产生猪瘟的原因。第二是养猪的饲料价格上涨。猪瘟以及养猪 饲料价格上涨使得养猪的民营企业家的投资营商环境急速恶化。按我的理论,投资营商环境急速恶化之下养猪的民营企业家会做什么,他就把应该投资不投资的钱挤 压出来形成虚拟资金打入股市和楼市,所以我们仔细研究猪肉价格上升的原因就是把大猪宰了之后“后继无猪”了,大家不养小猪了。那么不养小猪,就像各位一样 炒楼炒股去啦。这就是我们刚刚所讲的二元经济和虚拟资金充分解释了最近通货膨胀的原因,这个二元经济的本质是来自地方政府以GDP为纲的理念所造成的恶 果。这是我所谓的第三个经济危机。


第四大危机也是地方政府以GDP为纲的理念造成了整个经济结构的失衡。      

大家想一下,我们各级地方政府建高速路、高架、铁路都是固定投资多。他的固定投资成本是多少,我想拿我们国家和日本经济崩溃之前做个比较,我们固定投资的 比重占45%,而当时日本是30%,消费我们是占了35%,而日本占了58%。这么大量的投资,国内消费是不足的。那么国内消费不了,我们每年以10%的 经济增长,造出那么多的经济、货品卖给谁呢?卖给老外嘛!而这造成大量贸易顺差。那么当时日本贸易顺差所占GDP的比重为4.5%,而中国是9%,而且各 地政府对于出口本身的思维是错的,叫出口创汇。出口创汇在我看来是个罪恶的代名词,出口创汇就是拿我们有限的资源图利外国人。举个例子,中国那么一个缺少 资源的国家敢把煤炭卖给日本人,而日本人拿去填海了。竟然把我们的树木砍下来做成毫无附加价值的筷子卖给日本,日本为什么不砍自己的树呢?他自己不会做那 个傻事的,他不会干破坏资源的事,给谁干呢?给中国人干!你用煤炭、筷子等毫无附加价值的东西流血赚取必定贬值的外汇,美其名是出口创汇,实际上就是罪恶 的代名词。地方政府在以GDP为纲的理念基础上出口创汇的理念使得我国经济严重失衡,造成大幅贸易顺差。我们有高达1.4万亿的贸易顺差,如果说你以 1.4万亿为傲,这1.4万亿都是一系列错误政策的必然结果。什么结果呢?给了欧美一个极好的机会压迫人民币升值。我今天实在想不出我国政府有什么理由不 让人民币继续升值。人民币必然升值就是我所谓的第四个经济危机。      

第五大危机 我就把前面四项的危机做个整合,一个是利率的上升,一个是错误的宏观调控造成利率的上升,地方政府错误的GDP为纲的政策及出口创汇政策造成汇率的上升。      

两大金融参数的上升使得我国企业已经难以维持生计的投资营商环境继续恶化而逐渐流血而死。你看我国哪个出口型的制造业日子好过的,日子是一天比一天难过。 所以他更不会投资了,所以他一定会像我前面所说的那样把应该投资的而不投资的钱挤压出虚拟资金进入楼市股市,那么你们可能会问我了,到底虚拟资金有多少? 告诉各位一个数字,那就是我们企业家应该投资而不投资的钱和欧美相比,80%投入股市楼市,造成泡沫。所以汇率一上升泡沫现象更严重,而表面上看起来和日 本当时一样,哪里知道原因是非常复杂的。中国企业家不投资了,去炒股炒楼了。我想请各位思考一个问题,你应该投资而不投资,这个投资的空间给谁了呢?这个 空下来的空间必然的为外资企业所用。利率汇率的上升造成了营商环境的困难,其必然结果给外资进入中国提供一个绝佳的机会,诸位看一下我们的民营企业家,这 种卖公司、卖工厂的现象比比皆是,是由于营商环境投资困难。这就是我所谓的第五个危机。      

第六大个危机我想请各位抬头仰望着蓝天,你们发现在07年的中国天空上翱翔着两只秃鹰,秃鹰是干什么的呢?秃鹰是吃尸体的。我前面就说过了,中国的制造 业,因为前面所说的所有的理论而逐渐流血而死,头上的秃鹰就飞下来啃食我们的尸体。头上两只秃鹰是什么呢?一个是产业资本,第二个是金融资本。      

各位都知道青岛啤酒是我国的著名品牌,你们还认为青岛啤酒还是中国企业吗?告诉你一个数字,青岛国资局控股30%,但是你们知不知道第二大股东是谁?来自 美国的安海斯-布希公司控股27%,只要他再多买4%的H股,我们中国的青岛啤酒就会在一夜之间变成外资企业。各位还记得徐工的故事吗?美国的凯利基金要 收购徐工,当时包括我在内很多的学者专家在媒体上对这件事情大家大法,由于大家的努力,成功制止了资产流失的现象。当时的地方政府以什么理由卖给外国人 呢?是以负的净资产卖给外国人。负的净资产就可以卖了吗?你有没有那么一点点经济常识啊。一个公司的价值不取决于净资产,而是取决于有没有持续经营的能 力。对于徐工的价值取决于未来持续盈收的能力,还好没卖,假如以100块钱卖掉,按照我所理解的凯利基金或者类似的基金,一定会把徐工分拆上市或者卖掉, 赚到一万块钱。所以从100块价格到一万块他可以赚100倍以上。那么各位了不了解,收购青岛啤酒的安海斯-布希公司就是产业资本,收购徐工的就是金融资 本,这两个资本的危害性我们还没有看出来,还在乐观的招商引资,你知不知道招商引资使得这两只秃鹰来席卷中国奄奄一息的制造业。那么你们可能要问我了,郎 教授这样是不是太悲观了,中国制造业我们自己都做不下去了,给外国人能赚钱吗?如果你们有类似的想法呢,你们就太不了解资本主义国家了!为什么我说的话那 么像社会主义经济学家,而我们社会主义经济学家讲的话更像资本主义经济学家。其实错了,我是真正吃资本主义奶水长大的资本主义经济学家,所以我对他的理解 是非常透彻的,我所以讲的这么社会主义是因为我理解了。各位想一想我们中国的企业经营不下去,负的利润,你以为外国人经营不下去吗?小看人家了。当他透过 产业资本和金融资本收购我们这些所谓不赚钱的制造业之后,他会把你融入到国际产业链的分工里面去。这是我最近做的一系列的研究,什么叫做国际产业链的分 工,那就是在国际产业链中真正做制造的是一块钱,而做这种采购、仓储、订单处理、批发、零售这整条产业链叫做软三元。国际产业链的分工是怎么回事呢?本质 就是靠软三元赚取利润,硬一元再亏损了不起亏一块钱,也不可能亏一块钱,软三元可以赚多少钱回来啊!以玩具业为例,一个巴比娃娃我们的出厂价一块美金,在 美国的沃尔玛零售价格是9.99元美金,那一块美金,原料占了0.65%,生产价0.35%,你晓不晓得我们能赚多少钱,一美分了不起了吧。我们是剥削我 们的劳动者,浪费我们的资源。用这么贱的一块钱的价格卖到美国去,他最后以9.9的价格卖给消费者,他们席卷了所有的利润。我们的商务部长薄熙来针对这个 现象提出他的看法,他的看法一部分对的,那就是我们中国的制造环节是不赚钱的,而我们透过我们的牺牲,让美国人欧洲人享受我们的产品,这是对的。可是各位 再想一想你需要呼吁吗?难道美国不知道吗?你把他看的太傻了吧。你都知道他会不知道吗?怎么可能呢?难道他们不知道他们进货价格是一美金吗?难道他们不知 道我们的劳动者几乎拿不到利润吗?难道他不知道是9.9吗?那为什么还要继续压迫我们,提高利率提高汇率,让我们仅有的一分钱都不让我们赚呢。这就是我所 谓的金融战争。其目的就是让你倒闭,你倒闭之后,我们头上两只秃鹰贱价把你收购,融入国际产业链里面去,你这里亏钱无所谓,软三元可以赚回来。硬一元里 面,根据我最近的研究结果显示,原料成本、制造成本可以省下来的钱是25%。我们再压低我们的工资、压低原料成本价格只是把整条产业链的一块钱里面的 25%省下一点点而已,真正的大头都被软三元拿去了。那么这种格局是我们国家到现在还没认识的。我们还在继续的招商引资,最后的结果呢就是把我们的制造业 拱手让给外国人。      

我请各位注意一下,我讲的现象已经发生了,你们认为我们中国有廉价劳动力的优势嘛,这样以为的话你就大错特错了!劳动力的优势最多使你硬一元里面,最多节 省25%的钱,仅此而已。可是整个国际产业的分工呢,它从软三元赚取利润。所以最近上海来了两家外资企业,一家是西班牙的ZARA,一家是瑞典的企业 H&M。ZARA80%的生产在欧洲,他为什么不来中国生产呢?我们不是有廉价劳动力吗?人家来都不来。那个不重要!重要的国际产业的分工整合最 重要,所以ZARA在欧洲进行80%的生产。他的衣服做出来不但款式新颖、时尚、潮流、质地好,还有就是比你国产的还便宜。H&M更便宜,比 ZARA便宜30%,他的大衣质地好不讲了,时尚不讲了,还是国际名影视红星麦当娜所设计。多少钱一件,大家知道吗?500-800一件。你说我们哪能造 的出来!人家为什么用这么低的价格进入中国呢?那就是国际产业化的分工,人家开始向软三元要利润。他通过软三元节省大量成本而放弃硬一元,因此才能以最高 的利润和最低的成本进入中国。这是2007年开始发生的现象,我们要密切地注意。如果我们还对廉价劳动力有任何幻想的话你太可怜了。举个例子,我们有两家 著名的企业就开始幻想,一家是TCL的李东生,第二是台湾的明基叫BENQ,这两家要搞国际化。怎么搞呢?他们想利用中国廉价劳动力配合上国外的品牌和技 术,想走出去,想国际化。按照郎教授的理论你不会成功的。为什么?你除非走入软三元!靠硬一元你是走不出去的,因为廉价劳动力最多只能省下25%的钱。所 以各位都知道TCL合作以及收购了阿尔卡特以及法国汤姆逊,明基收购西门子的移动业务,一两年之后彻底轰然跨台。为什么?你走不出去!为什么走不出去?因 为你企业的战略都是错的,错在哪里呢,劳动成本已经不重要了!最终要搞国际产业的整合,放弃硬一元,向软三元要利润,这才是国际化。我们有多少企业知道这 个呢?我们天天喊的国际化招商引资竟而把我们整个产业链拱手让给外国人,竟而容许他们洗劫我们。我为什么说是洗劫我们?我想请出一位农夫给各位做个说明。 你问一问农民:你如何灌溉?农民会告诉你灌溉之前,打开水闸之前先要挖沟渠,把水通过沟渠引到需要水的地方才叫灌溉。如果你傻不拉机的问农民,如果我忘了 挖沟渠呢?农民肯定会骂你:你傻呀,你不挖沟渠的话你打开水闸,洪水漫流大地不都把良田都淹没了吗?农民都知道的事,我们好多人都不知道的。各位了不了解 什么叫国际化?中国目前所推行的国际化就是打开水闸之前忘了挖沟渠,中国是一个没有沟渠的国际化。什么叫沟渠?法制化的游戏规则叫沟渠。那么各位,我们国 际化之前有过法制化的游戏规则吗?我们哪一个行业有这种法制化的游戏规则?我们哪个行业不是百分百的开门让外资进来!到最后的结果就是外资携其软三元的优 势,在一个没有法制化的游戏规则的中国,就像洪水一样漫流大地把中国的良田美地完全侵略了,这就是国际化的结果。这个现象很重要,据美国麦肯锡公司去年的 预测,五年之后,外资零售业将占领80%的中国市场,各位来宾这么一听是不是很高兴呀,不错呀,沃尔马、家乐福很好呀,进入中国呢,灯火通明、物品种类齐 全、服务态度良好、价格物品低廉,像家乐福,时不时的贴出广告:半径5公里之内,如果同样的货品你买到更便宜的,愿意用数倍的差价弥补你的损失。你们不知 道这个事吗!一听,好好的外资哟,他席卷中国有什么不好呀!中国对他们来讲是一个天堂,因为从来就没有法制化的游戏规则。当他们一旦席卷80%的零售市场 之后,你们相不相信我下面的预测,那就是外资零售业将联合垄断,是上抬销货价格剥削消费者,下压进货价格剥削生产者,把中间利润迅速扩大,合法的汇出中 国,你相不相信?这就是我们的未来,因为我们的国际化是一个没有法制化的游戏规则的国际化,所以一定是洪水淹没了我们的良田。而且我们的制造业还在向硬一 元要利润的阶段,人家已经放弃硬一元开始向软三元要利润了,所以他凭借着高利润低成本的优势杀入中国,你将无可抵御,因为你在国际化之前忘了挖沟渠,没办 法......! 

这是我讲的第六个危机.      

第七个危机  
继制造业之后我国的金融业将为外资所席卷.      
金融的改革包括汇率的改革以及银行的改革.这里我想先和各位谈谈银行的改革,我们像建行等那么多银行上市,大家知道不知道帮他们做顾问都是哪些公司吗?摩 根、美林、高盛。这些公司也是美国中央银行的股东,你们知道吗?我们要搞银行改革竟然要请美国中央银行的股东来当我们的顾问,你说你是傻呢还是怎么回事 呢!他给你做顾问的目的是图利于你呢还是图利于美国中央银行呢?我给各位一个数据,世界银行在2000年出了一个研究报告,讲的是银行改革。各位相不相信 我国从事银行改革的所有相关人员没有一个人念过这篇报告,我们的银行改革全是想象出来的,他们的这个报告是靠数字说话的。他们收集了过去全世界250多次 银行危机,他想了解各国银行如何进行改革,研究结果是这样显示的:250多次银行危机里面有92次找不到资料,141次各国政府包括美国在内都束手无策, 3次加强监管,14次放松监管。也就是说90%的案例里面各国政府束手无策,因此这篇文章的结论很有意思,它说:全世界的织组包括国际货币基金、世界银行 以及各大银行还有学术界的泰斗,没有一人知道如何改革银行。既然全世界没有一个人知道如何改革银行,你中国怎么敢改呢?而且你的顾问都是美国中央银行的股 东帮你做的顾问,因此给你的建议呢就是改制、上市。改制、上市很有意思,你们都有从全国各地来的,从东北一直到珠海,你到任何一个乡下去的话你都能看到一 个建行,它的网点分布之广是你不可想象的。我也是建行的客户,虽然建设银行的服务很糟糕,态度也不好,为什么我还用建设银行呢?分行特多,这是他唯一的原 因。就算我们政府完全开放银行,让花旗银行进来,他没有能力开那么多的分行的,你要知道要开像建设银行一样多的分行,要租下多少楼盘。你要投入多少钱去做 硬件与软件的开发,投入多少钱多少时间去做人员的培训,你知道不知道这么一趟下没有几万亿美金做不到的。所以他们会怎么做?所以这些聪明的顾问们就不会这 么做,他一定会要求你建设银行上市,只要你建设银行上市,美国的银行买了你20%的股份,你所有分行赚的钱他就拿走20%。同时, 银行经营在中国是垄断 经营,垄断经营都要牌照费的,建行为什么不交牌照费呢?因为他是国有银行不需要交牌照费。你上市之后卖给花旗银行,你为什么不收牌照费呢?他忘了, 他根 本不知道要收牌照费!所以外资银行包括美国银行等等,可以以最便宜的价格,以不交牌照费的方式大量购买建设银行的股票,你只要买到20%所有分行的利润你 都要分20%。看一下最新结果。这次因为美国次级债缘故损失了不少金钱,他们的首席执行官本周很骄傲的说了一句话:我们在次级债的损失远远的小于我们在建 设银行的投资。他们在建行赚了多少钱呢?赚了1300亿!!那么我们的水平呢?我们的外汇管理在娄继伟的管理之下投资美国的黑石,惨败而归, 这就是水 平!!人家为什么知道这么做呢, 因为金融战已经开始了。我们金融战的水平和一百五十年前大刀对洋枪的水平差不多。你看看美国银行和建行的故事,再看看外 汇投资基金和黑石公司的故事,一比较下来你就豁然开朗。为什么美国要逼迫我们银行上市,因上市才是美国最快收购我国银行的捷径。我们竟然听话照办!另外的 改革叫做汇率的浮动化.我一直反对这种改革,原因在哪里呢?因为我们中国没有人才!!我们中国可能有很多方面的人才,但是在外汇操作方面,我可以这么讲包 括郎教授本人在内我们全国13亿人口没有一个专家.没有专家你怎么敢开放人民币浮动?各位了不了解1997年,为何索罗斯阻击亚洲?造成亚洲金融危机.那 就是因为亚洲资产泡沫化.亚洲当时除了四小龙之外还有四小虎,对于欧美各国严重威胁,所以索罗斯开始阻击泰铢.因为泰铢刚刚改成浮动汇率,阻击下来,亚洲 各国尤其四小虎再也没有喘息的机会了.他们也没放过香港,同时也阻击香港.香港当时也是资产高估,股市很好,18000点,楼市价格迅速上升,整个资产升 值情况下阻击港币是最好的,他们同时卖空港币同时卖空恒生股市期货,什么叫卖空?以股票市场为例,今天向某证券公司借出一张股票以今天假设50元卖掉,明 天股价跌到30元, 我再买回来了把股票还给证券公司,叫卖空.50元卖的,30元买的,就赚20元差价.所以卖空的目的就是赌你股票会跌.外汇也是一 样,什么叫卖空?一样,向某金融机构借出港币卖掉,明天跌了再买回来再还给他,意义是一样的.他们同时卖空港币同是卖空恒生指数期货,当时大量卖空港币, 把港币卖给香港的金融管理局,所以市场上缺少港币了.因此银行的拆借利率大幅提高,最高到280%的拆借利率.280%的拆借利率大家知道对股票市场会有 什么打击吗?股价大跌,从18000点跌到了98年的6660点,跌了三分之二的市值.可是国际炒家在事先就大量卖空恒生指数期货,所以赚了三分之二的市 值,到了最后香港全体老百姓买单,因为股市大跌,楼盘也跟着大跌,索罗斯等人席卷一把之后离开了香港,所以当时产生了索罗斯振荡.在2006年之前,只要 恒生指数到了18000点之后呢,就会大幅波动叫做索罗斯振荡,把香港老百姓给吓到了.现在为什么到了三万点了呢?因为国内老百姓搞不清楚,从深圳通过很 多方法大量炒港股,就把港股拉上去了.香港老百姓吃过亏,知道的!国内老百姓没有吃过亏不知道,搞不清楚的!香港还是固定汇率的,是联系汇率,他要是浮动 汇率那更惨.那各位知不知道,为什么在这么严厉的亚洲金融风暴之下,中国能幸免于难呢?你以为是我们领导人英明吗?不是, 而是当时我们采取了两个最传统 最古老的制度,一个是固定汇率另外一个最重要的是外汇管制,由于固定汇率及外汇管制,保护了中国,不是由于我们能力好,而是这两个原因.按照我这个故事讲 起来的话,如果国际炒家要阻击今天中国的泡沫和当初的印尼,马来西亚,泰国和香港一样,到处都是泡沫,股市泡沫、楼市泡沫、通货膨胀,要阻击这种泡沫经 济,像席卷香港一样来席卷我们的财富,什么方法是最好的?那就是像泰国一样,阻击浮动汇率,那是最好阻击的,连联系汇率的香港都能阻击,何况是浮动汇率. 而这也是为什么美国政府不断的逼迫人民币汇率浮动化的原因.只要配合上浮动汇率,外资自由进出,再加上股指期货的推出,将使的中国的金融市场完全暴露于国 际炒家的阻击之下,而这一切我们基本上都完全具备了.我们有没有泡沫,有!有没有通货膨胀,有!有没有股指期货,快了!有没有浮动汇率,我们正在努力!经 过我们政府以及我们老百姓的逐渐努力,我相信在不久的将来我们一定会给国际炒家一个非常好的阻击中国的机会.大家拭目以待!这就是我所谓的第七个经济危 机.  

        
第八大危机  

2008年一月一号开始的外资银行可以经营人民币业务的危机.      
我们的中国银行,放款客户里面有多少好客户?我给各位一个数字,10%差不多了吧!其它的呢,不敢说全部,很多都是骗子,包括地方政府在内.他们怎么骗 呢?拿到钱直接当利润处理,我们很多地方政府不就是这么干的吗!存款客户有多少好客户呢?20%了不起了吧!百分之八十的是什么客户呢?都是我们那种小老 百姓的客户,今天存一百,明天提80,这种客户只是提高了交易成本,银行赚不到他的钱的.银行真正能赚钱的客户,是20%的大客户,20大的好的存款客户 和10不到的好的放款客户,你们认为有没有可能在明年一月一号之后大量转移到外资银行呢?我敢讲一句话, 外资银行有一个好处,那就是在国家进行宏观调控 的时候不会没有理由的给你收紧银根,那事他不会干的.我们的内资银行个个在干!如果大家都转过去了呢?如果这种好的存款和好的放款客户都转过去了,那这叫 我国内资银行如何经营?银行失血的现象是不是会发生呢,经营不济的情况会不会发生呢?更严重的,就是我前面所讲的一连串的不理解国际化的前提之下,是不是 给予外资银行进一步的进入内资银行参与经营的机会呢?所以明年一月一号开始的外资银行经营人民币业务给我国金融体系又给加上一个第八项的不稳定因素.当然 我们在批评外资银行的时候首先要检讨自己,外资并没在要求我们马上开放,给了我们三年的缓冲期.我们三年在做什么事了,你们猜一下?我们都在这种外资顾问 的指导之下,改制上市去了.你有没有把银行的最基础工作做好?什么工作呢,把银行的信用搞好,把银行的服务品质搞好,有没有把银行真正做成服务客户的平 台.没有!你根本没有!搞什么呢,大家从上到下热火朝天按照美国中央银行股东的要求改制上市去了.这三年下来,我们银行的信用如何?服务态度如何?和外资 银行相比,优劣立判!如果你是个大的存款客户,你有没有可能转到外资银行里面去呢?你知不知道你转到外资银行当存款客户有什么好处吗?你相不相信他们立刻 给你一个全球通用的钻石卡或金卡?你可以存人民币到欧洲到美国随意消费,你相不相信?多好呀!他们还需要开分行吗?因为大客户基本在城市,他只要把握了广 东,上海,北京,差不多就席卷我们大部分的最优质的客户了.他不需要开分行了, 开分行 越开到偏僻的地方,小客户越多, 都是今天存一百明天提八十的, 都赚不了钱的.所以他们只要寄守在大城市把服务做好,继续鼓动中国的银行去上市,那么他们就是最终的赢家.而我们目前也正在努力的帮助他们完成他们的愿 望.      

总结那么这八项危机,我相信我已经囊括了2007年底的诸多经济现象,你们所看到的这一切,你们诸多的不理解,经过今天我的八大危机的分析之后呢,我相信 你们都已经豁然开朗,这就是今天中国的经济危机.作为一个独立的学者,我也希望通过这个机会把我的观念透过媒体发表出来,我认为这就是媒体存在的目的,我 们不需要歌功颂德,我们需要尽早的理解国际形式!尽早理解什么叫国际化!给我们的政府给我们的老百姓提供一个不同的角度和观点.我也希望政府按我讲的八大 危机,提出一些合理的治理办法,这是我今天跟各位花了一个小时时间谈话的最终目的,谢谢各位,刚好一个小时.

Thursday, December 13, 2007

nstalling a WLan Card with Atheros Chipset under Debian Linux and NetBSD

http://www.fehu.org/atheros-en.html

Installing a WLan Card with Atheros Chipset under Debian Linux and NetBSD

1. General

1.1 Supported Hardware

1.2 Supported Modes

2. Installation under Debian Linux

2.1 Required Packages

2.2 Installation Process

2.3 Activating the Card

3. Configuration under NetBSD

4. Configuration of wpa_supplicant with WPA-PSK

5. Further Information

5.1 Wireless Applications

5.2 Links

5.3 Manpages



1. General


1.1 Supported Hardware

Basically all cards with a chipset of the type ar5210/ar5211/ar5212 are supported. To be sure, look at http://madwifi.org/wiki/Compatibility. That list will be continously adjusted by users. Another good source of information for chipsets and characteristics is also http://www.wifi.com.ar/doc/wifi/wlan_adapters.html


1.2 Supported Modes

Both the Linux MadWifi driver and the NetBSD driver are based on a binary HAL (Atheros Hardware Abstraction Layer) and its kernel module ath_hal, which enable access to the firmware. That HAL is "closed source" and is developed including an object file by Sam Leffler, employee at Atheros, compiled for different architectures and made available to the OpenSource-community. Which modes are supported and which not depends mainly on the work of that man.

The documentation, forums and mailing lists give information about available modes

  • 802.11a/b/g, in mixed mode
  • 128-bit WEP, WPA-PSK through wpa_supplicant(Linux)
  • Turbo Mode (802.11a only, Channels 42,50,58,152,160)
  • Running the card with specific countrycodes (not in turbo mode!)


2. Installation under Debian Linux

The installation of the kernel drivers is possible both under kernel 2.4 and 2.6.


2.1 Required Packages

The underlaying system is a Debian Sarge with kernel 2.6.7-1-386. Please adapt it to your needs. Following packages must be installed:

cvs
wireless-tools
kernel-headers-2.6.7-1-386
sharutils
uudecode
libc6-dev

2.2 Installation Process

Now it is time to check out the MadWifi drivers from the CVS-repository and install them:

$ cd /usr/src
$ cvs -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/madwifi login
$ cvs -z3 -d:pserver:anonymous@cvs.sourceforge.net:/cvsroot/madwifi co madwifi
$ cd madwifi
$ make
$ make install
$ make clean

The installed modules will be located in /lib/modules/2.6.7-1-386/net.


2.3 Activating the Card

General tools for configuring WLan cards can be found in the "wireless-tools" package. They will be explained shortly below:

  • iwconfig edits basic WLan settings like SSID, WEP-Key or channels, among other things. iwconfig(8)
  • iwlist allows scanning, listing frequencies, bit-rates, peers and accesspoints, among other things. iwlist(8)
  • iwpriv allows editing of driver specific (private) settings like turbo mode and frequence ranges, among other things. iwpriv(8)

Before loading the modules it is recommended to verify in which frequence range, subband and channel the accesspoint is running.
The driver works default in 802.11a/b/g in the 5 GHz subband 1+3:

$ iwlist ath0 chan
Channel 01 : 2.412 GHz
Channel 02 : 2.417 GHz
Channel 03 : 2.422 GHz
Channel 04 : 2.427 GHz
Channel 05 : 2.432 GHz
Channel 06 : 2.437 GHz
Channel 07 : 2.442 GHz
Channel 08 : 2.447 GHz
Channel 09 : 2.452 GHz
Channel 10 : 2.457 GHz
Channel 11 : 2.462 GHz
Channel 36 : 5.18 GHz
Channel 40 : 5.2 GHz
Channel 42 : 5.21 GHz
Channel 44 : 5.22 GHz
Channel 48 : 5.24 GHz
Channel 50 : 5.25 GHz
Channel 52 : 5.26 GHz
Channel 56 : 5.28 GHz
Channel 58 : 5.29 GHz
Channel 60 : 5.3 GHz
Channel 64 : 5.32 GHz
Channel 149 : 5.745 GHz
Channel 152 : 5.76 GHz
Channel 153 : 5.765 GHz
Channel 157 : 5.785 GHz
Channel 160 : 5.8 GHz
Channel 161 : 5.805 GHz
Channel 165 : 5.825 GHz

If the card should be operated with these settings, try

$ modprobe ath_pci

To get support for country specific frequencies, it is necessary to assign the parameter "countrycode" while loading the module.
For example: Germany, 5 GHz subband 1+2

$ modprobe ath_pci countrycode=276
$ iwlist ath0 chan
ath0 255 channels in total; available frequencies :
Channel 01 : 2.412 GHz
Channel 02 : 2.417 GHz
Channel 03 : 2.422 GHz
Channel 04 : 2.427 GHz
Channel 05 : 2.432 GHz
Channel 06 : 2.437 GHz
Channel 07 : 2.442 GHz
Channel 08 : 2.447 GHz
Channel 09 : 2.452 GHz
Channel 10 : 2.457 GHz
Channel 11 : 2.462 GHz
Channel 12 : 2.467 GHz
Channel 13 : 2.472 GHz
Channel 36 : 5.18 GHz
Channel 40 : 5.2 GHz
Channel 44 : 5.22 GHz
Channel 48 : 5.24 GHz
Channel 52 : 5.26 GHz
Channel 56 : 5.28 GHz
Channel 60 : 5.3 GHz
Channel 64 : 5.32 GHz
Channel 100 : 5.5 GHz
Channel 104 : 5.52 GHz
Channel 108 : 5.54 GHz
Channel 112 : 5.56 GHz
Channel 116 : 5.58 GHz
Channel 120 : 5.6 GHz
Channel 124 : 5.62 GHz
Channel 128 : 5.64 GHz
Channel 132 : 5.66 GHz
Channel 136 : 5.68 GHz
Channel 140 : 5.7 GHz
Current Frequency:2.412GHz (channel 01)

These "countrycodes" can be viewed at http://www.unicode.org/onlinedat/countries.html. They are stored in the EEPROM of the card and aren't writeable.

If the modules are loaded with false settings unload the driver

$ modprobe -r ath_pci

and go on with experimentation.

If the card should be bound to 802.11a,b or g, do

$ iwpriv ath0 mode X

where X stands for 0=802.11a/b/g, 1=802.11a, 2=802.11b, or 3=802.11g.

The turbo mode is only possible in the 5Ghz band and limited to a few channels in subband 1+3. The countrycodes must not be set!

$ modprobe ath_pci
$ iwpriv ath0 mode 1
$ iwpriv ath0 turbo 1
$ iwlist ath0 chan
ath0 255 channels in total; available frequencies :
Channel 42 : 5.21 GHz
Channel 50 : 5.25 GHz
Channel 58 : 5.29 GHz
Channel 152 : 5.76 GHz
Channel 160 : 5.8 GHz
Current Frequency:5.21GHz (channel 42)

But even with the latest drivers it wasn't possible to convince Linux to enable turbo mode... After allocating an IP you get the error message "ath0: unable to reset hardware; hal status 12"

An example for connecting to an AP with WEP

$ modprobe ath_pci countrycode=276
$ iwconfig ath0 essid blabla
$ iwconfig ath0 key 14929429325878194789834632
$ ifconfig ath0 192.168.1.66

If you want to save your settings for automatical loading after a reboot, you have to adapt /etc/modprobe.d/aliases for 2.6, or alternatively for 2.4 /etc/modutils/aliases, and /etc/network/interfaces.

$ echo "alias ath0 ath_pci"  >> /etc/modprobe.d/aliases
$ echo "options ath_pci countrycode=276" >> /etc/modprobe.d/aliases
$ update-modules
$ cat /etc/network/interface
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp

# Wireless
auto ath0

# static
#iface ath0 inet static
# address 192.168.1.66
# netmask 255.255.255.0
# broadcast 192.168.1.255
# up /sbin/iwpriv ath0 mode 3
# up /sbin/iwconfig ath0 essid "blabla" rate 54M key 14929429325878194789834632

# dhcp
iface ath0 inet dhcp
pre-up /sbin/iwpriv ath0 mode 3
pre-up /sbin/iwconfig ath0 essid "blabla" rate 54M key 14929429325878194789834632

3. Installation under NetBSD

Tested and documented: Hubert Feyrer


With NetBSD the setup is easier because the driver is already in the kernel provided that you use NetBSD 2.0 or higher.
The drivers default settings, the various modes, limitations etc. are equal to the Linux MadWifi driver due to the identical HAL.

Listing the modes can be done with

$ ifconfig -m ath0

To use the 5-GHz band, run

$ ifconfig ath0 ssid blabla media autoselect mode 11a

Enabling the turbo mode

$ ifconfig ath0 ssid blabla chan 58 media autoselect mode 11a mediaopt turbo"

The "countrycode" is adjustable with the following command:

sysctl -w hw.ath.countrycode=276

To configure the card automatically after a system boot add the following information to /etc/rc.conf, for instance :
...

#
# WaveLAN+Internet Config
# static
#ifconfig_ath0="10.0.0.1 netmask 255.255.0.0 ssid blabla chan 58 media autoselect mode 11a mediaopt turbo"

# dhcp
ifconfig_ath0="mode 11a nwkey 0x14929429325878194789834632 ssid fehu.org"
dhclient=YES
dhclient_flags="ath0"

...

Further information:
* ath(4) Manpage
* http://www.netbsd.org/Documentation/network/wavelan.html



4. Configuration of wpa_supplicant with WPA-PSK


Unfortunately at the moment WPA is only possible with Linux.
The next steps explain how to enable WPA-PSK authentication (WPA-Personal) with wpa_supplicant-0.3.8 .

First install libssl-dev, then download wpa_supplicant

cd /usr/src
$ wget http://hostap.epitest.fi/releases/wpa_supplicant-0.3.8.tar.gz

Now let's install the drivers. To compile them you have to create a .config in the same directory. Either edit defconfig and copy it to .config , or make a new file like me. I will show the content with "cat"

$ tar xvzf wpa_supplicant-0.3.8.tar.gz
$ cd wpa_supplicant-0.3.8
$ cat .config

CONFIG_DRIVER_MADWIFI=y
CFLAGS += -I/usr/src/madwifi
CONFIG_DRIVER_WEXT=y
CONFIG_IEEE8021X_EAPOL=y
CONFIG_EAP_MD5=y
CONFIG_EAP_MSCHAPV2=y
CONFIG_EAP_TLS=y
CONFIG_EAP_PEAP=y
CONFIG_EAP_TTLS=y
CONFIG_EAP_GTC=y
CONFIG_EAP_OTP=y
CONFIG_EAP_LEAP=y
CONFIG_PKCS12=y
CONFIG_CTRL_IFACE=y

$ make && make install

Then you have to build a configuration-file for wpa_supplicant. Again either edit wpa_supplicant.conf and copy it to somewhere or make a new file. I called it wpa-fehu.conf and placed it in /etc. The content is

ctrl_interface=/var/run/wpa_supplicant
eapol_version=1
network={
ssid="fehu.org"
scan_ssid=1
priority=5
proto=WPA
key_mgmt=WPA-PSK
pairwise=CCMP TKIP
group=CCMP TKIP WEP104 WEP40
#psk="mysecretpassword"
psk=283fe364eb0ce1352c1831dee1ed08481eda8ba62583d6c6589a386cbd6957af
}

In "psk=" you have to insert your Pre-Shared-Key. To get this key wpa_supplicant provides a tool called "wpa_passphrase"

$ wpa_passphrase fehu.org mysecretpassword
network={
ssid="fehu.org"
#psk="mysecretpassword"
psk=283fe364eb0ce1352c1831dee1ed08481eda8ba62583d6c6589a386cbd6957af
}

Start wpa_supplicant. After a few seconds your card should be connected to your AP. The option "-d" is only necessary for debugging

$ wpa_supplicant -w -i ath0 -c /etc/wpa-fehu.conf -D madwifi -d

If you want automatical configuration at boot time, adapt /etc/network/interfaces

$ cat /etc/network/interfaces
# This file describes the network interfaces available on your system
# and how to activate them. For more information, see interfaces(5).

# The loopback network interface
auto lo
iface lo inet loopback

# The primary network interface
auto eth0
iface eth0 inet dhcp

# Wireless
auto ath0

# static
#iface ath0 inet static
# address 192.168.1.66
# netmask 255.255.255.0
# broadcast 192.168.1.255
# up /sbin/iwconfig ath0 rate 54M
# up /usr/local/sbin/wpa_supplicant -B -w -i ath0 -Dmadwifi -c /etc/wpa-fehu.conf

# dhcp
iface ath0 inet dhcp
pre-up /usr/local/sbin/wpa_supplicant -B -w -i ath0 -Dmadwifi -c /etc/wpa-fehu.conf
pre-up /sbin/iwconfig ath0 rate 54M


5. Further information


5.1 Wireless Applications

* http://apradar.sourceforge.net/ ApRadar
* http://www.janmorgenstern.de/projects-software.html Wavemon
* http://www.open1x.org/ Open Source Implementation of IEEE 802.1X
* http://www.simandl.cz/stranky/linux/wifimon/wifimon_a.htm wifimon
* http://airsnort.shmoo.com/ Airsnort
* http://www.kismetwireless.net/ Kismet
* http://sourceforge.net/projects/wmwave/ wmwave

5.2 Links

* http://www.mattfoster.clara.co.uk./madwifi-faq.htm Unofficial MadWifi-Faq
* http://sourceforge.net/projects/madwifi MadWifi Project Page
* http://sourceforge.net/mail/?group_id=82936 MadWifi Mailinglisten
* http://www.hpl.hp.com/personal/Jean_Tourrilhes/Linux/Tools.html Wireless Toolsy

5.3 Manpages

Linux:

NetBSD:

Friday, November 30, 2007

Linux Netfilter实现机制和扩展技术

Linux Netfilter实现机制和扩展技术

2.4.x的内核相对于2.2.x在IP协议栈部分有比较大的改动, Netfilter-iptables更是其一大特色,由于它功能强大,并且与内核完美结合,因此迅速成为Linux平台下进行网络应用扩展的主要利器, 这些扩展不仅包括防火墙的实现--这只是Netfilter-iptables的基本功能--还包括各种报文处理工作(如报文加密、报文分类统计等),甚 至还可以借助Netfilter-iptables机制来实现虚拟专用网(VPN)。本文将致力于深入剖析Netfilter-iptables的组织结 构,并详细介绍如何对其进行扩展。Netfilter目前已在ARP、IPv4和IPv6中实现,考虑到IPv4是目前网络应用的主流,本文仅就IPv4 的Netfilter实现进行分析。

要想理解Netfilter的工作原理,必须从对Linux IP报文处理流程的分析开始,Netfilter正是将自己紧密地构建在这一流程之中的。

1. IP Packet Flowing

IP 协议栈是Linux操作系统的主要组成部分,也是Linux的特色之一,素以高效稳定著称。Netfilter与IP协议栈是密切结合在一起的,要想理解 Netfilter的工作方式,必须理解IP协议栈是如何对报文进行处理的。下面将通过一个经由IP Tunnel传输的TCP报文的流动路径,简要介绍一下IPv4协议栈(IP层)的结构和报文处理过程。

IP Tunnel是2.0.x内核就已经提供了的虚拟局域网技术,它在内核中建立一个虚拟的网络设备,将正常的报文(第二层)封装在IP报文中,再通过TCP/IP网络进行传送。如果在网关之间建立IP Tunnel,并配合ARP报文的解析,就可以实现虚拟局域网。

我们从报文进入IP Tunnel设备准备发送开始。

1.1报文发送

ipip模块创建tunnel设备(设备名为tunl0~tunlx)时,设置报文发送接口(hard_start_xmit)为ipip_tunnel_xmit(),流程见下图:


图1 报文发送流程

1.2 报文接收

报文接收从网卡驱动程序开始,当网卡收到一个报文时,会产生一个中断,其驱动程序中的中断服务程序将调用确定的接收函数来处理。以下仍以IP Tunnel报文为例,网卡驱动程序为de4x5。流程分成两个阶段:驱动程序中断服务程序阶段和IP协议栈处理阶段,见下图:


图2 报文接收流程之驱动程序阶段


图3 报文接收流程之协议栈阶段

如果报文需要转发,则在上图红箭头所指处调用ip_forward():


图4 报文转发流程

从上面的流程可以看出,Netfilter以NF_HOOK()的形式出现在报文处理的过程之中。





回页首


2. Netfilter Frame

Netfilter 是2.4.x内核引入的,尽管它提供了对2.0.x内核中的ipfw以及2.2.x内核中的ipchains的兼容,但实际上它的工作和意义远不止于此。 从上面对IP报文的流程分析中可以看出,Netfilter和IP报文的处理是完全结合在一起的,同时由于其结构相对独立,又是可以完全剥离的。这种机制 也是Netfilter-iptables既高效又灵活的保证之一。

在剖析Netfilter机制之前,我们还是由浅入深的从Netfilter的使用开始。

2.1 编译

在Networking Options中选定Network packet filtering项,并将其下的IP:Netfilter Configurations小节的所有选项设为Module模式。编译并安装新内核,然后重启,系统的核内Netfilter就配置好了。以下对相关的 内核配置选项稍作解释,也可以参阅编译系统自带的Help:

【Kernel/User netlink socket】建立一类PF_NETLINK套接字族,用于核心与用户进程通信。当Netfilter需要使用用户队列来管理某些报文时就要使用这一机制;

【Network packet filtering (replaces ipchains)】Netfilter主选项,提供Netfilter框架;

【Network packet filtering debugging】Netfilter主选项的分支,支持更详细的Netfilter报告;

【IP: Netfilter Configuration】此节下是netfilter的各种选项的集合:

【Connection tracking (required for masq/NAT)】连接跟踪,用于基于连接的报文处理,比如NAT;

【IP tables support (required for filtering/masq/NAT)】这是Netfilter的框架,NAT等应用的容器;

【ipchains (2.2-style) support】ipchains机制的兼容代码,在新的Netfilter结构上实现了ipchains接口;

【ipfwadm (2.0-style) support】2.0内核防火墙ipfwadm兼容代码,基于新的Netfilter实现。

2.2 总体结构

Netfilter 是嵌入内核IP协议栈的一系列调用入口,设置在报文处理的路径上。网络报文按照来源和去向,可以分为三类:流入的、流经的和流出的,其中流入和流经的报文 需要经过路由才能区分,而流经和流出的报文则需要经过投递,此外,流经的报文还有一个FORWARD的过程,即从一个NIC转到另一个NIC。 Netfilter就是根据网络报文的流向,在以下几个点插入处理过程:

NF_IP_PRE_ROUTING,在报文作路由以前执行;

NF_IP_FORWARD,在报文转向另一个NIC以前执行;

NF_IP_POST_ROUTING,在报文流出以前执行;

NF_IP_LOCAL_IN,在流入本地的报文作路由以后执行;

NF_IP_LOCAL_OUT,在本地报文做流出路由前执行。

如图所示:


图5 Netfilter HOOK位置

Netfilter 框架为多种协议提供了一套类似的钩子(HOOK),用一个struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS]二维数组结构存储,一维为协议族,二维为上面提到的各个调用入口。每个希望嵌入 Netfilter中的模块都可以为多个协议族的多个调用点注册多个钩子函数(HOOK),这些钩子函数将形成一条函数指针链,每次协议栈代码执行到 NF_HOOK()函数时(有多个时机),都会依次启动所有这些函数,处理参数所指定的协议栈内容。

每个注册的钩子函数经过处理后都将返回下列值之一,告知Netfilter核心代码处理结果,以便对报文采取相应的动作:

NF_ACCEPT:继续正常的报文处理;

NF_DROP:将报文丢弃;

NF_STOLEN:由钩子函数处理了该报文,不要再继续传送;

NF_QUEUE:将报文入队,通常交由用户程序处理;

NF_REPEAT:再次调用该钩子函数。

2.3 IPTables

Netfilter -iptables由两部分组成,一部分是Netfilter的"钩子",另一部分则是知道这些钩子函数如何工作的一套规则--这些规则存储在被称为 iptables的数据结构之中。钩子函数通过访问iptables来判断应该返回什么值给Netfilter框架。

在现有(kernel 2.4.21)中已内建了三个iptables:filter、nat和mangle,绝大部分报文处理功能都可以通过在这些内建(built-in)的表格中填入规则完成:

filter,该模块的功能是过滤报文,不作任何修改,或者接受,或者拒绝。它在NF_IP_LOCAL_IN、NF_IP_FORWARD和NF_IP_LOCAL_OUT三处注册了钩子函数,也就是说,所有报文都将经过filter模块的处理。

nat,网络地址转换(Network Address Translation),该模块以Connection Tracking模块为基础,仅对每个连接的第一个报文进行匹配和处理,然后交由Connection Tracking模块将处理结果应用到该连接之后的所有报文。nat在NF_IP_PRE_ROUTING、NF_IP_POST_ROUTING注册了 钩子函数,如果需要,还可以在NF_IP_LOCAL_IN和NF_IP_LOCAL_OUT两处注册钩子,提供对本地报文(出/入)的地址转换。nat 仅对报文头的地址信息进行修改,而不修改报文内容,按所修改的部分,nat可分为源NAT(SNAT)和目的NAT(DNAT)两类,前者修改第一个报文 的源地址部分,而后者则修改第一个报文的目的地址部分。SNAT可用来实现IP伪装,而DNAT则是透明代理的实现基础。

mangle,属于可以进行报文内容修改的IP Tables,可供修改的报文内容包括MARK、TOS、TTL等,mangle表的操作函数嵌入在Netfilter的NF_IP_PRE_ROUTING和NF_IP_LOCAL_OUT两处。

内核编程人员还可以通过注入模块,调用Netfilter的接口函数创建新的iptables。在下面的Netfilter-iptables应用中我们将进一步接触Netfilter的结构和使用方式。

2.4 Netfilter配置工具

iptables是专门针对2.4.x内核的Netfilter制作的核外配置工具,通过socket接口对Netfilter进行操作,创建socket的方式如下:

socket(TC_AF, SOCK_RAW, IPPROTO_RAW)

其中TC_AF就是AF_INET。核外程序可以通过创建一个"原始IP套接字"获得访问Netfilter的句柄,然后通过getsockopt()和setsockopt()系统调用来读取、更改Netfilter设置,详情见下。

iptables功能强大,可以对核内的表进行操作,这些操作主要指对其中规则链的添加、修改、清除,它的命令行参数主要 可分为四类:指定所操作的IP Tables(-t);指定对该表所进行的操作(-A、-D等);规则描述和匹配;对iptables命令本身的指令(-n等)。在下面的例子中,我们通 过iptables将访问10.0.0.1的53端口(DNS)的TCP连接引导到192.168.0.1地址上。

iptables -t nat -A PREROUTING -p TCP -i eth0 -d 10.0.0.1 --dport 53 -j DNAT --to-destination 192.168.0.1

由于iptables是操作核内Netfilter的用户界面,有时也把Netfilter-iptables简称为iptables,以便与ipchains、ipfwadm等老版本的防火墙并列。

2.5 iptables核心数据结构

2.5.1 表

在Linux内核里,iptables用struct ipt_table表示,定义如下(include/linux/netfilter_ipv4/ip_tables.h):

struct ipt_table
{
struct list_head list;
/* 表链 */
char name[IPT_TABLE_MAXNAMELEN];
/* 表名,如"filter"、"nat"等,为了满足自动模块加载的设计,包含该表的模块应命名为iptable_'name'.o */
struct ipt_replace *table;
/* 表模子,初始为initial_table.repl */
unsigned int valid_hooks;
/* 位向量,标示本表所影响的HOOK */
rwlock_t lock;
/* 读写锁,初始为打开状态 */
struct ipt_table_info *private;
/* iptable的数据区,见下 */
struct module *me;
/* 是否在模块中定义 */
};
struct ipt_table_info是实际描述表的数据结构(net/ipv4/netfilter/ip_tables.c):
struct ipt_table_info
{
unsigned int size;
/* 表大小 */
unsigned int number;
/* 表中的规则数 */
unsigned int initial_entries;
/* 初始的规则数,用于模块计数 */
unsigned int hook_entry[NF_IP_NUMHOOKS];
/* 记录所影响的HOOK的规则入口相对于下面的entries变量的偏移量 */
unsigned int underflow[NF_IP_NUMHOOKS];
/* 与hook_entry相对应的规则表上限偏移量,当无规则录入时,相应的hook_entry和underflow均为0 */
char entries[0] ____cacheline_aligned;
/* 规则表入口 */
};

例如内建的filter表初始定义如下(net/ipv4/netfilter/iptable_filter.c):

static struct ipt_table packet_filter
= { { NULL, NULL }, // 链表
"filter", // 表名
&initial_table.repl, // 初始的表模板
FILTER_VALID_HOOKS,// 定义为((1 << NF_IP6_LOCAL_IN) | (1 << NF_IP6_FORWARD) | (1 << NF_IP6_LOCAL_OUT)),
即关心INPUT、FORWARD、OUTPUT三点
RW_LOCK_UNLOCKED,// 锁
NULL, // 初始的表数据为空
THIS_MODULE // 模块标示
};

经过调用ipt_register_table(&packet_filter)后,filter表的private数据区即参照模板填好了。

2.5.2 规则

规则用struct ipt_entry结构表示,包含匹配用的IP头部分、一个Target和0个或多个Match。由于Match数不定,所以一条规则实际的占用空间是可变的。结构定义如下(include/linux/netfilter_ipv4):

struct ipt_entry
{
struct ipt_ip ip;
/* 所要匹配的报文的IP头信息 */
unsigned int nfcache;
/* 位向量,标示本规则关心报文的什么部分,暂未使用 */
u_int16_t target_offset;
/* target区的偏移,通常target区位于match区之后,而match区则在ipt_entry的末尾;
初始化为sizeof(struct ipt_entry),即假定没有match */
u_int16_t next_offset;
/* 下一条规则相对于本规则的偏移,也即本规则所用空间的总和,
初始化为sizeof(struct ipt_entry)+sizeof(struct ipt_target),即没有match */
unsigned int comefrom;
/* 位向量,标记调用本规则的HOOK号,可用于检查规则的有效性 */
struct ipt_counters counters;
/* 记录该规则处理过的报文数和报文总字节数 */
unsigned char elems[0];
/*target或者是match的起始位置 */
}

规则按照所关注的HOOK点,被放置在struct ipt_table::private->entries之后的区域,比邻排列。

2.5.3 规则填写过程

在了解了iptables在核心中的数据结构之后,我们再通过遍历一次用户通过iptables配置程序填写规则的过程,来了解这些数据结构是如何工作的了。

一个最简单的规则可以描述为拒绝所有转发报文,用iptables命令表示就是:

iptables -A FORWARD -j DROP;

iptables应用程序将命令行输入转换为程序可 读的格式(iptables-standalone.c::main()::do_command(),然后再调用libiptc库提供的 iptc_commit()函数向核心提交该操作请求。在libiptc/libiptc.c中定义了iptc_commit()(即TC_COMMIT ()),它根据请求设置了一个struct ipt_replace结构,用来描述规则所涉及的表(filter)和HOOK点(FORWARD)等信息,并在其后附接当前这条规则--一个 struct ipt_entry结构(实际上也可以是多个规则entry)。组织好这些数据后,iptc_commit()调用setsockopt()系统调用来启 动核心处理这一请求:

setsockopt(
sockfd, //通过socket(TC_AF, SOCK_RAW, IPPROTO_RAW)创建的套接字,其中TC_AF即AF_INET
TC_IPPROTO, //即IPPROTO_IP
SO_SET_REPLACE, //即IPT_SO_SET_REPLACE
repl, //struct ipt_replace结构
sizeof(*repl) + (*handle)->entries.size) //ipt_replace加上后面的ipt_entry

核心对于setsockopt()的处理是从协议栈中一层层传递上来的,调用过程如下图所示:


图6 规则填写过程

nf_sockopts 是在iptables进行初始化时通过nf_register_sockopt()函数生成的一个struct nf_sockopt_ops结构,对于ipv4来说,在net/ipv4/netfilter/ip_tables.c中定义了一个 ipt_sockopts变量(struct nf_sockopt_ops),其中的set操作指定为do_ipt_set_ctl(),因此,当nf_sockopt()调用对应的set操作时, 控制将转入net/ipv4/netfilter/ip_tables.c::do_ipt_set_ctl()中。

对于IPT_SO_SET_REPLACE命令,do_ipt_set_ctl()调用do_replace()来处 理,该函数将用户层传入的struct ipt_replace和struct ipt_entry组织到filter(根据struct ipt_replace::name项)表的hook_entry[NF_IP_FORWARD]所指向的区域,如果是添加规则,结果将是filter表 的private(struct ipt_table_info)项的hook_entry[NF_IP_FORWARD]和underflow[NF_IP_FORWARD]的差值扩大 (用于容纳该规则),private->number加1。

2.5.4 规则应用过程

以上描述 了规则注入核内iptables的过程,这些规则都挂接在各自的表的相应HOOK入口处,当报文流经该HOOK时进行匹配,对于与规则匹配成功的报文,调 用规则对应的Target来处理。仍以转发的报文为例,假定filter表中添加了如上所述的规则:拒绝所有转发报文。

如1.2节所示,经由本地转发的报文经过路由以后将调用ip_forward()来处理,在ip_forward()返回前,将调用如下代码:

NF_HOOK(PF_INET, NF_IP_FORWARD, skb, skb->dev, dev2, ip_forward_finish)
NF_HOOK是这样一个宏(include/linux/netfilter.h):
#define NF_HOOK(pf, hook, skb, indev, outdev, okfn) \
(list_empty(&nf_hooks[(pf)][(hook)]) \
? (okfn)(skb) \
: nf_hook_slow((pf), (hook), (skb), (indev), (outdev), (okfn)))

也就是说,如果nf_hooks[PF_INET] [NF_IP_FORWARD]所指向的链表为空(即该钩子上没有挂处理函数),则直接调用ip_forward_finish(skb)完成 ip_forward()的操作;否则,则调用net/core/netfilter.c::nf_hook_slow()转入Netfilter的处 理。

这里引入了一个nf_hooks链表二维数组:

struct list_head nf_hooks[NPROTO][NF_MAX_HOOKS];

每一个希望使用Netfilter挂钩的表都需要将 表处理函数在nf_hooks数组的相应链表上进行注册。对于filter表来说,在其初始化 (net/ipv4/netfilter/iptable_filter.c::init())时,调用了net/core/netfilter.c:: nf_register_hook(),将预定义的三个struct nf_hook_ops结构(分别对应INPUT、FORWARD、OUTPUT链)连入链表中:

struct nf_hook_ops
{
struct list_head list;
//链表
nf_hookfn *hook;
//处理函数指针
int pf;
//协议号
int hooknum;
//HOOK号
int priority;
//优先级,在nf_hooks链表中各处理函数按优先级排序
};

对于filter表来说,FORWARD点的 hook设置成ipt_hook(),它将直接调用ipt_do_table()。几乎所有处理函数最终都将调用ipt_do_table()来查询表中 的规则,以调用对应的target。下图所示即为在FORWARD点上调用nf_hook_slow()的过程:


图7 规则应用流程

2.5.5 Netfilter的结构特点

由 上可见,nf_hooks链表数组是联系报文处理流程和iptables的纽带,在iptables初始化(各自的init()函数)时,一方面调用 nf_register_table()建立规则容器,另一方面还要调用nf_register_hook()将自己的挂钩愿望表达给Netfilter 框架。初始化完成之后,用户只需要通过用户级的iptables命令操作规则容器(添加规则、删除规则、修改规则等),而对规则的使用则完全不用操心。如 果一个容器内没有规则,或者nf_hooks上没有需要表达的愿望,则报文处理照常进行,丝毫不受Netfilter-iptables的影响;即使报文 经过了过滤规则的处理,它也会如同平时一样重新回到报文处理流程上来,因此从宏观上看,就像在行车过程中去了一趟加油站。

Netfilter不仅仅有此高效的设计,同时还具备很大的灵活性,这主要表现在Netfilter-iptables 中的很多部分都是可扩充的,包括Table、Match、Target以及Connection Track Protocol Helper,下面一节将介绍这方面的内容。





回页首


3. Netfilter-iptables Extensions

Netfilter 提供的是一套HOOK框架,其优势是就是易于扩充。可供扩充的Netfilter构件主要包括Table、Match、Target和 Connection Track Protocol Helper四类,分别对应四套扩展函数。所有扩展都包括核内、核外两个部分,核内部分置于 /net/ipv4/netfilter/下,模块名为ipt_'name'.o;核外部分置于 /extensions/下,动态链接库名为libipt_'name'.so。

3.1 Table

Table 在以上章节中已经做过介绍了,它作为规则存储的媒介,决定了该规则何时能起作用。系统提供的filter、nat、mangle涵盖了所有的HOOK点, 因此,大部分应用都可以围绕这三个已存在的表进行,但也允许编程者定义自己的拥有特殊目的的表,这时需要参考已有表的struct ipt_table定义创建新的ipt_table数据结构,然后调用ipt_register_table()注册该新表,并调用 ipt_register_hook()将新表与Netfilter HOOK相关联。

对表进行扩展的情形并不多见,因此这里也不详述。

3.2 Match & Target

Match和Target是Netfilter-iptables中最常使用的功能,灵活使用Match和Target,可以完成绝大多数报文处理功能。

3.2.1 Match数据结构

核心用struct ipt_match表征一个Match数据结构:

struct ipt_match
{
struct list_head list;
/* 通常初始化成{NULL,NULL},由核心使用 */
const char name[IPT_FUNCTION_MAXNAMELEN];
/* Match的名字,同时也要求包含该Match的模块文件名为ipt_'name'.o */
int (*match)(const struct sk_buff *skb,
const struct net_device *in,
const struct net_device *out,
const void *matchinfo,
int offset,
const void *hdr,
u_int16_t datalen,
int *hotdrop);
/* 返回非0表示匹配成功,如果返回0且hotdrop设为1,则表示该报文应当立刻丢弃 */
int (*checkentry)(const char *tablename,
const struct ipt_ip *ip,
void *matchinfo,
unsigned int matchinfosize,
unsigned int hook_mask);
/* 在使用本Match的规则注入表中之前调用,进行有效性检查,如果返回0,规则就不会加入iptables中 */
void (*destroy)(void *matchinfo, unsigned int matchinfosize);
/* 在包含本Match的规则从表中删除时调用,与checkentry配合可用于动态内存分配和释放 */
struct module *me;
/* 表示当前Match是否为模块(NULL为否) */
};

定义好一个ipt_match结构后,可调用ipt_register_match()将本Match注册到ipt_match链表中备用,在模块方式下,该函数通常在init_module()中执行。

3.2.2 Match的用户级设置

要使用核心定义的Match(包括已有的和自定义的),必须在用户级的iptables程序中有所说明,iptables源代码也提供了已知的核心Match,但未知的Match则需要自行添加说明。

在iptables中,一个Match用struct iptables_match表示:

struct iptables_match
{
struct iptables_match *next;
/* Match链,初始为NULL */
ipt_chainlabel name;
/* Match名,和核心模块加载类似,作为动态链接库存在的Iptables Extension的命名规则为libipt_'name'.so
(对于ipv6为libip6t_'name'.so),
以便于iptables主程序根据Match名加载相应的动态链接库 */
const char *version;
/* 版本信息,一般设为NETFILTER_VERSION */
size_t size;
/* Match数据的大小,必须用IPT_ALIGN()宏指定对界 */
size_t userspacesize;
/*由于内核可能修改某些域,因此size可能与确切的用户数据不同,这时就应该把不会被改变的数据放在数据区的前面部分,
而这里就应该填写被改变的数据区大小;一般来说,这个值和size相同 */
void (*help)(void);
/* 当iptables要求显示当前match的信息时(比如iptables -m ip_ext -h),就会调用这个函数,
输出在iptables程序的通用信息之后 */
void (*init)(struct ipt_entry_match *m, unsigned int *nfcache);
/* 初始化,在parse之前调用 */
int (*parse)(int c, char **argv, int invert, unsigned int *flags,
const struct ipt_entry *entry,
unsigned int *nfcache,
struct ipt_entry_match **match);
/* 扫描并接收本match的命令行参数,正确接收时返回非0,flags用于保存状态信息 */
void (*final_check)(unsigned int flags);
/* 当命令行参数全部处理完毕以后调用,如果不正确,应该退出(exit_error()) */
void (*print)(const struct ipt_ip *ip,
const struct ipt_entry_match *match, int numeric);
/* 当查询当前表中的规则时,显示使用了当前match的规则的额外的信息 */
void (*save)(const struct ipt_ip *ip,
const struct ipt_entry_match *match);
/* 按照parse允许的格式将本match的命令行参数输出到标准输出,用于iptables-save命令 */
const struct option *extra_opts;
/* NULL结尾的参数列表,struct option与getopt(3)使用的结构相同 */
/* 以下参数由iptables内部使用,用户不用关心 */
unsigned int option_offset;
struct ipt_entry_match *m;
unsigned int mflags;
unsigned int used;
}
struct option {
const char *name;
/* 参数名称,用于匹配命令行输入 */
int has_arg;
/* 本参数项是否允许带参数,0表示没有,1表示有,2表示可有可无 */
int *flag;
/* 指定返回的参数值内容,如果为NULL,则直接返回下面的val值,否则返回0,val存于flag所指向的位置 */
int val;
/* 缺省的参数值 */
}

如对于--opt 参数来讲,在struct option中定义为{"opt",1,0,'1'},表示opt带参数值,如果出现-opt 参数,则返回'1'用于parse()中的int c参数。

实际使用时,各个函数都可以为空,只要保证name项与核心的对应Match名字相同就可以了。在定义了 iptables_match之后,可以调用register_match()让iptables主体识别这个新Match。当iptables命令中第 一次指定使用名为ip_ext的Match时,iptables主程序会自动加载libipt_ip_ext.so,并执行其中的_init()接口,所 以register_match()操作应该放在_init()中执行。

3.2.3 Target数据结构

Target数据结构struct ipt_target和struct ipt_match基本相同,不同之处只是用target函数指针代替match函数指针:

struct ipt_target
{
……
unsigned int (*target)(struct sk_buff **pskb,
unsigned int hooknum,
const struct net_device *in,
const struct net_device *out,
const void *targinfo,
void *userdata);
/* 如果需要继续处理则返回IPT_CONTINUE(-1),否则返回NF_ACCEPT、NF_DROP等值,它的调用者根据它的返回值来判断如何处理它处理过的报文*/
……
}

与ipt_register_match()对应,Target使用ipt_register_target()来进行注册,但文件命名、使用方法等均与Match相同。

3.2.4 Target的用户级设置

Target的用户级设置使用struct iptables_target结构,与struct iptables_match完全相同。register_target()用于注册新Target,方法也与Match相同。

3.3 Connection Track Protocol Helper

前 面提到,NAT仅对一个连接(TCP或UDP)的第一个报文进行处理,之后就依靠Connection Track机制来完成对后续报文的处理。Connection Track是一套可以和NAT配合使用的机制,用于在传输层(甚至应用层)处理与更高层协议相关的动作。

关于Connection Track,Netfilter中的实现比较复杂,而且实际应用频率不高,因此这里就不展开了,以后专文介绍。

3.4 iptables patch机制

对于Netfilter-iptables扩展工作,用户当然可以直接修改源代码并编译安装,但为了标准化和简便起见,在iptables源码包提供了一套patch机制,希望用户按照其格式要求进行扩展,而不必分别修改内核和iptables代码。

和Netfilter-iptables的结构特点相适应,对iptables进行扩展也需要同时修改内核和 iptables程序代码,因此patch也分为两个部分。在iptables-1.2.8中,核内补丁由patch-o-matic包提供, iptables-1.2.8的源码中的extensions目录则为iptables程序本身的补丁。

patch-o-matic提供了一个'runme'脚本来给核心打patch,按照它的规范,核内补丁应该包括五个部分,且命名有一定的规范,例如,如果Target名为ip_ext,那么这五个部分的文件名和功能分别为:

  • ip_ext.patch
    主文件,内容为diff格式的核心.c、.h源文件补丁,实际使用时类似给内核打patch(patch -p0
  • ip_ext.patch.config.in
    对<>/net/ipv4/netfilter/Config.in文件的修改,第一行是原Config.in中的一行,以指示 补丁添加的位置,后面则是添加在以上匹配行之后的内容。这个补丁的作用是使核心的配置界面中支持新增加的补丁选项;
  • ip_ext.patch.configure.help
    /Documentation/Configure.help的修改,第一行为原Configure.help中的一行帮助索引,以下几行的内容添加在这一行相关的帮助之后。这个补丁的作用是补充内核配置时对新增加的选项的说明;
  • ip_ext.patch.help
    用于runme脚本显示本patch的帮助信息;
  • ip_ext.patch.makefile
  • /net/ipv4/netfilter/Makefile的修改,和前两个文件的格式相同,用于在指定的位置上添加用于生成ipt_ip_ext.o的make指令。

示例可以参看patch-o-matic下的源文件。

iptables本身的扩展稍微简单一些,那就是在extensions目录下增加一个libipt_ip_ext.c的文件,然后在本子目录的Makefile的PF_EXT_SLIB宏中附加一个ip_ext字符串。

第一次安装时,可以在iptables的根目录下运行make pending-patches命令,此命令会自动调用runme脚本,将所有patch-o-matic下的patch文件打到内核中,之后需要重新配置和编译内核。

如果只需要安装所要求的patch,可以在patch-o-matic目录下直接运行runme ip_ext,它会完成ip_ext patch的安装。之后,仍然要重编内核以使patch生效。

iptables本身的make/make install过程可以编译并安装好libipt_ip_ext.so,之后,新的iptables命令就可以通过加载libipt_ip_ext.so来识别ip_ext target了。

Extensions还可以定义头文件,一般这个头文件核内核外都要用,因此,通常将其放置在/include/linux/netfilter_ipv4/目录下,在.c文件里指定头文件目录为 linux/netfilter_ipv4/。

灵活性是Netfilter-iptables机制的一大特色,因此,扩展Netfilter-iptables也是它的应用的关键。为了与此目标相适应,Netfilter-iptables在结构上便于扩展,同时也提供了一套扩展的方案,并有大量扩展样例可供参考。





回页首


4. 案例:用Netfilter实现VPN

虚拟专用网的关键就是隧道(Tunnel)技术,即将报文封装起来通过公用网络。利用Netfilter-iptables对报文的强大处理能力,完全可以以最小的开发成本实现一个高可配置的VPN。

本文第一部分即描述了IP Tunnel技术中报文的流动过程,从中可见,IP Tunnel技术的特殊之处有两点:

  • 一个特殊的网络设备tunl0~tunlx--发送时,用指定路由的办法将需要封装的内网报文交给该网络设备来处理,在"网卡驱动程序"中作封装,然后再作为正常的IP报文交给真正的网络设备发送出去;
  • 一个特殊的IP层协议IPIP--从外网传来的封装报文拥有一个特殊的协议号(IPIP),报文最终在该协议的处理程序(ipip_rcv())中解封,恢复内网IP头后,将报文注入IP协议栈底层(netif_rx())重新开始收包流程。

从中不难看出,在报文流出tunlx设备之后(即完成封装之后)需要经过OUTPUT的 Netfilter HOOK点,而在报文解封之前(ipip_rcv()得到报文之前),也要经过Netfilter的INPUT HOOK点,因此,完全有可能在这两个HOOK上做文章,完成报文的封装和解封过程。报文的接收过程可以直接沿用IPIP的处理方法,即自定义一个专门的 协议,问题的关键即在于如何获得需要封装的外发报文,从而与正常的非VPN报文相区别。我们的做法是利用Netfilter-iptables对IP头信 息的敏感程度,在内网中使用标准的内网专用IP段(如192.168.xxx.xxx),从而通过IP地址将其区分开。基于IP地址的VPN配置既方便现 有系统管理、又便于今后VPN系统升级后的扩充,而且可以结合Netfilter-iptables的防火墙设置,将VPN和防火墙有机地结合起来,共同 维护一个安全的专用网络。

在我们的方案中,VPN采用LAN-LAN方式(当然,Dial-in方式在技术上并没有什么区别),在LAN网关处设 置我们的VPN管理组件,从而构成一个安全网关。LAN内部的节点既可以正常访问防火墙限制以外非敏感的外网(如Internet的大部分站点),又可以 通过安全网关的甄别,利用VPN访问其他的专用网LAN。

由于本应用与原有的三个表在功能和所关心的HOOK点上有所不同,因此我们仿照filter表新建了一个vpn表,VPN功能分布在以下四个部分中:

  • iptables ENCRYPT Target:对于发往安全子网的报文,要求经过ENCRYPT target处理,加密原报文,产生认证码,并将报文封装在公网IPIP_EXT报文头中。ENCRYPT Target配置在vpn表的OUTPUT和FORWARD HOOK点上,根据目的方IP地址来区分是否需要经过ENCRYPT target加密处理。
  • IPIP_EXT协议:在接收该协议报文的处理函数IPIP_EXT_rcv()中用安全子网的IP地址信息代替公网间传输的隧道报文头中的IP地址,然后重新注入IP协议栈底层。
  • iptables IPIP_EXT Match:匹配报文头的协议标识是否为自定义的IPIP_EXT。经过IPIP_EXT_rcv()处理之后的报文必须是IPIP_EXT协议类型的,否则应丢弃。
  • iptables DECRYPT Target:对于接收到的来自安全子网的报文,经过IPIP_EXT协议处理之后,将IP头恢复为安全子网之间通信的IP头,再进入DECRYPT target处理,对报文进行完全解密和解封。

整个报文传输的流程可以用下图表示:


图8 VPN报文流动过程

对于外出报文(源于本地或内网),使用内部地址在FORWARD/OUTPUT点匹配成功,执行ENCRYPT,从Netfilter中返回后作为本地IPIP_EXT协议的报文继续往外发送。

对于接收到的报文,如果协议号为IPPROTO_IPIP_EXT,则匹配IPIP_EXT的Match成功,否则将在 INPUT点被丢弃;继续传送的报文从IP层传给IPIP_EXT的协议处理代码接收,在其中恢复内网IP的报文头后调用netif_rx()重新流入协 议栈。此时的报文将在INPUT/FORWARD点匹配规则,并执行DECRYPT,只有通过了DECRYPT的报文才能继续传送到本机的上层协议或者内 网。

附:iptables设置指令(样例):

iptables -t vpn -P FORWARD DROP
iptables -t vpn -A OUTPUT -d 192.168.0.0/24 -j ENCRYPT
iptables -t vpn -A INPUT -s 192.168.0.0/24 -m ipip_ah -j DECRYPT
iptables -t vpn -A FORWARD -s 192.168.0.0/24 -d 192.168.1.0 -j DECRYPT
iptables -t vpn -A FORWARD -s 192.168.1.0/24 -d 192.168.0.0/24 -j ENCRYPT

其中192.168.0.0/24是目的子网,192.168.1.0/24是本地子网



参考资料

  • [Linus Torvalds,2003] Linux内核源码v2.4.21

  • [Paul Russell,2002] Linux netfilter Hacking HOWTO v1.2

  • [Paul Russell,2002] iptables源码v1.2.1a

  • [Paul Russell,2000] LinuxWorld: San Jose August 2000,Netfilter Tutorial

  • [Oskar Andreasson,2001] iptables Tutorial 1.0.9

Linux网络代码导读v0.2 - collide的专栏 - CSDNBlog

Linux网络代码导读v0.2

作者:yawl <>
主页:http://www.nsfocus.com

1 前言

许多人在分析linux代码时对网络部分(主要是src/linux/net,src/linux/include/net及
src/linux/include/linux目录下的文件)比较感兴趣,确实,尽管已经从书本上学到了大量的
TCP/IP原理,不读源码的话,头脑中还是建立不起具体的印象。而分析这部分代码的一个问
题便是代码众多而资料很少。这篇文章的目的就是勾勒出一个框架,让读者能够大致能够了
解TCP/IP究竟是怎么工作的。以前见到的许多代码分析都是基于2.0内核的,在新的内核中
许多函数变了名字,这尤其给初学者带来了困难,本文是以2.4.0-test9的代码作例子,这样
对照代码时可能更清晰些。

其实网络部分的代码我只对防火墙部分一行行仔细分析过,其他许多地方也只是一知半解,
如果理解有误,欢迎指正。

建议在看本文的同时,用source insight(www.soucedyn.com)建立一个项目,同时看代码,
这样可能效果更好点。我也用过其他的一些工具,但在分析大量的代码的时候,没有一个工
具比它更方便的了。


2 正文

ISO的七层模型都非常熟悉了,当然,对于internet,用四层模型更为适合。在这两份模型里,
网络协议以层次的形式出现。而LINUX的内核代码中,严格分出清楚的层次却比较困难,因
为除了一些"内核线程(kernel thread外)",整个内核其实是个单一的进程。因此所谓"网络层
",只是一组相关的函数,而各层之间大多通过一般的函数调用的方式完成交互。

而从逻辑上,网络部分的代码更应该这样分层更为合理:
.BSD socket层:这一部分处理BSD socket相关操作,每个socket在内核中以struct socket结
构体现。
这一部分的文件主要有:/net/socket.c /net/protocols.c etc

.INET socket层:BSD socket是个可以用于各种网络协议的接口,而当用于tcp/ip,即建立
了AF_INET形式的socket时,还需要保留些额外的参数,于是就有了struct sock结构。
文件主要有:/net/ipv4/protocol.c /net/ipv4/af_inet.c /net/core/sock.c etc

.TCP/UDP层:处理传输层的操作,传输层用struct inet_protocol和struct proto两个结构表
示。
文件主要有:/net/ipv4/udp.c /net/ipv4/datagram.c /net/ipv4/tcp.c /net/ipv4/tcp_input.c
/net/ipv4//tcp_output.c /net/ipv4/tcp_minisocks.c /net/ipv4/tcp_output.c
/net/ipv4/tcp_timer.c etc

.IP层:处理网络层的操作,网络层用struct packet_type结构表示。
文件主要有:/net/ipv4/ip_forward.c ip_fragment.c ip_input.c ip_output.c etc.

.数据链路层和驱动程序:每个网络设备以struct net_device表示,通用的处理在dev.c中,
驱动程序都在/driver/net目录下。

网络部分还有很多其他文件,如防火墙,路由等,一般根据看到名字便能猜测出相应的处
理,此处不再赘述。

现在我要给出一张表,全文的内容就是为了说明这张表(如果你觉得我在文章中的语言比较
乏味,尽可抛掉他们,结合这张表自己看代码)。在我最初看网络部分代码时,比较喜欢
《linux kernel internals》的第八章的一段,其中有一个进程A通过网络远程向另一进程B发
包的例子,详细介绍了一个数据包如何从网络堆栈中走过的过程。我觉得这样可以更迅速的
帮助读者看清森林的全貌,因此本文参照这种结构来
叙述。

^
| sys_read fs/read_write.c
| sock_read net/socket.c
| sock_recvmsg net/socket.c
| inet_recvmsg net/ipv4/af_inet.c
| udp_recvmsg net/ipv4/udp.c
| skb_recv_datagram net/core/datagram.c
| -------------------------------------------
| sock_queue_rcv_skb include/net/sock.h
| udp_queue_rcv_skb net/ipv4/udp.c
| udp_rcv net/ipv4/udp.c
| ip_local_deliver_finish net/ipv4/ip_input.c
| ip_local_deliver net/ipv4/ip_input.c
| ip_recv net/ipv4/ip_input.c
| net_rx_action net/dev.c
| -------------------------------------------
| netif_rx net/dev.c
| el3_rx driver/net/3c309.c
| el3_interrupt driver/net/3c309.c

==========================

| sys_write fs/read_write.c
| sock_writev net/socket.c
| sock_sendmsg net/socket.c
| inet_sendmsg net/ipv4/af_inet.c
| udp_sendmsg net/ipv4/udp.c
| ip_build_xmit net/ipv4/ip_output.c
| output_maybe_reroute net/ipv4/ip_output.c
| ip_output net/ipv4/ip_output.c
| ip_finish_output net/ipv4/ip_output.c
| dev_queue_xmit net/dev.c
| --------------------------------------------
| el3_start_xmit driver/net/3c309.c
V



我们假设的环境如下:有两台主机通过互联网联在一起,其中一台机子运行这一个进程A,
另外一台运行进程B,进程A将向进程B发出一条信息,比如"Hello",而B接受此信息。
TCP处理本身非常复杂,为了便于叙述,在后面我们将用UDP作为例子。


2.1 建立套接字

在数据发送之前,要建立一个套接字(socket),在两边的程序中都会调用如下语句:

...
int sockfd;
sockfd=socket(AF_INET,SOCK_DGRAM,0);
...

这是个系统调用,因此会通过0x80中断进入系统内核,调用内核中的相应函数.当寻找系统调
用在内核中的对应流程时,一般前面加入"sys_"再找就是了,如对fork来说,就是调用
sys_fork。但是socket相关调用有些特殊,所有的这类调用都是通过一个入口,即
sys_socketcall进入系统内核,然后再通过参数调用具体的sys_socket,socket_bind等函数。

sys_socket会调用sock_create产生一个struct socket结构(见include/linux/net.h),每个套
接字在内核中都有一个这样的结构对应,在初始化了此结构的一些通用成员后(如分配
inode,根据第二个参数为type项赋值等),会根据其一个参数作响应的调度,即这
一句:
...
net_families[family]->create(sock, protocol);
...

我们的程序的第一个参数是AF_INET,所以此函数指针会指向inet_create();
(net_families是个数组,保留了网络协议族(net families)的信息,而这些协议族用
sock_register加载。)

在struct socket结构结构中最重要的信息保留在struct sock结构中,这个结构在网络代码中
经常使用,建议把它和其他常见结构(如struct sk_buff)打印出来放在手边。在inet_create
会为此结构分配内存,并根据套接字类型(其实就是socket函数的第二个参数),作各自不
同的初始化:
...
if (sk->prot->init)
sk->prot->init(sk);
...

如果类型是SOCK_STREAM的话会调用tcp_v4_init_sock,而SOCK_DGRAM类型的
socket没有额外的初始化了,到此socket调用结束。

还有一个值得注意的地方是当inet_create()调用完后,会接着调用sock_map_fd函数,这
个函数中会为套接字分配一个文件描述符并分配一个file文件。在应用层便可象处理文件一样
处理套接字了。

开始的时候可能有些流程难以跟下去,主要便是这些函数指针的实际指向会根据类型变化。


2.2 发送数据

当进程A想发送数据时,程序中会调用如下语句(如果用sendto函数的话会走类似的流程,
略):
...
write(sockfd,"Hello",strlen("Hello"));
...

write在内核中对应的函数就是sys_write,此函数首先根据文件描述符找到struct file结构,
如果此文件存在(file指针非空)且可写(file->f_mode & FMODE_WRITE为true),便调
用此文件结构的写操作:
...
if (file->f_op && (write = file->f_op->write) != NULL)
ret = write(file, buf, count, &file->f_pos);
...

其中f_op是个struct file_operations结构指针,在sock_map_fd中将其指向socket_file_ops,
其定义如下(/net/socket.c):
static struct file_operations socket_file_ops = {
llseek: sock_lseek,
read: sock_read,
write: sock_write,
poll: sock_poll,
ioctl: sock_ioctl,
mmap: sock_mmap,
open: sock_no_open, /* special open code to disallow open via /proc */
release: sock_close,
fasync: sock_fasync,
readv: sock_readv,
writev: sock_writev
};

此时wirte函数指针显然指向了sock_write,我们跟下去看,此函数将一个字符串缓冲整理成
struct msghdr,最后调用了sock_sendmsg.

sock_sendmsg中的scm_send我不了解(scm是Socket level control messages的简写),好
在它也不是很关键,我们注意到这句:
...
sock->ops->sendmsg(sock, msg, size, &scm);
...

又是个函数指针,sock->ops在inet_create()函数中被初始化,由于我们我们是UDP的套
接字,sock->ops指向了inet_dgram_ops(即sock->ops = &inet_dgram_ops;),其定义在
net/ipv4/Af_inet.c中:
struct proto_ops inet_dgram_ops = {
family: PF_INET,

release: inet_release,
bind: inet_bind,
connect: inet_dgram_connect,
socketpair: sock_no_socketpair,
accept: sock_no_accept,
getname: inet_getname,
poll: datagram_poll,
ioctl: inet_ioctl,
listen: sock_no_listen,
shutdown: inet_shutdown,
setsockopt: inet_setsockopt,
getsockopt: inet_getsockopt,
sendmsg: inet_sendmsg,
recvmsg: inet_recvmsg,
mmap: sock_no_mmap,
};

因此我们要看得便是inet_sendmsg()函数了,而马上,这个函数又通过函数指针调用了另
一函数:
...
sk->prot->sendmsg(sk, msg, size);
...

我们不得不再次寻找其具体指向。看到这里,说点题外话,怎么才能找到其具体定义呢?我
一般是这样:对上例而言,sk是个struct sock结构,到其定义(linux/net/sock.h中)出看到
prot是个struct proto结构,此时我们便在源代码树中寻找所有此结构的实例(这些诸如跳到
定义,寻找引用等工作在source insight中实在太方便快速了^_^),很快便会发现诸如
udp_prot,tcp_prot,raw_prot等,猜测是用了udp_prot,便再找一下它在源代码中的引用情
况,果然发现在inet_create中有这么一句:
...
prot=&udp_prot;
...

其实如果前面看inet_create函数时仔细一点会早点发现了,但我总没有这么细心:)。

我们顺着udp_sendmsg往下走:
在这个函数的主要作用是填充UDP头(源端口,目的端口等),接着调用了
ip_route_output,作用是查找出去的路由,而后:
...
ip_build_xmit(sk,
(sk->no_check == UDP_CSUM_NOXMIT ?
udp_getfrag_nosum :
udp_getfrag),
&ufh, ulen, &ipc, rt, msg->msg_flags);
...

ip_build_xmit函数的很大比例是生成sk_buff,并为数据包加入IP头。
后面有这么一句:
...
NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL,
rt->u.dst.dev,output_maybe_reroute);
...

简单的说,在没有防火墙代码干预的情况下,你可以将此处理解为直接调用
output_maybe_reroute,(具体可参看绿盟月刊14期中的《内核防火墙netfilter入门 》)
而output_maybe_reroute中只有一句:
return skb->dst->output(skb);

依旧照上面的方法(不过这个确实不太好找),发现其实这个指针是在ip_route_output中指
定的,(提示:ip_route_output_slow中:rth->u.dst.output=ip_output;),ip_route_output的
作用便是查找路由,并将结果记录到skb->dst中。

于是,我们开始看ip_output函数了,而它马上又走向了ip_finish_output~~。
每个网络设备,如网卡,在内核中由一个net_device表示,在ip_finish_output中找到其用到
的设备(也是在ip_route_output中初始化的),这个参数在会传给netfilter在
NF_IP_POST_ROUTING点登记的函数,结束后调用ip_finish_output2,而这个函数中又会
调用:
...
hh->hh_output(skb);
...

闲话少叙,实际调用了dev_queue_xmit,到此我们完成了TCP/IP层的工作,开始数据链路
层的处理。

在做了一些判断之后,实际的调用是这句:
...
dev->hard_start_xmit(skb, dev);
...

这个函数是在网卡的驱动程序中定义的,每个不同的网卡有不同的处理,我的网卡是比较通
用的3c509(其驱动程序是3c509.c),在网卡处理化的时候(el3_probe),有:
...
dev->hard_start_xmit = &el3_start_xmit;
...

再往下便是IO操作,将数据包真正的发到网络上去,至此发送过程结束。

中间我说的有些草率,完全没顾的上中间的如出错,阻塞,分片等特殊处理,只是将理想的
过程描述出来。
这篇短文的目的也只是帮助大家建立个大致的印象,其实每个地方的都有非常复杂的处理
(尤其是TCP部分)。


2.3 接受数据

当有数据到达网卡的时候,会产生一个硬件中断,然后调用网卡驱动程序中的函数来处理,
对我的3c509网卡来说,其处理函数为:el3_interrupt。(相应的IRQ号是在系统启动,网卡
初始化时通过request_irq函数决定的。)这个中断处理程序首先要做的当然就是进行一些IO
操作将数据读入(读IO用inw函数),当数据帧成功接受后,执行el3_rx(dev)进一步处理。

在el3_rx中,收到的数据报会被封装成struct sk_buff,并脱离驱动程序,转到通用的处理函
数netif_rx(dev.c)中。为了CPU的效率,上层的处理函数的将采用软中断的方式激活,
netif_rx的一个重要工作就是将传入的sk_buff放到等候队列中,并置软中断标志位,然后便
可放心返回,等待下一次网络数据包的到来:
...
__skb_queue_tail(&queue->input_pkt_queue,skb);
__cpu_raise_softirq(this_cpu, NET_RX_SOFTIRQ);
...

这个地方在2.2内核中一直被称为"底半"处理--bottom half,其内部实现基本类似,目的是快
速的从中断中返回。

过了一段时间后,一次CPU调度会由于某些原因会发生(如某进程的时间片用完)。在进程
调度函数即schedule()中,会检查有没有软中断发生,若有则运行相应的处理函数:
...
if (softirq_active(this_cpu) & softirq_mask(this_cpu))
goto handle_softirq;
handle_softirq_back:
...
...
handle_softirq:
do_softirq();
goto handle_softirq_back;
...

在系统初始化的时候,具体说是在net_dev_init中,此软中断的处理函数被定为
net_rx_action:
...
open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
...

当下一次进程调度被执行的时候,系统会检查是否发生NET_TX_SOFTIRQ软中断,若有则
调用net_rx_action。

net_tx_action函数既是2.2版本中的net_bh函数,在内核中有两个全局变量用来登记网络层
的,一个是链表ptype_all,另外一个是数组ptype_base[16],他们记载了所有内核能够处理
的第三层(按照OSI7层模型)协议。每个网络层的接收处理由一个
struct packet_type表示,而这个结构将通dev_add_pack函数将他们登记到ptype_all或
ptype_base中。只有packet_type中的type项为ETH_P_ALL时,才会登记到ptype_all链表
中,否则如ip_packet_type,会在数组ptype_base[16]找到相应的位置。两者不同点是如果
是以ETH_P_ALL类型登记,那么处理函数会受到所有类型的包,否则只能处理自己登记的类
型的。

skb->protocol是在el3_rx中赋值的,其实就是以太帧头信息中提取出的上层协议名,对于我
们的例子来说,这个值是ETH_P_IP,所以在net_tx_action中,会选择IP层的接收处理函
数,而从ip_packet_type 不难看出,这个函数便是ip_recv()。
pt_prev->func(实际指向ip_recv)前面有一个atomic_inc(&skb->users)操作(在2.2内核
中这个地方是一句skb_clone,原理类似),目的是增加这个sk_buff的引用数。网络层的接
收函数在处理完或因为某些原因要丢弃此sk_buff时(如防火墙)会调用kfree_skb,而
kfree_skb中首先会检查是否还有其他地方需要此函数,如果没有地方再用,才真正释放此内
存(__kfree_skb),否则只是计数器减一。

现在我们便来看看ip_recv(net/ipv4/ip_input.c)。这个函数的操作是非常清晰的:首先检查
这个包的合法性(版本号,长度,校验和等是否正确),如果合法则进行接下来的处理。在
2.4内核中,为了灵活处理防火墙代码,将原来的一个ip_recv分成了两部分,即将将原来的
的ip_recv的后半段独立出一个ip_rcv_finish函数。在ip_rcv_finish中,一部分是带有IP选项
(如源路由等)的IP包,例外就是通过ip_route_input查找路由,并将结果记录到skb->dst
中。此时接收到的包有两种,发往本地进程(需要传往上层协议)或转发(用作网关时),
此时需要的处理函数也不相同,如果传往本地,则调用ip_local_deliver(/net/ipv4/ip_input.c),
否则调用ip_forward(/net/ipv4/ip_forward.c).skb->dst->input这个函数指针会将数据报领上
正确的道路。

对我们的例子而言,此时应该是调用ip_local_deliver的时候了。
发来的包很有可能是碎片包,这样的话则首先应该把它们组装好再传给上层协议,这当然也
是ip_local_deliver函数所做的第一份工作,如果组装成功(返回的sk_buff不为空),则继续
处理(详细的组装算法可参见绿盟月刊13期中的《IP分片重组的分析和常见碎片攻击》)。
但此时代码又被netfilter一分为二了,象前面一样,我们直接到后半段,即
ip_local_deliver_finish(/net/ipv4/ip_input.c)中去。

传输层(如TCP,UDP,RAW)的处理被登记到了inet_protos中(通过
inet_add_protocol)。ip_local_deliver_finish会根据IP头信息中的上层协议信息(即
iph->protocol),调用相应的处理函数。为了简便,我们采用了udp,此时的ipprot->handler
实际便是udp_rcv了。

前面已经提到,在应用程序中建立的每个socket在内核中有一个struct socket/struct sock对
应。udp_rcv会通过udp_v4_lookup首先找到在内核中的sock,然后将其作参数调用
udp_queue_rcv_skb(/net/ipv4/udp.c)。马上,sock_queue_rcv_skb函数被调用,此函数
将sk_buff放入等待队列,然后通知上层数据到达:
...
kb_set_owner_r(skb, sk);
skb_queue_tail(&sk->receive_queue, skb);
if (!sk->dead)
sk->data_ready(sk,skb->len);
return 0;
...

sk->data_ready的定义在sock结构初始化的时候(sock_init_data):
...
sk->data_ready=sock_def_readable;
...

现在我们便要从上往下看起了:
进程B要接收数据报,在程序里调用:
...
read(sockfd,buff,sizeof(buff));
...

此系统调用在内核中的函数是sys_read(fs/read_write.c)以下的处理类似write的操作,不再
详述.udp_recvmsg函数会调用skb_recv_datagram,如果数据还没有到达,且socket设为阻
塞模式时,进程会挂起(signal_pending(current)),直到data_ready通知进程资源得到满
足后继续处理(wake_up_interruptible(sk->sleep);)。

2.4 skbuff

网络代码中有大量的处理涉及对sk_buff的操作,尽管此文中尽量将其回避了,但在仔细分析
的时候则必须对此作分析,数据包在网络协议层是以sk_buff的形式传送处理的,可以说它是
网络部分最重要的数据结构。具体分析建议参看alan cox的《Network Buffers And Memory
Management》,这篇发表在1996年10月的linux journal上。

这里引用phrack 55-12期中的一幅图,尽管它只描绘了sk_buff的极小的一个侧面,但却非常
有用,尤其是当你像我一样总忘记了skb_put是向前还是向后调指针的时候:)

--- -----------------hand
^ | |
| | | ^ skb_push
| | | |
| -----------------data--- ---
| | | ^ |
true | | | v skb_pull
size | | len
| | | | ^ skb_trim
| | | v |
| -----------------tail--- ---
| | | |
| | | v skb_put
v | |
--- -----------------end

linux网络层效率:在linux的网络层代码中指针被大量应用,其目的就是避免数据拷贝这类耗
费系统资源的操作。一个数据包的数据段部分在读入或发出时只经过两次拷贝,即从网卡中
考到核心态内存,和从核心态内存考到用户态内存。前些天看到,在一些提高sniffer抓包效
率的尝试中,turbo packet(一个内核补丁)采用了核心态和
用户态共享一段内存的办法,又减少了一次数据拷贝,进一步提高了效率。


3 后记:
这次的投稿又是到了最后关头仓促写出来的,看着里面拙劣的文笔,实在觉得有点对不住观
众~~如果有时间我会把这部分好好重写的,其实这也是我一直的愿望:)


4 参考文献:

[1.] phrack 55-12期
[2.] 2nd Edition
[3.] Network Buffers And Memory Management Alan Cox
http://www2.linuxjournal.com/lj-issues/issue30/1312.html
[4.] 浙大源码分析报告《Linux网络设备分析》潘纲
[5.] Linux IP Networking--A Guide to the Implementation and Modification of theLinux
Poptocol Stack
Glenn Herrin May 31,2000 http://www.movement.uklinux.net/linux-net.pdf

IP包的生成和发送接口 - collide的专栏

IP包的生成和发送接口

====================
(1) Linux内核中有3种基本的IP包生成器, 它们分别为ip_build_xmit(), ip_queue_xmit(),
ip_build_and_send_pkt(). ip_build_and_send_pkt()是一简单的IP包头封装接口,
它接照输入包的路由添加一个IP包头后直接输出,不进行分片处理, 用于tcp_v4_send_synack()中.
ip_send_reply()是基于ip_build_xmit()的一个函数,
用于tcp_v4_send_ack()和tcp_v4_send_reset()中.

(2) ip_build_xmit()使用用户定义的回调函数直接读取用户数据片段生成IP包输出.
如果需要分片,ip_build_xmit()按照最后一个片段到第一个片段的顺序来生成IP包,
这是因为第一个IP包片段的数据区可能包含对整个IP包数据区的校验码,
在回调函数中用户可能会计算输出数据的校验码,
采用从后向前的输出顺序可使校验码自然地写到第一个片段中.

(3) ip_queue_xmit()完成面向连接套接字输出包的路由和IP包头封装. 当套接字处于连接状态时,
所有从套接字发出的包都具有确定的路由, 无需为每一个输出包查询它的目的入口,
可将套接字直接绑定到路由入口上, 这由套接字的目的缓冲指针(dst_cache)来完成.
ip_queue_xmit()首先为输入包建立IP包头, 经过本地包过滤器后,
再将IP包分片输出(ip_fragment), 如果需要的话.

(4) IP包生成器的输出经过本地包过滤器后输入包的路由入口, 对于点播地址来说,
输入到IP输出器中(ip_output); 对于广播或同播地址来说, 输入到IP同播输出器(ip_mc_output).
在IP输出器中, 再经过路由后过滤器,
进入路由的"邻居"入口(dst->neighbour->output)或硬件帧头缓冲入口(dst->hh->hh_output).
邻居是指与主机自已在网络接口设备层次上直达的相邻主机.
邻居负责解析输出包的硬件投送地址, 将包投递给相邻的目的主机或网关主机.
当邻居成功解析包的硬件投送地址时, 将在包的目的入口上创建硬件帧头缓冲结构(dst->hh),
使得后继包可以直接使用组装好的帧头, 直接将包传递给包调度器(dev_queue_xmit).
包调度器按照包的优先级进行重排, 最后将包提交给设备驱动程序发送(dev->hard_start_xmit).

IP包生成接口
------------
; net/ipv4/ip_output.c:

int sysctl_ip_default_ttl = IPDEFTTL; 缺省的IP包生存期为64

/*
* Add an ip header to a skbuff and send it out.
*/
int ip_build_and_send_pkt(struct sk_buff *skb, struct sock *sk,
对包的数据体添加IP头后直接输出
u32 saddr, u32 daddr, struct ip_options *opt)
{
struct rtable *rt = (struct rtable *)skb->dst;
struct iphdr *iph;

/* Build the IP header. */
if (opt)
iph=(struct iphdr *)skb_push(skb,sizeof(struct iphdr) + opt->optlen);
else
iph=(struct iphdr *)skb_push(skb,sizeof(struct iphdr));

iph->version = 4;
iph->ihl = 5;
iph->tos = sk->protinfo.af_inet.tos;
iph->frag_off = 0;
if (ip_dont_fragment(sk, &rt->u.dst)) 如果IP包的目的入口禁止分片
iph->frag_off |= htons(IP_DF);
iph->ttl = sk->protinfo.af_inet.ttl; 取套接字协议选项中的生存期
iph->daddr = rt->rt_dst; 取IP包路由的目的地址
iph->saddr = rt->rt_src; 取IP包路由的源地址
iph->protocol = sk->protocol; 取套接字IP协议代码
iph->tot_len = htons(skb->len); IP包总长度
ip_select_ident(iph, &rt->u.dst); 为IP包分配标识号, 禁止分片的IP包标识为零
skb->nh.iph = iph;

if (opt && opt->optlen) {
iph->ihl += opt->optlen>>2;
ip_options_build(skb, opt, daddr, rt, 0); 设置IP选项区
}
ip_send_check(iph); 设置IP包头的校验和

/* Send it out. */
return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,
output_maybe_reroute); 过滤输出并且目的路径可能会被改变
}

int ip_build_xmit(struct sock *sk,
int getfrag (const void *,
char *,
unsigned int,
unsigned int), 取数据片段的函数指针
const void *frag, 以上函数的调用参数
unsigned length,
struct ipcm_cookie *ipc, IP包配置信息
struct rtable *rt,
int flags) 从用户数据建立IP包
{
int err;
struct sk_buff *skb;
int df;
struct iphdr *iph;

/*
* Try the simple case first. This leaves fragmented frames, and by
* choice RAW frames within 20 bytes of maximum size(rare) to the long path
*/

if (!sk->protinfo.af_inet.hdrincl) { 如果IP包头不由用户创建
length += sizeof(struct iphdr); 取IP包总长

/*
* Check for slow path.
*/
if (length > rt->u.dst.pmtu || ipc->opt != NULL) 如果包长度大于目的入口的最大片断长
return ip_build_xmit_slow(sk,getfrag,frag,length,ipc,rt,flags);
} else {
if (length > rt->u.dst.dev->mtu) { 如果包长大于目的入口设备的最大片段长
ip_local_error(sk, EMSGSIZE, rt->rt_dst, sk->dport, rt->u.dst.dev->mtu);
return -EMSGSIZE;
}
}
if (flags&MSG_PROBE) 测试操作
goto out;

/*
* Do path mtu discovery if needed.
*/
df = 0;
if (ip_dont_fragment(sk, &rt->u.dst)) 如果禁止分片
df = htons(IP_DF);

/*
* Fast path for unfragmented frames without options.
*/
{
int hh_len = (rt->u.dst.dev->hard_header_len + 15)&~15;

skb = sock_alloc_send_skb(sk, length+hh_len+15,
0, flags&MSG_DONTWAIT, &err); 为套接字分配发送包
if(skb==NULL)
goto error;
skb_reserve(skb, hh_len); 保留硬件帧头空间
}

skb->priority = sk->priority; 取套接字的优先级
skb->dst = dst_clone(&rt->u.dst); 取路由的目的入口

skb->nh.iph = iph = (struct iphdr *)skb_put(skb, length);

if(!sk->protinfo.af_inet.hdrincl) {
iph->version=4;
iph->ihl=5;
iph->tos=sk->protinfo.af_inet.tos;
iph->tot_len = htons(length);
iph->frag_off = df;
iph->ttl=sk->protinfo.af_inet.mc_ttl;
ip_select_ident(iph, &rt->u.dst);
if (rt->rt_type != RTN_MULTICAST)
iph->ttl=sk->protinfo.af_inet.ttl;
iph->protocol=sk->protocol;
iph->saddr=rt->rt_src;
iph->daddr=rt->rt_dst;
iph->check=0;
iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
err = getfrag(frag, ((char *)iph)+iph->ihl*4,0, length-iph->ihl*4);
; 读取用户一片数据
}
else 如果IP包头由用户创建, 直接将用户数据读入IP头所在位置
err = getfrag(frag, (void *)iph, 0, length);

if (err)
goto error_fault;

err = NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,
output_maybe_reroute);
if (err > 0)
err = sk->protinfo.af_inet.recverr ? net_xmit_errno(err) : 0;
if (err)
goto error;
out:
return 0;

error_fault:
err = -EFAULT;
kfree_skb(skb);
error:
IP_INC_STATS(IpOutDiscards);
return err;
}
static int ip_build_xmit_slow(struct sock *sk,
int getfrag (const void *,
char *,
unsigned int,
unsigned int),
const void *frag,
unsigned length,
struct ipcm_cookie *ipc,
struct rtable *rt,
int flags) 建立IP选项区或者分片输出
{
unsigned int fraglen, maxfraglen, fragheaderlen;
int err;
int offset, mf;
int mtu;
u16 id = 0;

int hh_len = (rt->u.dst.dev->hard_header_len + 15)&~15;
int nfrags=0;
struct ip_options *opt = ipc->opt;
int df = 0;

mtu = rt->u.dst.pmtu;
if (ip_dont_fragment(sk, &rt->u.dst))
df = htons(IP_DF);

length -= sizeof(struct iphdr);

if (opt) {
fragheaderlen = sizeof(struct iphdr) + opt->optlen;
maxfraglen = ((mtu-sizeof(struct iphdr)-opt->optlen) & ~7) + fragheaderlen;
} else {
fragheaderlen = sizeof(struct iphdr);

/*
* Fragheaderlen is the size of 'overhead' on each buffer. Now work
* out the size of the frames to send.
*/

maxfraglen = ((mtu-sizeof(struct iphdr)) & ~7) + fragheaderlen;
} 求最大IP包长

if (length + fragheaderlen > 0xFFFF) {
ip_local_error(sk, EMSGSIZE, rt->rt_dst, sk->dport, mtu);
return -EMSGSIZE;
}

/*
* Start at the end of the frame by handling the remainder.
*/

offset = length - (length % (maxfraglen - fragheaderlen));
取最后一个片段的数据偏移量

/*
* Amount of memory to allocate for final fragment.
*/

fraglen = length - offset + fragheaderlen; 求取后一个片段IP包全长

if (length-offset==0) { 如果用户数据恰好是最大单片数据长度的整数倍
fraglen = maxfraglen;
offset -= maxfraglen-fragheaderlen;
}

/*
* The last fragment will not have MF (more fragments) set.
*/

mf = 0;

/*
* Don't fragment packets for path mtu discovery.
*/

if (offset > 0 && sk->protinfo.af_inet.pmtudisc==IP_PMTUDISC_DO) {
ip_local_error(sk, EMSGSIZE, rt->rt_dst, sk->dport, mtu);
return -EMSGSIZE;
}
if (flags&MSG_PROBE)
goto out;

/*
* Begin outputting the bytes.
*/

do {
char *data;
struct sk_buff * skb;

/*
* Get the memory we require with some space left for alignment.
*/

skb = sock_alloc_send_skb(sk, fraglen+hh_len+15, 0, flags&MSG_DONTWAIT, &err);
if (skb == NULL)
goto error;

/*
* Fill in the control structures
*/

skb->priority = sk->priority;
skb->dst = dst_clone(&rt->u.dst);
skb_reserve(skb, hh_len);

/*
* Find where to start putting bytes.
*/

data = skb_put(skb, fraglen);
skb->nh.iph = (struct iphdr *)data;

/*
* Only write IP header onto non-raw packets
*/

{
struct iphdr *iph = (struct iphdr *)data;

iph->version = 4;
iph->ihl = 5;
if (opt) {
iph->ihl += opt->optlen>>2;
ip_options_build(skb, opt,
ipc->addr, rt, offset);
}
iph->tos = sk->protinfo.af_inet.tos;
iph->tot_len = htons(fraglen - fragheaderlen + iph->ihl*4);
iph->frag_off = htons(offset>>3)|mf|df;
iph->id = id;
if (!mf) {
if (offset || !df) {
/* Select an unpredictable ident only
* for packets without DF or having
* been fragmented.
*/
__ip_select_ident(iph, &rt->u.dst);
id = iph->id;
}

/*
* Any further fragments will have MF set.
*/
mf = htons(IP_MF);
}
if (rt->rt_type == RTN_MULTICAST)
iph->ttl = sk->protinfo.af_inet.mc_ttl;
else
iph->ttl = sk->protinfo.af_inet.ttl;
iph->protocol = sk->protocol;
iph->check = 0;
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
data += iph->ihl*4;
}

/*
* User data callback
*/

if (getfrag(frag, data, offset, fraglen-fragheaderlen)) {
err = -EFAULT;
kfree_skb(skb);
goto error;
}

offset -= (maxfraglen-fragheaderlen); 片段从后向前进行分割, 是为了方便TCP包的校验
fraglen = maxfraglen;

nfrags++;

err = NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL,
skb->dst->dev, output_maybe_reroute);
if (err) {
if (err > 0)
err = sk->protinfo.af_inet.recverr ? net_xmit_errno(err) : 0;
if (err)
goto error;
}
} while (offset >= 0);

if (nfrags>1)
ip_statistics[smp_processor_id()*2 + !in_softirq()].IpFragCreates += nfrags;
out:
return 0;

error:
IP_INC_STATS(IpOutDiscards);
if (nfrags>1)
ip_statistics[smp_processor_id()*2 + !in_softirq()].IpFragCreates += nfrags;
return err;
}

/*
* Generic function to send a packet as reply to another packet.
* Used to send TCP resets so far. ICMP should use this function too.
*
* Should run single threaded per socket because it uses the sock
* structure to pass arguments.
*/
void ip_send_reply(struct sock *sk, struct sk_buff *skb, struct ip_reply_arg *arg,
unsigned int len)
{
struct {
struct ip_options opt;
char data[40]; 存放IP选项块
} replyopts;
struct ipcm_cookie ipc;
u32 daddr;
struct rtable *rt = (struct rtable*)skb->dst;

if (ip_options_echo(&replyopts.opt, skb)) 将包skb的IP选项刷新到replyopts结构中
return;

daddr = ipc.addr = rt->rt_src;
ipc.opt = NULL;

if (replyopts.opt.optlen) {
ipc.opt = &replyopts.opt;

if (ipc.opt->srr)
daddr = replyopts.opt.faddr;
}

if (ip_route_output(&rt, daddr, rt->rt_spec_dst, RT_TOS(skb->nh.iph->tos), 0))
return;

/* And let IP do all the hard work.

This chunk is not reenterable, hence spinlock.
Note that it uses the fact, that this function is called
with locally disabled BH and that sk cannot be already spinlocked.
*/
bh_lock_sock(sk);
sk->protinfo.af_inet.tos = skb->nh.iph->tos;
sk->priority = skb->priority;
sk->protocol = skb->nh.iph->protocol;
ip_build_xmit(sk, ip_reply_glue_bits, arg, len, &ipc, rt, MSG_DONTWAIT);
bh_unlock_sock(sk);

ip_rt_put(rt);
}
struct ip_reply_arg {
struct iovec iov[2];
int n_iov; /* redundant */
u32 csum;
int csumoffset; /* u16 offset of csum in iov[0].iov_base */
/* -1 if not needed */
};
/*
* Fetch data from kernel space and fill in checksum if needed.
*/
static int ip_reply_glue_bits(const void *dptr, char *to, unsigned int offset,
unsigned int fraglen)
{
struct ip_reply_arg *dp = (struct ip_reply_arg*)dptr;
u16 *pktp = (u16 *)to;
struct iovec *iov;
int len;
int hdrflag = 1;

iov = &dp->iov[0];
if (offset >= iov->iov_len) {
offset -= iov->iov_len;
iov++;
hdrflag = 0;
}
len = iov->iov_len - offset;
if (fraglen > len) { /* overlapping. */
dp->csum = csum_partial_copy_nocheck(iov->iov_base+offset, to, len,
dp->csum);
offset = 0;
fraglen -= len;
to += len;
iov++;
}

dp->csum = csum_partial_copy_nocheck(iov->iov_base+offset, to, fraglen,
dp->csum);

if (hdrflag && dp->csumoffset)
*(pktp + dp->csumoffset) = csum_fold(dp->csum); /* fill in checksum */
return 0;
}

int ip_queue_xmit(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
struct ip_options *opt = sk->protinfo.af_inet.opt;
struct rtable *rt;
struct iphdr *iph;

/* Make sure we can route this packet. */
rt = (struct rtable *)__sk_dst_check(sk, 0); 取套接字所缓冲的发送包的目的路由入口
if (rt == NULL) { 如果尚未缓冲
u32 daddr;

/* Use correct destination address if we have options. */
daddr = sk->daddr; 取套接字的对端地址作为目的地址
if(opt && opt->srr) 如果具有信源路由选项
daddr = opt->faddr; 取信源路由的转发地址作为目的地址

/* If this fails, retransmit mechanism of transport layer will
* keep trying until route appears or the connection times itself
* out.
*/
if (ip_route_output(&rt, daddr, sk->saddr,
RT_TOS(sk->protinfo.af_inet.tos) | RTO_CONN | sk->localroute,
sk->bound_dev_if)) 查询目的地址的路由目的入口
goto no_route;
__sk_dst_set(sk, &rt->u.dst); 将该路由入口缓冲到套接字上
}
skb->dst = dst_clone(&rt->u.dst); 将路由入口绑定到发送包

if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)
goto no_route; 如果是指定严格信源路由并且其转发地址不等于网关地址,则操作失败

/* OK, we know where to send it, allocate and build IP header. */
iph = (struct iphdr *) skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen :
0));
*((__u16 *)iph) = htons((4 <<>protinfo.af_inet.tos & 0xff));
iph->tot_len = htons(skb->len);
iph->frag_off = 0;
iph->ttl = sk->protinfo.af_inet.ttl;
iph->protocol = sk->protocol;
iph->saddr = rt->rt_src;
iph->daddr = rt->rt_dst;
skb->nh.iph = iph;
/* Transport layer set skb->h.foo itself. */

if(opt && opt->optlen) { 建立IP选项区
iph->ihl += opt->optlen >> 2;
ip_options_build(skb, opt, sk->daddr, rt, 0);
}

return NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,
ip_queue_xmit2); 过滤输出

no_route:
IP_INC_STATS(IpOutNoRoutes);
kfree_skb(skb);
return -EHOSTUNREACH;
}

/* Queues a packet to be sent, and starts the transmitter if necessary.
* This routine also needs to put in the total length and compute the
* checksum. We use to do this in two stages, ip_build_header() then
* this, but that scheme created a mess when routes disappeared etc.
* So we do it all here, and the TCP send engine has been changed to
* match. (No more unroutable FIN disasters, etc. wheee...) This will
* most likely make other reliable transport layers above IP easier
* to implement under Linux.
*/
static inline int ip_queue_xmit2(struct sk_buff *skb)
{
struct sock *sk = skb->sk;
struct rtable *rt = (struct rtable *)skb->dst;
struct net_device *dev;
struct iphdr *iph = skb->nh.iph;

dev = rt->u.dst.dev;

/* This can happen when the transport layer has segments queued
* with a cached route, and by the time we get here things are
* re-routed to a device with a different MTU than the original
* device. Sick, but we must cover it.
*/
if (skb_headroom(skb) <>hard_header_len && dev->hard_header) {
如果包可用的硬件帧头空间不足
struct sk_buff *skb2;

skb2 = skb_realloc_headroom(skb, (dev->hard_header_len + 15) & ~15);
复制并重新分配原包
kfree_skb(skb);
if (skb2 == NULL)
return -ENOMEM;
if (sk)
skb_set_owner_w(skb2, sk); 设置包的拥有套接字
skb = skb2;
iph = skb->nh.iph;
}

if (skb->len > rt->u.dst.pmtu)
goto fragment;

if (ip_dont_fragment(sk, &rt->u.dst))
iph->frag_off |= __constant_htons(IP_DF);

ip_select_ident(iph, &rt->u.dst);

/* Add an IP checksum. */
ip_send_check(iph);

skb->priority = sk->priority;
return skb->dst->output(skb);

fragment:
if (ip_dont_fragment(sk, &rt->u.dst)) {
/* Reject packet ONLY if TCP might fragment
* it itself, if were careful enough.
*/
iph->frag_off |= __constant_htons(IP_DF);
NETDEBUG(printk(KERN_DEBUG "sending pkt_too_big to self\n"));

icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(rt->u.dst.pmtu));
kfree_skb(skb);
return -EMSGSIZE;
}
ip_select_ident(iph, &rt->u.dst);
return ip_fragment(skb, skb->dst->output);
}

/*
* This IP datagram is too large to be sent in one piece. Break it up into
* smaller pieces (each of size equal to IP header plus
* a block of the data of the original IP data part) that will yet fit in a
* single device frame, and queue such a frame for sending.
*
* Yes this is inefficient, feel free to submit a quicker one.
*/

int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff*))
{
struct iphdr *iph;
unsigned char *raw;
unsigned char *ptr;
struct net_device *dev;
struct sk_buff *skb2;
unsigned int mtu, hlen, left, len;
int offset;
int not_last_frag;
struct rtable *rt = (struct rtable*)skb->dst;
int err = 0;

dev = rt->u.dst.dev;

/*
* Point into the IP datagram header.
*/

raw = skb->nh.raw;
iph = (struct iphdr*)raw;

/*
* Setup starting values.
*/

hlen = iph->ihl * 4;
left = ntohs(iph->tot_len) - hlen; /* Space per frame */
mtu = rt->u.dst.pmtu - hlen; /* Size of data space */
ptr = raw + hlen; /* Where to start from */

/*
* Fragment the datagram.
*/

offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
not_last_frag = iph->frag_off & htons(IP_MF);

/*
* Keep copying data until we run out.
*/

while(left > 0) {
len = left;
/* IF: it doesn't fit, use 'mtu' - the data space left */
if (len > mtu)
len = mtu;
/* IF: we are not sending upto and including the packet end
then align the next start on an eight byte boundary */
if (len < left) {
len &= ~7;
}
/*
* Allocate buffer.
*/

if ((skb2 = alloc_skb(len+hlen+dev->hard_header_len+15,GFP_ATOMIC)) == NULL) {
NETDEBUG(printk(KERN_INFO "IP: frag: no memory for new fragment!\n"));
err = -ENOMEM;
goto fail;
}

/*
* Set up data on packet
*/

skb2->pkt_type = skb->pkt_type;
skb2->priority = skb->priority;
skb_reserve(skb2, (dev->hard_header_len+15)&~15);
skb_put(skb2, len + hlen);
skb2->nh.raw = skb2->data;
skb2->h.raw = skb2->data + hlen;

/*
* Charge the memory for the fragment to any owner
* it might possess
*/

if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
skb2->dst = dst_clone(skb->dst);
skb2->dev = skb->dev;

/*
* Copy the packet header into the new buffer.
*/

memcpy(skb2->nh.raw, raw, hlen);

/*
* Copy a block of the IP datagram.
*/
memcpy(skb2->h.raw, ptr, len);
left -= len;

/*
* Fill in the new header fields.
*/
iph = skb2->nh.iph;
iph->frag_off = htons((offset >> 3));

/* ANK: dirty, but effective trick. Upgrade options only if
* the segment to be fragmented was THE FIRST (otherwise,
* options are already fixed) and make it ONCE
* on the initial skb, so that all the following fragments
* will inherit fixed options.
*/
if (offset == 0)
ip_options_fragment(skb);

/*
* Added AC : If we are fragmenting a fragment that's not the
* last fragment then keep MF on each bit
*/
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF);
ptr += len;
offset += len;

#ifdef CONFIG_NETFILTER
/* Connection association is same as pre-frag packet */
skb2->nfct = skb->nfct;
nf_conntrack_get(skb2->nfct);
#ifdef CONFIG_NETFILTER_DEBUG
skb2->nf_debug = skb->nf_debug;
#endif
#endif

/*
* Put this fragment into the sending queue.
*/

IP_INC_STATS(IpFragCreates);

iph->tot_len = htons(len + hlen);

ip_send_check(iph);

err = output(skb2);
if (err)
goto fail;
}
kfree_skb(skb);
IP_INC_STATS(IpFragOKs);
return err;

fail:
kfree_skb(skb);
IP_INC_STATS(IpFragFails);
return err;
}

IP包的输出接口
--------------

/* Don't just hand NF_HOOK skb->dst->output, in case netfilter hook
changes route */
static inline int 发送包的目的入口指针可能被包过滤器所改变,
因此不能向包过滤器直接提供目的入口的输出函数
output_maybe_reroute(struct sk_buff *skb)
{
return skb->dst->output(skb); 通过目的入口输出, 对于点播(UNICAST)地址来说,
指向ip_output()
}

int ip_output(struct sk_buff *skb) 点播地址的IP包输出口
{
#ifdef CONFIG_IP_ROUTE_NAT
struct rtable *rt = (struct rtable*)skb->dst;
#endif

IP_INC_STATS(IpOutRequests);

#ifdef CONFIG_IP_ROUTE_NAT
if (rt->rt_flags&RTCF_NAT)
ip_do_nat(skb);
#endif

return ip_finish_output(skb);
}
__inline__ int ip_finish_output(struct sk_buff *skb)
{
struct net_device *dev = skb->dst->dev;

skb->dev = dev; 设置包的输出设备
skb->protocol = __constant_htons(ETH_P_IP); 设置包的帧类型

return NF_HOOK(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev,
ip_finish_output2);
}
static inline int ip_finish_output2(struct sk_buff *skb)
{
struct dst_entry *dst = skb->dst; 取IP包绑定的目的入口
struct hh_cache *hh = dst->hh; 取目的入口的硬件帧头缓冲结构

#ifdef CONFIG_NETFILTER_DEBUG
nf_debug_ip_finish_output2(skb);
#endif /*CONFIG_NETFILTER_DEBUG*/

if (hh) { 如果帧头缓冲结构存在
read_lock_bh(&hh->hh_lock);
memcpy(skb->data - 16, hh->hh_data, 16); 直接拷贝硬件帧头
read_unlock_bh(&hh->hh_lock);
skb_push(skb, hh->hh_len);
return hh->hh_output(skb); 通过帧头缓冲输出, 一般指向dev_queue_xmit()
} else if (dst->neighbour) 通过邻居结构(设备硬件地址解析和缓冲结构)输出
return dst->neighbour->output(skb); 指向neigh_resolve_output()
; 关于neighbour结构和arp过程可看自已"Linux硬件地址解析过程"的帖子
printk(KERN_DEBUG "khm\n");
kfree_skb(skb);
return -EINVAL;
}

IP包创建过程中选项区的操作
--------------------------

; net/ipv4/ip_options.c:

struct ip_options { IP选项区索引结构, 当接收到IP包时,
包缓冲的控制块(cb)前部即为此结构
__u32 faddr; /* Saved first hop address */
unsigned char optlen;
unsigned char srr;
unsigned char rr;
unsigned char ts;
unsigned char is_setbyuser:1, /* Set by setsockopt? */
is_data:1, /* Options in __data, rather than skb */
is_strictroute:1, /* Strict source route */
srr_is_hit:1, /* Packet destination addr was our one */
is_changed:1, /* IP checksum more not valid */
rr_needaddr:1, /* Need to record addr of outgoing dev */
ts_needtime:1, /* Need to record timestamp */
ts_needaddr:1; /* Need to record addr of outgoing dev */
unsigned char router_alert;
unsigned char __pad1;
unsigned char __pad2;
unsigned char __data[0];
};
struct inet_skb_parm
{
struct ip_options opt; /* Compiled IP options */
unsigned char flags;

#define IPSKB_MASQUERADED 1
#define IPSKB_TRANSLATED 2
#define IPSKB_FORWARDED 4
};

#define IPCB(skb) ((struct inet_skb_parm*)((skb)->cb))

/*
* Options "fragmenting", just fill options not
* allowed in fragments with NOOPs.
* Simple and stupid 8), but the most efficient way.
*/

void ip_options_fragment(struct sk_buff * skb)
{
unsigned char * optptr = skb->nh.raw; ??? 不是指向IP头吗,怎么用作IP选项区???
struct ip_options * opt = &(IPCB(skb)->opt); 取包缓冲控制块(cb)
int l = opt->optlen;
int optlen;

while (l > 0) {
switch (*optptr) {
case IPOPT_END:
return;
case IPOPT_NOOP:
l--;
optptr++;
continue;
}
optlen = optptr[1];
if (optlen<2>l)
return;
if (!IPOPT_COPIED(*optptr)) 将所有没有片断复制标志的选项置换成空选项
memset(optptr, IPOPT_NOOP, optlen);
l -= optlen;
optptr += optlen;
}
opt->ts = 0;
opt->rr = 0;
opt->rr_needaddr = 0;
opt->ts_needaddr = 0;
opt->ts_needtime = 0;
return;
}

/*
* Write options to IP header, record destination address to
* source route option, address of outgoing interface
* (we should already know it, so that this function is allowed be
* called only after routing decision) and timestamp,
* if we originate this datagram.
*
* daddr is real destination address, next hop is recorded in IP header.
* saddr is address of outgoing interface.
*/

void ip_options_build(struct sk_buff * skb, struct ip_options * opt,
u32 daddr, struct rtable *rt, int is_frag)
{
unsigned char * iph = skb->nh.raw;

memcpy(&(IPCB(skb)->opt), opt, sizeof(struct ip_options));
将IP选项参数复制到包缓冲上
memcpy(iph+sizeof(struct iphdr), opt->__data, opt->optlen);
将IP选项数据块复制到IP包选项区
opt = &(IPCB(skb)->opt);
opt->is_data = 0;

if (opt->srr) 将地址daddr拷贝到信源路由选项表的最后一项
memcpy(iph+opt->srr+iph[opt->srr+1]-4, &daddr, 4);

if (!is_frag) { 如果不是片段
if (opt->rr_needaddr) 记录输出设备的地址
ip_rt_get_source(iph+opt->rr+iph[opt->rr+2]-5, rt);
if (opt->ts_needaddr) 记录时戳地址
ip_rt_get_source(iph+opt->ts+iph[opt->ts+2]-9, rt);
if (opt->ts_needtime) { 记录时戳
struct timeval tv;
__u32 midtime;
do_gettimeofday(&tv);
midtime = htonl((tv.tv_sec % 86400) * 1000 + tv.tv_usec / 1000);
memcpy(iph+opt->ts+iph[opt->ts+2]-5, &midtime, 4);
}
return;
}
if (opt->rr) { 填充成空选项
memset(iph+opt->rr, IPOPT_NOP, iph[opt->rr+1]);
opt->rr = 0;
opt->rr_needaddr = 0;
}
if (opt->ts) {
memset(iph+opt->ts, IPOPT_NOP, iph[opt->ts+1]);
opt->ts = 0;
opt->ts_needaddr = opt->ts_needtime = 0;
}
}

/*
* Provided (sopt, skb) points to received options,
* build in dopt compiled option set appropriate for answering.
* i.e. invert SRR option, copy anothers,
* and grab room in RR/TS options.
*
* NOTE: dopt cannot point to skb.
*/

int ip_options_echo(struct ip_options * dopt, struct sk_buff * skb)
{
struct ip_options *sopt;
unsigned char *sptr, *dptr;
int soffset, doffset;
int optlen;
u32 daddr;

memset(dopt, 0, sizeof(struct ip_options)); 将输出选项区索引结构清零

dopt->is_data = 1;

sopt = &(IPCB(skb)->opt);

if (sopt->optlen == 0) {
dopt->optlen = 0;
return 0;
}

sptr = skb->nh.raw;
dptr = dopt->__data; 指向输出选项区开始

if (skb->dst)
daddr = ((struct rtable*)skb->dst)->rt_spec_dst;
else
daddr = skb->nh.iph->daddr;

if (sopt->rr) {
optlen = sptr[sopt->rr+1]; 取IP包路由记录表长度
soffset = sptr[sopt->rr+2]; 取当前地址记录索引
dopt->rr = dopt->optlen + sizeof(struct iphdr); 取输出选项区尾部
memcpy(dptr, sptr+sopt->rr, optlen); 复制路由记录表
if (sopt->rr_needaddr && soffset <= optlen) {
if (soffset + 3 > optlen)
return -EINVAL;
dptr[2] = soffset + 4; 更新地址记录索引
dopt->rr_needaddr = 1;
}
dptr += optlen; 更新选项区输出指针
dopt->optlen += optlen; 更新选项区长度变量
}
if (sopt->ts) {
optlen = sptr[sopt->ts+1]; 取时戳表选项长度
soffset = sptr[sopt->ts+2]; 取当前时戳记录索引
dopt->ts = dopt->optlen + sizeof(struct iphdr);
memcpy(dptr, sptr+sopt->ts, optlen); 复制时戳表
if (soffset <= optlen) {
if (sopt->ts_needaddr) {
if (soffset + 3 > optlen)
return -EINVAL;
dopt->ts_needaddr = 1;
soffset += 4;
}
if (sopt->ts_needtime) {
if (soffset + 3 > optlen)
return -EINVAL;
if ((dptr[3]&0xF) != IPOPT_TS_PRESPEC) {
dopt->ts_needtime = 1;
soffset += 4;
} else { 如果为取指定地址时戳
dopt->ts_needtime = 0;

if (soffset + 8 <= optlen) {
__u32 addr;

memcpy(&addr, sptr+soffset-1, 4);
if (inet_addr_type(addr) != RTN_LOCAL) {
dopt->ts_needtime = 1;
soffset += 8;
}
}
}
}
dptr[2] = soffset;
}
dptr += optlen;
dopt->optlen += optlen;
}
if (sopt->srr) {
unsigned char * start = sptr+sopt->srr;
u32 faddr;

optlen = start[1];
soffset = start[2];
doffset = 0;
if (soffset > optlen)
soffset = optlen + 1;
soffset -= 4;
if (soffset > 3) {
memcpy(&faddr, &start[soffset-1], 4);
for (soffset-=4, doffset=4; soffset > 3; soffset-=4, doffset+=4)
memcpy(&dptr[doffset-1], &start[soffset-1], 4);
/*
* RFC1812 requires to fix illegal source routes.
*/
if (memcmp(&skb->nh.iph->saddr, &start[soffset+3], 4) == 0)
doffset -= 4;
}
if (doffset > 3) {
memcpy(&start[doffset-1], &daddr, 4);
dopt->faddr = faddr;
dptr[0] = start[0];
dptr[1] = doffset+3;
dptr[2] = 4;
dptr += doffset+3;
dopt->srr = dopt->optlen + sizeof(struct iphdr);
dopt->optlen += doffset+3;
dopt->is_strictroute = sopt->is_strictroute;
}
}
while (dopt->optlen & 3) {
*dptr++ = IPOPT_END;
dopt->optlen++;
}
return 0;
}

其余一些相关函数
----------------

static inline struct dst_entry *
__sk_dst_check(struct sock *sk, u32 cookie) 校验套接字所缓冲的目的入口
{
struct dst_entry *dst = sk->dst_cache;

if (dst && dst->obsolete && dst->ops->check(dst, cookie) == NULL) {
sk->dst_cache = NULL;
return NULL;
}

return dst;
}
static struct dst_entry * ipv4_dst_check(struct dst_entry * dst, u32 cookie)
{ ; ipv4协议的目的入口校验函数
dst_release(dst);
return NULL;
}
static inline
int ip_dont_fragment(struct sock *sk, struct dst_entry *dst) 是否允许IP分片
{
return (sk->protinfo.af_inet.pmtudisc == IP_PMTUDISC_DO ||
(sk->protinfo.af_inet.pmtudisc == IP_PMTUDISC_WANT &&
!(dst->mxlock&(1<
}
static inline void ip_select_ident(struct iphdr *iph, struct dst_entry *dst)
分配IP包标识号
{
if (iph->frag_off&__constant_htons(IP_DF))
iph->id = 0;
else
__ip_select_ident(iph, dst);
}

/* Generate a checksum for an outgoing IP datagram. */
__inline__ void ip_send_check(struct iphdr *iph) 校验IP包头
{
iph->check = 0;
iph->check = ip_fast_csum((unsigned char *)iph, iph->ihl);
}

#define SKB_EXT_ERR(skb) ((struct sock_exterr_skb *) ((skb)->cb))

void ip_local_error(struct sock *sk, int err, u32 daddr, u16 port, u32 info)
生成出错信息包
{
struct sock_exterr_skb *serr;
struct iphdr *iph;
struct sk_buff *skb;

if (!sk->protinfo.af_inet.recverr)
return;

skb = alloc_skb(sizeof(struct iphdr), GFP_ATOMIC);
if (!skb)
return;

iph = (struct iphdr*)skb_put(skb, sizeof(struct iphdr));
skb->nh.iph = iph;
iph->daddr = daddr;

serr = SKB_EXT_ERR(skb); 取包缓冲控制块(cb)
serr->ee.ee_errno = err; 错误码
serr->ee.ee_origin = SO_EE_ORIGIN_LOCAL; 错误来源
serr->ee.ee_type = 0;
serr->ee.ee_code = 0;
serr->ee.ee_pad = 0;
serr->ee.ee_info = info; 出错参数
serr->ee.ee_data = 0;
serr->addr_offset = (u8*)&iph->daddr - skb->nh.raw;
serr->port = port;

skb->h.raw = skb->tail;
skb_pull(skb, skb->tail - skb->data);

if (sock_queue_err_skb(sk, skb)) 加入套接字的出错信息包队列
kfree_skb(skb);
}
static inline int sock_queue_err_skb(struct sock *sk, struct sk_buff *skb)
{
/* Cast skb->rcvbuf to unsigned... It's pointless, but reduces
number of warnings when compiling with -W --ANK
*/
if (atomic_read(&sk->rmem_alloc) + skb->truesize >= (unsigned)sk->rcvbuf)
return -ENOMEM;
skb_set_owner_r(skb, sk);
skb_queue_tail(&sk->error_queue,skb);
if (!sk->dead)
sk->data_ready(sk,skb->len); 唤醒套接字等待队列
return 0;
}

/*
* Queue a received datagram if it will fit. Stream and sequenced
* protocols can't normally use this as they need to fit buffers in
* and play with them.
*
* Inlined as it's very short and called for pretty much every
* packet ever received.
*/

static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
设置发送包的拥有套接字
{
sock_hold(sk);
skb->sk = sk;
skb->destructor = sock_wfree;
atomic_add(skb->truesize, &sk->wmem_alloc); 刷新发送内存的分配量
}
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
设置接收包的拥有套接字
{
skb->sk = sk;
skb->destructor = sock_rfree;
atomic_add(skb->truesize, &sk->rmem_alloc); 刷新接收内存的分配量
}
/*
* Write buffer destructor automatically called from kfree_skb.
*/
void sock_wfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;

/* In case it might be waiting for more memory. */
atomic_sub(skb->truesize, &sk->wmem_alloc);
sk->write_space(sk); 唤醒正在等待写的进程
sock_put(sk);
}

/*
* Read buffer destructor automatically called from kfree_skb.
*/
void sock_rfree(struct sk_buff *skb)
{
struct sock *sk = skb->sk;

atomic_sub(skb->truesize, &sk->rmem_alloc);
}

static inline long sock_sndtimeo(struct sock *sk, int noblock) 取套接字的发送超时
{
return noblock ? 0 : sk->sndtimeo;
}
static inline int sock_error(struct sock *sk) 读取并且复位套接字错误号
{
int err=xchg(&sk->err,0);
return -err;
}

/*
We do not cache source address of outgoing interface,
because it is used only by IP RR, TS and SRR options,
so that it out of fast path.

BTW remember: "addr" is allowed to be not aligned
in IP options!
*/

void ip_rt_get_source(u8 *addr, struct rtable *rt) 取路由输出设备的地址
{
u32 src;
struct fib_result res;

if (rt->key.iif == 0)
src = rt->rt_src;
else if (fib_lookup(&rt->key, &res) == 0) {
#ifdef CONFIG_IP_ROUTE_NAT
if (res.type == RTN_NAT)
src = inet_select_addr(rt->u.dst.dev, rt->rt_gateway, RT_SCOPE_UNIVERSE);
else
#endif
src = FIB_RES_PREFSRC(res);
fib_res_put(&res);
} else
src = inet_select_addr(rt->u.dst.dev, rt->rt_gateway, RT_SCOPE_UNIVERSE);
memcpy(addr, &src, 4);
}

struct sk_buff *sock_alloc_send_skb(struct sock *sk, unsigned long size,
unsigned long fallback, int noblock, int *errcode) 分配套接字的发送包
{
int err;
struct sk_buff *skb;
long timeo;

timeo = sock_sndtimeo(sk, noblock); 取套接字的发送超时

while (1) {
unsigned long try_size = size;

err = sock_error(sk); 读取并且复位套接字的错误码
if (err != 0)
goto failure;

/*
* We should send SIGPIPE in these cases according to
* 1003.1g draft 6.4. If we (the user) did a shutdown()
* call however we should not.
*
* Note: This routine isnt just used for datagrams and
* anyway some datagram protocols have a notion of
* close down.
*/

err = -EPIPE;
if (sk->shutdown&SEND_SHUTDOWN)
goto failure;

if (atomic_read(&sk->wmem_alloc) <>sndbuf) {
如果分配给套接字的发送内存小于其发送缓冲区
if (fallback) {
/* The buffer get won't block, or use the atomic queue.
* It does produce annoying no free page messages still.
*/
skb = alloc_skb(size, GFP_BUFFER);
if (skb)
break;
try_size = fallback;
}
skb = alloc_skb(try_size, sk->allocation);
if (skb)
break;
err = -ENOBUFS;
goto failure;
}

/*
* This means we have too many buffers for this socket already.
*/

set_bit(SOCK_ASYNC_NOSPACE, &sk->socket->flags);
set_bit(SOCK_NOSPACE, &sk->socket->flags);
err = -EAGAIN;
if (!timeo)
goto failure;
if (signal_pending(current))
goto interrupted;
timeo = sock_wait_for_wmem(sk, timeo);
}

skb_set_owner_w(skb, sk);
return skb;

interrupted:
err = sock_intr_errno(timeo);
failure:
*errcode = err;
return NULL;
}
/* It is almost wait_for_tcp_memory minus release_sock/lock_sock.
I think, these locks should be removed for datagram sockets.
*/
static long sock_wait_for_wmem(struct sock * sk, long timeo)
等待套接字可分配的发送内存
{
DECLARE_WAITQUEUE(wait, current);

clear_bit(SOCK_ASYNC_NOSPACE, &sk->socket->flags);
add_wait_queue(sk->sleep, &wait);
for (;;) {
if (signal_pending(current))
break;
set_bit(SOCK_NOSPACE, &sk->socket->flags);
set_current_state(TASK_INTERRUPTIBLE);
if (atomic_read(&sk->wmem_alloc) <>sndbuf)
break;
if (sk->shutdown & SEND_SHUTDOWN)
break;
if (sk->err)
break;
timeo = schedule_timeout(timeo);
}
__set_current_state(TASK_RUNNING);
remove_wait_queue(sk->sleep, &wait);
return timeo;
}


Edited by lucian_yao on 07/02/01 02:38
PM.

如何发掘出更多退休的钱?

如何发掘出更多退休的钱? http://bbs.wenxuecity.com/bbs/tzlc/1328415.html 按照常规的说法,退休的收入必须得有退休前的80%,或者是4% withdrawal rule,而且每年还得要加2-3%对付通胀,这是一个很大...