如何做与openstack对接的用户验证

可以在openstack horizon中加一个文件夹Security,里面按照horizon的要求放置好子目录和文件。


nsfocus@controller:/usr/src/horizon$ ls
build  HACKING.rst  horizon.egg-info  Makefile   MANIFEST.in            openstack_dashboard  requirements.txt  security   setup.py  test-requirements.txt  tox.ini
doc    horizon      LICENSE           manage.py  openstack-common.conf  README.rst           run_tests.sh      setup.cfg  static    tools
nsfocus@controller:/usr/src/horizon$ cd security/
nsfocus@controller:/usr/src/horizon/security$ ls
ads     appmanager  byod          dashboard.pyc  flowviewer  __init__.py   knowledge       misc       models.pyc    static     waf
adsapp  assetcheck  dashboard.py  devicemanager  ids         __init__.pyc  liveprotection  models.py  moduleviewer  templates  webprotection
nsfocus@controller:/usr/src/horizon/security$ vi liveprotection/
__init__.py   __init__.pyc  panel.py      panel.pyc     templates/    urls.py       urls.pyc      views.py      views.pyc
查看liveprotection/view.py
from django.core.urlresolvers import reverse_lazy # noqa 
from django.utils.translation import ugettext_lazy as _ # noqa 
from horizon import tables 
from horizon import exceptions 
from openstack_dashboard import api 
from django.http import HttpResponse 
from django.template import RequestContext, loader 
from horizon import forms def index(request): template_name = 'security/liveprotection/index.html' 
template = loader.get_template(template_name) context = RequestContext(request, { 'target': 'http://liveprotection.research.intra.nsfocus.com:8000/?username=%s&token=%s'%(request.user,request.user.token.id), }) 
return HttpResponse(template.render(context))

在liveprotection页面中,处理函数为:

def index(request):
    print request.GET
    error = None
    username = auth.verify(request)
    if username is None:
        error = "You are not logined in"
    return render_to_response("routers.html")

from datetime import datetime
import time
import client

import iso8601
def parse_isotime(timestr):
    """Parse time from ISO 8601 format."""
    try:
        return iso8601.parse_date(timestr)
    except iso8601.ParseError as e:
        raise ValueError(six.text_type(e))
    except TypeError as e:
        raise ValueError(six.text_type(e))

def valid_token(expires):
    #return datetime.now() > time.replace(tzinfo=None)
    now = time.time()
    return now < expires

def verify(request):

    token = None
    expire = None
    username = None
    verified_username = None

    if request.session.has_key("token"):
        token = request.session["token"]
    if request.session.has_key("expires"):
        expires = request.session["expires"]
    if request.session.has_key("username"):
        username = request.session["username"]

    if not token is None :
        if valid_token(expires):
            return username
    print 'session :%s' %request.session
    print 'Get :%s'%request.GET
    token =request.GET.get('token')
    username =request.GET.get('username')
    print 'username :%s'%username
    if token is None or username is None:
        return None
    headers = {
        "X-Auth-Token": token,
        }
    code, data = client.http_request(url="http://192.168.19.1:35357/v2.0"+"/tokens/"+token+"?belongsTo", headers=headers)
    print '__________________________++++++++++++++++++++++++_____________________'
    print data
    if code != "200":
        valid = 0
        print "token invalid: return code:%s" % code
    else:
        expires_dt = parse_isotime(data["access"]["token"]["expires"])
        expires_dt = expires_dt.replace(tzinfo=None)
        expires = long(time.mktime(expires_dt.timetuple()))

        verified_username = data["access"]["user"]["username"]
        if valid_token(expires):
            valid = 1
        else:
            valid = 0

    print 'valid :%d'%valid
    print 'token :%s'%token
    if valid:
        request.session["username"] = verified_username
        request.session["token"] = token
        request.session["expires"] = expires
    return verified_username

Django国际化

本文在django 1.7验证过

1 在项目settings.py中添加两行(+表示添加行):


+ LOCALE_PATHS = (BASE_DIR+ "/locale",)

MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',

2 在views.py中标记要翻译的字符串,格式为{% trans ‘字符串’%} 如

