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可通過程式設計實作,這樣就從軟體層面上直接控制了網絡裝置中的資料轉發,進而定義整個網絡
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代理功能
我們将建立如上圖一樣的拓撲網絡,具有三個虛拟交換機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)
- h1和h2代表兩個使用者的主機,能通過同一個交換機直接互聯
- h3代表網站伺服器,裡面有h1和h2想要通路的網絡資源
- h1和h2并不能直接通路h3,需要通過prox代理伺服器轉發package
- h1和h2并不知道代理伺服器prox的存在,而且無法ping通prox
- 所有的連接配接為雙向有效(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]