blob: 0df73e4a14e75dbd1f272690e359b3dfd268b47e [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;
Yi Tseng4fa05832017-08-17 13:08:31 -0700120 private Ip4Address relayAgentIp = null;
Yi Tseng51301292017-07-28 13:02:59 -0700121
Yi Tsenge72fbb52017-08-02 15:03:31 -0700122 @Activate
123 protected void activate() {
124 hostService.addListener(hostListener);
125 }
126
127 @Deactivate
128 protected void deactivate() {
129 hostService.removeListener(hostListener);
130 this.dhcpConnectMac = null;
131 this.dhcpConnectVlan = null;
Yi Tseng4fa05832017-08-17 13:08:31 -0700132
133 if (dhcpGatewayIp != null) {
134 hostService.stopMonitoringIp(dhcpGatewayIp);
135 } else if (dhcpServerIp != null) {
136 hostService.stopMonitoringIp(dhcpServerIp);
137 }
Yi Tsenge72fbb52017-08-02 15:03:31 -0700138 }
139
Yi Tseng51301292017-07-28 13:02:59 -0700140 @Override
141 public void setDhcpServerIp(IpAddress dhcpServerIp) {
142 checkNotNull(dhcpServerIp, "DHCP server IP can't be null");
143 checkState(dhcpServerIp.isIp4(), "Invalid server IP for DHCPv4 relay handler");
144 this.dhcpServerIp = dhcpServerIp.getIp4Address();
145 }
146
147 @Override
148 public void setDhcpServerConnectPoint(ConnectPoint dhcpServerConnectPoint) {
149 checkNotNull(dhcpServerConnectPoint, "Server connect point can't null");
150 this.dhcpServerConnectPoint = dhcpServerConnectPoint;
151 }
152
153 @Override
154 public void setDhcpConnectMac(MacAddress dhcpConnectMac) {
155 this.dhcpConnectMac = dhcpConnectMac;
156 }
157
158 @Override
159 public void setDhcpConnectVlan(VlanId dhcpConnectVlan) {
160 this.dhcpConnectVlan = dhcpConnectVlan;
161 }
162
163 @Override
164 public void setDhcpGatewayIp(IpAddress dhcpGatewayIp) {
165 if (dhcpGatewayIp != null) {
166 checkState(dhcpGatewayIp.isIp4(), "Invalid gateway IP for DHCPv4 relay handler");
167 this.dhcpGatewayIp = dhcpGatewayIp.getIp4Address();
168 } else {
169 // removes gateway config
170 this.dhcpGatewayIp = null;
171 }
172 }
173
174 @Override
175 public Optional<IpAddress> getDhcpServerIp() {
176 return Optional.ofNullable(dhcpServerIp);
177 }
178
179 @Override
180 public Optional<IpAddress> getDhcpGatewayIp() {
181 return Optional.ofNullable(dhcpGatewayIp);
182 }
183
184 @Override
185 public Optional<MacAddress> getDhcpConnectMac() {
186 return Optional.ofNullable(dhcpConnectMac);
187 }
188
189 @Override
Yi Tsenge72fbb52017-08-02 15:03:31 -0700190 public void setDefaultDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
191 if (configs.size() == 0) {
192 // no config to update
193 return;
194 }
195
196 // TODO: currently we pick up first DHCP server config.
197 // Will use other server configs in the future for HA.
198 DhcpServerConfig serverConfig = configs.iterator().next();
199 checkState(serverConfig.getDhcpServerConnectPoint().isPresent(),
200 "Connect point not exists");
201 checkState(serverConfig.getDhcpServerIp4().isPresent(),
202 "IP of DHCP server not exists");
203 Ip4Address oldServerIp = this.dhcpServerIp;
204 Ip4Address oldGatewayIp = this.dhcpGatewayIp;
205
206 // stop monitoring gateway or server
207 if (oldGatewayIp != null) {
208 hostService.stopMonitoringIp(oldGatewayIp);
209 } else if (oldServerIp != null) {
210 hostService.stopMonitoringIp(oldServerIp);
211 }
212
213 this.dhcpServerConnectPoint = serverConfig.getDhcpServerConnectPoint().get();
214 this.dhcpServerIp = serverConfig.getDhcpServerIp4().get();
215 this.dhcpGatewayIp = serverConfig.getDhcpGatewayIp4().orElse(null);
216
217 // reset server mac and vlan
218 this.dhcpConnectMac = null;
219 this.dhcpConnectVlan = null;
220
221 log.info("DHCP server connect point: " + this.dhcpServerConnectPoint);
222 log.info("DHCP server IP: " + this.dhcpServerIp);
223
224 IpAddress ipToProbe = MoreObjects.firstNonNull(this.dhcpGatewayIp, this.dhcpServerIp);
225 String hostToProbe = this.dhcpGatewayIp != null ? "gateway" : "DHCP server";
226
227 if (ipToProbe == null) {
228 log.warn("Server IP not set, can't probe it");
229 return;
230 }
231
232 log.info("Probing to resolve {} IP {}", hostToProbe, ipToProbe);
233 hostService.startMonitoringIp(ipToProbe);
234
235 Set<Host> hosts = hostService.getHostsByIp(ipToProbe);
236 if (!hosts.isEmpty()) {
237 Host host = hosts.iterator().next();
238 this.dhcpConnectVlan = host.vlan();
239 this.dhcpConnectMac = host.mac();
240 }
Yi Tseng4fa05832017-08-17 13:08:31 -0700241
242 this.relayAgentIp = serverConfig.getRelayAgentIp4().orElse(null);
Yi Tsenge72fbb52017-08-02 15:03:31 -0700243 }
244
245 @Override
246 public void setIndirectDhcpServerConfigs(Collection<DhcpServerConfig> configs) {
247 log.warn("Indirect config feature for DHCPv4 handler not implement yet");
248 }
249
Yi Tseng4fa05832017-08-17 13:08:31 -0700250 @Override
Yi Tseng51301292017-07-28 13:02:59 -0700251 public void processDhcpPacket(PacketContext context, BasePacket payload) {
252 checkNotNull(payload, "DHCP payload can't be null");
253 checkState(payload instanceof DHCP, "Payload is not a DHCP");
254 DHCP dhcpPayload = (DHCP) payload;
255 if (!configured()) {
256 log.warn("Missing DHCP relay server config. Abort packet processing");
257 return;
258 }
259
260 ConnectPoint inPort = context.inPacket().receivedFrom();
Yi Tseng51301292017-07-28 13:02:59 -0700261 checkNotNull(dhcpPayload, "Can't find DHCP payload");
262 Ethernet packet = context.inPacket().parsed();
263 DHCP.MsgType incomingPacketType = dhcpPayload.getOptions().stream()
264 .filter(dhcpOption -> dhcpOption.getCode() == OptionCode_MessageType.getValue())
265 .map(DhcpOption::getData)
266 .map(data -> DHCP.MsgType.getType(data[0]))
267 .findFirst()
268 .orElse(null);
269 checkNotNull(incomingPacketType, "Can't get message type from DHCP payload {}", dhcpPayload);
270 switch (incomingPacketType) {
271 case DHCPDISCOVER:
Yi Tsengdcef2c22017-08-05 20:34:06 -0700272 // Try update host if it is directly connected.
273 if (directlyConnected(dhcpPayload)) {
274 updateHost(context, dhcpPayload);
275 }
276
277 // Add the gateway IP as virtual interface IP for server to understand
Yi Tseng51301292017-07-28 13:02:59 -0700278 // the lease to be assigned and forward the packet to dhcp server.
279 Ethernet ethernetPacketDiscover =
Yi Tsengdcef2c22017-08-05 20:34:06 -0700280 processDhcpPacketFromClient(context, packet);
Yi Tseng51301292017-07-28 13:02:59 -0700281 if (ethernetPacketDiscover != null) {
282 writeRequestDhcpRecord(inPort, packet, dhcpPayload);
283 handleDhcpDiscoverAndRequest(ethernetPacketDiscover);
284 }
285 break;
286 case DHCPOFFER:
287 //reply to dhcp client.
288 Ethernet ethernetPacketOffer = processDhcpPacketFromServer(packet);
289 if (ethernetPacketOffer != null) {
290 writeResponseDhcpRecord(ethernetPacketOffer, dhcpPayload);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700291 sendResponseToClient(ethernetPacketOffer, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700292 }
293 break;
294 case DHCPREQUEST:
295 // add the gateway ip as virtual interface ip for server to understand
296 // the lease to be assigned and forward the packet to dhcp server.
297 Ethernet ethernetPacketRequest =
Yi Tsengdcef2c22017-08-05 20:34:06 -0700298 processDhcpPacketFromClient(context, packet);
Yi Tseng51301292017-07-28 13:02:59 -0700299 if (ethernetPacketRequest != null) {
300 writeRequestDhcpRecord(inPort, packet, dhcpPayload);
301 handleDhcpDiscoverAndRequest(ethernetPacketRequest);
302 }
303 break;
304 case DHCPACK:
305 // reply to dhcp client.
306 Ethernet ethernetPacketAck = processDhcpPacketFromServer(packet);
307 if (ethernetPacketAck != null) {
308 writeResponseDhcpRecord(ethernetPacketAck, dhcpPayload);
309 handleDhcpAck(ethernetPacketAck, dhcpPayload);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700310 sendResponseToClient(ethernetPacketAck, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700311 }
312 break;
313 case DHCPRELEASE:
314 // TODO: release the ip address from client
315 break;
316 default:
317 break;
318 }
319 }
320
321 /**
Yi Tsengdcef2c22017-08-05 20:34:06 -0700322 * Updates host to host store according to DHCP payload.
323 *
324 * @param context the packet context
325 * @param dhcpPayload the DHCP payload
326 */
327 private void updateHost(PacketContext context, DHCP dhcpPayload) {
328 ConnectPoint location = context.inPacket().receivedFrom();
329 HostLocation hostLocation = new HostLocation(location, System.currentTimeMillis());
330 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
331 VlanId vlanId = VlanId.vlanId(context.inPacket().parsed().getVlanID());
332 HostId hostId = HostId.hostId(macAddress, vlanId);
333 HostDescription desc = new DefaultHostDescription(macAddress, vlanId, hostLocation);
334 hostStore.createOrUpdateHost(DhcpRelayManager.PROVIDER_ID, hostId, desc, false);
335 }
336
337 /**
Yi Tseng51301292017-07-28 13:02:59 -0700338 * Checks if this app has been configured.
339 *
340 * @return true if all information we need have been initialized
341 */
342 public boolean configured() {
343 return dhcpServerConnectPoint != null && dhcpServerIp != null;
344 }
345
346 /**
Yi Tsengdcef2c22017-08-05 20:34:06 -0700347 * Returns the first interface ip from interface.
Yi Tseng51301292017-07-28 13:02:59 -0700348 *
Yi Tsengdcef2c22017-08-05 20:34:06 -0700349 * @param iface interface of one connect point
Yi Tseng51301292017-07-28 13:02:59 -0700350 * @return the first interface IP; null if not exists an IP address in
351 * these interfaces
352 */
Yi Tseng4fa05832017-08-17 13:08:31 -0700353 private Ip4Address getFirstIpFromInterface(Interface iface) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700354 checkNotNull(iface, "Interface can't be null");
355 return iface.ipAddressesList().stream()
Yi Tseng51301292017-07-28 13:02:59 -0700356 .map(InterfaceIpAddress::ipAddress)
357 .filter(IpAddress::isIp4)
358 .map(IpAddress::getIp4Address)
359 .findFirst()
360 .orElse(null);
361 }
362
363 /**
Yi Tsengdcef2c22017-08-05 20:34:06 -0700364 * Gets Interface facing to the server.
365 *
366 * @return the Interface facing to the server; null if not found
367 */
368 public Interface getServerInterface() {
369 if (dhcpServerConnectPoint == null || dhcpConnectVlan == null) {
370 return null;
371 }
372 return interfaceService.getInterfacesByPort(dhcpServerConnectPoint)
373 .stream()
374 .filter(iface -> iface.vlan().equals(dhcpConnectVlan) ||
375 iface.vlanUntagged().equals(dhcpConnectVlan) ||
376 iface.vlanTagged().contains(dhcpConnectVlan) ||
377 iface.vlanNative().equals(dhcpConnectVlan))
378 .findFirst()
379 .orElse(null);
380 }
381
382 /**
Yi Tseng51301292017-07-28 13:02:59 -0700383 * Build the DHCP discover/request packet with gateway IP(unicast packet).
384 *
385 * @param context the packet context
386 * @param ethernetPacket the ethernet payload to process
Yi Tseng51301292017-07-28 13:02:59 -0700387 * @return processed packet
388 */
389 private Ethernet processDhcpPacketFromClient(PacketContext context,
Yi Tsengdcef2c22017-08-05 20:34:06 -0700390 Ethernet ethernetPacket) {
391 Ip4Address clientInterfaceIp =
392 interfaceService.getInterfacesByPort(context.inPacket().receivedFrom())
393 .stream()
394 .map(Interface::ipAddressesList)
395 .flatMap(Collection::stream)
396 .map(InterfaceIpAddress::ipAddress)
397 .filter(IpAddress::isIp4)
398 .map(IpAddress::getIp4Address)
399 .findFirst()
400 .orElse(null);
401 if (clientInterfaceIp == null) {
402 log.warn("Can't find interface IP for client interface for port {}",
403 context.inPacket().receivedFrom());
404 return null;
405 }
406 Interface serverInterface = getServerInterface();
407 if (serverInterface == null) {
408 log.warn("Can't get server interface, ignore");
409 return null;
410 }
Yi Tseng4fa05832017-08-17 13:08:31 -0700411 Ip4Address ipFacingServer = getFirstIpFromInterface(serverInterface);
412 MacAddress macFacingServer = serverInterface.mac();
413 if (ipFacingServer == null || macFacingServer == null) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700414 log.warn("No IP address for server Interface {}", serverInterface);
Yi Tseng51301292017-07-28 13:02:59 -0700415 return null;
416 }
417 if (dhcpConnectMac == null) {
418 log.warn("DHCP {} not yet resolved .. Aborting DHCP "
419 + "packet processing from client on port: {}",
420 (dhcpGatewayIp == null) ? "server IP " + dhcpServerIp
421 : "gateway IP " + dhcpGatewayIp,
Yi Tsengdcef2c22017-08-05 20:34:06 -0700422 context.inPacket().receivedFrom());
Yi Tseng51301292017-07-28 13:02:59 -0700423 return null;
424 }
425 // get dhcp header.
426 Ethernet etherReply = (Ethernet) ethernetPacket.clone();
Yi Tseng4fa05832017-08-17 13:08:31 -0700427 etherReply.setSourceMACAddress(macFacingServer);
Yi Tseng51301292017-07-28 13:02:59 -0700428 etherReply.setDestinationMACAddress(dhcpConnectMac);
429 etherReply.setVlanID(dhcpConnectVlan.toShort());
430 IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
Yi Tseng4fa05832017-08-17 13:08:31 -0700431 ipv4Packet.setSourceAddress(ipFacingServer.toInt());
Yi Tseng51301292017-07-28 13:02:59 -0700432 ipv4Packet.setDestinationAddress(dhcpServerIp.toInt());
433 UDP udpPacket = (UDP) ipv4Packet.getPayload();
434 DHCP dhcpPacket = (DHCP) udpPacket.getPayload();
435
Yi Tsengdcef2c22017-08-05 20:34:06 -0700436 if (directlyConnected(dhcpPacket)) {
Yi Tseng51301292017-07-28 13:02:59 -0700437 ConnectPoint inPort = context.inPacket().receivedFrom();
438 VlanId vlanId = VlanId.vlanId(ethernetPacket.getVlanID());
439 // add connected in port and vlan
440 CircuitId cid = new CircuitId(inPort.toString(), vlanId);
441 byte[] circuitId = cid.serialize();
442 DhcpOption circuitIdSubOpt = new DhcpOption();
443 circuitIdSubOpt
444 .setCode(CIRCUIT_ID.getValue())
445 .setLength((byte) circuitId.length)
446 .setData(circuitId);
447
448 DhcpRelayAgentOption newRelayAgentOpt = new DhcpRelayAgentOption();
449 newRelayAgentOpt.setCode(OptionCode_CircuitID.getValue());
450 newRelayAgentOpt.addSubOption(circuitIdSubOpt);
451
452 // Removes END option first
453 List<DhcpOption> options = dhcpPacket.getOptions().stream()
454 .filter(opt -> opt.getCode() != OptionCode_END.getValue())
455 .collect(Collectors.toList());
456
457 // push relay agent option
458 options.add(newRelayAgentOpt);
459
460 // make sure option 255(End) is the last option
461 DhcpOption endOption = new DhcpOption();
462 endOption.setCode(OptionCode_END.getValue());
463 options.add(endOption);
464
465 dhcpPacket.setOptions(options);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700466
467 // Sets giaddr to IP address from the Interface which facing to
468 // DHCP client
469 dhcpPacket.setGatewayIPAddress(clientInterfaceIp.toInt());
Yi Tseng51301292017-07-28 13:02:59 -0700470 }
471
Yi Tseng4fa05832017-08-17 13:08:31 -0700472 // replace giaddr if relay agent IP is set
473 // FIXME for both direct and indirect case now, should be separated
474 if (relayAgentIp != null) {
475 dhcpPacket.setGatewayIPAddress(relayAgentIp.toInt());
476 }
477
Yi Tseng51301292017-07-28 13:02:59 -0700478 udpPacket.setPayload(dhcpPacket);
Yi Tsengdcef2c22017-08-05 20:34:06 -0700479 // As a DHCP relay, the source port should be server port(67) instead
480 // of client port(68)
481 udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
Yi Tseng51301292017-07-28 13:02:59 -0700482 udpPacket.setDestinationPort(UDP.DHCP_SERVER_PORT);
483 ipv4Packet.setPayload(udpPacket);
484 etherReply.setPayload(ipv4Packet);
485 return etherReply;
486 }
487
488 /**
489 * Writes DHCP record to the store according to the request DHCP packet (Discover, Request).
490 *
491 * @param location the location which DHCP packet comes from
492 * @param ethernet the DHCP packet
493 * @param dhcpPayload the DHCP payload
494 */
495 private void writeRequestDhcpRecord(ConnectPoint location,
496 Ethernet ethernet,
497 DHCP dhcpPayload) {
498 VlanId vlanId = VlanId.vlanId(ethernet.getVlanID());
499 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
500 HostId hostId = HostId.hostId(macAddress, vlanId);
501 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
502 if (record == null) {
503 record = new DhcpRecord(HostId.hostId(macAddress, vlanId));
504 } else {
505 record = record.clone();
506 }
507 record.addLocation(new HostLocation(location, System.currentTimeMillis()));
508 record.ip4Status(dhcpPayload.getPacketType());
509 record.setDirectlyConnected(directlyConnected(dhcpPayload));
510 if (!directlyConnected(dhcpPayload)) {
511 // Update gateway mac address if the host is not directly connected
512 record.nextHop(ethernet.getSourceMAC());
513 }
514 record.updateLastSeen();
515 dhcpRelayStore.updateDhcpRecord(HostId.hostId(macAddress, vlanId), record);
516 }
517
518 /**
519 * Writes DHCP record to the store according to the response DHCP packet (Offer, Ack).
520 *
521 * @param ethernet the DHCP packet
522 * @param dhcpPayload the DHCP payload
523 */
524 private void writeResponseDhcpRecord(Ethernet ethernet,
525 DHCP dhcpPayload) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700526 Optional<Interface> outInterface = getClientInterface(ethernet, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700527 if (!outInterface.isPresent()) {
528 log.warn("Failed to determine where to send {}", dhcpPayload.getPacketType());
529 return;
530 }
531
532 Interface outIface = outInterface.get();
533 ConnectPoint location = outIface.connectPoint();
Yi Tsengdcef2c22017-08-05 20:34:06 -0700534 VlanId vlanId = getVlanIdFromOption(dhcpPayload);
535 if (vlanId == null) {
536 vlanId = outIface.vlan();
537 }
Yi Tseng51301292017-07-28 13:02:59 -0700538 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
539 HostId hostId = HostId.hostId(macAddress, vlanId);
540 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
541 if (record == null) {
542 record = new DhcpRecord(HostId.hostId(macAddress, vlanId));
543 } else {
544 record = record.clone();
545 }
546 record.addLocation(new HostLocation(location, System.currentTimeMillis()));
547 if (dhcpPayload.getPacketType() == DHCP.MsgType.DHCPACK) {
548 record.ip4Address(Ip4Address.valueOf(dhcpPayload.getYourIPAddress()));
549 }
550 record.ip4Status(dhcpPayload.getPacketType());
551 record.setDirectlyConnected(directlyConnected(dhcpPayload));
552 record.updateLastSeen();
553 dhcpRelayStore.updateDhcpRecord(HostId.hostId(macAddress, vlanId), record);
554 }
555
556 /**
557 * Build the DHCP offer/ack with proper client port.
558 *
559 * @param ethernetPacket the original packet comes from server
560 * @return new packet which will send to the client
561 */
562 private Ethernet processDhcpPacketFromServer(Ethernet ethernetPacket) {
563 // get dhcp header.
564 Ethernet etherReply = (Ethernet) ethernetPacket.clone();
565 IPv4 ipv4Packet = (IPv4) etherReply.getPayload();
566 UDP udpPacket = (UDP) ipv4Packet.getPayload();
567 DHCP dhcpPayload = (DHCP) udpPacket.getPayload();
568
569 // determine the vlanId of the client host - note that this vlan id
570 // could be different from the vlan in the packet from the server
Yi Tsengdcef2c22017-08-05 20:34:06 -0700571 Interface clientInterface = getClientInterface(ethernetPacket, dhcpPayload).orElse(null);
Yi Tseng51301292017-07-28 13:02:59 -0700572
Yi Tsengdcef2c22017-08-05 20:34:06 -0700573 if (clientInterface == null) {
Yi Tseng51301292017-07-28 13:02:59 -0700574 log.warn("Cannot find the interface for the DHCP {}", dhcpPayload);
575 return null;
576 }
Yi Tsengdcef2c22017-08-05 20:34:06 -0700577 VlanId vlanId;
578 if (clientInterface.vlanTagged().isEmpty()) {
579 vlanId = clientInterface.vlan();
580 } else {
581 // might be multiple vlan in same interface
582 vlanId = getVlanIdFromOption(dhcpPayload);
583 }
584 if (vlanId == null) {
585 vlanId = VlanId.NONE;
586 }
587 etherReply.setVlanID(vlanId.toShort());
588 etherReply.setSourceMACAddress(clientInterface.mac());
Yi Tseng51301292017-07-28 13:02:59 -0700589
Yi Tsengdcef2c22017-08-05 20:34:06 -0700590 if (!directlyConnected(dhcpPayload)) {
591 // if client is indirectly connected, try use next hop mac address
592 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
593 HostId hostId = HostId.hostId(macAddress, vlanId);
594 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
595 if (record != null) {
596 // if next hop can be found, use mac address of next hop
597 record.nextHop().ifPresent(etherReply::setDestinationMACAddress);
598 } else {
599 // otherwise, discard the packet
600 log.warn("Can't find record for host id {}, discard packet", hostId);
601 return null;
602 }
Yi Tsengc03fa242017-08-17 17:43:38 -0700603 } else {
604 etherReply.setDestinationMACAddress(dhcpPayload.getClientHardwareAddress());
Yi Tsengdcef2c22017-08-05 20:34:06 -0700605 }
606
Yi Tseng51301292017-07-28 13:02:59 -0700607 // we leave the srcMac from the original packet
Yi Tseng51301292017-07-28 13:02:59 -0700608 // figure out the relay agent IP corresponding to the original request
Yi Tseng4fa05832017-08-17 13:08:31 -0700609 Ip4Address ipFacingClient = getFirstIpFromInterface(clientInterface);
610 if (ipFacingClient == null) {
Yi Tseng51301292017-07-28 13:02:59 -0700611 log.warn("Cannot determine relay agent interface Ipv4 addr for host {}/{}. "
612 + "Aborting relay for dhcp packet from server {}",
Yi Tsengdcef2c22017-08-05 20:34:06 -0700613 etherReply.getDestinationMAC(), clientInterface.vlan(),
Yi Tseng51301292017-07-28 13:02:59 -0700614 ethernetPacket);
615 return null;
616 }
617 // SRC_IP: relay agent IP
618 // DST_IP: offered IP
Yi Tseng4fa05832017-08-17 13:08:31 -0700619 ipv4Packet.setSourceAddress(ipFacingClient.toInt());
Yi Tseng51301292017-07-28 13:02:59 -0700620 ipv4Packet.setDestinationAddress(dhcpPayload.getYourIPAddress());
621 udpPacket.setSourcePort(UDP.DHCP_SERVER_PORT);
622 if (directlyConnected(dhcpPayload)) {
623 udpPacket.setDestinationPort(UDP.DHCP_CLIENT_PORT);
624 } else {
625 // forward to another dhcp relay
626 udpPacket.setDestinationPort(UDP.DHCP_SERVER_PORT);
627 }
628
629 udpPacket.setPayload(dhcpPayload);
630 ipv4Packet.setPayload(udpPacket);
631 etherReply.setPayload(ipv4Packet);
632 return etherReply;
633 }
634
Yi Tsengdcef2c22017-08-05 20:34:06 -0700635 /**
636 * Extracts VLAN ID from relay agent option.
637 *
638 * @param dhcpPayload the DHCP payload
639 * @return VLAN ID from DHCP payload; null if not exists
640 */
641 private VlanId getVlanIdFromOption(DHCP dhcpPayload) {
642 DhcpRelayAgentOption option = (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
643 if (option == null) {
644 return null;
645 }
646 DhcpOption circuitIdSubOption = option.getSubOption(CIRCUIT_ID.getValue());
647 if (circuitIdSubOption == null) {
648 return null;
649 }
650 try {
651 CircuitId circuitId = CircuitId.deserialize(circuitIdSubOption.getData());
652 return circuitId.vlanId();
653 } catch (IllegalArgumentException e) {
654 // can't deserialize the circuit ID
655 return null;
656 }
657 }
658
659 /**
660 * Removes DHCP relay agent information option (option 82) from DHCP payload.
661 * Also reset giaddr to 0
662 *
663 * @param ethPacket the Ethernet packet to be processed
664 * @return Ethernet packet processed
665 */
666 private Ethernet removeRelayAgentOption(Ethernet ethPacket) {
667 Ethernet ethernet = (Ethernet) ethPacket.clone();
668 IPv4 ipv4 = (IPv4) ethernet.getPayload();
669 UDP udp = (UDP) ipv4.getPayload();
670 DHCP dhcpPayload = (DHCP) udp.getPayload();
671
672 // removes relay agent information option
673 List<DhcpOption> options = dhcpPayload.getOptions();
674 options = options.stream()
675 .filter(option -> option.getCode() != OptionCode_CircuitID.getValue())
676 .collect(Collectors.toList());
677 dhcpPayload.setOptions(options);
678 dhcpPayload.setGatewayIPAddress(0);
679
680 udp.setPayload(dhcpPayload);
681 ipv4.setPayload(udp);
682 ethernet.setPayload(ipv4);
683 return ethernet;
684 }
685
Yi Tseng51301292017-07-28 13:02:59 -0700686
687 /**
688 * Check if the host is directly connected to the network or not.
689 *
690 * @param dhcpPayload the dhcp payload
691 * @return true if the host is directly connected to the network; false otherwise
692 */
693 private boolean directlyConnected(DHCP dhcpPayload) {
Yi Tseng2cf59912017-08-24 14:47:34 -0700694 DhcpRelayAgentOption relayAgentOption =
695 (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
Yi Tseng51301292017-07-28 13:02:59 -0700696
697 // Doesn't contains relay option
698 if (relayAgentOption == null) {
699 return true;
700 }
701
Yi Tseng2cf59912017-08-24 14:47:34 -0700702 // check circuit id, if circuit id is invalid, we say it is an indirect host
703 DhcpOption circuitIdOpt = relayAgentOption.getSubOption(CIRCUIT_ID.getValue());
Yi Tseng51301292017-07-28 13:02:59 -0700704
Yi Tseng2cf59912017-08-24 14:47:34 -0700705 try {
706 CircuitId.deserialize(circuitIdOpt.getData());
Yi Tseng51301292017-07-28 13:02:59 -0700707 return true;
Yi Tseng2cf59912017-08-24 14:47:34 -0700708 } catch (Exception e) {
709 // invalid circuit id
710 return false;
Yi Tseng51301292017-07-28 13:02:59 -0700711 }
Yi Tseng51301292017-07-28 13:02:59 -0700712 }
713
714
715 /**
716 * Send the DHCP ack to the requester host.
717 * Modify Host or Route store according to the type of DHCP.
718 *
719 * @param ethernetPacketAck the packet
720 * @param dhcpPayload the DHCP data
721 */
722 private void handleDhcpAck(Ethernet ethernetPacketAck, DHCP dhcpPayload) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700723 Optional<Interface> outInterface = getClientInterface(ethernetPacketAck, dhcpPayload);
Yi Tseng51301292017-07-28 13:02:59 -0700724 if (!outInterface.isPresent()) {
725 log.warn("Can't find output interface for dhcp: {}", dhcpPayload);
726 return;
727 }
728
729 Interface outIface = outInterface.get();
730 HostLocation hostLocation = new HostLocation(outIface.connectPoint(), System.currentTimeMillis());
731 MacAddress macAddress = MacAddress.valueOf(dhcpPayload.getClientHardwareAddress());
Yi Tsengdcef2c22017-08-05 20:34:06 -0700732 VlanId vlanId = getVlanIdFromOption(dhcpPayload);
733 if (vlanId == null) {
734 vlanId = outIface.vlan();
735 }
Yi Tseng51301292017-07-28 13:02:59 -0700736 HostId hostId = HostId.hostId(macAddress, vlanId);
737 Ip4Address ip = Ip4Address.valueOf(dhcpPayload.getYourIPAddress());
738
739 if (directlyConnected(dhcpPayload)) {
740 // Add to host store if it connect to network directly
741 Set<IpAddress> ips = Sets.newHashSet(ip);
742 HostDescription desc = new DefaultHostDescription(macAddress, vlanId,
743 hostLocation, ips);
744
745 // Replace the ip when dhcp server give the host new ip address
746 hostStore.createOrUpdateHost(DhcpRelayManager.PROVIDER_ID, hostId, desc, false);
747 } else {
748 // Add to route store if it does not connect to network directly
749 // Get gateway host IP according to host mac address
Yi Tsengdcef2c22017-08-05 20:34:06 -0700750 // TODO: remove relay store here
Yi Tseng51301292017-07-28 13:02:59 -0700751 DhcpRecord record = dhcpRelayStore.getDhcpRecord(hostId).orElse(null);
752
753 if (record == null) {
754 log.warn("Can't find DHCP record of host {}", hostId);
755 return;
756 }
757
758 MacAddress gwMac = record.nextHop().orElse(null);
759 if (gwMac == null) {
760 log.warn("Can't find gateway mac address from record {}", record);
761 return;
762 }
763
764 HostId gwHostId = HostId.hostId(gwMac, record.vlanId());
765 Host gwHost = hostService.getHost(gwHostId);
766
767 if (gwHost == null) {
768 log.warn("Can't find gateway host {}", gwHostId);
769 return;
770 }
771
772 Ip4Address nextHopIp = gwHost.ipAddresses()
773 .stream()
774 .filter(IpAddress::isIp4)
775 .map(IpAddress::getIp4Address)
776 .findFirst()
777 .orElse(null);
778
779 if (nextHopIp == null) {
780 log.warn("Can't find IP address of gateway {}", gwHost);
781 return;
782 }
783
784 Route route = new Route(Route.Source.STATIC, ip.toIpPrefix(), nextHopIp);
785 routeStore.updateRoute(route);
786 }
Yi Tseng51301292017-07-28 13:02:59 -0700787 }
788
789 /**
790 * forward the packet to ConnectPoint where the DHCP server is attached.
791 *
792 * @param packet the packet
793 */
794 private void handleDhcpDiscoverAndRequest(Ethernet packet) {
795 // send packet to dhcp server connect point.
796 if (dhcpServerConnectPoint != null) {
797 TrafficTreatment t = DefaultTrafficTreatment.builder()
798 .setOutput(dhcpServerConnectPoint.port()).build();
799 OutboundPacket o = new DefaultOutboundPacket(
800 dhcpServerConnectPoint.deviceId(), t, ByteBuffer.wrap(packet.serialize()));
801 if (log.isTraceEnabled()) {
802 log.trace("Relaying packet to dhcp server {}", packet);
803 }
804 packetService.emit(o);
805 } else {
806 log.warn("Can't find DHCP server connect point, abort.");
807 }
808 }
809
810
811 /**
812 * Gets output interface of a dhcp packet.
813 * If option 82 exists in the dhcp packet and the option was sent by
814 * ONOS (gateway address exists in ONOS interfaces), use the connect
815 * point and vlan id from circuit id; otherwise, find host by destination
816 * address and use vlan id from sender (dhcp server).
817 *
818 * @param ethPacket the ethernet packet
819 * @param dhcpPayload the dhcp packet
820 * @return an interface represent the output port and vlan; empty value
821 * if the host or circuit id not found
822 */
Yi Tsengdcef2c22017-08-05 20:34:06 -0700823 private Optional<Interface> getClientInterface(Ethernet ethPacket, DHCP dhcpPayload) {
Yi Tseng51301292017-07-28 13:02:59 -0700824 VlanId originalPacketVlanId = VlanId.vlanId(ethPacket.getVlanID());
825 IpAddress gatewayIpAddress = Ip4Address.valueOf(dhcpPayload.getGatewayIPAddress());
Yi Tsengdcef2c22017-08-05 20:34:06 -0700826
827 // get all possible interfaces for client
828 Set<Interface> clientInterfaces = interfaceService.getInterfacesByIp(gatewayIpAddress);
Yi Tseng51301292017-07-28 13:02:59 -0700829 DhcpRelayAgentOption option = (DhcpRelayAgentOption) dhcpPayload.getOption(OptionCode_CircuitID);
830
831 // Sent by ONOS, and contains circuit id
Yi Tsengdcef2c22017-08-05 20:34:06 -0700832 if (!clientInterfaces.isEmpty() && option != null) {
Yi Tseng51301292017-07-28 13:02:59 -0700833 DhcpOption circuitIdSubOption = option.getSubOption(CIRCUIT_ID.getValue());
834 try {
835 CircuitId circuitId = CircuitId.deserialize(circuitIdSubOption.getData());
836 ConnectPoint connectPoint = ConnectPoint.deviceConnectPoint(circuitId.connectPoint());
837 VlanId vlanId = circuitId.vlanId();
Yi Tsengdcef2c22017-08-05 20:34:06 -0700838 return clientInterfaces.stream()
839 .filter(iface -> iface.vlanUntagged().equals(vlanId) ||
840 iface.vlan().equals(vlanId) ||
841 iface.vlanNative().equals(vlanId) ||
842 iface.vlanTagged().contains(vlanId))
843 .filter(iface -> iface.connectPoint().equals(connectPoint))
844 .findFirst();
Yi Tseng51301292017-07-28 13:02:59 -0700845 } catch (IllegalArgumentException ex) {
846 // invalid circuit format, didn't sent by ONOS
847 log.debug("Invalid circuit {}, use information from dhcp payload",
848 circuitIdSubOption.getData());
849 }
850 }
851
852 // Use Vlan Id from DHCP server if DHCP relay circuit id was not
853 // sent by ONOS or circuit Id can't be parsed
Yi Tsengdcef2c22017-08-05 20:34:06 -0700854 // TODO: remove relay store from this method
Yi Tseng51301292017-07-28 13:02:59 -0700855 MacAddress dstMac = valueOf(dhcpPayload.getClientHardwareAddress());
856 Optional<DhcpRecord> dhcpRecord = dhcpRelayStore.getDhcpRecord(HostId.hostId(dstMac, originalPacketVlanId));
Yi Tsengdcef2c22017-08-05 20:34:06 -0700857 ConnectPoint clientConnectPoint = dhcpRecord
Yi Tseng51301292017-07-28 13:02:59 -0700858 .map(DhcpRecord::locations)
859 .orElse(Collections.emptySet())
860 .stream()
861 .reduce((hl1, hl2) -> {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700862 // find latest host connect point
Yi Tseng51301292017-07-28 13:02:59 -0700863 if (hl1 == null || hl2 == null) {
864 return hl1 == null ? hl2 : hl1;
865 }
866 return hl1.time() > hl2.time() ? hl1 : hl2;
867 })
Yi Tsengdcef2c22017-08-05 20:34:06 -0700868 .orElse(null);
Yi Tseng51301292017-07-28 13:02:59 -0700869
Yi Tsengdcef2c22017-08-05 20:34:06 -0700870 if (clientConnectPoint != null) {
871 return interfaceService.getInterfacesByPort(clientConnectPoint)
872 .stream()
873 .filter(iface -> iface.vlan().equals(originalPacketVlanId) ||
874 iface.vlanUntagged().equals(originalPacketVlanId))
875 .findFirst();
876 }
877 return Optional.empty();
Yi Tseng51301292017-07-28 13:02:59 -0700878 }
879
880 /**
881 * Send the response DHCP to the requester host.
882 *
883 * @param ethPacket the packet
884 * @param dhcpPayload the DHCP data
885 */
886 private void sendResponseToClient(Ethernet ethPacket, DHCP dhcpPayload) {
Yi Tsengdcef2c22017-08-05 20:34:06 -0700887 Optional<Interface> outInterface = getClientInterface(ethPacket, dhcpPayload);
888 if (directlyConnected(dhcpPayload)) {
889 ethPacket = removeRelayAgentOption(ethPacket);
890 }
891 if (!outInterface.isPresent()) {
892 log.warn("Can't find output interface for client, ignore");
893 return;
894 }
895 Interface outIface = outInterface.get();
896 TrafficTreatment treatment = DefaultTrafficTreatment.builder()
897 .setOutput(outIface.connectPoint().port())
898 .build();
899 OutboundPacket o = new DefaultOutboundPacket(
900 outIface.connectPoint().deviceId(),
901 treatment,
902 ByteBuffer.wrap(ethPacket.serialize()));
903 if (log.isTraceEnabled()) {
904 log.trace("Relaying packet to DHCP client {} via {}, vlan {}",
905 ethPacket,
906 outIface.connectPoint(),
907 outIface.vlan());
908 }
909 packetService.emit(o);
Yi Tseng51301292017-07-28 13:02:59 -0700910 }
Yi Tsenge72fbb52017-08-02 15:03:31 -0700911
912 class InternalHostListener implements HostListener {
913 @Override
914 public void event(HostEvent event) {
915 switch (event.type()) {
916 case HOST_ADDED:
917 case HOST_UPDATED:
918 hostUpdated(event.subject());
919 break;
920 case HOST_REMOVED:
921 hostRemoved(event.subject());
922 break;
923 case HOST_MOVED:
924 hostMoved(event.subject());
925 break;
926 default:
927 break;
928 }
929 }
930 }
931
932 /**
933 * Handle host move.
934 * If the host DHCP server or gateway and it moved to the location different
935 * to user configured, unsets the connect mac and vlan
936 *
937 * @param host the host
938 */
939 private void hostMoved(Host host) {
940 if (this.dhcpServerConnectPoint == null) {
941 return;
942 }
943 if (this.dhcpGatewayIp != null) {
944 if (host.ipAddresses().contains(this.dhcpGatewayIp) &&
945 !host.locations().contains(this.dhcpServerConnectPoint)) {
946 this.dhcpConnectMac = null;
947 this.dhcpConnectVlan = null;
948 }
949 return;
950 }
951 if (this.dhcpServerIp != null) {
952 if (host.ipAddresses().contains(this.dhcpServerIp) &&
953 !host.locations().contains(this.dhcpServerConnectPoint)) {
954 this.dhcpConnectMac = null;
955 this.dhcpConnectVlan = null;
956 }
957 }
958 }
959
960 /**
961 * Handle host updated.
962 * If the host is DHCP server or gateway, update connect mac and vlan.
963 *
964 * @param host the host
965 */
966 private void hostUpdated(Host host) {
967 if (this.dhcpGatewayIp != null) {
968 if (host.ipAddresses().contains(this.dhcpGatewayIp)) {
969 this.dhcpConnectMac = host.mac();
970 this.dhcpConnectVlan = host.vlan();
971 }
972 return;
973 }
974 if (this.dhcpServerIp != null) {
975 if (host.ipAddresses().contains(this.dhcpServerIp)) {
976 this.dhcpConnectMac = host.mac();
977 this.dhcpConnectVlan = host.vlan();
978 }
979 }
980 }
981
982 /**
983 * Handle host removed.
984 * If the host is DHCP server or gateway, unset connect mac and vlan.
985 *
986 * @param host the host
987 */
988 private void hostRemoved(Host host) {
989 if (this.dhcpGatewayIp != null) {
990 if (host.ipAddresses().contains(this.dhcpGatewayIp)) {
991 this.dhcpConnectMac = null;
992 this.dhcpConnectVlan = null;
993 }
994 return;
995 }
996 if (this.dhcpServerIp != null) {
997 if (host.ipAddresses().contains(this.dhcpServerIp)) {
998 this.dhcpConnectMac = null;
999 this.dhcpConnectVlan = null;
1000 }
1001 }
1002 }
Yi Tseng51301292017-07-28 13:02:59 -07001003}