experimental wireless IP subnet roaming with hostapd(8)

classic Classic list List threaded Threaded
1 message Options
Reply | Threaded
Open this post in threaded view
|

experimental wireless IP subnet roaming with hostapd(8)

Reyk Floeter-2
hi,

during the EuroBSDCon, while listening to claudio@'s talk about
OpenOSPFD, i had an idea to implement IP-based wireless roaming with
hostapd(8) in conjunction with ospfd(8). indeed, this idea and the
attached diff is very experimental... i need testers and comments ;)

why? it's a common problem of stupid large wireless networks to share
a collision domain between all access points and their stations (ARP
traffic, broadcasts, ...). on the other hand, any setups with an
individual subnet per access point didn't allow roaming. the answer by
cizzco-eeh and other vendors is a "CAPWAP" architecture - a central
access point appliance controlling a bunch of dumb wireless
termination points... that's not what we like to see in OpenBSD-based
proactive wireless networks.

1) the recipe is simple with the following ingredients:

- some access points running OpenBSD, hostapd(8), and ospfd(8);
  connected to the uplink with their own subnets.
- an uplink router / firewall running ospfd(8); connected to each
  subnet (i.e. by using several vlan(4) devices or many ports on a
  OSPF- capable routing switch).
- ...some known stations.

2) the intended roaming procedure is as follows:

- a station associates to one of the hostap interfaces.
- hostapd sends an ADD.notify message to the iapp interface.
- the station lookup is performed in the "address roaming" table,
  the assigned IP address and subnet is added as an alias to the
  according hostap interface.
- ospfd announces the new subnet via multicast to the iapp interface;
  the internal network and the other access points in their subnets.
- the uplink router listens to the ospf messages from the access
  point, adds the new route, and handles proper forwarding.
- other access points running hostapd are receiving the ADD.notify
  message to remove the IP address and subnet from their hostap
  interfaces.
- the wireless station uses the assigned IP address as it's default
  gateway in the same subnet.
- ...IPSec can be handled properly, it should be even possible to use
  IPSec roaming between the access points for crypto load
  distribution...

3) architecture:

[ client ] => ROAMING
    |172.23.1.2/30
    :
    :
    .172.23.1.1/30 (default gw)
[ AP1 ]         [ AP2 ]         [ AP3 ]         [ AP4 ]
   |172.42.1.2/30  |172.42.1.5/30  |172.42.1.9/30  |172.23.42.1.13/30
   |               +--+         +--+               |
   |                  |         |                  |
   +---------------[ uplink router ]---------------+
                           |
                      ( NETWORK )

4) configuration of the first access point:

# ifconfig ath0 nwid humppa media autoselect mode 11b chan 6 up
# ifconfig ath1 nwid humppa media autoselect mode 11b chan 11 up
# ifconfig sis0 172.42.1.2 netmask 255.255.255.252
# route add default 172.42.1.1

the interesting part of /etc/hostapd.conf:
---snip---
table <stations> {
        00:05:4e:11:d4:02 -> 172.23.1.1/30,
        00:06:44:e0:a4:03 -> 172.23.1.5/30,
        00:05:40:2b:d2:07 -> 172.23.1.9/30,
        00:02:42:24:d4:a2 -> 172.23.1.13/30,
}
set hostap interface { ath0, ath1 }
set iapp interface sis0
set iapp mode multicast
set iapp handle subtype address roaming <stations>
---snap---

very simple configuration in /etc/ospfd.conf:
---snip---
redistribute connected
area 0.0.0.1 {
        interface sis0 {
                auth-type crypt
                auth-md 1 secret
        }
}
---snap---

5) configuration of the first station (00:05:4e:11:d4:02):

# ifconfig ral0 172.23.1.2 netmask 255.255.255.252
# route add default 172.23.1.1

6) TODO and notes

- implement IAPP MOVE and CACHE messages for a better and safer handoff
- implement interoperation with dhcrelay(8) or dhcpd(8) for dynamic
  address configuration
