天天看點

SDN開發實戰(1)-透明HTTP代理[Openflow+floodlight]1. SDN2. 透明HTTP代理

1. SDN

軟體定義網絡(Software Defined Network, SDN),是Emulex網絡一種新型網絡創新架構,是網絡虛拟化的一種實作方式,其核心技術OpenFlow通過将網絡裝置控制面與資料面分離開來,進而實作了網絡流量的靈活控制,使網絡作為管道變得更加智能

知乎解釋:https://www.zhihu.com/question/20279620

Openflow+floodlight

Openflow是SDN實作的重要的一個技術手段,由斯坦福高性能網絡實驗室開發,如今已形成了Openflow論壇。在Openflow架構中(如下圖),每個主機(host)連接配接着Openflow交換機(Openflow Switch),交換機中的流量表(flow table)由Openflow的控制器控制,通過監視并改變每個Switch中的流量表,各個主機之間的通訊能夠很靈活的被Controller控制,而Controller可通過程式設計實作,這樣就從軟體層面上直接控制了網絡裝置中的資料轉發,進而定義整個網絡

SDN開發實戰(1)-透明HTTP代理[Openflow+floodlight]1. SDN2. 透明HTTP代理

Openflow架構中的控制器有很多開源庫可以實作:

  • Java: Beacon, Floodlight
  • Python: POX, Ryu, NOX (Deprecated)
  • Ruby: Trema

此部落格使用基于Java的Floodlight庫開發控制器,用Mininet來模拟虛拟的主機和Openflow Switch,Mininet是輕量級的軟體定義網絡系統平台,同時提供了對 OpenFlow 協定的支援,下面給出幾個有用的傳送門:

  • Openflow入門教程
  • Mininet安裝
  • Floodlight安裝和入門教程

2. 透明HTTP代理

代理伺服器的功能就是代理使用者通路網絡資訊,代理分正向代理、反向代理、透明代理等,透明代理就是指使用者并不知道代理伺服器的存在,代理伺服器會修改使用者發送的request fields(封包),并會傳送真實IP。關于代理伺服器請看這裡

這裡,我們為了學習SDN開發,做出的透明HTTP代理應用,并不是真正意義上的透明代理,因為我們并不是注重在代理伺服器本身,而是研究如何通過openflow+floodlight實作控制整個網絡的轉發,模拟的網絡功能可以實作透明HTTP代理功能

SDN開發實戰(1)-透明HTTP代理[Openflow+floodlight]1. SDN2. 透明HTTP代理

我們将建立如上圖一樣的拓撲網絡,具有三個虛拟交換機s1、s2、s3(使用的是Open vSwitch,而非标準的Openflow Switch),四個虛拟主機h1、h2、h3、prox,以及一個控制器c0:

  • 其中,prox為具有代理伺服器的虛拟主機,10.0.0.x代表每個主機的IP位址
  • hx-eth0代表主機hx的網卡擴充卡,sx-ethx則代表交換機sx的第x個網卡sx-ethx
  • 控制器c0由Floodlight實作,虛拟交換機和主機由Mininet模拟,之間使用TCP通訊,端口6653

根據上面的描述,我們可以看出隻有h1和h2連接配接着同一個交換機,prox和h3分别連接配接各自的交換機s2和s3,是以我們現在定義各個主機的角色和整個網絡的轉發政策(Policy)

  1. h1和h2代表兩個使用者的主機,能通過同一個交換機直接互聯
  2. h3代表網站伺服器,裡面有h1和h2想要通路的網絡資源
  3. h1和h2并不能直接通路h3,需要通過prox代理伺服器轉發package
  4. h1和h2并不知道代理伺服器prox的存在,而且無法ping通prox
  5. 所有的連接配接為雙向有效(bi-bridge)

3. 代碼實作[Github]

3.1 Floodlight

我們使用的是最新版的v1.3版本的Floodlight (master), 請先參考Floodlight官方教程-How to Write a Module,編寫一個自定義的控制器其實也就是增加一個子產品,一個繼承了IOFMessageListener和IFloodlightModule接口的java類,是以需要覆寫接口中所有的方法。

1.我們建立一個TransHttpProxyDemo類如下:

package net.floodlightcontroller.transHttpProxy;

import java.util.Collection;
import java.util.Map;

import org.projectfloodlight.openflow.protocol.OFMessage;
import org.projectfloodlight.openflow.protocol.OFType;
import org.projectfloodlight.openflow.types.MacAddress;

