Update AP selection heuristics for auto-join.
authorstsp <stsp@openbsd.org>
Wed, 15 Aug 2018 18:45:43 +0000 (18:45 +0000)
committerstsp <stsp@openbsd.org>
Wed, 15 Aug 2018 18:45:43 +0000 (18:45 +0000)
We now prefer stronger crypto over weaker crypto over none, prefer 5 GHz
band with reasonable RSSI, and use RSSI as a tie-breaker with a slight
advantage for 5GHz. Candidate APs are now ranked by a score which is
calculated based on these attributes.

There is likely room for improvements to make these heuristics
work well across many different environments, but it's a start.

ok phessler@

sys/net80211/ieee80211_node.c

index bd64aea..191fcfc 100644 (file)
@@ -1,4 +1,4 @@
-/*     $OpenBSD: ieee80211_node.c,v 1.141 2018/08/13 15:19:52 stsp Exp $       */
+/*     $OpenBSD: ieee80211_node.c,v 1.142 2018/08/15 18:45:43 stsp Exp $       */
 /*     $NetBSD: ieee80211_node.c,v 1.14 2004/05/09 09:18:47 dyoung Exp $       */
 
 /*-
@@ -191,7 +191,6 @@ ieee80211_del_ess(struct ieee80211com *ic, char *nwid, int all)
                                return;
                }
        }
-
 }
 
 int
@@ -296,118 +295,221 @@ ieee80211_add_ess(struct ieee80211com *ic, int wpa, int wep)
 }
 
 int
-ieee80211_ess_is_better(struct ieee80211com *ic, struct ieee80211_node *nicur,
-    struct ieee80211_node *selni)
+ieee80211_ess_cmp_crypto(struct ieee80211_node *nicur,
+    struct ieee80211_node *nican)
 {
-       uint8_t                  min_5ghz_rssi;
+       if ((nican->ni_rsnprotos & IEEE80211_PROTO_RSN) &&
+           (nicur->ni_rsnprotos & IEEE80211_PROTO_RSN) == 0)
+               return 1;
+       if ((nican->ni_rsnprotos & IEEE80211_PROTO_RSN) == 0 &&
+           (nicur->ni_rsnprotos & IEEE80211_PROTO_RSN))
+               return -1;
+
+       if ((nican->ni_rsnprotos & IEEE80211_PROTO_WPA) &&
+           (nicur->ni_rsnprotos & IEEE80211_PROTO_WPA) == 0)
+               return 1;
+       if ((nican->ni_rsnprotos & IEEE80211_PROTO_WPA) == 0 &&
+           (nicur->ni_rsnprotos & IEEE80211_PROTO_WPA))
+               return -1;
 
-       /* anything is better than nothing */
-       if (selni == NULL)
+       if ((nican->ni_capinfo & IEEE80211_CAPINFO_PRIVACY) &&
+           (nicur->ni_capinfo & IEEE80211_CAPINFO_PRIVACY) == 0)
                return 1;
