blob: deeaa360582553e8722dd9735ba5d99f5f853609 [file] [log] [blame]
Yi Tseng51301292017-07-28 13:02:59 -07001/*
2 * Copyright 2017-present Open Networking Foundation
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 *
16 */
17
18package org.onosproject.dhcprelay;
19
Yi Tsenge72fbb52017-08-02 15:03:31 -070020import com.google.common.base.MoreObjects;
Yi Tseng51301292017-07-28 13:02:59 -070021import com.google.common.collect.Sets;
Yi Tsenge72fbb52017-08-02 15:03:31 -070022import org.apache.felix.scr.annotations.Activate;
Yi Tseng51301292017-07-28 13:02:59 -070023import org.apache.felix.scr.annotations.Component;
Yi Tsenge72fbb52017-08-02 15:03:31 -070024import org.apache.felix.scr.annotations.Deactivate;
Yi Tseng51301292017-07-28 13:02:59 -070025import org.apache.felix.scr.annotations.Property;
26import org.apache.felix.scr.annotations.Reference;
27import org.apache.felix.scr.annotations.ReferenceCardinality;
28import org.apache.felix.scr.annotations.Service;
29import org.onlab.packet.BasePacket;
30import org.onlab.packet.DHCP;
31import org.onlab.packet.Ethernet;
32import org.onlab.packet.IPv4;
33import org.onlab.packet.Ip4Address;
34import org.onlab.packet.IpAddress;
35import org.onlab.packet.MacAddress;
36import org.onlab.packet.UDP;
37import org.onlab.packet.VlanId;
38import org.onlab.packet.dhcp.CircuitId;
39import org.onlab.packet.dhcp.DhcpOption;
40import org.onlab.packet.dhcp.DhcpRelayAgentOption;
41import org.onosproject.dhcprelay.api.DhcpHandler;
Yi Tsenge72fbb52017-08-02 15:03:31 -070042import org.onosproject.dhcprelay.config.DhcpServerConfig;
Yi Tseng51301292017-07-28 13:02:59 -070043import org.onosproject.dhcprelay.store.DhcpRecord;
44import org.onosproject.dhcprelay.store.DhcpRelayStore;
Yi Tsenge72fbb52017-08-02 15:03:31 -070045import org.onosproject.net.host.HostEvent;
46import org.onosproject.net.host.HostListener;
Ray Milkeyfacf2862017-08-03 11:58:29 -070047import org.onosproject.net.intf.Interface;
48import org.onosproject.net.intf.InterfaceService;
Ray Milkey69ec8712017-08-08 13:00:43 -070049import org.onosproject.routeservice.Route;
50import org.onosproject.routeservice.RouteStore;
Yi Tseng51301292017-07-28 13:02:59 -070051import org.onosproject.net.ConnectPoint;
52import org.onosproject.net.Host;
53import org.onosproject.net.HostId;
54import org.onosproject.net.HostLocation;
55import org.onosproject.net.flow.DefaultTrafficTreatment;
56import org.onosproject.net.flow.TrafficTreatment;
57import org.onosproject.net.host.DefaultHostDescription;
58import org.onosproject.net.host.HostDescription;
59import org.onosproject.net.host.HostService;
60import org.onosproject.net.host.HostStore;
61import org.onosproject.net.host.InterfaceIpAddress;
62import org.onosproject.net.packet.DefaultOutboundPacket;
63import org.onosproject.net.packet.OutboundPacket;
64import org.onosproject.net.packet.PacketContext;
65import org.onosproject.net.packet.PacketService;
66import org.slf4j.Logger;
67import org.slf4j.LoggerFactory;
68
69import java.nio.ByteBuffer;
Yi Tsengdcef2c22017-08-05 20:34:06 -070070import java.util.Collection;
Yi Tseng51301292017-07-28 13:02:59 -070071import java.util.Collections;
72import java.util.List;
73import java.util.Optional;
74import java.util.Set;
75import java.util.stream.Collectors;
76
77import static com.google.common.base.Preconditions.checkNotNull;
78import static com.google.common.base.Preconditions.checkState;
79import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_CircuitID;
80import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_END;
81import static org.onlab.packet.DHCP.DHCPOptionCode.OptionCode_MessageType;
82import static org.onlab.packet.MacAddress.valueOf;
83import static org.onlab.packet.dhcp.DhcpRelayAgentOption.RelayAgentInfoOptions.CIRCUIT_ID;
84
85@Component
86@Service
87@Property(name = "version", value = "4")
88public class Dhcp4HandlerImpl implements DhcpHandler {
89 private static Logger log = LoggerFactory.getLogger(Dhcp4HandlerImpl.class);
90
91 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
92 protected DhcpRelayStore dhcpRelayStore;
93
94 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
95 protected PacketService packetService;
96
97 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
98 protected HostStore hostStore;
99
100 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
101 protected RouteStore routeStore;
102
103 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
104 protected InterfaceService interfaceService;
105
106 @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
107 protected HostService hostService;
108
Yi Tsenge72fbb52017-08-02 15:03:31 -0700109 private InternalHostListener hostListener = new InternalHostListener();
110
Yi Tseng51301292017-07-28 13:02:59 -0700111 private Ip4Address dhcpServerIp = null;
112 // dhcp server may be connected directly to the SDN network or
113 // via an external gateway. When connected directly, the dhcpConnectPoint, dhcpConnectMac,
114 // and dhcpConnectVlan refer to the server. When connected via the gateway, they refer
115 // to the gateway.
116 private ConnectPoint dhcpServerConnectPoint = null;
117 private MacAddress dhcpConnectMac = null;
118 private VlanId dhcpConnectVlan = null;
119 private Ip4Address dhcpGatewayIp = null;
120
Yi Tsenge72fbb52017-08-02 15:03:31 -0700121 @Activate
122 protected void activate() {
123 hostService.addListener(hostListener);
124 }
125
126 @Deactivate
127 protected void deactivate() {
128 hostService.removeListener(hostListener);
129 this.dhcpConnectMac = null;
130 this.dhcpConnectVlan = null;
131 }
132
Yi Tseng51301292017-07-28 13:02:59 -0700133 @Override
134 public void setDhcpServerIp(IpAddress dhcpServerIp) {
135 checkNotNull(dhcpServerIp, "DHCP server IP can't be null");
136 checkState(dhcpServerIp.isIp4(), "Invalid server IP for DHCPv4 relay handler");
137 this.dhcpServerIp = dhcpServerIp.getIp4Address();
138 }
139
140 @Override
141 public void setDhcpServerConnectPoint(ConnectPoint dhcpServerConnectPoint) {
142 checkNotNull(dhcpServerConnectPoint, "Server connect point can't null");
143 this.dhcpServerConnectPoint = dhcpServerConnectPoint;
144 }
145
146 @Override
147 public void setDhcpConnectMac(MacAddress dhcpConnectMac) {
148 this.dhcpConnectMac = dhcpConnectMac;
149 }
150
151 @Override
152 public void setDhcpConnectVlan(VlanId dhcpConnectVlan) {
153 this.dhcpConnectVlan = dhcpConnectVlan;
154 }
155
156 @Override
157 public void setDhcpGatewayIp(IpAddress dhcpGatewayIp) {
158 if (dhcpGatewayIp != null) {
159 checkState(dhcpGatewayIp.isIp4(), "Invalid gateway IP for DHCPv4 relay handler");
160 this.dhcpGatewayIp = dhcpGatewayIp.getIp4Address();
161 } else {
162 // removes gateway config
163 this.dhcpGatewayIp = null;
164 }
165 }
166
167 @Override
168 public Optional<IpAddress> getDhcpServerIp() {
169 return Optional.ofNullable(dhcpServerIp);
170 }
171
172 @Override
173 public Optional<IpAddress> getDhcpGatewayIp() {
174 return Optional.ofNullable(dhcpGatewayIp);
175 }
176
177 @Override
178 public Optional<MacAddress> getDhcpConnectMac() {
179 return Optional.ofNullable(dhcpConnectMac);
180 }
181
182 @Override
Yi Tsenge72fbb52017-08-02 15:03:31 -0700183 public void setDefaultDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
184 if (configs.size() == 0) {
185 // no config to update
186 return;
187 }
188
189 // TODO: currently we pick up first DHCP server config.
190 // Will use other server configs in the future for HA.
191 DhcpServerConfig serverConfig = configs.iterator().next();
192 checkState(serverConfig.getDhcpServerConnectPoint().isPresent(),
193 "Connect point not exists");
194 checkState(serverConfig.getDhcpServerIp4().isPresent(),
195 "IP of DHCP server not exists");
196 Ip4Address oldServerIp = this.dhcpServerIp;
197 Ip4Address oldGatewayIp = this.dhcpGatewayIp;
198
199 // stop monitoring gateway or server
200 if (oldGatewayIp != null) {
201 hostService.stopMonitoringIp(oldGatewayIp);
202 } else if (oldServerIp != null) {
203 hostService.stopMonitoringIp(oldServerIp);
204 }
205
206 this.dhcpServerConnectPoint = serverConfig.getDhcpServerConnectPoint().get();
207 this.dhcpServerIp = serverConfig.getDhcpServerIp4().get();
208 this.dhcpGatewayIp = serverConfig.getDhcpGatewayIp4().orElse(null);
209
210 // reset server mac and vlan
211 this.dhcpConnectMac = null;
212 this.dhcpConnectVlan = null;
213
214 log.info("DHCP server connect point: " + this.dhcpServerConnectPoint);
215 log.info("DHCP server IP: " + this.dhcpServerIp);
216
217 IpAddress ipToProbe = MoreObjects.firstNonNull(this.dhcpGatewayIp, this.dhcpServerIp);
218 String hostToProbe = this.dhcpGatewayIp != null ? "gateway" : "DHCP server";
219
220 if (ipToProbe == null) {
221 log.warn("Server IP not set, can't probe it");
222 return;
223 }
224
225 log.info("Probing to resolve {} IP {}", hostToProbe, ipToProbe);
226 hostService.startMonitoringIp(ipToProbe);
227
228 Set<Host> hosts = hostService.getHostsByIp(ipToProbe);
229 if (!hosts.isEmpty()) {
230 Host host = hosts.iterator().next();
231 this.dhcpConnectVlan = host.vlan();
232 this.dhcpConnectMac = host.mac();
233 }
234 }
235
236 @Override
237 public void setIndirectDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
238 log.warn("Indirect config feature for DHCPv4 handler not implement yet");
239 }
240
Yi Tseng51301292017-07-28 13:02:59 -0700241 public void processDhcpPacket(PacketContext context, BasePacket payload) {
242 checkNotNull(payload, "DHCP payload can't be null");
243 checkState(payload instanceof DHCP, "Payload is not a DHCP");
244 DHCP dhcpPayload = (DHCP) payload;
245 if (!configured()) {
246 log.warn("Missing DHCP relay server config. Abort packet processing");
247 return;
248 }
249
250 ConnectPoint inPort = context.inPacket().receivedFrom();
Yi Tseng51301292017-07-28 13:02:59 -0700251 checkNotNull(dhcpPayload, "Can't find DHCP payload");
252 Ethernet packet = context.inPacket().parsed();
253 DHCP.MsgType incomingPacketType = dhcpPayload.getOptions().stream()
254 .filter(dhcpOption -> dhcpOption.getCode() == OptionCode_MessageType.getValue())
255 .map(DhcpOption::getData)
256 .map(data -> DHCP.MsgType.getType(data[0]))
257 .findFirst()
258 .orElse(null);
259 checkNotNull(incomingPacketType, "Can't get message type from DHCP payload {}", dhcpPayload);
260 switch (incomingPacketType) {
261 case DHCPDISCOVER:
Yi Tsengdcef2c22017-08-05 20:34:06 -0700262 // Try update host if it is directly connected.
263 if (directlyConnected(dhcpPayload)) {
264 updateHost(context, dhcpPayload);
265 }
266
267 // Add the gateway IP as virtual interface IP for server to understand
Yi Tseng51301292017-07-28 13:02:59 -0700268 // the lease to be assigned and forward the packet to dhcp server.
269 Ethernet ethernetPacketDiscover =
Yi Tsengdcef2c22017-08-05 20:34:06 -0700270 processDhcpPacketFromClient(context, packet);
Yi Tseng51301292017-07-28 13:02:59 -0700271 if (ethernetPacketDiscover != null) {
272 writeRequestDhcpRecord(inPort, packet, dhcpPayload);
273 handleDhcpDiscoverAndRequest(ethernetPacketDiscover);
274 }
275 break;
276 case DHCPOFFER:
277 //reply to dhcp client.
278 Ethernet ethernetPacketOffer = processDhcpPacketFromServer(packet);
279 if (ethernetPacketOffer != null) {
280 writeResponseDhcpRecord(ethernetPacketOffer, dhcpPayload);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700281 sendResponseToClient(ethernetPacketOffer, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700282 }
283 break;
284 case DHCPREQUEST:
285 // add the gateway ip as virtual interface ip for server to understand
286 // the lease to be assigned and forward the packet to dhcp server.
287 Ethernet ethernetPacketRequest =
Yi Tsengdcef2c22017-08-05 20:34:06 -0700288 processDhcpPacketFromClient(context, packet);
Yi Tseng51301292017-07-28 13:02:59 -0700289 if (ethernetPacketRequest != null) {
290 writeRequestDhcpRecord(inPort, packet, dhcpPayload);
291 handleDhcpDiscoverAndRequest(ethernetPacketRequest);
292 }
293 break;
294 case DHCPACK:
295 // reply to dhcp client.
296 Ethernet ethernetPacketAck = processDhcpPacketFromServer(packet);
297 if (ethernetPacketAck != null) {
298 writeResponseDhcpRecord(ethernetPacketAck, dhcpPayload);
299 handleDhcpAck(ethernetPacketAck, dhcpPayload);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700300 sendResponseToClient(ethernetPacketAck, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700301 }
302 break;
303 case DHCPRELEASE:
304 // TODO: release the ip address from client
305 break;
306 default:
307 break;
308 }
309 }
310
311 /**
Yi Tsengdcef2c22017-08-05 20:34:06 -0700312 * Updates host to host store according to DHCP payload.
313 *
314 * @param context the packet context
315 * @param dhcpPayload the DHCP payload
316 */
317 private void updateHost(PacketContext context, DHCP dhcpPayload) {
318 ConnectPoint location = context.inPacket().receivedFrom();
319 HostLocation hostLocation = new HostLocation(location, System.currentTimeMillis());
320 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
321 VlanId vlanId = VlanId.vlanId(context.inPacket().parsed().getVlanID());
322 HostId hostId = HostId.hostId(macAddress, vlanId);
323 HostDescription desc = new DefaultHostDescription(macAddress, vlanId, hostLocation);
324 hostStore.createOrUpdateHost(DhcpRelayManager.PROVIDER_ID, hostId, desc, false);
325 }
326
327 /**
Yi Tseng51301292017-07-28 13:02:59 -0700328 * Checks if this app has been configured.
329 *
330 * @return true if all information we need have been initialized
331 */
332 public boolean configured() {
333 return dhcpServerConnectPoint != null && dhcpServerIp != null;
334 }
335
336 /**
Yi Tsengdcef2c22017-08-05 20:34:06 -0700337 * Returns the first interface ip from interface.
Yi Tseng51301292017-07-28 13:02:59 -0700338 *
Yi Tsengdcef2c22017-08-05 20:34:06 -0700339 * @param iface interface of one connect point
Yi Tseng51301292017-07-28 13:02:59 -0700340 * @return the first interface IP; null if not exists an IP address in
341 * these interfaces
342 */
Yi Tsengdcef2c22017-08-05 20:34:06 -0700343 private Ip4Address getRelayAgentIPv4Address(Interface iface) {
344 checkNotNull(iface, "Interface can't be null");
345 return iface.ipAddressesList().stream()
Yi Tseng51301292017-07-28 13:02:59 -0700346 .map(InterfaceIpAddress::ipAddress)
347 .filter(IpAddress::isIp4)
348 .map(IpAddress::getIp4Address)
349 .findFirst()
350 .orElse(null);
351 }
352
353 /**
Yi Tsengdcef2c22017-08-05 20:34:06 -0700354 * Gets Interface facing to the server.
355 *
356 * @return the Interface facing to the server; null if not found
357 */
358 public Interface getServerInterface() {
359 if (dhcpServerConnectPoint == null || dhcpConnectVlan == null) {
360 return null;
361 }
362 return interfaceService.getInterfacesByPort(dhcpServerConnectPoint)
363 .stream()
364 .filter(iface -> iface.vlan().equals(dhcpConnectVlan) ||
365 iface.vlanUntagged().equals(dhcpConnectVlan) ||
366 iface.vlanTagged().contains(dhcpConnectVlan) ||
367 iface.vlanNative().equals(dhcpConnectVlan))
368 .findFirst()
369 .orElse(null);
370 }
371
372 /**
Yi Tseng51301292017-07-28 13:02:59 -0700373 * Build the DHCP discover/request packet with gateway IP(unicast packet).
374 *
375 * @param context the packet context
376 * @param ethernetPacket the ethernet payload to process
Yi Tseng51301292017-07-28 13:02:59 -0700377 * @return processed packet
378 */
379 private Ethernet processDhcpPacketFromClient(PacketContext context,
Yi Tsengdcef2c22017-08-05 20:34:06 -0700380 Ethernet ethernetPacket) {
381 Ip4Address clientInterfaceIp =
382 interfaceService.getInterfacesByPort(context.inPacket().receivedFrom())
383 .stream()
384 .map(Interface::ipAddressesList)
385 .flatMap(Collection::stream)
386 .map(InterfaceIpAddress::ipAddress)
387 .filter(IpAddress::isIp4)
388 .map(IpAddress::getIp4Address)
389 .findFirst()
390 .orElse(null);
391 if (clientInterfaceIp == null) {
392 log.warn("Can't find interface IP for client interface for port {}",
393 context.inPacket().receivedFrom());
394 return null;
395 }
396 Interface serverInterface = getServerInterface();
397 if (serverInterface == null) {
398 log.warn("Can't get server interface, ignore");
399 return null;
400 }
401 Ip4Address relayAgentIp = getRelayAgentIPv4Address(serverInterface);
402 MacAddress relayAgentMac = serverInterface.mac();
Yi Tseng51301292017-07-28 13:02:59 -0700403 if (relayAgentIp == null || relayAgentMac == null) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700404 log.warn("No IP address for server Interface {}", serverInterface);
Yi Tseng51301292017-07-28 13:02:59 -0700405 return null;
406 }
407 if (dhcpConnectMac == null) {
408 log.warn("DHCP {} not yet resolved .. Aborting DHCP "
409 + "packet processing from client on port: {}",
410 (dhcpGatewayIp == null) ? "server IP " + dhcpServerIp
411 : "gateway IP " + dhcpGatewayIp,
Yi Tsengdcef2c22017-08-05 20:34:06 -0700412 context.inPacket().receivedFrom());
Yi Tseng51301292017-07-28 13:02:59 -0700413 return null;
414 }
415 // get dhcp header.
416 Ethernet etherReply = (Ethernet) ethernetPacket.clone();
417 etherReply.setSourceMACAddress(relayAgentMac);
418 etherReply.setDestinationMACAddress(dhcpConnectMac);
419 etherReply.setVlanID(dhcpConnectVlan.toShort());
420 IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
421 ipv4Packet.setSourceAddress(relayAgentIp.toInt());
422 ipv4Packet.setDestinationAddress(dhcpServerIp.toInt());
423 UDP udpPacket = (UDP) ipv4Packet.getPayload();
424 DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
425
Yi Tsengdcef2c22017-08-05 20:34:06 -0700426 if (directlyConnected(dhcpPacket)) {
Yi Tseng51301292017-07-28 13:02:59 -0700427 ConnectPoint inPort = context.inPacket().receivedFrom();
428 VlanId vlanId = VlanId.vlanId(ethernetPacket.getVlanID());
429 // add connected in port and vlan
430 CircuitId cid = new CircuitId(inPort.toString(), vlanId);
431 byte[] circuitId = cid.serialize();
432 DhcpOption circuitIdSubOpt = new DhcpOption();
433 circuitIdSubOpt
434 .setCode(CIRCUIT_ID.getValue())
435 .setLength((byte) circuitId.length)
436 .setData(circuitId);
437
438 DhcpRelayAgentOption newRelayAgentOpt = new DhcpRelayAgentOption();
439 newRelayAgentOpt.setCode(OptionCode_CircuitID.getValue());
440 newRelayAgentOpt.addSubOption(circuitIdSubOpt);
441
442 // Removes END option first
443 List<DhcpOption> options = dhcpPacket.getOptions().stream()
444 .filter(opt -> opt.getCode() != OptionCode_END.getValue())
445 .collect(Collectors.toList());
446
447 // push relay agent option
448 options.add(newRelayAgentOpt);
449
450 // make sure option 255(End) is the last option
451 DhcpOption endOption = new DhcpOption();
452 endOption.setCode(OptionCode_END.getValue());
453 options.add(endOption);
454
455 dhcpPacket.setOptions(options);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700456
457 // Sets giaddr to IP address from the Interface which facing to
458 // DHCP client
459 dhcpPacket.setGatewayIPAddress(clientInterfaceIp.toInt());
Yi Tseng51301292017-07-28 13:02:59 -0700460 }
461
462 udpPacket.setPayload(dhcpPacket);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700463 // As a DHCP relay, the source port should be server port(67) instead
464 // of client port(68)
465 udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
Yi Tseng51301292017-07-28 13:02:59 -0700466 udpPacket.setDestinationPort(UDP.DHCP_SERVER_PORT);
467 ipv4Packet.setPayload(udpPacket);
468 etherReply.setPayload(ipv4Packet);
469 return etherReply;
470 }
471
472 /**
473 * Writes DHCP record to the store according to the request DHCP packet (Discover, Request).
474 *
475 * @param location the location which DHCP packet comes from
476 * @param ethernet the DHCP packet
477 * @param dhcpPayload the DHCP payload
478 */
479 private void writeRequestDhcpRecord(ConnectPoint location,
480 Ethernet ethernet,
481 DHCP dhcpPayload) {
482 VlanId vlanId = VlanId.vlanId(ethernet.getVlanID());
483 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
484 HostId hostId = HostId.hostId(macAddress, vlanId);
485 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
486 if (record == null) {
487 record = new DhcpRecord(HostId.hostId(macAddress, vlanId));
488 } else {
489 record = record.clone();
490 }
491 record.addLocation(new HostLocation(location, System.currentTimeMillis()));
492 record.ip4Status(dhcpPayload.getPacketType());
493 record.setDirectlyConnected(directlyConnected(dhcpPayload));
494 if (!directlyConnected(dhcpPayload)) {
495 // Update gateway mac address if the host is not directly connected
496 record.nextHop(ethernet.getSourceMAC());
497 }
498 record.updateLastSeen();
499 dhcpRelayStore.updateDhcpRecord(HostId.hostId(macAddress, vlanId), record);
500 }
501
502 /**
503 * Writes DHCP record to the store according to the response DHCP packet (Offer, Ack).
504 *
505 * @param ethernet the DHCP packet
506 * @param dhcpPayload the DHCP payload
507 */
508 private void writeResponseDhcpRecord(Ethernet ethernet,
509 DHCP dhcpPayload) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700510 Optional<Interface> outInterface = getClientInterface(ethernet, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700511 if (!outInterface.isPresent()) {
512 log.warn("Failed to determine where to send {}", dhcpPayload.getPacketType());
513 return;
514 }
515
516 Interface outIface = outInterface.get();
517 ConnectPoint location = outIface.connectPoint();
Yi Tsengdcef2c22017-08-05 20:34:06 -0700518 VlanId vlanId = getVlanIdFromOption(dhcpPayload);
519 if (vlanId == null) {
520 vlanId = outIface.vlan();
521 }
Yi Tseng51301292017-07-28 13:02:59 -0700522 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
523 HostId hostId = HostId.hostId(macAddress, vlanId);
524 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
525 if (record == null) {
526 record = new DhcpRecord(HostId.hostId(macAddress, vlanId));
527 } else {
528 record = record.clone();
529 }
530 record.addLocation(new HostLocation(location, System.currentTimeMillis()));
531 if (dhcpPayload.getPacketType() == DHCP.MsgType.DHCPACK) {
532 record.ip4Address(Ip4Address.valueOf(dhcpPayload.getYourIPAddress()));
533 }
534 record.ip4Status(dhcpPayload.getPacketType());
535 record.setDirectlyConnected(directlyConnected(dhcpPayload));
536 record.updateLastSeen();
537 dhcpRelayStore.updateDhcpRecord(HostId.hostId(macAddress, vlanId), record);
538 }
539
540 /**
541 * Build the DHCP offer/ack with proper client port.
542 *
543 * @param ethernetPacket the original packet comes from server
544 * @return new packet which will send to the client
545 */
546 private Ethernet processDhcpPacketFromServer(Ethernet ethernetPacket) {
547 // get dhcp header.
548 Ethernet etherReply = (Ethernet) ethernetPacket.clone();
549 IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
550 UDP udpPacket = (UDP) ipv4Packet.getPayload();
551 DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
552
553 // determine the vlanId of the client host - note that this vlan id
554 // could be different from the vlan in the packet from the server
Yi Tsengdcef2c22017-08-05 20:34:06 -0700555 Interface clientInterface = getClientInterface(ethernetPacket, dhcpPayload).orElse(null);
Yi Tseng51301292017-07-28 13:02:59 -0700556
Yi Tsengdcef2c22017-08-05 20:34:06 -0700557 if (clientInterface == null) {
Yi Tseng51301292017-07-28 13:02:59 -0700558 log.warn("Cannot find the interface for the DHCP {}", dhcpPayload);
559 return null;
560 }
Yi Tsengdcef2c22017-08-05 20:34:06 -0700561 VlanId vlanId;
562 if (clientInterface.vlanTagged().isEmpty()) {
563 vlanId = clientInterface.vlan();
564 } else {
565 // might be multiple vlan in same interface
566 vlanId = getVlanIdFromOption(dhcpPayload);
567 }
568 if (vlanId == null) {
569 vlanId = VlanId.NONE;
570 }
571 etherReply.setVlanID(vlanId.toShort());
572 etherReply.setSourceMACAddress(clientInterface.mac());
Yi Tseng51301292017-07-28 13:02:59 -0700573
Yi Tsengdcef2c22017-08-05 20:34:06 -0700574 if (!directlyConnected(dhcpPayload)) {
575 // if client is indirectly connected, try use next hop mac address
576 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
577 HostId hostId = HostId.hostId(macAddress, vlanId);
578 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
579 if (record != null) {
580 // if next hop can be found, use mac address of next hop
581 record.nextHop().ifPresent(etherReply::setDestinationMACAddress);
582 } else {
583 // otherwise, discard the packet
584 log.warn("Can't find record for host id {}, discard packet", hostId);
585 return null;
586 }
Yi Tsengc03fa242017-08-17 17:43:38 -0700587 } else {
588 etherReply.setDestinationMACAddress(dhcpPayload.getClientHardwareAddress());
Yi Tsengdcef2c22017-08-05 20:34:06 -0700589 }
590
Yi Tseng51301292017-07-28 13:02:59 -0700591 // we leave the srcMac from the original packet
Yi Tseng51301292017-07-28 13:02:59 -0700592 // figure out the relay agent IP corresponding to the original request
Yi Tsengdcef2c22017-08-05 20:34:06 -0700593 Ip4Address relayAgentIP = getRelayAgentIPv4Address(clientInterface);
Yi Tseng51301292017-07-28 13:02:59 -0700594 if (relayAgentIP == null) {
595 log.warn("Cannot determine relay agent interface Ipv4 addr for host {}/{}. "
596 + "Aborting relay for dhcp packet from server {}",
Yi Tsengdcef2c22017-08-05 20:34:06 -0700597 etherReply.getDestinationMAC(), clientInterface.vlan(),
Yi Tseng51301292017-07-28 13:02:59 -0700598 ethernetPacket);
599 return null;
600 }
601 // SRC_IP: relay agent IP
602 // DST_IP: offered IP
603 ipv4Packet.setSourceAddress(relayAgentIP.toInt());
604 ipv4Packet.setDestinationAddress(dhcpPayload.getYourIPAddress());
605 udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
606 if (directlyConnected(dhcpPayload)) {
607 udpPacket.setDestinationPort(UDP.DHCP_CLIENT_PORT);
608 } else {
609 // forward to another dhcp relay
610 udpPacket.setDestinationPort(UDP.DHCP_SERVER_PORT);
611 }
612
613 udpPacket.setPayload(dhcpPayload);
614 ipv4Packet.setPayload(udpPacket);
615 etherReply.setPayload(ipv4Packet);
616 return etherReply;
617 }
618
Yi Tsengdcef2c22017-08-05 20:34:06 -0700619 /**
620 * Extracts VLAN ID from relay agent option.
621 *
622 * @param dhcpPayload the DHCP payload
623 * @return VLAN ID from DHCP payload; null if not exists
624 */
625 private VlanId getVlanIdFromOption(DHCP dhcpPayload) {
626 DhcpRelayAgentOption option = (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
627 if (option == null) {
628 return null;
629 }
630 DhcpOption circuitIdSubOption = option.getSubOption(CIRCUIT_ID.getValue());
631 if (circuitIdSubOption == null) {
632 return null;
633 }
634 try {
635 CircuitId circuitId = CircuitId.deserialize(circuitIdSubOption.getData());
636 return circuitId.vlanId();
637 } catch (IllegalArgumentException e) {
638 // can't deserialize the circuit ID
639 return null;
640 }
641 }
642
643 /**
644 * Removes DHCP relay agent information option (option 82) from DHCP payload.
645 * Also reset giaddr to 0
646 *
647 * @param ethPacket the Ethernet packet to be processed
648 * @return Ethernet packet processed
649 */
650 private Ethernet removeRelayAgentOption(Ethernet ethPacket) {
651 Ethernet ethernet = (Ethernet) ethPacket.clone();
652 IPv4 ipv4 = (IPv4) ethernet.getPayload();
653 UDP udp = (UDP) ipv4.getPayload();
654 DHCP dhcpPayload = (DHCP) udp.getPayload();
655
656 // removes relay agent information option
657 List<DhcpOption> options = dhcpPayload.getOptions();
658 options = options.stream()
659 .filter(option -> option.getCode() != OptionCode_CircuitID.getValue())
660 .collect(Collectors.toList());
661 dhcpPayload.setOptions(options);
662 dhcpPayload.setGatewayIPAddress(0);
663
664 udp.setPayload(dhcpPayload);
665 ipv4.setPayload(udp);
666 ethernet.setPayload(ipv4);
667 return ethernet;
668 }
669
Yi Tseng51301292017-07-28 13:02:59 -0700670
671 /**
672 * Check if the host is directly connected to the network or not.
673 *
674 * @param dhcpPayload the dhcp payload
675 * @return true if the host is directly connected to the network; false otherwise
676 */
677 private boolean directlyConnected(DHCP dhcpPayload) {
678 DhcpOption relayAgentOption = dhcpPayload.getOption(OptionCode_CircuitID);
679
680 // Doesn't contains relay option
681 if (relayAgentOption == null) {
682 return true;
683 }
684
685 IpAddress gatewayIp = IpAddress.valueOf(dhcpPayload.getGatewayIPAddress());
686 Set<Interface> gatewayInterfaces = interfaceService.getInterfacesByIp(gatewayIp);
687
688 // Contains relay option, and added by ONOS
689 if (!gatewayInterfaces.isEmpty()) {
690 return true;
691 }
692
693 // Relay option added by other relay agent
694 return false;
695 }
696
697
698 /**
699 * Send the DHCP ack to the requester host.
700 * Modify Host or Route store according to the type of DHCP.
701 *
702 * @param ethernetPacketAck the packet
703 * @param dhcpPayload the DHCP data
704 */
705 private void handleDhcpAck(Ethernet ethernetPacketAck, DHCP dhcpPayload) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700706 Optional<Interface> outInterface = getClientInterface(ethernetPacketAck, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700707 if (!outInterface.isPresent()) {
708 log.warn("Can't find output interface for dhcp: {}", dhcpPayload);
709 return;
710 }
711
712 Interface outIface = outInterface.get();
713 HostLocation hostLocation = new HostLocation(outIface.connectPoint(), System.currentTimeMillis());
714 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
Yi Tsengdcef2c22017-08-05 20:34:06 -0700715 VlanId vlanId = getVlanIdFromOption(dhcpPayload);
716 if (vlanId == null) {
717 vlanId = outIface.vlan();
718 }
Yi Tseng51301292017-07-28 13:02:59 -0700719 HostId hostId = HostId.hostId(macAddress, vlanId);
720 Ip4Address ip = Ip4Address.valueOf(dhcpPayload.getYourIPAddress());
721
722 if (directlyConnected(dhcpPayload)) {
723 // Add to host store if it connect to network directly
724 Set<IpAddress> ips = Sets.newHashSet(ip);
725 HostDescription desc = new DefaultHostDescription(macAddress, vlanId,
726 hostLocation, ips);
727
728 // Replace the ip when dhcp server give the host new ip address
729 hostStore.createOrUpdateHost(DhcpRelayManager.PROVIDER_ID, hostId, desc, false);
730 } else {
731 // Add to route store if it does not connect to network directly
732 // Get gateway host IP according to host mac address
Yi Tsengdcef2c22017-08-05 20:34:06 -0700733 // TODO: remove relay store here
Yi Tseng51301292017-07-28 13:02:59 -0700734 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
735
736 if (record == null) {
737 log.warn("Can't find DHCP record of host {}", hostId);
738 return;
739 }
740
741 MacAddress gwMac = record.nextHop().orElse(null);
742 if (gwMac == null) {
743 log.warn("Can't find gateway mac address from record {}", record);
744 return;
745 }
746
747 HostId gwHostId = HostId.hostId(gwMac, record.vlanId());
748 Host gwHost = hostService.getHost(gwHostId);
749
750 if (gwHost == null) {
751 log.warn("Can't find gateway host {}", gwHostId);
752 return;
753 }
754
755 Ip4Address nextHopIp = gwHost.ipAddresses()
756 .stream()
757 .filter(IpAddress::isIp4)
758 .map(IpAddress::getIp4Address)
759 .findFirst()
760 .orElse(null);
761
762 if (nextHopIp == null) {
763 log.warn("Can't find IP address of gateway {}", gwHost);
764 return;
765 }
766
767 Route route = new Route(Route.Source.STATIC, ip.toIpPrefix(), nextHopIp);
768 routeStore.updateRoute(route);
769 }
Yi Tseng51301292017-07-28 13:02:59 -0700770 }
771
772 /**
773 * forward the packet to ConnectPoint where the DHCP server is attached.
774 *
775 * @param packet the packet
776 */
777 private void handleDhcpDiscoverAndRequest(Ethernet packet) {
778 // send packet to dhcp server connect point.
779 if (dhcpServerConnectPoint != null) {
780 TrafficTreatment t = DefaultTrafficTreatment.builder()
781 .setOutput(dhcpServerConnectPoint.port()).build();
782 OutboundPacket o = new DefaultOutboundPacket(
783 dhcpServerConnectPoint.deviceId(), t, ByteBuffer.wrap(packet.serialize()));
784 if (log.isTraceEnabled()) {
785 log.trace("Relaying packet to dhcp server {}", packet);
786 }
787 packetService.emit(o);
788 } else {
789 log.warn("Can't find DHCP server connect point, abort.");
790 }
791 }
792
793
794 /**
795 * Gets output interface of a dhcp packet.
796 * If option 82 exists in the dhcp packet and the option was sent by
797 * ONOS (gateway address exists in ONOS interfaces), use the connect
798 * point and vlan id from circuit id; otherwise, find host by destination
799 * address and use vlan id from sender (dhcp server).
800 *
801 * @param ethPacket the ethernet packet
802 * @param dhcpPayload the dhcp packet
803 * @return an interface represent the output port and vlan; empty value
804 * if the host or circuit id not found
805 */
Yi Tsengdcef2c22017-08-05 20:34:06 -0700806 private Optional<Interface> getClientInterface(Ethernet ethPacket, DHCP dhcpPayload) {
Yi Tseng51301292017-07-28 13:02:59 -0700807 VlanId originalPacketVlanId = VlanId.vlanId(ethPacket.getVlanID());
808 IpAddress gatewayIpAddress = Ip4Address.valueOf(dhcpPayload.getGatewayIPAddress());
Yi Tsengdcef2c22017-08-05 20:34:06 -0700809
810 // get all possible interfaces for client
811 Set<Interface> clientInterfaces = interfaceService.getInterfacesByIp(gatewayIpAddress);
Yi Tseng51301292017-07-28 13:02:59 -0700812 DhcpRelayAgentOption option = (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
813
814 // Sent by ONOS, and contains circuit id
Yi Tsengdcef2c22017-08-05 20:34:06 -0700815 if (!clientInterfaces.isEmpty() && option != null) {
Yi Tseng51301292017-07-28 13:02:59 -0700816 DhcpOption circuitIdSubOption = option.getSubOption(CIRCUIT_ID.getValue());
817 try {
818 CircuitId circuitId = CircuitId.deserialize(circuitIdSubOption.getData());
819 ConnectPoint connectPoint = ConnectPoint.deviceConnectPoint(circuitId.connectPoint());
820 VlanId vlanId = circuitId.vlanId();
Yi Tsengdcef2c22017-08-05 20:34:06 -0700821 return clientInterfaces.stream()
822 .filter(iface -> iface.vlanUntagged().equals(vlanId) ||
823 iface.vlan().equals(vlanId) ||
824 iface.vlanNative().equals(vlanId) ||
825 iface.vlanTagged().contains(vlanId))
826 .filter(iface -> iface.connectPoint().equals(connectPoint))
827 .findFirst();
Yi Tseng51301292017-07-28 13:02:59 -0700828 } catch (IllegalArgumentException ex) {
829 // invalid circuit format, didn't sent by ONOS
830 log.debug("Invalid circuit {}, use information from dhcp payload",
831 circuitIdSubOption.getData());
832 }
833 }
834
835 // Use Vlan Id from DHCP server if DHCP relay circuit id was not
836 // sent by ONOS or circuit Id can't be parsed
Yi Tsengdcef2c22017-08-05 20:34:06 -0700837 // TODO: remove relay store from this method
Yi Tseng51301292017-07-28 13:02:59 -0700838 MacAddress dstMac = valueOf(dhcpPayload.getClientHardwareAddress());
839 Optional<DhcpRecord> dhcpRecord = dhcpRelayStore.getDhcpRecord(HostId.hostId(dstMac, originalPacketVlanId));
Yi Tsengdcef2c22017-08-05 20:34:06 -0700840 ConnectPoint clientConnectPoint = dhcpRecord
Yi Tseng51301292017-07-28 13:02:59 -0700841 .map(DhcpRecord::locations)
842 .orElse(Collections.emptySet())
843 .stream()
844 .reduce((hl1, hl2) -> {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700845 // find latest host connect point
Yi Tseng51301292017-07-28 13:02:59 -0700846 if (hl1 == null || hl2 == null) {
847 return hl1 == null ? hl2 : hl1;
848 }
849 return hl1.time() > hl2.time() ? hl1 : hl2;
850 })
Yi Tsengdcef2c22017-08-05 20:34:06 -0700851 .orElse(null);
Yi Tseng51301292017-07-28 13:02:59 -0700852
Yi Tsengdcef2c22017-08-05 20:34:06 -0700853 if (clientConnectPoint != null) {
854 return interfaceService.getInterfacesByPort(clientConnectPoint)
855 .stream()
856 .filter(iface -> iface.vlan().equals(originalPacketVlanId) ||
857 iface.vlanUntagged().equals(originalPacketVlanId))
858 .findFirst();
859 }
860 return Optional.empty();
Yi Tseng51301292017-07-28 13:02:59 -0700861 }
862
863 /**
864 * Send the response DHCP to the requester host.
865 *
866 * @param ethPacket the packet
867 * @param dhcpPayload the DHCP data
868 */
869 private void sendResponseToClient(Ethernet ethPacket, DHCP dhcpPayload) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700870 Optional<Interface> outInterface = getClientInterface(ethPacket, dhcpPayload);
871 if (directlyConnected(dhcpPayload)) {
872 ethPacket = removeRelayAgentOption(ethPacket);
873 }
874 if (!outInterface.isPresent()) {
875 log.warn("Can't find output interface for client, ignore");
876 return;
877 }
878 Interface outIface = outInterface.get();
879 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
880 .setOutput(outIface.connectPoint().port())
881 .build();
882 OutboundPacket o = new DefaultOutboundPacket(
883 outIface.connectPoint().deviceId(),
884 treatment,
885 ByteBuffer.wrap(ethPacket.serialize()));
886 if (log.isTraceEnabled()) {
887 log.trace("Relaying packet to DHCP client {} via {}, vlan {}",
888 ethPacket,
889 outIface.connectPoint(),
890 outIface.vlan());
891 }
892 packetService.emit(o);
Yi Tseng51301292017-07-28 13:02:59 -0700893 }
Yi Tsenge72fbb52017-08-02 15:03:31 -0700894
895 class InternalHostListener implements HostListener {
896 @Override
897 public void event(HostEvent event) {
898 switch (event.type()) {
899 case HOST_ADDED:
900 case HOST_UPDATED:
901 hostUpdated(event.subject());
902 break;
903 case HOST_REMOVED:
904 hostRemoved(event.subject());
905 break;
906 case HOST_MOVED:
907 hostMoved(event.subject());
908 break;
909 default:
910 break;
911 }
912 }
913 }
914
915 /**
916 * Handle host move.
917 * If the host DHCP server or gateway and it moved to the location different
918 * to user configured, unsets the connect mac and vlan
919 *
920 * @param host the host
921 */
922 private void hostMoved(Host host) {
923 if (this.dhcpServerConnectPoint == null) {
924 return;
925 }
926 if (this.dhcpGatewayIp != null) {
927 if (host.ipAddresses().contains(this.dhcpGatewayIp) &&
928 !host.locations().contains(this.dhcpServerConnectPoint)) {
929 this.dhcpConnectMac = null;
930 this.dhcpConnectVlan = null;
931 }
932 return;
933 }
934 if (this.dhcpServerIp != null) {
935 if (host.ipAddresses().contains(this.dhcpServerIp) &&
936 !host.locations().contains(this.dhcpServerConnectPoint)) {
937 this.dhcpConnectMac = null;
938 this.dhcpConnectVlan = null;
939 }
940 }
941 }
942
943 /**
944 * Handle host updated.
945 * If the host is DHCP server or gateway, update connect mac and vlan.
946 *
947 * @param host the host
948 */
949 private void hostUpdated(Host host) {
950 if (this.dhcpGatewayIp != null) {
951 if (host.ipAddresses().contains(this.dhcpGatewayIp)) {
952 this.dhcpConnectMac = host.mac();
953 this.dhcpConnectVlan = host.vlan();
954 }
955 return;
956 }
957 if (this.dhcpServerIp != null) {
958 if (host.ipAddresses().contains(this.dhcpServerIp)) {
959 this.dhcpConnectMac = host.mac();
960 this.dhcpConnectVlan = host.vlan();
961 }
962 }
963 }
964
965 /**
966 * Handle host removed.
967 * If the host is DHCP server or gateway, unset connect mac and vlan.
968 *
969 * @param host the host
970 */
971 private void hostRemoved(Host host) {
972 if (this.dhcpGatewayIp != null) {
973 if (host.ipAddresses().contains(this.dhcpGatewayIp)) {
974 this.dhcpConnectMac = null;
975 this.dhcpConnectVlan = null;
976 }
977 return;
978 }
979 if (this.dhcpServerIp != null) {
980 if (host.ipAddresses().contains(this.dhcpServerIp)) {
981 this.dhcpConnectMac = null;
982 this.dhcpConnectVlan = null;
983 }
984 }
985 }
Yi Tseng51301292017-07-28 13:02:59 -0700986}