{% load i18n %}


  • {% trans 'User' %}
  • 3 建立locale目录结构

    $ mkdir locale/en -p
    $ mkdir locale/zh_CN -p
    $ python manage.py makemessages -a
    processing locale zh_CN
    processing locale en

    此时会新建./locale/{lang}/LC_MESSAGES/django.po文件,lang表示语言,中文为zh_CN。
    其中django.po文件包含了django目录中所有需要翻译的字符串。如

    #, fuzzy
    msgid ""
    msgstr ""
    "Project-Id-Version: PACKAGE VERSION\n"
    "Report-Msgid-Bugs-To: \n"
    "POT-Creation-Date: 2016-04-07 10:33+0800\n"
    "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
    "Last-Translator: FULL NAME \n"
    "Language-Team: LANGUAGE \n"
    "Language: \n"
    "MIME-Version: 1.0\n"
    "Content-Type: text/plain; charset=UTF-8\n"
    "Content-Transfer-Encoding: 8bit\n"

    #: templates/base.html:138
    msgid "User"
    msgstr ""

    4 翻译相关语言下的django.po。对于每个msgid的字符串,修改对应的翻译语句,如上例为

    #: templates/base.html:138
    msgid "User"
    msgstr ""

    5 编译:
    $ python manage.py compilemessages
    processing file django.po in /usr/src/appstoreserver/appstore/locale/zh_CN/LC_MESSAGES
    processing file django.po in /usr/src/appstoreserver/appstore/locale/en/LC_MESSAGES

    6 done

    创新沙盒:“软件定义安全”不再是实验室产物

    自从著名咨询机构Gartner在《The Impact of Software-Defined Data Centers on Information Security》 一文中提出软件定义安全(Software Defined Security,SDS)的概念后,软件定义与安全的结合已成为业界的前沿发展热点。背后的原因很直观:软件定义安全强调了安全控制平面与数据平面分离,从而在控制平面上可灵活调整应用策略,快速变更安全业务。

    在安全可被软件定义后,新的安全业务可在企业网络中可新上线,特别是在数据中心中,可实现计算、存储、网络和安全的弹性控制,实现软件定义的数据中心SDDC。正是因为这些优秀的特性,解决了企业客户长期面临的安全管理和运营“痛点”,软件定义安全自从开始就引起了学术界和工业界极大的关注。

    各大厂商都开始做相关的研究和研发工作,RSA大会一直是厂商们展现自己最新工作的舞台。如Check Point在RSA 2014大会上宣布推出软件定义防护(Software Defined Protection,SDP)革新性安全架构,可在当今日新月异的IT和威胁环境中为企业提供虚拟化的边界防护。赛门铁克也在RSA 2015提出使用软件定义网络技术对APT攻击进行取证的话题也提供了一种安全事件事后快速分析的新思路 。

    而RSA大会开到了第25个年头时,我们惊喜地发现更多的公司在展示在软件定义安全的领域的工作,特别是在体现创新的Innovation Sandbox(创新沙盒)竞赛中,10家经过专业评审的公司,居然有3家与这个话题有关,分别在不同的方面做出了开创性的工作。
    如Versa Networks公司,强调在软件定义广域网(SD-WAN)和分支(Branch)网络的环境中,通过虚拟化网络功能(VNF)技术,将各种各样异构的网络功能编程通用的组件,可快速在相应的网络中部署,大大减少了企业部署相应业务的开销,提高了整个过程的敏捷程度。

    Skyport Systems公司同样也是为企业提供高效的安全计算基础设施,但按照传统建立边界思维,攻击者在进入系统内部后就容易进一步攻击内部其他重要资源。该公司的逻辑是,所有的资源都是零信任,这样即便内部某资源被攻破,那么从该点作为跳板进一步攻击也是困难的。那么这里就涉及到软件定义的访问控制,例如如何做到“零信任”条件下各处的访问控制策略快速调整。该公司在B轮融资中获得3000万美元。

    再如Phantom Cyber公司认为在大量出现攻击的场景下,花费大量的人力去发现解决问题已不太现实。与前两个公司不同,Phantom Cyber从应用层入手,构建自动化、可编排的安全应用体系。它支持多种主流的数据分析平台,可利用较为高层的脚本实现安全运维自动化。

    当然除了这些初创公司,还有很多公司也在基于自身产品做相关的工作。如在29日的Session环节,VMWare的安全产品部门SVP Tom Corn就演示了在NSX的环境中,如何可按需定义微分段(MicroSegmentation),并对任意APP间快速添加加密处理。厂商展示区域,Catbird公司的软件定义安全架构 通过微分区(Micro-Segmentation)在虚拟环境中划分不同的区域,并通过编排将安全策略下发给多种类型的安全设备,并作用在区域级别或虚拟机级别。这些工作都体现了各家在成熟产品线通过软件定义做了很多延展性的工作。

    绿盟科技自2013年开始研究SDN和软件定义安全,研发了包括软件定义的抗DDoS、流量异常检测和Web安全等原型系统,并在2015年发布了软件定义安全的白皮书,探讨在该领域的进展。

    创新沙盒中10个产品中出现了三个能体现SDS的产品,笔者认为其背后的原因有几个:其一,作为软件定义安全的支撑技术,如VNF/NFV、SDN方案,在国外已经有一些成熟的应用,如NSX已经代替Vsphere成为VMWare成长最快的产品,Cisco的ACI方案也与很多安全厂商有合作;其二,企业的高效安全运营需求,直接催生了安全编排这些应用层面的创新;其三,也是最重要的,出于企业对降低成本的天然需求,软件定义的理念转换为实际产品的动力十足。

    RSA大会的创新沙盒一直是硅谷安全行业的风向标,今年的沙盒竞赛体现了软件定义安全确实不只是一些实验室的原型系统,一些初创企业已经开始将其作为重点,根据企业在安全运营方面出现的存在各种问题,有针对性的提出了自己的解决方案。我们有理由做出判断,软件定义安全恐怕离真正的产品化和商用已经不远了。

    UPDATE:创新沙盒的获胜者已公布,即Phantom。除去其他领域的竞争者,Phantom能在上述其他两家公司脱颖而出,笔者认为主要是因为它从客户的业务作为切入点,强调了业务层面的编排,且一开始便是开放架构设计,可支持第三方的VNF组成服务链。给客户安全运维提供高效手段,降低运维成本的同时,打消了客户被厂商锁定(Vendor Lock-in)的顾虑,从而拥有更好的商业模式(事实上,该公司在种子轮融资中获得了270万美元)。

    ssh翻墙简单说明

    1 购买vps。设置用户名user和密码pass
    2 新建ssh profile。以xshell为例,新建一个profile,输入vps的ip、port、user和pass
    3 建立隧道。在profile属性->连接->SSH->隧道->添加,类型为Dynamic,侦听端口1080。同时启用转发x11(不知是否需要),保存profile。

    vps-tunnel

    4 连接vps,默认情况下连接成功。存在两 会等时候,可能被封掉…
    5 安装浏览器扩展,如firefox的foxyproxy
    6 以foxyproxy为例,新建一个proxy,URL匹配模式为要翻墙的url,如*google*,代理服务器细节中选择手动配置代理服务器,主机为127.0.0.1,端口1080,socks代理。确认
    foxyproxy

    7 启用该proxy,即可正常翻墙

    使用cloud-init实现虚拟机信息管理

    为什么要用cloud-init

    不同种类的设备VM启动总是一件非常麻烦的事情,例如安全设备有WAF、IPS等,每种设备的网络接口、启动脚本互不一样,即便同一种设备,其主机名、网络地址等也不一样。那么如何对这些VM启动过程进行管理,并完成所有数据的配置呢?

    在这之前,我的实习生是怎么做的:将一台VM的管理口网络地址设置为192.168.2.100,然后每次启动实例之后定时访问http://192.168.2.100/somepath,当成功访问这个页面之后,使用REST接口配置该机器的IP地址为所需的新地址(如200.0.0.2);这个时候网络会短暂不同,然后在访问http://200.0.0.2/somepath,当成功访问之后,接下来配置各种值。 整个过程比较麻烦,所有的配置都需要实现REST接口,无法做到自定义启动脚本的更新;最不可接受的是,这个过程是串行的,当要启动100个VM时,只能一个VM一个VM顺序启动,否则两个VM都有同一个地址(192.168.2.100),那么网络访问就可能出现问题了。 不过受到各种Stack管理虚拟机用到cloud-init的启发,我认为我们也可以使用这套工具实现上述过程的。

    什么是cloud-init

    cloud-init(简称ci)在AWS、Openstack和Cloudstack上都有使用,所以应该算是事实上的云主机元数据管理标准。那么问题来了,google相关的文档,发现中文这方面几乎没有,Stacker你们再搞虾米呢?当然话说回来英文的资料除了官网外几乎也没有什么,我花了近一周的时间才弄明白了。

    首先要明确的是cloud-init在工作之前,VM是从DHCP服务器获取到了IP,所有DHCP发现不是cloud-init的事情。当你在Openstack中用ubuntu cloud VM启动卡在cloud-init界面时,多半是因为DHCP还没获取IP,而不是cloud-init本身的问题。那么cloud-init主要走什么呢?它向一台数据服务器获取元数据(meta data)和用户数据(user data),前者是指VM的必要信息,如主机名、网络地址等;后者是系统或用户需要的数据和文件,如用户组信息、启动脚本等。当cloud-init获取这些信息后,开始使用一些模块对数据进行处理,如新建用户、启动脚本等。

    cloud-init工作原理

    首先,数据服务器开启HTTP服务,cloud-init会向数据服务器发送请求,确认数据源模块,依次获取版本、数据类型和具体数据内容信息。

    确认数据源模块

    cloud-init会查找/etc/cloud/cloud.cfg.d/90_dpkg.cfg中的datasource_list变量,依次使用其中的数据源模块,选择一个可用的数据源模块。如我的配置文件中:datasource_list: [ Nsfocus, NoCloud, AltCloud, CloudStack, ConfigDrive, Ec2, MAAS, OVF, None ],那么ci首先调用$PYTHON_HOME/dist-packages/cloudinit/sources/DataSourceNsfocus.py中类DataSourceNsfocus的get_data函数,当且仅当访问链接DEF_MD_URL为正常时,这个数据源被认为是OK的。

    在我的实践中,CloudStack的DEF_MD_URL为DHCP的服务器ip,而Openstack和AWS则为一个常值169.254.169.254,然后在宿主机的中做一个iptables重定向,这样就到了我们的服务器监听端口8807:

    $ sudo ip netns exec ns-router iptables -L -nvx -t nat
    Chain PREROUTING (policy ACCEPT 169850 packets, 21565088 bytes)
        pkts      bytes target     prot opt in     out     source               destination         
          47     2820 REDIRECT   tcp  --  *      *       0.0.0.0/0            169.254.169.254      tcp dpt:80 redir ports 8807
    $ sudo ip netns exec ns-router iptables -L -nvx
    Chain INPUT (policy ACCEPT 97027 packets, 8636621 bytes)
        pkts      bytes target     prot opt in     out     source               destination         
           0        0 ACCEPT     tcp  --  *      *       0.0.0.0/0            127.0.0.1            tcp dpt:8807
    

    一些系统假设

    需要说明的是,虽然每个数据源访问的入口都是get_data,但每个数据服务的格式和位置是不一样的,元数据可能在/nsfocus/latest/metadata/,也可能在/latest/metadata.json,也就是说数据源模块根据自己系统的规定,访问相应的数据,并根据ci的规定,指定如何将这些数据与ci接下来的处理模块对应上。

    那么我们的数据访问地址是这样的:

    --namespace
               |
               |------version
                            |
                            |---------meta_data.json
                            |---------meta_data
                            |                  |---------public-hostname
                            |                  |---------network_config
                            |
                            |---------user_data
    

    其中,namespace为nsfocus,meta_data.json是一个json文件,里面包含所有元数据。
    其次,我们的数据服务器IP为111.0.0.2

    获得元数据

    因为获取是HTTP的形式,所以以curl为例说明下面过程:

    $ curl http://111.0.0.2/nsfocus
    1.0
    latest
    $ curl http://111.0.0.2/nsfocus/latest
    meta_data
    user_data
    meta_data.json
    $ curl http://111.0.0.2/nsfocus/latest/meta_data
    public-hostname
    local-ipv4
    network_config
    ...
    $ curl http://111.0.0.2/nsfocus/latest/meta_data/local-ipv4
    111.0.0.11
    $ curl http://111.0.0.2/nsfocus/latest/meta_data.json
    {"files": {}, "public_keys": {"controller": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCxtEfzf8I0jA7IHDRHJtDq3nTcTXAWgFYEsAV0i7WU6v8gvFr/R+DTvkVdFGgbM/qhVNWUehmPicENac6xldbL5ov6J7c8Y+UytPwJCt13IzDHXaL1BxVYUV6dpe6SYGYohNQ2KZYkG/95NzjxI1Max5DDvU8mbpEz/KyphowseburknQTkOTEigJ7CKM4G1eGVhBHKRHXbNsoPZwJnqvIHIpDcwGaj+OgVGF+o3ytH4twrwNwUFiWrUaxo9j2uRTSejYRh1eC9KOYXTnXInzV1xCVHYs/x+eIzav+2oM8hgR3xr1efgSU2sMzXrp+mJAPzHaAyAat+s7AMDu9tKrd marvel@marvel-ThinkPad-X230"}, "hostname": "waf-ba-0001", "id": "waf-ba-0001", "network_config": {"content_path": "latest/meta_data/network_config"}}
    

    这个meta_data.json是我们参考Openstack的标准,自己实现的。当获得meta_data.json后,DataSourceNsfocus解析里面的字段,填入自己的数据结构中,如放入DataSourceNsfocus的result字典中。

                if found and translator:
                    try:
                        data = translator(data)
                    except Exception as e:
                        raise BrokenMetadata("Failed to process "
                                             "path %s: %s" % (path, e)) 
                if found:
                    results[name] = data
    

    这样,如hostname就存为self.result[‘meta’][‘hostname’]。

    供其他处理模块使用的获取元数据函数

    在上一阶段,元数据的提供、获取和存储都是很自由的,那么这些数据怎么被使用,例如hostname怎么设置呢?那就需要根据ci的标准实现一些接口,如设置hostname就需要我们实现DataSourceNsfocus的get_hostname方法:

        def get_hostname(self, fqdn=False):
            return self.metadata.get("hostname")
    

    这样,其他模块如set_hostname和update_hostname就会使用这个方法正确设置主机名了。如果你想设置其他数据,可参考cloud-init数据源参考的介绍。了解还有哪些处理模块,可读一下/etc/cloud/cloud.cfg文件。

    至此,一些VM所需的常用配置已经搞定,那么如果我们想做一些流程方面的自动下发和运行该怎么做呢?则需要设置一下user_data。

    获取用户数据

    用户数据包括几类:

    • 配置文件(Cloud Config Data),类型为Content-Type: text/cloud-config,系统配置文件,如管理用户等,与/etc/cloud下的cloud.cfg最后合并配置项,更多的配置细节参考 配置样例
    • 启动任务(Upstart Job),类型为Content-Type: text/upstart-job,建立Upstart的服务
    • 用户数据脚本(User-Data Script),类型为Content-Type: text/x-shellscript,用户自定义的脚本,在启动时执行
    • 包含文件(Include File),类型为Content-Type: text/x-include-url,该文件内容是一个链接,这个链接的内容是一个文件,
    • (Cloud Boothook),类型为Content-Type: text/cloud-boothook,
    • 压缩内容( Gzip Compressed Content),
    • 处理句柄(Part Handler),类型为Content-Type: text/part-handler,内容为python脚本,根据用户数据文件的类型做相应的处理
    • 多部分存档(Mime Multi Part archive),当客户端需要下载多个上述用户数据文件时,可用Mime编码为Mime Multi Part archive一次下载

    实例

    我在data目录下面建立三个文件:

    cloud.config

    groups:
      - nsfocus: [nsfocus]
    users:
      - default
      - name: nsfocus
        lock-passwd: false 
        sudo: ALL=(ALL) NOPASSWD:ALL
    system_info:
      default_user:
        name: nsfocus
        groups: [nsfocus,sudo]
    bootcmd:
      - echo "#HOSTS\n127.0.0.1    localhost\n::1    localhost ip6-localhost\nff02::1    ip6-allnodes\nff03::1    ip6-allrouters\n#ip#    #host#" > /etc/hosts
    runcmd:
      - [echo, "RUNCMD: welcome to nsfocus-------------------------------------------"]
    final_message: "Welcome to NSFOCUS SECURITY #type#====================================="
    

    这是一个cloud-config文件,内容表示新建一个nsfocus的用户,归于nsfocus和sudo组,在启动时运行bootcmd的命令更新hosts,启动最后输出final_message。

    nsfocus-init.script

    $ cat nsfocus-init.script 
    #!/bin/bash
    echo "this is a startup script from nsfocus" 
    echo "this is a startup script from nsfocus" >> /tmp/nsfocus-init-script
    

    这是一个测试脚本,在系统启动时会被调用

    nsfocus-init.upstart

    $ cat nsfocus-init.upstart 
    description "a nsfocus upstart job"
    
    start on cloud-config
    console output
    task
    script
    echo "====BEGIN======="
    echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB"
    echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB" >> /tmp/hello
    echo "=====END========"
    end script
    

    这是一个测试访问,在系统启动时会被启动

    HTTP服务器收到/nsfocus/latest/user_data时,作如下处理:

    from email.mime.multipart import MIMEMultipart
    from email.mime.text import MIMEText
    def encode_mime(fps):
        combined_message = MIMEMultipart()
        for fn, patterns in fps:
            print fn
            (filename, format_type) = fn.split(":", 1)
            print filename
            print "---"
            with open(filename) as fh: 
                contents = fh.read()
            for (p, v) in patterns:
                contents = contents.replace(p, v)
        
            sub_message = MIMEText(contents, format_type, sys.getdefaultencoding())
            sub_message.add_header('Content-Disposition', 'attachment; filename="%s"' % (filename[filename.rindex("/")+1:]))
            combined_message.attach(sub_message)
        return str(combined_message)
    
    #main process
    #....blablabla
          if subtype == "user_data":
                if len(arr) == 0:
                    res = encode_mime([
                        ("./data/nsfocus-init.upstart:upstart-job",[]),
                        ("./data/nsfocus-init.script:x-shellscript",[]),
                        ("./data/cloud.config:cloud-config",[('#ip#', device.management_ip), ('#host#',device.id), ('#type#', device.type)])])
                    return self.gen_resp(200, res)
    

    虚拟机启动之后,服务器收到请求,返回下面的内容:

    From nobody Fri Dec 26 15:34:36 2014
    Content-Type: multipart/mixed; boundary="===============5883341837158849895=="
    MIME-Version: 1.0
    
    --===============5883341837158849895==
    MIME-Version: 1.0
    Content-Type: text/upstart-job; charset="us-ascii"
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment; filename="nsfocus-init.upstart"
    
    description "a nsfocus upstart job"
    
    start on cloud-config
    console output
    task
    script
    echo "====BEGIN======="
    echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB"
    echo "HELLO From nsfocus Upstart Job, $UPSTART_JOB" >> /tmp/hello
    echo "=====END========"
    end script
    
    --===============5883341837158849895==
    MIME-Version: 1.0
    Content-Type: text/x-shellscript; charset="us-ascii"
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment; filename="nsfocus-init.script"
    
    #!/bin/bash
    echo "this is a startup script from nsfocus" 
    echo "this is a startup script from nsfocus" >> /tmp/nsfocus-init-script
    
    --===============5883341837158849895==
    MIME-Version: 1.0
    Content-Type: text/cloud-config; charset="us-ascii"
    Content-Transfer-Encoding: 7bit
    Content-Disposition: attachment; filename="cloud.config"
    
    groups:
      - nsfocus: [nsfocus]
      - dev
    users:
      - default
      - name: nsfocus
        lock-passwd: false 
        sudo: ALL=(ALL) NOPASSWD:ALL
    system_info:
      default_user:
        name: nsfocus
        groups: [nsfocus,sudo]
    bootcmd:
      - echo "#HOSTS\n127.0.0.1    localhost\n::1    localhost ip6-localhost\nff02::1    ip6-allnodes\nff03::1    ip6-allrouters\n111.0.0.12    waf-ba-0001" > /etc/hosts
    runcmd:
      - [echo, "RUNCMD: welcome to nsfocus-------------------------------------------"]
    
    final_message: "Welcome to NSFOCUS SECURITY waf====================================="
    
    --===============5883341837158849895==--
    

    VM启动界面打印如下信息,且主机名变成了我们预定的值,说明确实获取meta-data和user-data成功,脚本运行也成功了。不过要说明一点,upstart在Ubuntu上没问题,但Debian没通过,可能当前阶段Debian的启动机制还有一些区别,所以还是使用bootcmd或启动脚本的方式启动。

    Ubuntu登陆页面

    参考文献

    cloud-init数据源参考 http://cloudinit.readthedocs.org/en/latest/topics/datasources.html
    dnsmasq参考 http://www.thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
    更多样例 https://github.com/number5/cloud-init/blob/master/doc/examples/

    Openstack中构造伪造源地址攻击

    前一篇是将防御,这一篇简单说一下openstack下如何发动伪造源ip的DoS攻击
    1 修改iptables和ebtables的设定,例如攻击vm为100.0.0.17,那么查询相应的iptables规则:

    neutron port-list |grep 17
    | 091f121f-8cd7-4a02-b1d8-53866ab25d3d |      | fa:16:3e:f7:9d:70 | {"subnet_id": "57b30eb5-e9ee-489f-85ea-77bcaa6249e5", "ip_address": "100.0.0.17"}     |
    

    找到iptables中含有091f121f-8的链:neutron-openvswi-s091f121f-8,neutron-openvswi-i091f121f-8,neutron-openvswi-o091f121f-8,和nova-instance-xxx(与该vm对应的)插入一条优先级最高的ACCEPT链

    找到ebtables中nat表中091f121f-8的链:I-tap091f121f-8c-arp-ip,I-tap091f121f-8c-ipv4-ip,插入一条优先级最高的ACCEPT链

    2 安装ruby,下载pentibox

    3 启动pentibox,选择Network tools->Net DoS Tester -> 任意一种即可发动攻击

    如果第1步中加了规则还是不通,请检查源主机和目的主机的安全组是否允许

    下面的我写的脚本,希望对大家有所帮助

    #!/bin/bash

    ip=$1
    echo $ip
    id=`neutron port-list |grep "$ip"|awk '{print $2}'`
    echo $id
    if [ -z $id ]
    then
    echo "id null"
    exit 1
    fi
    itid=`expr substr $id 1 10`
    echo $itid
    etid=`expr substr $id 1 11`
    echo $etid

    novarule=`iptables -L -nvx|grep $ip|grep nova |awk '{print $3}'`
    echo $novarule
    #clear previous rules
    iptables -D neutron-openvswi-s$itid -j ACCEPT
    iptables -D neutron-openvswi-i$itid -j ACCEPT
    iptables -D neutron-openvswi-o$itid -j ACCEPT
    iptables -D $novarule -j ACCEPT

    ebtables -t nat -I I-tap$etid-arp-ip -j ACCEPT
    ebtables -t nat -I I-tap$etid-ipv4-ip -j ACCEPT

    #create rules
    iptables -I neutron-openvswi-s$itid -j ACCEPT
    iptables -I neutron-openvswi-i$itid -j ACCEPT
    iptables -I neutron-openvswi-o$itid -j ACCEPT
    iptables -I $novarule -j ACCEPT

    ebtables -t nat -I I-tap$etid-arp-ip -j ACCEPT
    ebtables -t nat -I I-tap$etid-ipv4-ip -j ACCEPT

    Floodlight的流量控制

    floodlight在bigswitch退出opendaylight后算是活过来了,新的版本在负载均衡中加入了一些机制,可以参见net.floodlight.loadbalancer

    不过我关心的是FL对流量的控制,特别是判断packet_in速率的机制,这个在前几个月的Floodlight是没有的。

    现在FL的处理机制是在收到packet_in数据包后,首先判断是否超过阈值,在inputThrottled方法中,计算每1000个数据包所花的时间,得出速率。如果速率超过packetInRateThresholdHigh阈值,则进入监控限制流量模式(enablePacketInThrottle)

    在该模式下,依次检查packet_in数据包的src mac(频率为每50个packet_in一次)和in port(频率为每100个packet_in一次),如果近期(1s内)有该类数据包则将其禁止。

    这种方法的初衷应该是防止controller的负载过高,例如禁止mac是为了防止host发送过多包,禁止in port是为了禁止sw转发过多包。但后者有可能出现在恶意vm发动DDoS攻击(一个in port,但无数src mac),客观上却使控制器免受DDoS攻击,自然也让域内的VM被DDoS。

    贴一下主干代码:

     
    /**
         * Determine if this message should be dropped.
         *
         * We compute the current rate by taking a timestamp every 100 messages.
         * Could change to a more complex scheme if more accuracy is needed.
         *
         * Enable throttling if the rate goes above packetInRateThresholdHigh
         * Disable throttling when the rate drops below packetInRateThresholdLow
         *
         * While throttling is enabled, we do the following:
         *  - Remove duplicate packetIn's mapped to the same OFMatch
         *  - After filtering, if packetIn rate per host (mac) is above
         *    packetInRatePerMacThreshold, push a flow mod to block mac on port
         *  - After filtering, if packetIn rate per port is above
         *    packetInRatePerPortThreshold, push a flow mod to block port
         *  - Allow blocking flow mods have a hard timeout and expires automatically
         *
         * TODO: keep a history of all events related in input throttling
         *
         * @param ofm
         * @return
         */
        @Override
        public boolean inputThrottled(OFMessage ofm) {
            if (ofm.getType() != OFType.PACKET_IN) {
                return false;
            }
            ctrSwitchPktin.updateCounterNoFlush();
            // Compute current packet in rate
            messageCount++;
            if (messageCount % 1000 == 0) {
                long now = System.currentTimeMillis();
                if (now != lastMessageTime) {
                    currentRate = (int) (1000000 / (now - lastMessageTime));
                    lastMessageTime = now;
                } else {
                    currentRate = Integer.MAX_VALUE;
                }
            }
            if (!packetInThrottleEnabled) {
                if (currentRate <= packetInRateThresholdHigh) {
                    return false; // most common case
                }
                enablePacketInThrottle();
            } else if (currentRate < packetInRateThresholdLow) {
                disablePacketInThrottle();
                return false;
            }
    
            // Now we are in the slow path where we need to do filtering
            // First filter based on OFMatch
            OFPacketIn pin = (OFPacketIn)ofm;
            OFMatch match = new OFMatch();
            match.loadFromPacket(pin.getPacketData(), pin.getInPort());
            if (ofMatchCache.update(match)) {
               ctrSwitchPktinDrops.updateCounterNoFlush();
                return true;
            }
    
            // We have packet in with a distinct flow, check per mac rate
            messageCountUniqueOFMatch++;
            if ((messageCountUniqueOFMatch % packetInRatePerMacThreshold) == 1) {
                checkPerSourceMacRate(pin);
            }
    
            // Check per port rate
            if ((messageCountUniqueOFMatch % packetInRatePerPortThreshold) == 1) {
                checkPerPortRate(pin);
            }
            return false;
        }
    

    让我们计算一下这种机制的效率,由于阈值packetInRateThresholdHigh设置的是1000,那么now-lastMessageTime应为1000ms=1s;考虑到controller每1000个packet_in进行判断,也就是说入包的极限是每秒1000个packet_in数据包,即1000条新流。这个在小范围内是没问题的,但Nick McKeown等人在经典文章“OpenFlow: Enabling Innovation in Campus Networks”中提到Preliminary results suggested that an Ethane controller based on a low-cost desktop PC could process over 10,000 new flows per second — enough for a large college campus. 显然每秒1k条流的处理规模还是太小,那么单单增加阈值也不能解决问题:因为controller并不知道那些流是正常哪些是异常的,当前场景是小规模的还是大规模的,特别是在一些突然场景下。所以简单的流量控制在一定程度上是有效的,但专门的安全设备也是必需的。

    Floodlight路由机制解析

    路由部分是floodlight最核心的机制,这两天仔细读了一下floodlight这部分的代码,总算有了大体上的了解,与各位分享。

    本文中的floodlight(FL)与控制器/网络控制器(NC, nework controller ) 等术语等同,交换机(SW)默认为openflow-enabled switch,不再赘述。

    首先谈一下SDN控制器的路由原理:当交换机收到一个不能被当前流表各条流匹配的数据包时,会把这个数据包以openflow的格式(PACKET_IN)发送给控制器。控制器经过路由决策后,同样以openflow的格式(PACKET_OUT)的方式将该数据包的下一跳信息回给该交换机。

    如果学过最短路径计算的同学一定知道,两点之间的最短路径计算首先要知道任意两点之间是否相连,如果两者间有link相连还需知道其长度。所以总体而言,FL路由包括两部分:拓扑更新和路由计算,前者是定时事先完成的,形成一个全局的拓扑结构,后者是收到数据包后运行时完成的,根据拓扑生成路由策略。

    首先看拓扑更新。SDN环境中拓扑是集中控制的,故FL需要了解全局的拓扑环境,所以FL在链路更新时自动更新拓扑。

      1 链路更新

    当交换机加入SDN环境后,控制器通过LLDP协议定时地获得该交换机与其他设备连接的link信息,然后添加或更新这些link(LinkDiscoveryManager.addOrUpdateLink()),最后将这些link更新的事件添加到updates队列中(LinkDiscoveryManager.handleLldp())。

      2 拓扑更新

    计算路由的关键在于路径更新和路由计算,该过程为:
    2.1 启动 首先拓扑管理启动(TopologyManager.startUp())时新起一个线程UpdateTopologyWorker,间隔500ms重复运行 。
    2.2 更新拓扑 如果之前在第一步中有一些链路、交换机等更新,那么LDUpdates队列中会有元素,那么依次取出这些链路更新的元素(TopologyManager.updateTopology()/TopologyManager.applyUpdates()),判断其类型(如链路更新/删除、端口增加/删除),进行响应处理。
    以链路更新为例,调用TopologyManager.addOrUpdateLink()方法,判断链路类型(多跳、单跳或隧道),再把link添加到相应交换机的端口中,如果是多跳的则再删除可能的单跳link
    2.3 计算拓扑路径 每次新建一个实例(TopologyManager.createNewInstance()),初始化过程中计算拓扑路径(TopologyInstance.compute()),具体地:
    2.3.1 归类 将现有所有相连的link置于同一个簇中,从而形成多个簇结构(identifyOpenflowDomains())
    2.3.2 建簇 遍历所有link(addLinksToOpenflowDomains()),如果link连接的是同一个簇,则将其加入该簇,所以该方法统计了所有簇内的link。FL这么做也是减少路由计算的开销,不然若干个大二层网络中共有n个节点,就需要n平方的存储开销,计算效率也会下降,如果将不相连的节点分开就减少了上述开销。
    2.3.3 遍历所有簇 对于每个簇内sw,形成一个BroadcastTree结构(包括两个属性:cost记录到其他簇内sw的路径长度,links记录其他sw的link),最终将信息保存到destinationRootedTrees中(caculateShortestPathTreeInClusters ()和calculateBroadcastNodePortsInClusters()),核心思想就是使用dijkstra计算簇内任意两点的最短路径,保存到该点的cost和下一跳。
    2.4 定义路径计算模式 这样,FL就获得了从一个点出发所有相连的所有路径和点。那么只要给定一个源SW节点和一个目的SW节点,那就能知道这两者间的最短路径长度和。早在2.3开始TopologyInstance在定期新建实例时,就定义了根据拓扑计算路径的方法:

     
    pathcache = CacheBuilder.newBuilder().concurrencyLevel(4)
                        .maximumSize(1000L)
                        .build(
                                new CacheLoader() {
                                    public Route load(RouteId rid) {
                                        return pathCacheLoader.load(rid);
                                    }
                                });

    这里的pathcache是一个类似hash表的结构,每当被调用get时执行pathCacheLoader.load(rid),所以这里没有真正计算路由路径,只是一个注册回调。具体运行时在后面3.6中的getRoute方法中被调用:

     
                result = pathcache.get(id);
    

    在TopologyInstance.buildRoute方法实现该路由rid的计算:先确定目的sw,因为2.3.2中以获得源目的sw的最短路径,然后根据nexthoplinks迭代查找该路径上的所有sw,最终形成一个path。
    虽然2.4中没有最终计算节点间的路径,但是2.3中使用dijkstra计算了任意两点间的距离,基本上已经完成了90%的路由计算功能,在大二层网络中这也是不小的开销。

      3 路由计算

    当完成拓扑计算后,FL在运行时可计算输入的数据包应走的路由。这里还需说明一下,FL的某些模块监听PACKET_IN数据包,Controller收到这个数据包后,会逐个通知这些模块,调用其receive方法:

     
                        for (IOFMessageListener listener : listeners) {
                            pktinProcTime.recordStartTimeComp(listener);
                            cmd = listener.receive(sw, m, bc);
                            pktinProcTime.recordEndTimeComp(listener);
    
                            if (Command.STOP.equals(cmd)) {
                                break;
                            }
                        }
    

    更详细的模块加载和监听机制可参考我之前写的这篇文章http://blog.marvelplanet.tk/?p=424
    那么回到路由计算这部分来,Controller会依次调用以下模块:
    3.1 LinkDiscoveryManager链路处理模块,如果数据包是LLDP处理该消息,如步骤1中在此处会有处理;如果是正常的数据包则略过,所以这里忽略
    3.2 TopologyManager拓扑管理模块,查看源端口在TopologyInstance是否允许转发
    3.3 DeviceManager设备模块,通过源目SW的id和port相连的device找到源设备和目的设备,如找不到则放弃,否则暂存入一个上下文结构context
    3.4 Firewall防火墙模块,判断该数据包的源目的主机是否可以通信,如不在规则表中则继续
    3.5 LoadBalancer负载均衡模块,如是ARP数据包则查看是否在vipIPToId中,如不在则继续。这里关系不大,忽略。
    3.6 ForwardBase路由模块,首先在上下文context查找之前模块是否已有路由策略,如无则检查是否数据包是广播的,如是则Flood。否则调用doForwardFlow,我们主要看这个方法,具体的,从上下文中获取3.3中解析的源目的主机信息,如果不在同一个iland上(我的理解是这两台主机在FL的知识库中没有路径,也许中间有别的传统方法相连,也许根本不相连),则Flood;如果在同一个端口,那就是同一台主机,则放弃;否则调用routingEngine.getRoute获得路由。这个才是最重要的,也是2.4中没有完成的最终部分,这里使用了TopologyInstance的getRoute方法,使用2.4的方法计算出路由路径,然后调用将路由所对应的路径推送给交换机,如果是多跳路径,则将路由策略推送给该路径上的所有交换机(ForwardBase.pushRoute())。

    最后给出一个场景吧,假定h1和h2是主机,s1、s2和s3是交换机,拓扑是h1-s1-s2-s3-h2。当h1 ping h2,首先h1发送ARP包,由于该数据包是广播的,那么最终ForwardBase会执行doFlood,所有交换机都会广播这个数据包,最后h2收到s3广播的的ARP请求后,向h1发送ARP响应。h1收到ARP响应后发送ping请求,该包在交换机以PACKET_IN的形式发送给floodlight,此时floodlight知道h1所在的交换机s3,所以调用doForwardFlow,计算出
    s1到s3的路径s1->s2,s2->s3,然后将OFFlowMod命令发送给s1、s2和s3。最终数据包通过s1、s2和s3直接被发送到h2,搞定。

    p.s.1 Floodlight在计算拓扑calculateShortestPathTreeInClusters时,首先会将所有的路径置为失效,然后在计算所有两点间距离,如果每次(间隔为500ms)存在一条链路变动,那么就需要重做所有计算,在大二层网络中会不会出现性能瓶颈?
    p.s.2 Floodlight的计算出路由后,下发的流只包括源目的主机的mac,流的其他字段都是通配符,这么做的逻辑应该是“我只做路由和转发功能,别的不用控制”。这样会给北向的APP造成很大的麻烦,例如在做DDoS的防御时,需要检查每条流的目的端口,如果流的dst_transport_addr是*的话,显然无法检测攻击,甚至连FL自带的Firewall都力不从心。
    p.s. 一般而言,交换机的端口与与其他端口相连,有两个对称的link。但TopologyManager中有一类特殊的端口broadcastDomainPorts,这类是广播端口,不是一般的端口,没有对称的link。

    raspberry wlan0 ifconfig fail

    树莓派的wifi网卡总是上不了网,搞了很久都找不到解决办法

    提示错误是当运行ifconfig wlan0分配ip时出错:

    [20014.112395] wlan0: deauthenticating from xxx by local choice (reason=3)

    但是lsusb时发现设备还在,应该不是电源的问题,百思不得其解

    google了很久终于发现当前居然有wpa的进程(我的wlan0是无密码的)
    pi@raspberrypi ~ $ ps aux|grep wpa
    root 2071 0.0 0.3 5952 860 ? Ss 23:47 0:00 /sbin/wpa_supplicant -s -B -P /var/run/wpa_supplicant.wlan0.pid -i wlan0 -W -D nl80211,wext -c /etc/wpa_supplicant/wpa_supplicant.conf
    root 2153 0.0 0.1 2276 380 ? Ss 23:47 0:00 /sbin/wpa_cli -B -P /var/run/wpa_action.wlan0.pid -i wlan0 -p /var/run/wpa_supplicant -a /sbin/wpa_action
    pi 2726 0.0 0.3 3544 868 pts/1 S+ 23:51 0:00 grep –color=auto wpa

    所以解决办法就是杀wpa
    pi@raspberrypi ~ $ sudo killall wpa_supplicant

    发现重新连接ap就没问题了
    [ 322.419859] wlan0: RX AssocResp from 4c:e6:76:a3:d5:82 (capab=0x401 status=0 aid=4)
    [ 322.435926] wlan0: associated

    谨慎怀疑是因为 /etc/network/interface中出现的一段wpa的问题:

    allow-hotplug wlan0
    iface wlan0 inet manual
    wpa-roam /etc/wpa_supplicant/wpa_supplicant.conf
    那么解决办法就是将其注释掉即可。。。

    iface default inet dhcp

    ubuntu 12.04上安装puppet

    发现官方的说明问题很多,不过全做一个参考吧

    puppet的agent端安装没有问题,但是master涉及到postgresql数据库的配置,简单运行puppet-enterprise-installer会出错

    我在安装目录的answers目录下发现很多配置文件,其中full_suite_existing_postgres.sample最符合master的需求,于是将其复制到安装的根目录,修改其中的一些配置项,主要是将其中数据库的host设置为master的域名,对应的数据库设置用户名密码。

    然后配置postgreSQL数据库,可以参考这篇文章,设置好postgres密码后,进入
    nsfocus@controller:~$ psql -U postgres -W -h 192.168.19.1
    Password for user postgres:
    psql (9.1.9)
    SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)
    Type "help" for help.

    postgres=# CREATE USER "pe-puppetdb" PASSWORD 'onemorestrongpass';
    CREATE ROLE
    postgres=# CREATE DATABASE "pe-puppetdb" OWNER "pe-puppetdb" ENCODING 'utf8' LC_CTYPE 'en_US.utf8' LC_COLLATE 'en_US.utf8' template template0;
    CREATE DATABASE
    postgres=# CREATE DATABASE "console_auth" OWNER "auth_user" ENCODING 'utf8' LC_CTYPE 'en_US.utf8' LC_COLLATE 'en_US.utf8' template template0;
    CREATE DATABASE

    然后运行./puppet-enterprise-installer -a full_suite_existing_postgres.sample

    如果还有错误,可以查看当前目录下的install_log.lastrun.[hostname]文件,看看最后FATAL的信息