import net.floodlightcontroller.core.FloodlightContext;
import net.floodlightcontroller.core.IOFMessageListener;
import net.floodlightcontroller.core.IOFSwitch;
import net.floodlightcontroller.core.module.FloodlightModuleContext;
import net.floodlightcontroller.core.module.FloodlightModuleException;
import net.floodlightcontroller.core.module.IFloodlightModule;
import net.floodlightcontroller.core.module.IFloodlightService;

public class TransHttpProxyDemo implements IOFMessageListener, IFloodlightModule {

    @Override
    public String getName() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public boolean isCallbackOrderingPrereq(OFType type, String name) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public boolean isCallbackOrderingPostreq(OFType type, String name) {
        // TODO Auto-generated method stub
        return false;
    }

    @Override
    public Collection<Class<? extends IFloodlightService>> getModuleServices() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Map<Class<? extends IFloodlightService>, IFloodlightService> getServiceImpls() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public Collection<Class<? extends IFloodlightService>> getModuleDependencies() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void init(FloodlightModuleContext context)
            throws FloodlightModuleException {
        // TODO Auto-generated method stub

    }

    @Override
    public void startUp(FloodlightModuleContext context) {
        // TODO Auto-generated method stub

    }

    @Override
    public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) {
        // TODO Auto-generated method stub
        return null;
    }
}
           

2.然後我們事先定義四個主機的Mac位址以及一個調試主機Magic的Mac位址(後面會講),然後是主機連接配接的模式的枚舉(直接連接配接、通過代理、無法連接配接),然後是一些我們即将用到的變量,相關類的包請自行用eclipse導入:

protected static final MacAddress MAGIC = MacAddress.of("00:11:00:11:00:11");
protected static final MacAddress H1 = MacAddress.of("00:00:00:00:00:01");
protected static final MacAddress H2 = MacAddress.of("00:00:00:00:00:02");
protected static final MacAddress H3 = MacAddress.of("00:00:00:00:00:03");
protected static final MacAddress PX = MacAddress.of("00:00:00:00:00:04");

protected enum RouteMode {
    ROUTE_DIRECT, ROUTE_PROXY, ROUTE_DROP,
};

protected Logger log;
protected IRoutingService routingEngine;
protected IOFSwitchService switchEngine;
protected IFloodlightProviderService floodlightProvider;
protected Map<MacAddress, SwitchPort> mac_to_switchport;
           

3.在getModuleDependencies方法中告訴Floodlight這個類将依賴IFloodlightProviderService類:

@Override // IFloodlightModule
public Collection<Class<? extends IFloodlightService>> getModuleDependencies() {
    Collection<Class<? extends IFloodlightService>> l = new ArrayList<Class<? extends IFloodlightService>>();
    l.add(IFloodlightProviderService.class);
    return l;
}
           

4.初始化定義的變量,通過context.getServiceImpl()方法從context中獲得需要的類并指派給這些全局變量

@Override // IFloodlightModule
public void init(FloodlightModuleContext context) throws FloodlightModuleException {
    floodlightProvider = context.getServiceImpl(IFloodlightProviderService.class);
    routingEngine = context.getServiceImpl(IRoutingService.class);
    switchEngine = context.getServiceImpl(IOFSwitchService.class);
    log = LoggerFactory.getLogger("TransHttpProxyDemo");
    mac_to_switchport = new HashMap<MacAddress, SwitchPort>();
}
           

5.為了能獲得Switch中實際傳輸的,啟動階段使floodlightProvider監聽傳入控制器的PACKET_IN包,當switch收到一條需轉發的以太網幀(Ethernet)但是卻無法比對目前的轉發表時,會将建構一種PACKET_IN類型的Openflow包交給控制器請求處理,這時,我們能通過調用floodlightProvider變量中的方法來擷取switch中這個實際的以太網幀

@Override // IFloodlightModule
public void startUp(FloodlightModuleContext context) {
    floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this);
}
           

6.下面我們編寫receive方法來對控制器收到的每個PACKET_IN包進行處理:

@Override
public net.floodlightcontroller.core.IListener.Command receive(IOFSwitch sw, OFMessage msg,
        FloodlightContext cntx) {

    // 首先确認收到msg的類型為PACKET_IN
    if (msg.getType() != OFType.PACKET_IN) {
        return Command.CONTINUE;
    }

    // 取出以太幀eth并确認以太幀中的載荷為Ipv4類型的資料報
    OFPacketIn pki = (OFPacketIn) msg;
    Ethernet eth = IFloodlightProviderService.bcStore.get(cntx, IFloodlightProviderService.CONTEXT_PI_PAYLOAD);

    IPacket p = eth.getPayload();
    if (!(p instanceof IPv4)) {
        return Command.CONTINUE;
    }

    // 獲得這個eth傳入switch時的網絡擴充卡端口,注意由于Floodlight版本不同是以獲得方式有所不同
    OFPort in_port = (pki.getVersion().compareTo(OFVersion.OF_12) < ) ? pki.getInPort()
            : pki.getMatch().get(MatchField.IN_PORT);

    // 獲得這個eth在swith中緩存隊列中的id
    OFBufferId bufid = pki.getBufferId();

    // 獲得這個eth的源Mac位址和目的Mac位址
    MacAddress dl_src = eth.getSourceMACAddress();
    MacAddress dl_dst = eth.getDestinationMACAddress();

    // 如果目的位址比對調試Mac位址,則發送丢包指令,并儲存這個eth的源MAC位址和SwitchPort

    if (dl_dst.equals(MAGIC)) {
        SwitchPort tmp = new SwitchPort(sw.getId(), in_port);
        mac_to_switchport.put(dl_src, tmp);
        send_drop_rule(tmp, bufid, dl_src, dl_dst);
        return Command.STOP;
    }

    // 調用process_pkt方法處理
    process_pkt(sw, in_port, bufid, dl_src, dl_dst);
    return Command.STOP;
}
           

Note: 引入調試Mac位址的目的是,将所有嘗試向調試Mac位址發送包的主機的Mac位址和與之連接配接的switch及端口儲存在mac_to_switchport,這樣能夠事先掌握所有主機與和與之連接配接的switch資訊,以便後面建立各個host之間的轉發管道

7.對于繼續處理的以太幀eth,編寫process_pkt方法進一步處理

private void process_pkt(IOFSwitch sw, OFPort in_port, OFBufferId bufid, MacAddress dl_src, MacAddress dl_dst) {
RouteMode rm;
    SwitchPort sp_src, sp_dst, sp_prx;

    log.debug("packet_in: " + sw.getId() + ":" + in_port + " " + dl_src + " --> " + dl_dst);

    // 嘗試從mac_to_switchport中取出此eth的源位址、目标位址、代理位址對應的SwitchPort
    sp_src = mac_to_switchport.get(dl_src);
    sp_dst = mac_to_switchport.get(dl_dst);
    sp_prx = mac_to_switchport.get(PX);

    if (sp_src == null) {
        log.error("unknown source port");
        return;
    } else if (sp_dst == null) {
        log.error("unknown dest port");
        return;
    } else if (sp_prx == null) {
        log.error("unknown proxy port");
        return;
    }

    // 判斷源位址和目的位址之間的路由模式
    rm = getCommMode(dl_src, dl_dst);
    log.info("packet_in: routing mode: " + rm);

    // 丢包模式
    if (rm == RouteMode.ROUTE_DROP) {
        send_drop_rule(sp_src, bufid, dl_src, dl_dst);

    // 代理模式
    } else if (rm == RouteMode.ROUTE_PROXY) {
        create_route(sp_src, sp_prx, dl_src, dl_dst, OFBufferId.NO_BUFFER);
        create_route(sp_prx, sp_dst, dl_src, dl_dst, OFBufferId.NO_BUFFER);
        create_route(sp_dst, sp_prx, dl_dst, dl_src, OFBufferId.NO_BUFFER);
        create_route(sp_prx, sp_src, dl_dst, dl_src, bufid);

    // 直連模式
    } else { 
        create_route(sp_src, sp_dst, dl_src, dl_dst, OFBufferId.NO_BUFFER);
        create_route(sp_dst, sp_src, dl_dst, dl_src, bufid);
    }
}
           

8.通過package的源Mac位址和目的Mac位址來判斷package的轉發模式,這裡我們手動設定四種h1、h2、h3、prox之間的路由模式