- test it with some more access points and many stations
- security considerations

thanks for all the beer and good ideas at EuroBSDCon in basel...
nevertheless, beer is nice but some donations would be even better ;)...

reyk

Index: Makefile
===================================================================
RCS file: /cvs/src/usr.sbin/hostapd/Makefile,v
retrieving revision 1.4
diff -u -p -r1.4 Makefile
--- Makefile 30 Jul 2005 17:18:24 -0000 1.4
+++ Makefile 1 Dec 2005 03:05:35 -0000
@@ -1,7 +1,8 @@
 # $OpenBSD: Makefile,v 1.4 2005/07/30 17:18:24 reyk Exp $
 
 PROG= hostapd
-SRCS= privsep.c apme.c handle.c iapp.c llc.c hostapd.c print-802_11.c parse.y
+SRCS= privsep.c apme.c handle.c iapp.c llc.c hostapd.c \
+ print-802_11.c roaming.c parse.y
 MAN= hostapd.8 hostapd.conf.5
 LDADD= ${LIBEVENT}
 CFLAGS+= -Wall -I${.CURDIR}
Index: apme.c
===================================================================
RCS file: /cvs/src/usr.sbin/hostapd/apme.c,v
retrieving revision 1.8
diff -u -p -r1.8 apme.c
--- apme.c 1 Dec 2005 01:11:30 -0000 1.8
+++ apme.c 1 Dec 2005 03:05:35 -0000
@@ -126,6 +126,10 @@ hostapd_apme_term(struct hostapd_apme *a
 
  TAILQ_REMOVE(&cfg->c_apmes, apme, a_entries);
 
+ /* Remove all dynamic roaming addresses */
+ if (cfg->c_flags & HOSTAPD_CFG_F_PRIV)
+ hostapd_roaming_term(apme);
+
  hostapd_log(HOSTAPD_LOG_DEBUG,
     "%s: Host AP interface removed\n", apme->a_iface);
 
@@ -253,6 +257,7 @@ void
 hostapd_apme_frame(struct hostapd_apme *apme, u_int8_t *buf, u_int len)
 {
  struct hostapd_config *cfg = (struct hostapd_config *)apme->a_cfg;
+ struct hostapd_iapp *iapp = &cfg->c_iapp;
  struct hostapd_apme *other_apme;
  struct hostapd_node node;
  struct ieee80211_frame *wh;
@@ -310,9 +315,14 @@ hostapd_apme_frame(struct hostapd_apme *
  TAILQ_FOREACH(other_apme, &cfg->c_apmes, a_entries) {
  if (apme == other_apme)
  continue;
+ if (iapp->i_flags & HOSTAPD_IAPP_F_ROAMING)
+ hostapd_roaming_del(other_apme, &node);
  if (hostapd_apme_delnode(other_apme, &node) == 0)
  cfg->c_stats.cn_tx_apme++;
  }
+
+ if (iapp->i_flags & HOSTAPD_IAPP_F_ROAMING)
+ hostapd_roaming_add(apme, &node);
 
  hostapd_iapp_add_notify(apme, &node);
 
Index: hostapd.conf.5
===================================================================
RCS file: /cvs/src/usr.sbin/hostapd/hostapd.conf.5,v
retrieving revision 1.23
diff -u -p -r1.23 hostapd.conf.5
--- hostapd.conf.5 1 Dec 2005 02:03:57 -0000 1.23
+++ hostapd.conf.5 1 Dec 2005 03:05:35 -0000
@@ -157,6 +157,30 @@ messages.
 This option is enabled by default.
 .It Xo
 .Op Ic not
+.Ic address roaming
+.Aq Ar table
+.Xc
+Enable enable roaming of IP addresses.
+After successful association of a station,
+.Xr hostapd 8
+will lookup the station address in the table
+.Aq Ar table
+and configure any assigned IP address on the
+.Em hostap interface .
+Received
+.Em ADD.notify
+will trigger the deletion of the configured addresses,
+it indicates that the station left the
+.Em hostap .
+This option allows the implementation of large wireless networks
+without the need for sharing the same collision domain, if used in
+combination with a routing daemon like
+.Xr ospfd 8
+or
+.Xr bgpd 8 .
+It is disabled by default.
+.It Xo
+.Op Ic not
 .Ic radiotap
 .Xc
 Receive
Index: hostapd.h
===================================================================
RCS file: /cvs/src/usr.sbin/hostapd/hostapd.h,v
retrieving revision 1.14
diff -u -p -r1.14 hostapd.h
--- hostapd.h 1 Dec 2005 02:03:58 -0000 1.14
+++ hostapd.h 1 Dec 2005 03:05:36 -0000
@@ -267,8 +267,15 @@ struct hostapd_iapp {
 
 #define HOSTAPD_IAPP_F_ADD_NOTIFY 0x01
 #define HOSTAPD_IAPP_F_RADIOTAP 0x02
+#define HOSTAPD_IAPP_F_ROAMING_ADDRESS 0x04
 #define HOSTAPD_IAPP_F_DEFAULT \
  (HOSTAPD_IAPP_F_ADD_NOTIFY | HOSTAPD_IAPP_F_RADIOTAP)
+#define HOSTAPD_IAPP_F_ROAMING \
+ (HOSTAPD_IAPP_F_ROAMING_ADDRESS)
+#define HOSTAPD_IAPP_F_ADD \
+ (HOSTAPD_IAPP_F_ADD_NOTIFY | HOSTAPD_IAPP_F_ROAMING)
+
+ struct hostapd_table *i_addr_tbl;
 };
 
 struct hostapd_config {
@@ -354,6 +361,8 @@ int hostapd_priv_apme_getnode(struct ho
     struct hostapd_node *);
 int hostapd_priv_apme_setnode(struct hostapd_apme *,
     struct hostapd_node *node, int);
+int hostapd_priv_roaming(struct hostapd_apme *, struct hostapd_node *,
+    int);
 
 void hostapd_apme_init(struct hostapd_apme *);
 int hostapd_apme_deauth(struct hostapd_apme *);
@@ -385,6 +394,13 @@ int hostapd_llc_send_xid(struct hostapd
 int hostapd_handle_input(struct hostapd_apme *, u_int8_t *, u_int);
 
 void hostapd_print_ieee80211(u_int, u_int, u_int8_t *, u_int);
+
+void hostapd_roaming_term(struct hostapd_apme *);
+int hostapd_roaming(struct hostapd_apme *, struct hostapd_node *, int);
+int hostapd_roaming_add(struct hostapd_apme *,
+    struct hostapd_node *node);
+int hostapd_roaming_del(struct hostapd_apme *,
+    struct hostapd_node *node);
 
 __END_DECLS
 
Index: iapp.c
===================================================================
RCS file: /cvs/src/usr.sbin/hostapd/iapp.c,v
retrieving revision 1.13
diff -u -p -r1.13 iapp.c
--- iapp.c 1 Dec 2005 02:03:58 -0000 1.13
+++ iapp.c 1 Dec 2005 03:05:36 -0000
@@ -258,17 +258,22 @@ hostapd_iapp_input(int fd, short sig, vo
  * any allocated resources. Otherwise the received
  * ADD.notify message will be ignored.
  */
- if (cfg->c_flags & HOSTAPD_CFG_F_APME) {
+ if (iapp->i_flags & HOSTAPD_IAPP_F_ADD &&
+    cfg->c_flags & HOSTAPD_CFG_F_APME) {
  TAILQ_FOREACH(apme, &cfg->c_apmes, a_entries) {
- if ((ret = hostapd_apme_delnode(apme,
+ if (iapp->i_flags & HOSTAPD_IAPP_F_ROAMING)
+ hostapd_roaming_del(apme, &node);
+ if (iapp->i_flags & HOSTAPD_IAPP_F_ADD_NOTIFY &&
+    (ret = hostapd_apme_delnode(apme,
     &node)) == 0)
  cfg->c_stats.cn_tx_apme++;
  }
  } else
  ret = 0;
 
- hostapd_log(HOSTAPD_LOG, "%s: %s ADD notification "
-    "for %s at %s\n",
+ hostapd_log(iapp->i_flags & HOSTAPD_IAPP_F_ADD ?
+    HOSTAPD_LOG : HOSTAPD_LOG_VERBOSE,
+    "%s: %s ADD notification for %s at %s\n",
     iapp->i_iface, ret == 0 ?
     "received" : "ignored",
     etheraddr_string(node.ni_macaddr),
Index: parse.y
===================================================================
RCS file: /cvs/src/usr.sbin/hostapd/parse.y,v
retrieving revision 1.17
diff -u -p -r1.17 parse.y
--- parse.y 1 Dec 2005 02:03:58 -0000 1.17
+++ parse.y 1 Dec 2005 03:05:37 -0000
@@ -127,7 +127,7 @@ u_int negative;
 %token ERROR CONST TABLE NODE DELETE ADD LOG VERBOSE LIMIT QUICK SKIP
 %token REASON UNSPECIFIED EXPIRE LEAVE ASSOC TOOMANY NOT AUTHED ASSOCED
 %token RESERVED RSN REQUIRED INCONSISTENT IE INVALID MIC FAILURE OPEN
-%token ADDRESS PORT ON NOTIFY
+%token ADDRESS PORT ON NOTIFY ROAMING
 %token <v.string> STRING
 %token <v.val> VALUE
 %type <v.val> number
@@ -283,6 +283,18 @@ iappsubtype : not ADD NOTIFY
  {
  HOSTAPD_IAPP_FLAG(RADIOTAP);
  }
+ | not ADDRESS ROAMING table
+ {
+ if ((hostapd_cfg.c_iapp.i_addr_tbl =
+    hostapd_table_lookup(&hostapd_cfg, $4)) == NULL) {
+ yyerror("undefined table <%s>", $4);
+ free($4);
+ YYERROR;
+ }
+ free($4);
+
+ HOSTAPD_IAPP_FLAG(ROAMING_ADDRESS);
+ }
  ;
 
 eventopt : /* empty */
@@ -1018,6 +1030,7 @@ lookup(char *token)
  { "resend", RESEND },
  { "reserved", RESERVED },
  { "response", RESPONSE },
+ { "roaming", ROAMING },
  { "rsn", RSN },
  { "sec", SEC },
  { "set", SET },
Index: privsep.c
===================================================================
RCS file: /cvs/src/usr.sbin/hostapd/privsep.c,v
retrieving revision 1.16
diff -u -p -r1.16 privsep.c
--- privsep.c 1 Dec 2005 01:11:30 -0000 1.16
+++ privsep.c 1 Dec 2005 03:05:37 -0000
@@ -57,6 +57,8 @@ enum hostapd_cmd_types {
  PRIV_APME_GETNODE, /* Get a node from the Host AP */
  PRIV_APME_ADDNODE, /* Delete a node from the Host AP */
  PRIV_APME_DELNODE, /* Delete a node from the Host AP */
+ PRIV_APME_ADDROAMING, /* Add an address to the kernel */
+ PRIV_APME_DELROAMING, /* Delete an address from the kernel */
  PRIV_LLC_SEND_XID /* Send IEEE 802.3 LLC XID frame */
 };
 
@@ -193,7 +195,7 @@ hostapd_priv(int fd, short sig, void *ar
  struct ieee80211_nodereq nr;
  struct ifreq ifr;
  unsigned long request;
- int ret, cmd;
+ int ret = 0, cmd;
 
  /* Terminate the event if we got an invalid signal */
  if (sig != EV_READ)
@@ -287,6 +289,19 @@ hostapd_priv(int fd, short sig, void *ar
  hostapd_must_write(fd, &ret, sizeof(int));
  break;
 
+ case PRIV_APME_ADDROAMING:
+ case PRIV_APME_DELROAMING:
+ hostapd_log(HOSTAPD_LOG_DEBUG,
+    "[priv]: msg PRIV_APME_[ADD|DEL]ROAMING received\n");
+
+ hostapd_must_read(fd, &node, sizeof(struct hostapd_node));
+
+ if ((apme = hostapd_priv_getapme(fd, cfg)) == NULL)
+ break;
+ ret = hostapd_roaming(apme, &node, cmd == PRIV_APME_ADDROAMING);
+ hostapd_must_write(fd, &ret, sizeof(int));
+ break;
+
  default:
  hostapd_fatal("[priv]: unknown command %d\n", cmd);
  }
@@ -394,6 +409,32 @@ hostapd_priv_llc_xid(struct hostapd_conf
  if (ret == 0)
  cfg->c_stats.cn_tx_llc++;
  return (ret);
+}
+
+int
+hostapd_priv_roaming(struct hostapd_apme *apme, struct hostapd_node *node,
+    int add)
+{
+ struct hostapd_config *cfg = (struct hostapd_config *)apme->a_cfg;
+ int ret, cmd;
+
+ if (priv_fd < 0)
+ hostapd_fatal("%s: called from privileged portion\n", __func__);
+
+ if ((cfg->c_flags & HOSTAPD_CFG_F_APME) == 0)
+ hostapd_fatal("%s: Host AP is not available\n", __func__);
+
+ if (add)
+ cmd = PRIV_APME_ADDROAMING;
+ else
+ cmd = PRIV_APME_DELROAMING;
+ hostapd_must_write(priv_fd, &cmd, sizeof(int));
+ hostapd_must_write(priv_fd, node, sizeof(struct hostapd_node));
+ hostapd_must_write(priv_fd, &apme->a_iface, IFNAMSIZ);
+
+ hostapd_must_read(priv_fd, &ret, sizeof(int));
+
+ return (ret);
 }
 
 /*
Index: roaming.c
===================================================================
RCS file: roaming.c
diff -N roaming.c
--- /dev/null 1 Jan 1970 00:00:00 -0000
+++ roaming.c 1 Dec 2005 03:05:37 -0000
@@ -0,0 +1,157 @@
+/* $OpenBSD$ */
+
+/*
+ * Copyright (c) 2005 Reyk Floeter <[hidden email]>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/param.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/sysctl.h>
+#include <sys/tree.h>
+#include <sys/ioctl.h>
+
+#include <net/if.h>
+#include <net/if_dl.h>
+#include <net/if_media.h>
+#include <net/if_arp.h>
+#include <net/if_llc.h>
+#include <net/route.h>
+
+#include <netinet/in.h>
+#include <netinet/if_ether.h>
+#include <arpa/inet.h>
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include "hostapd.h"
+
+int hostapd_roaming_addr(struct hostapd_apme *, struct hostapd_inaddr *, int);
+
+void
+hostapd_roaming_term(struct hostapd_apme *apme)
+{
+ struct hostapd_config *cfg = (struct hostapd_config *)apme->a_cfg;
+ struct hostapd_iapp *iapp = &cfg->c_iapp;
+ struct hostapd_entry *entry;
+
+ if (iapp->i_flags & HOSTAPD_IAPP_F_ROAMING_ADDRESS &&
+    iapp->i_addr_tbl != NULL) {
+ RB_FOREACH(entry, hostapd_tree, &iapp->i_addr_tbl->t_tree) {
+ if ((entry->e_flags & HOSTAPD_ENTRY_F_INADDR) == 0)
+ continue;
+ hostapd_roaming_addr(apme, &entry->e_inaddr, 0);
+ }
+ }
+}
+
+int
+hostapd_roaming(struct hostapd_apme *apme, struct hostapd_node *node, int add)
+{
+ struct hostapd_config *cfg = (struct hostapd_config *)apme->a_cfg;
+ struct hostapd_iapp *iapp = &cfg->c_iapp;
+ struct hostapd_entry *entry;
+ int ret;
+
+ if (iapp->i_flags & HOSTAPD_IAPP_F_ROAMING_ADDRESS &&
+    iapp->i_addr_tbl != NULL) {
+ if ((entry = hostapd_entry_lookup(iapp->i_addr_tbl,
+    node->ni_macaddr)) == NULL ||
+    (entry->e_flags & HOSTAPD_ENTRY_F_INADDR) == 0)
+ return (ESRCH);
+ if ((ret = hostapd_roaming_addr(apme, &entry->e_inaddr,
+    add)) != 0)
+ return (ret);
+ }
+
+ return (0);
+}
+
+
+int
+hostapd_roaming_addr(struct hostapd_apme *apme, struct hostapd_inaddr *addr,
+    int add)
+{
+ struct hostapd_config *cfg = (struct hostapd_config *)apme->a_cfg;
+ struct hostapd_iapp *iapp = &cfg->c_iapp;
+ struct sockaddr_in *ifaddr, *ifmask, *ifbroadaddr;
+ struct ifaliasreq ifra;
+
+ bzero(&ifra, sizeof(ifra));
+
+ ifaddr = (struct sockaddr_in *)&ifra.ifra_addr;
+ ifaddr->sin_family = AF_INET;
+ ifaddr->sin_len = sizeof(struct sockaddr_in);
+ ifaddr->sin_addr.s_addr = addr->in_v4.s_addr;
+
+ ifbroadaddr = (struct sockaddr_in *)&ifra.ifra_broadaddr;
+ ifbroadaddr->sin_family = AF_INET;
+ ifbroadaddr->sin_len = sizeof(struct sockaddr_in);
+ ifbroadaddr->sin_addr.s_addr =
+    addr->in_v4.s_addr | htonl(0xffffffffUL >> addr->in_netmask);
+
+ if (add) {
+ ifmask = (struct sockaddr_in *)&ifra.ifra_mask;
+ ifmask->sin_family = AF_INET;
+ ifmask->sin_len = sizeof(struct sockaddr_in);
+ ifmask->sin_addr.s_addr =
+    htonl(0xffffffffUL << (32 - addr->in_netmask));
+ }
+
+ strlcpy(ifra.ifra_name, apme->a_iface, sizeof(ifra.ifra_name));
+ if (ioctl(cfg->c_apme_ctl, SIOCDIFADDR, &ifra) < 0) {
+ if (errno != EADDRNOTAVAIL) {
+ hostapd_log(HOSTAPD_LOG_VERBOSE,
+    "%s/%s: failed to delete address %s\n",
+    apme->a_iface, iapp->i_iface,
+    inet_ntoa(addr->in_v4));
+ return (errno);
+ }
+ }
+ if (add && ioctl(cfg->c_apme_ctl, SIOCAIFADDR, &ifra) < 0) {
+ if (errno != EEXIST) {
+ hostapd_log(HOSTAPD_LOG_VERBOSE,
+    "%s/%s: failed to add address %s\n",
+    apme->a_iface, iapp->i_iface,
+    inet_ntoa(addr->in_v4));
+ return (errno);
+ }
+ }
+
+ hostapd_log(HOSTAPD_LOG_VERBOSE,
+    "%s/%s: %s address %s\n",
+    apme->a_iface, iapp->i_iface,
+    add ? "added" : "deleted",
+    inet_ntoa(addr->in_v4));
+
+ return (0);
+}
+
+int
+hostapd_roaming_add(struct hostapd_apme *apme, struct hostapd_node *node)
+{
+ return (hostapd_priv_roaming(apme, node, 1));
+}
+
+int
+hostapd_roaming_del(struct hostapd_apme *apme, struct hostapd_node *node)
+{
+ return (hostapd_priv_roaming(apme, node, 0));
+}