+       if ((nican->ni_capinfo & IEEE80211_CAPINFO_PRIVACY) == 0 &&
+           (nicur->ni_capinfo & IEEE80211_CAPINFO_PRIVACY))
+               return -1;
+
+       return 0;
+}
+
+int
+ieee80211_ess_cmp_band(struct ieee80211com *ic,
+    struct ieee80211_node *nicur, struct ieee80211_node *nican)
+{
+       uint8_t min_5ghz_rssi;
 
        if (ic->ic_max_rssi)
                min_5ghz_rssi = IEEE80211_RSSI_THRES_RATIO_5GHZ;
        else
                min_5ghz_rssi = (uint8_t)IEEE80211_RSSI_THRES_5GHZ;
 
-       /* First 5GHz with acceptable signal */
-       if ((IEEE80211_IS_CHAN_5GHZ(nicur->ni_chan) &&
-           !IEEE80211_IS_CHAN_5GHZ(selni->ni_chan)) &&
-           nicur->ni_rssi > min_5ghz_rssi)
+       if (IEEE80211_IS_CHAN_5GHZ(nican->ni_chan) &&
+           IEEE80211_IS_CHAN_2GHZ(nicur->ni_chan) &&
+           nican->ni_rssi > min_5ghz_rssi)
                return 1;
 
-       /* Prefer 5GHz N over not-N */
-       if ((IEEE80211_IS_CHAN_5GHZ(nicur->ni_chan) &&
-           nicur->ni_rssi > min_5ghz_rssi) &&
-           ieee80211_node_supports_ht(nicur))
-               return 1;
+       if (IEEE80211_IS_CHAN_5GHZ(nicur->ni_chan) &&
+           IEEE80211_IS_CHAN_5GHZ(nican->ni_chan) &&
+           nicur->ni_rssi > min_5ghz_rssi)
+               return -1;
 
-       /* Settle for just N */
-       if (ieee80211_node_supports_ht(nicur) &&
-           !ieee80211_node_supports_ht(selni))
-               return 1;
+       return 0;
+}
+
+uint8_t
+ieee80211_ess_adjust_rssi(struct ieee80211com *ic, struct ieee80211_node *ni)
+{
+       uint8_t rssi = ni->ni_rssi;
+
+       /*
+        * Slightly punish 2 GHz RSSI values since they are usually
+        * stronger than 5 GHz RSSI values.
+        */
+       if (IEEE80211_IS_CHAN_2GHZ(ni->ni_chan)) {
+               if (ic->ic_max_rssi) {
+                       uint8_t p = (5 * ic->ic_max_rssi) / 100;
+                       if (rssi >= p)
+                               rssi -= p; /* punish by 5% */
+               } else  {
+                       if (rssi >= 8)
+                               rssi -= 8; /* punish by 8 dBm */
+               }
+       }
 
-       /* Last resort of best rssi */
-       if (nicur->ni_rssi > selni->ni_rssi)
+       return rssi;
+}
+
+int
+ieee80211_ess_cmp_rssi(struct ieee80211com *ic,
+    struct ieee80211_node *nicur, struct ieee80211_node *nican)
+{
+       uint8_t cur_rssi = ieee80211_ess_adjust_rssi(ic, nicur);
+       uint8_t can_rssi = ieee80211_ess_adjust_rssi(ic, nican);
+
+       if (can_rssi > cur_rssi)
                return 1;
+       if (cur_rssi > can_rssi)
+               return -1;
 
        return 0;
 }
 
+/*
+ * Given two APs, determine the "better" one of the two.
+ * We compute a score based on the following attributes:
+ *
+ *  crypto: wpa2 > wpa1 > wep > open
+ *  band: 5 GHz > 2 GHz provided 5 GHz rssi is above threshold
+ *  rssi: rssi1 > rssi2 as a numeric comparison with a slight
+ *         disadvantage for 2 GHz APs
+ *
+ * Crypto carries most weight, followed by band, followed by rssi.
+ */
 int
