/* * Copyright 2011 Future Systems * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.krakenapps.captiveportal.impl; import java.io.IOException; import java.net.InetAddress; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Invalidate; import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.apache.felix.ipojo.annotations.Validate; import org.krakenapps.captiveportal.CaptivePortal; import org.krakenapps.pcap.decoder.arp.ArpPacket; import org.krakenapps.pcap.decoder.ethernet.EthernetFrame; import org.krakenapps.pcap.decoder.ethernet.EthernetHeader; import org.krakenapps.pcap.decoder.ethernet.EthernetType; import org.krakenapps.pcap.decoder.ethernet.MacAddress; import org.krakenapps.pcap.live.PcapDevice; import org.krakenapps.pcap.live.PcapDeviceManager; import org.krakenapps.pcap.util.Arping; import org.osgi.service.prefs.BackingStoreException; import org.osgi.service.prefs.Preferences; import org.osgi.service.prefs.PreferencesService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(name = "captive-portal") @Provides public class CaptivePortalService implements CaptivePortal, Runnable { private static final String REDIRECT_IP_KEY = "redirect_ip"; private static final String PCAP_DEVICE_KEY = "pcap_device"; private static final int ARP_TIMEOUT = 3000; private static final String POISON_INTERVAL_KEY = "poison_interval"; private static final String QUARANTINED_KEY = "quarantined"; private static final String MIRRORING_KEY = "mirroring"; private static final String GATEWAY_KEY = "gateway"; private final Logger logger = LoggerFactory.getLogger(CaptivePortalService.class.getName()); private String deviceName; private int poisonInterval; private boolean mirroringMode; private InetAddress redirectAddress; private InetAddress gatewayAddress; private MacAddress gatewayMac; private Set<InetAddress> quarantinedHosts; private FakeRouter fakeRouter; // arp poisoner private Thread poisonThread; private volatile boolean doStop; // ip-mac mappings private Map<InetAddress, IpMapping> arpCache; @Requires private PreferencesService prefsvc; @Validate public void start() throws BackingStoreException, IOException { quarantinedHosts = Collections.newSetFromMap(new ConcurrentHashMap<InetAddress, Boolean>()); arpCache = new ConcurrentHashMap<InetAddress, IpMapping>(); // loading Preferences root = prefsvc.getSystemPreferences(); String redirectIp = root.get(REDIRECT_IP_KEY, null); if (redirectIp != null) this.redirectAddress = InetAddress.getByName(redirectIp); this.deviceName = root.get(PCAP_DEVICE_KEY, null); this.poisonInterval = root.getInt(POISON_INTERVAL_KEY, 10000); this.mirroringMode = root.getBoolean(MIRRORING_KEY, false); this.gatewayAddress = InetAddress.getByName(root.get(GATEWAY_KEY, null)); Preferences node = root.node(QUARANTINED_KEY); for (String ip : node.childrenNames()) { quarantinedHosts.add(InetAddress.getByName(ip)); } // fake router if (deviceName != null) { fakeRouter = new FakeRouter(deviceName, this); fakeRouter.start(); } // thread start startPoisoner(); logger.info("kraken captive portal: poisoning thread started"); } @Invalidate public void stop() { quarantinedHosts.clear(); stopPoisoner(); fakeRouter.stop(); } private void startPoisoner() { doStop = false; poisonThread = new Thread(this, "Captive Portal ARP Poisoner"); poisonThread.start(); } private void stopPoisoner() { doStop = true; poisonThread.interrupt(); } @Override public InetAddress getRedirectAddress() { return redirectAddress; } @Override public void setRedirectAddress(InetAddress ip) { try { Preferences root = prefsvc.getSystemPreferences(); root.put(REDIRECT_IP_KEY, ip.getHostAddress()); root.flush(); root.sync(); this.redirectAddress = ip; } catch (BackingStoreException e) { logger.error("kraken captive portal: cannot set redirect ip", e); throw new RuntimeException(e); } } @Override public Map<InetAddress, MacAddress> getArpCache() { Map<InetAddress, MacAddress> m = new HashMap<InetAddress, MacAddress>(); for (InetAddress ip : arpCache.keySet()) { IpMapping mapping = arpCache.get(ip); m.put(ip, mapping.mac); } return m; } @Override public MacAddress getQuarantinedMac(InetAddress ip) { IpMapping mapping = arpCache.get(ip); if (mapping == null) return null; return mapping.mac; } @Override public void run() { try { while (!doStop) { try { if (deviceName != null) spoof(); Thread.sleep(poisonInterval); } catch (IOException e) { logger.error("kraken captive portal: io error", e); } } } catch (InterruptedException e) { logger.error("kraken captive portal: poisoning thread interrupted"); } catch (Exception e) { logger.error("kraken captive portal: poisoning thread error", e); } finally { logger.info("kraken captive portal: poisoning thread stopped"); } } @Override public void spoof() throws IOException { // ensure gateway mac address MacAddress mac = Arping.query(gatewayAddress, ARP_TIMEOUT); arpCache.put(gatewayAddress, new IpMapping(mac)); // spoof quarantined hosts for (InetAddress ip : quarantinedHosts) { try { spoof(ip); } catch (IOException e) { logger.error("kraken captive portal: pcap error", e); } } } private void spoof(InetAddress targetIp) throws IOException { // get gateway mac address gatewayMac = arpCache.get(gatewayAddress).mac; // get target host mac address IpMapping mapping = getTargetMac(targetIp); if (mapping == null) return; MacAddress targetMac = mapping.mac; PcapDevice device = PcapDeviceManager.open(deviceName, ARP_TIMEOUT); try { // spoof host (as gateway) sendArpRequest(device, device.getMetadata().getMacAddress(), gatewayAddress, targetMac, targetIp); // spoof gateway (as host) sendArpRequest(device, device.getMetadata().getMacAddress(), targetIp, gatewayMac, gatewayAddress); } finally { device.close(); } } private void unspoof(InetAddress targetIp) throws IOException { // get gateway mac address gatewayMac = arpCache.get(gatewayAddress).mac; // get target host mac address IpMapping mapping = getTargetMac(targetIp); if (mapping == null) return; MacAddress targetMac = mapping.mac; // remove from arp cache arpCache.remove(targetIp); // recover host arp cache PcapDevice device = PcapDeviceManager.open(deviceName, ARP_TIMEOUT); sendArpRequest(device, gatewayMac, gatewayAddress, targetMac, targetIp); sendArpRequest(device, targetMac, targetIp, gatewayMac, gatewayAddress); } private IpMapping getTargetMac(InetAddress targetIp) throws IOException { IpMapping mapping = arpCache.get(targetIp); if (mapping == null || isTimeout(mapping.updated)) { MacAddress mac = Arping.query(targetIp, ARP_TIMEOUT); if (mac == null) { logger.trace("kraken captive portal: host not found for {}", targetIp); return null; } else { mapping = new IpMapping(mac); arpCache.put(targetIp, mapping); } } return mapping; } private void sendArpRequest(PcapDevice device, MacAddress senderMac, InetAddress senderIp, MacAddress targetMac, InetAddress targetIp) { MacAddress deviceMac = device.getMetadata().getMacAddress(); ArpPacket p = ArpPacket.createRequest(senderMac, senderIp, new MacAddress("00:00:00:00:00:00"), targetIp); EthernetHeader ethernetHeader = new EthernetHeader(deviceMac, targetMac, EthernetType.ARP); EthernetFrame frame = new EthernetFrame(ethernetHeader, p.getBuffer()); try { logger.debug("kraken captive portal: send arp, sender [mac: {}, ip: {}], target [mac: {}, ip: {}]", new Object[] { senderMac, senderIp, targetMac, targetIp }); device.write(frame.getBuffer()); } catch (IOException e) { logger.error("kraken captive portal: arp request error", e); } } private boolean isTimeout(Date date) { Calendar c = Calendar.getInstance(); c.setTime(date); c.add(Calendar.MILLISECOND, ARP_TIMEOUT); Date expire = c.getTime(); Date now = new Date(); return now.after(expire); } @Override public String getPcapDeviceName() { return deviceName; } @Override public void setPcapDeviceName(String name) { try { Preferences root = prefsvc.getSystemPreferences(); root.put(PCAP_DEVICE_KEY, name); root.flush(); root.sync(); this.deviceName = name; // restart fake router fakeRouter.stop(); fakeRouter = new FakeRouter(name, this); logger.info("kraken captive portal: fake router restarted"); // restart poisoner stopPoisoner(); startPoisoner(); logger.info("kraken captive portal: arp poisoner restarted"); } catch (BackingStoreException e) { logger.error("kraken captive portal: cannot set pcap device name", e); throw new RuntimeException(e); } catch (IOException e) { logger.error("kraken captive portal: fake router setting error", e); throw new RuntimeException(e); } } @Override public int getPoisonInterval() { return poisonInterval; } @Override public void setPoisonInterval(int milliseconds) { try { Preferences root = prefsvc.getSystemPreferences(); root.putInt(POISON_INTERVAL_KEY, milliseconds); root.flush(); root.sync(); this.poisonInterval = milliseconds; } catch (BackingStoreException e) { throw new RuntimeException(e); } } @Override public MacAddress getGatewayMacAddress() { return gatewayMac; } @Override public InetAddress getGatewayAddress() { return gatewayAddress; } @Override public void setGatewayAddress(InetAddress address) { try { Preferences root = prefsvc.getSystemPreferences(); root.put(GATEWAY_KEY, address.getHostAddress()); root.flush(); root.sync(); this.gatewayAddress = address; } catch (BackingStoreException e) { logger.error("kraken captive portal: cannot set gateway address", e); throw new RuntimeException(e); } } @Override public boolean getMirroringMode() { return mirroringMode; } @Override public void setMirroringMode(boolean mirroringMode) { try { Preferences root = prefsvc.getSystemPreferences(); root.putBoolean(MIRRORING_KEY, mirroringMode); root.flush(); root.sync(); this.mirroringMode = mirroringMode; } catch (BackingStoreException e) { throw new RuntimeException(e); } } @Override public Collection<InetAddress> getQuarantinedHosts() { return new ArrayList<InetAddress>(quarantinedHosts); } @Override public void quarantineHost(InetAddress address) throws IOException { try { Preferences root = prefsvc.getSystemPreferences(); Preferences p = root.node(QUARANTINED_KEY); p.node(address.getHostAddress()); root.flush(); root.sync(); quarantinedHosts.add(address); spoof(address); } catch (BackingStoreException e) { logger.error("kraken captive portal: cannot quarantine host " + address.getHostAddress(), e); throw new RuntimeException(e); } } @Override public void unquarantineHost(InetAddress address) { try { Preferences root = prefsvc.getSystemPreferences(); Preferences p = root.node(QUARANTINED_KEY); p.node(address.getHostAddress()).removeNode(); root.flush(); root.sync(); quarantinedHosts.remove(address); unspoof(address); } catch (BackingStoreException e) { logger.error("kraken captive portal: cannot unquarantine host " + address.getHostAddress(), e); throw new RuntimeException(e); } catch (IOException e) { logger.error("kraken captive portal: cannot recover arp cache for " + address.getHostAddress(), e); throw new RuntimeException(e); } } private static class IpMapping { private MacAddress mac; private Date updated; public IpMapping(MacAddress mac) { this.mac = mac; this.updated = new Date(); } } }