private RouteMode getCommMode(MacAddress src, MacAddress dst) {

    // H1 <--> H2 : Direct
    if ((src.equals(H1) && dst.equals(H2)) || (src.equals(H2) && dst.equals(H1))) {
        log.info("pair: H1 <--> H2 : Direct");
        return RouteMode.ROUTE_DIRECT;
    }

    // H1 <--> PX : Drop
    else if ((src.equals(H1) && dst.equals(PX)) || (src.equals(PX) && dst.equals(H1))) {
        log.info("pair: H1 <--> PX : Drop");
        return RouteMode.ROUTE_DROP;
    }

    // H1 <--> H3 : Proxy
    else if ((src.equals(H1) && dst.equals(H3)) || (src.equals(H3) && dst.equals(H1))) {
        log.info("pair: H1 <--> H3 : Proxy");
        return RouteMode.ROUTE_PROXY;
    }

    // H2 <--> PX : Drop
    else if ((src.equals(H2) && dst.equals(PX)) || (src.equals(PX) && dst.equals(H2))) {
        log.info("pair: H2 <--> PX : Drop");
        return RouteMode.ROUTE_DROP;
    }

    // H2 <--> H3 : Proxy
    else if ((src.equals(H2) && dst.equals(H3)) || (src.equals(H3) && dst.equals(H2))) {
        log.info("pair: H2 <--> H3 : Proxy");
        return RouteMode.ROUTE_PROXY;
    }

    // H3 <--> PX : Drop
    else if ((src.equals(H3) && dst.equals(PX)) || (src.equals(PX) && dst.equals(H3))) {
        log.info("pair: H3 <--> PX : Drop");
        return RouteMode.ROUTE_DROP;
    } else {
        return RouteMode.ROUTE_DROP;
    }
}
           

9.直連模式需要在源主機和目的主機之間建構一條通道,而代理模式中個的這條通道則需要經過代理主機prox,由prox來中轉他們之間的資料報,但這兩種模式都需要找到這條通道的路徑,是以我們要編寫create_route方法

private void create_route(SwitchPort sp_src, SwitchPort sp_dst, MacAddress dl_src, MacAddress dl_dst,
        OFBufferId bufid) {

    // 通過routingEngine解析出路徑,由Floodlight實作
    Path route = routingEngine.getPath(sp_src.getNodeId(), sp_src.getPortId(), sp_dst.getNodeId(),
            sp_dst.getPortId());

    log.info("Route: " + route);

    // 路徑的表示為SwitchPort對象的List
    List<NodePortTuple> switchPortList = route.getPath();

    // 用write_flow方法為路徑中的每個SwitchPort建立FlowMod
    for (int indx = switchPortList.size() - ; indx > ; indx -= ) {
        DatapathId dpid = switchPortList.get(indx).getNodeId();
        OFPort out_port = switchPortList.get(indx).getPortId();
        OFPort in_port = switchPortList.get(indx - ).getPortId();
        write_flow(dpid, in_port, dl_src, dl_dst, out_port, (indx == ) ? bufid : OFBufferId.NO_BUFFER);
    }
}
           

以及編輯丢包模式中的send_drop_rule方法:

private void send_drop_rule(SwitchPort sw1, OFBufferId bufid, MacAddress src, MacAddress dst) {
    write_flow(sw1.getNodeId(), sw1.getPortId(), src, dst, null, bufid);
}
           

10.在Openflow中,交換機switch通過自身的flow tables來處理未來到達的package,而這種rules是能夠通過FlowMod對象修改(增删等),是以編輯最底層的write_flow方法

private void write_flow(DatapathId dpid, OFPort in_port, MacAddress dl_src, MacAddress dl_dst, OFPort out_port,
        OFBufferId bufid) {

    // 通過switchEngine獲得switch對象,dpid為switch的id
    IOFSwitch sw = switchEngine.getSwitch(dpid);

    // 獲得OF工廠
    OFFactory myFactory = sw.getOFFactory();

    // 構造OFActions,如果設定out_port為空則為丢包模式
    List<OFAction> actionList = new ArrayList<OFAction>();
    OFActions actions = myFactory.actions();
    if (out_port != null) {
        OFActionOutput output = actions.buildOutput().setPort(out_port).setMaxLen().build();
        actionList.add(output);
    } else {
        log.info("droping.....");
    }

    // 構造Match,用來比對package的源Mac位址和目的Mac位址以及switch端口
    Match match = myFactory.buildMatch().setExact(MatchField.ETH_SRC, dl_src).setExact(MatchField.ETH_DST, dl_dst)
            .setExact(MatchField.IN_PORT, in_port).setExact(MatchField.ETH_TYPE, EthType.IPv4).build();

    // 構造OFFlowAdd,設定rule的優先級為1
    // 若優先級為0,即使比對的package也不會按照rule正确轉發,而是再次傳遞控制器
    OFFlowAdd flowAdd = myFactory.buildFlowAdd().setBufferId(bufid).setMatch(match).setIdleTimeout()
            .setPriority().setActions(actionList).build();

    log.info("writing flowmod: " + flowAdd);

    sw.write(flowAdd);
}
           

3.2 Mininet和代理伺服器配置

詳見下一個教程SDN開發實戰(2)-透明HTTP代理[Openflow+floodlight]