-ieee80211_match_ess(struct ieee80211com *ic)
+ieee80211_ess_is_better(struct ieee80211com *ic,
+    struct ieee80211_node *nicur, struct ieee80211_node *nican)
+{
+       int score_cur = 0, score_can = 0;
+
+       switch (ieee80211_ess_cmp_crypto(nicur, nican)) {
+       case 1:
+               score_can += 4;
+               break;
+       case -1:
+               score_cur += 4;
+               break;
+       default:
+               break;
+       }
+               
+       switch (ieee80211_ess_cmp_band(ic, nicur, nican)) {
+       case 1:
+               score_can += 2;
+               break;
+       case -1:
+               score_cur += 2;
+               break;
+       default:
+               break;
+       }
+
+       switch (ieee80211_ess_cmp_rssi(ic, nicur, nican)) {
+       case 1:
+               score_can += 1;
+               break;
+       case -1:
+               score_cur += 1;
+               break;
+       default:
+               break;
+       }
+
+       return score_can > score_cur;
+}
+
+/* Determine whether a candidate AP belongs to a given ESS. */
+int
+ieee80211_match_ess(struct ieee80211_ess *ess, struct ieee80211_node *ni)
+{
+       if (ess->esslen != ni->ni_esslen)
+               return 0;
+       if (memcmp(ess->essid, ni->ni_essid, ess->esslen) != 0)
+               return 0;
+
+       if (ess->flags & (IEEE80211_F_PSK | IEEE80211_F_RSNON)) {
+               /* Ensure same WPA version. */
+               if ((ni->ni_rsnprotos & IEEE80211_PROTO_RSN) &&
+                   (ess->rsnprotos & IEEE80211_PROTO_RSN) == 0)
+                       return 0;
+               if ((ni->ni_rsnprotos & IEEE80211_PROTO_WPA) &&
+                   (ess->rsnprotos & IEEE80211_PROTO_WPA) == 0)
+                       return 0;
+       } else if (ess->flags & IEEE80211_F_WEPON) {
+               if ((ni->ni_capinfo & IEEE80211_CAPINFO_PRIVACY) == 0)
+                       return 0;
+       }
+
+       return 1;
+}
+
+void
+ieee80211_switch_ess(struct ieee80211com *ic)
 {
        struct ifnet            *ifp = &ic->ic_if;
        struct ieee80211_ess    *ess, *seless = NULL;
        struct ieee80211_node   *ni, *selni = NULL;
 
        if (!ISSET(ifp->if_flags, IFF_RUNNING))
-               return (0);
+               return;
 
-       /*
-        * Apple's iOS uses the algorithm described at
-        * https://support.apple.com/en-us/HT202831
-        *
-        * basically:
-        * last network recently joined
-        * then strongest security
-        * then rssi level
-        */
+       /* Find the best AP matching an entry on our ESS join list. */
+       RBT_FOREACH(ni, ieee80211_tree, &ic->ic_tree) {
+               if ((ic->ic_flags & IEEE80211_F_DESBSSID) &&
+                   !IEEE80211_ADDR_EQ(ic->ic_des_bssid, ni->ni_bssid))
+                       continue;
 
-       /* skip if it's the same network */
-       TAILQ_FOREACH(ess, &ic->ic_ess, ess_next) {
-               if (LINK_STATE_IS_UP(ifp->if_link_state) &&
-                   ess->esslen == ic->ic_des_esslen &&
-                   (memcmp(ic->ic_des_essid, ess->essid,
-                    IEEE80211_NWID_LEN) == 0)) {
-                       if (ifp->if_flags & IFF_DEBUG) {
-                               printf(" %s: staying on ",
-                                   ifp->if_xname);
-                               ieee80211_print_essid(ess->essid, ess->esslen);
-                               printf("\n");
-                       }
-                       return (0);
+               TAILQ_FOREACH(ess, &ic->ic_ess, ess_next) {
+                       if (ieee80211_match_ess(ess, ni))
+                               break;
                }
-       }
-
-       TAILQ_FOREACH(ess, &ic->ic_ess, ess_next) {
-               RBT_FOREACH(ni, ieee80211_tree, &ic->ic_tree) {
-                       if (memcmp(ess->essid, ni->ni_essid,
-                           IEEE80211_NWID_LEN) != 0 ||
-                           ni->ni_fails != 0)
-                               continue;
-
-                       /* make sure encryptions match */
-                       if (ess->flags &
-                           (IEEE80211_F_PSK | IEEE80211_F_RSNON)) {
-                               if ((ni->ni_capinfo &
-                                   IEEE80211_CAPINFO_PRIVACY) == 0)
-                                       continue;
-                       } else {
-                               if (ni->ni_capinfo & IEEE80211_CAPINFO_PRIVACY)
-                                       continue;
-                       }
+               if (ess == NULL)
+                       continue;
 
-                       if ((ic->ic_flags & IEEE80211_F_DESBSSID) &&
-                           !IEEE80211_ADDR_EQ(ic->ic_des_bssid, ni->ni_bssid))
-                               continue;
+               if (selni == NULL) {
+                       seless = ess;
+                       selni = ni;
+                       continue;
+               }
 
-                       if (ieee80211_ess_is_better(ic, ni, selni) > 0) {
-                               seless = ess;
-                               selni = ni;
-                       }
+               if (ieee80211_ess_is_better(ic, selni, ni)) {
+                       seless = ess;
+                       selni = ni;
                }
        }
 
        if (seless && !(seless->esslen == ic->ic_des_esslen &&
            (memcmp(ic->ic_des_essid, seless->essid,
             IEEE80211_NWID_LEN) == 0))) {
+               if (ifp->if_flags & IFF_DEBUG) {
+                       printf("%s: switching to network ", ifp->if_xname);
+                       ieee80211_print_essid(seless->essid, seless->esslen);
+                       printf("\n");
+
+               }
                ieee80211_set_ess(ic, seless->essid);
-               return (1);
        }
-
-       return (0);
 }
+
 void
 ieee80211_set_ess(struct ieee80211com *ic, char *nwid)
 {
@@ -1152,7 +1254,7 @@ ieee80211_end_scan(struct ifnet *ifp)
 
        /* Possibly switch which ssid we are associated with */
        if (!bgscan && ic->ic_opmode == IEEE80211_M_STA)
-               ieee80211_match_ess(ic);
+               ieee80211_switch_ess(ic);
 
        selbs = ieee80211_node_choose_bss(ic, bgscan, &curbs);
        if (bgscan) {