diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..51153d6d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,269 @@ +name: IWD CI + +# +# The basic flow of the CI is as follows: +# +# 1. Get all inputs, or default values, and set as 'setup' job output +# 2. Find any cached binaries (hostapd, wpa_supplicant, kernel etc) +# 3. Checkout all dependent repositories +# 4. Tar all local files. This is an unfortunate requirement since github jobs +# cannot share local files. Since there are multiple CI's acting on the same +# set of repositories it makes more sense to retain these and re-download +# them for each CI job. +# 5. Run each CI, currently 'main' and 'musl'. +# * 'main' is the default IWD CI which runs all the build steps as well +# as test-runner +# * 'musl' uses an alpine docker image to test the build on musl-libc +# +# Both CI's use the 'iwd-ci' repo which calls into 'ci-docker'. The +# 'ci-docker' action essentially re-implements the native Github docker +# action but allows arbitrary options to be passed in (e.g. privileged or +# mounting non-standard directories) +# + +on: + pull_request: + workflow_dispatch: + inputs: + tests: + description: Tests to run (comma separated, no spaces) + default: all + kernel: + description: Kernel version + default: '6.2' + hostapd_version: + description: Hostapd and wpa_supplicant version + default: 'hostap_2_11' + ell_ref: + description: ELL reference + default: refs/heads/workflow + + repository_dispatch: + types: [ell-dispatch] + +jobs: + setup: + runs-on: ubuntu-22.04 + outputs: + tests: ${{ steps.inputs.outputs.tests }} + kernel: ${{ steps.inputs.outputs.kernel }} + hostapd_version: ${{ steps.inputs.outputs.hostapd_version }} + ell_ref: ${{ steps.inputs.outputs.ell_ref }} + repository: ${{ steps.inputs.outputs.repository }} + ref_branch: ${{ steps.inputs.outputs.ref_branch }} + steps: + # + # This makes CI inputs consistent depending on how the CI was invoked: + # * pull_request trigger won't have any inputs, so these need to be set + # to default values. + # * workflow_dispatch sets all inputs from the user input + # * repository_dispatch sets all inputs based on the JSON payload of + # the request. + # + - name: Setup Inputs + id: inputs + run: | + if [ ${{ github.event_name }} == 'workflow_dispatch' ] + then + TESTS=${{ github.event.inputs.tests }} + KERNEL=${{ github.event.inputs.kernel }} + HOSTAPD_VERSION=${{ github.event.inputs.hostapd_version }} + ELL_REF=${{ github.event.inputs.ell_ref }} + REF="$GITHUB_REF" + REPO="$GITHUB_REPOSITORY" + elif [ ${{ github.event_name }} == 'repository_dispatch' ] + then + TESTS=all + KERNEL=5.19 + HOSTAPD_VERSION=09a281e52a25b5461c4b08d261f093181266a554 + ELL_REF=${{ github.event.client_payload.ref }} + REF=$ELL_REF + REPO=${{ github.event.client_payload.repo }} + else + TESTS=all + KERNEL=5.19 + HOSTAPD_VERSION=09a281e52a25b5461c4b08d261f093181266a554 + ELL_REF="refs/heads/workflow" + REF="$GITHUB_REF" + REPO="$GITHUB_REPOSITORY" + fi + + # + # Now that the inputs are sorted, set the output of this step to these + # values so future jobs can refer to them. + # + echo "tests=$TESTS" >> $GITHUB_OUTPUT + echo "kernel=$KERNEL" >> $GITHUB_OUTPUT + echo "hostapd_version=$HOSTAPD_VERSION" >> $GITHUB_OUTPUT + echo "ell_ref=$ELL_REF" >> $GITHUB_OUTPUT + echo "repository=$REPO" >> $GITHUB_OUTPUT + echo "ref_branch=$REF" >> $GITHUB_OUTPUT + + - name: Cache UML Kernel + id: cache-uml-kernel + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/cache/um-linux-${{ steps.inputs.outputs.kernel }} + key: um-linux-${{ steps.inputs.outputs.kernel }}_ubuntu22 + + - name: Cache Hostapd + id: cache-hostapd + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/cache/hostapd_${{ steps.inputs.outputs.hostapd_version }} + ${{ github.workspace }}/cache/hostapd_cli_${{ steps.inputs.outputs.hostapd_version }} + key: hostapd_${{ steps.inputs.outputs.hostapd_version }}_ssl3 + + - name: Cache WpaSupplicant + id: cache-wpas + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/cache/wpa_supplicant_${{ steps.inputs.outputs.hostapd_version }} + ${{ github.workspace }}/cache/wpa_cli_${{ steps.inputs.outputs.hostapd_version }} + key: wpa_supplicant_${{ steps.inputs.outputs.hostapd_version }}_ssl3 + + - name: Checkout IWD + uses: actions/checkout@v3 + with: + path: iwd + repository: IWDTestBot/iwd + token: ${{ secrets.ACTION_TOKEN }} + + - name: Checkout ELL + uses: actions/checkout@v3 + with: + path: ell + repository: IWDTestBot/ell + ref: ${{ steps.inputs.outputs.ell_ref }} + + - name: Checkout CiBase + uses: actions/checkout@v3 + with: + repository: IWDTestBot/cibase + path: cibase + + - name: Checkout CI + uses: actions/checkout@v3 + with: + repository: IWDTestBot/iwd-ci + path: iwd-ci + + - name: Tar files + run: | + FILES="iwd ell cibase iwd-ci" + + if [ "${{ steps.cache-uml-kernel.outputs.cache-hit }}" == 'true' ] + then + FILES+=" ${{ github.workspace }}/cache/um-linux-${{ steps.inputs.outputs.kernel }}" + fi + + if [ "${{ steps.cache-hostapd.outputs.cache-hit }}" == 'true' ] + then + FILES+=" ${{ github.workspace }}/cache/hostapd_${{ steps.inputs.outputs.hostapd_version }}" + FILES+=" ${{ github.workspace }}/cache/hostapd_cli_${{ steps.inputs.outputs.hostapd_version }}" + fi + if [ "${{ steps.cache-wpas.outputs.cache-hit }}" == 'true' ] + then + FILES+=" ${{ github.workspace }}/cache/wpa_supplicant_${{ steps.inputs.outputs.hostapd_version }}" + FILES+=" ${{ github.workspace }}/cache/wpa_cli_${{ steps.inputs.outputs.hostapd_version }}" + fi + + tar -cvf archive.tar $FILES + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: iwd-artifacts + path: | + archive.tar + + iwd-alpine-ci: + runs-on: ubuntu-22.04 + needs: setup + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: iwd-artifacts + + - name: Untar + run: tar -xf archive.tar + + - name: Modprobe pkcs8_key_parser + run: | + sudo modprobe pkcs8_key_parser + + - name: Alpine CI + uses: IWDTestBot/iwd-ci@master + with: + ref_branch: ${{ needs.setup.outputs.ref_branch }} + repository: ${{ needs.setup.outputs.repository }} + github_token: ${{ secrets.ACTION_TOKEN }} + email_token: ${{ secrets.EMAIL_TOKEN }} + patchwork_token: ${{ secrets.PATCHWORK_TOKEN }} + ci: musl + + iwd-ci: + runs-on: ubuntu-22.04 + needs: setup + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: iwd-artifacts + + - name: Untar + run: tar -xf archive.tar + + - name: Cache UML Kernel + id: cache-uml-kernel + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/cache/um-linux-${{ needs.setup.outputs.kernel }} + key: um-linux-${{ needs.setup.outputs.kernel }}_ubuntu22 + + - name: Cache Hostapd + id: cache-hostapd + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/cache/hostapd_${{ needs.setup.outputs.hostapd_version }} + ${{ github.workspace }}/cache/hostapd_cli_${{ needs.setup.outputs.hostapd_version }} + key: hostapd_${{ needs.setup.outputs.hostapd_version }}_ssl3 + + - name: Cache WpaSupplicant + id: cache-wpas + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/cache/wpa_supplicant_${{ needs.setup.outputs.hostapd_version }} + ${{ github.workspace }}/cache/wpa_cli_${{ needs.setup.outputs.hostapd_version }} + key: wpa_supplicant_${{ needs.setup.outputs.hostapd_version }}_ssl3 + + - name: Modprobe pkcs8_key_parser + run: | + sudo modprobe pkcs8_key_parser + echo ${{ needs.setup.outputs.ref_branch }} + echo ${{ needs.setup.outputs.repository }} + + - name: Run CI + uses: IWDTestBot/iwd-ci@master + with: + ref_branch: ${{ needs.setup.outputs.ref_branch }} + repository: ${{ needs.setup.outputs.repository }} + tests: ${{ needs.setup.outputs.tests }} + kernel: ${{ needs.setup.outputs.kernel }} + hostapd_version: ${{ needs.setup.outputs.hostapd_version }} + github_token: ${{ secrets.ACTION_TOKEN }} + email_token: ${{ secrets.EMAIL_TOKEN }} + patchwork_token: ${{ secrets.PATCHWORK_TOKEN }} + ci: main + + - name: Upload Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-runner-logs + path: ${{ github.workspace }}/log diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml new file mode 100644 index 00000000..91f9073d --- /dev/null +++ b/.github/workflows/coverity.yml @@ -0,0 +1,86 @@ +name: Coverity Scan and Submit +description: Runs a coverity scan, then sends results to the cloud +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +jobs: + scan-and-submit: + runs-on: ubuntu-22.04 + steps: + - name: Lookup latest tool + id: cache-lookup + run: | + hash=$(curl https://scan.coverity.com/download/cxx/linux64 \ + --data "token=${{ secrets.COVERITY_IWD_TOKEN }}&project=IWD&md5=1"); + echo "hash=${hash}" >> $GITHUB_OUTPUT + + - name: Get cached coverity tool + id: build-cache + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/cov-analysis + key: cov-build-cxx-linux64-${{ steps.cache-lookup.outputs.hash }} + + - name: Download Coverity Build Tool + if: steps.build-cache.outputs.cache-hit != 'true' + run: | + curl https://scan.coverity.com/download/cxx/linux64 \ + --no-progress-meter \ + --output cov-analysis.tar.gz \ + --data "token=${{ secrets.COVERITY_IWD_TOKEN }}&project=IWD" + shell: bash + working-directory: ${{ github.workspace }} + + - if: steps.build-cache.outputs.cache-hit != 'true' + run: mkdir cov-analysis + shell: bash + working-directory: ${{ github.workspace }} + + - if: steps.build-cache.outputs.cache-hit != 'true' + run: tar -xzf cov-analysis.tar.gz --strip 1 -C cov-analysis + shell: bash + working-directory: ${{ github.workspace }} + + - name: Checkout IWD + uses: actions/checkout@v3 + with: + path: ${{ github.workspace }}/iwd + repository: IWDTestBot/iwd + token: ${{ secrets.ACTION_TOKEN }} + + - name: Checkout ELL + uses: actions/checkout@v3 + with: + path: ${{ github.workspace }}/ell + repository: IWDTestBot/ell + token: ${{ secrets.ACTION_TOKEN }} + + - name: Configure IWD + run: | + cd ${{ github.workspace }}/iwd + ./bootstrap-configure --disable-manual-pages + + - name: Build with cov-build + run: | + export PATH="${{ github.workspace }}/cov-analysis/bin:${PATH}" + cov-build --dir cov-int make -j4 + shell: bash + working-directory: ${{ github.workspace }}/iwd + + - name: Tar results + run: tar -czvf cov-int.tgz cov-int + shell: bash + working-directory: ${{ github.workspace }}/iwd + + - name: Submit results to Coverity Scan + if: ${{ ! inputs.dry_run }} + run: | + curl \ + --form token="${{ secrets.COVERITY_IWD_TOKEN }}" \ + --form email="iwd.ci.bot@gmail.com" \ + --form file=@cov-int.tgz \ + "https://scan.coverity.com/builds?project=IWD" + shell: bash + working-directory: ${{ github.workspace }}/iwd diff --git a/.github/workflows/pw-to-pr-email.txt b/.github/workflows/pw-to-pr-email.txt new file mode 100644 index 00000000..0ad6d765 --- /dev/null +++ b/.github/workflows/pw-to-pr-email.txt @@ -0,0 +1,16 @@ +This is an automated email and please do not reply to this email. + +Dear Submitter, + +Thank you for submitting the patches to the IWD mailing list. +While preparing the CI tests, the patches you submitted couldn't be applied to the current HEAD of the repository. + +----- Output ----- +{} + +Please resolve the issue and submit the patches again. + + +--- +Regards, +IWDTestBot diff --git a/.github/workflows/pw-to-pr.json b/.github/workflows/pw-to-pr.json new file mode 100644 index 00000000..b4491413 --- /dev/null +++ b/.github/workflows/pw-to-pr.json @@ -0,0 +1,14 @@ +{ + "email": { + "enable": true, + "server": "smtp.gmail.com", + "port": 587, + "user": "iwd.ci.bot@gmail.com", + "starttls": true, + "default-to": "prestwoj@gmail.com", + "only-maintainers": false, + "maintainers": [ + "prestwoj@gmail.com" + ] + } +} diff --git a/.github/workflows/schedule_work.yml b/.github/workflows/schedule_work.yml new file mode 100644 index 00000000..cfc14fba --- /dev/null +++ b/.github/workflows/schedule_work.yml @@ -0,0 +1,43 @@ +name: Sync Upstream +on: + schedule: + - cron: "*/15 * * * *" + workflow_dispatch: + +jobs: + repo-sync: + runs-on: ubuntu-latest + steps: + + - uses: actions/checkout@v2 + with: + persist-credentials: false + fetch-depth: 0 + + - name: Manage Repo + uses: IWDTestBot/action-manage-repo@master + with: + src_repo: "https://git.kernel.org/pub/scm/network/wireless/iwd.git" + src_branch: "master" + dest_branch: "master" + workflow_branch: "workflow" + github_token: ${{ secrets.GITHUB_TOKEN }} + + create_pr: + needs: repo-sync + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Patchwork to PR + uses: IWDTestBot/action-patchwork-to-pr@master + with: + pw_key_str: "user" + github_token: ${{ secrets.ACTION_TOKEN }} + email_token: ${{ secrets.EMAIL_TOKEN }} + patchwork_token: ${{ secrets.PATCHWORK_TOKEN }} + config: https://raw.githubusercontent.com/IWDTestBot/iwd/workflow/.github/workflows/pw-to-pr.json + patchwork_id: "408" + email_message: https://raw.githubusercontent.com/IWDTestBot/iwd/workflow/.github/workflows/pw-to-pr-email.txt diff --git a/Makefile.am b/Makefile.am index 92adfa6e..c01cd4c4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -274,6 +274,8 @@ src_iwd_SOURCES = src/main.c linux/nl80211.h src/iwd.h \ src/dpp.c \ src/udev.c \ src/pmksa.h src/pmksa.c \ + src/vendor_quirks.h \ + src/vendor_quirks.c \ $(eap_sources) \ $(builtin_sources) diff --git a/autotests/testAPRoam/bad_neighbor_report_test.py b/autotests/testAPRoam/bad_neighbor_report_test.py new file mode 100644 index 00000000..4bf3b63b --- /dev/null +++ b/autotests/testAPRoam/bad_neighbor_report_test.py @@ -0,0 +1,133 @@ +#!/usr/bin/python3 + +import unittest +import sys + +sys.path.append('../util') +import iwd +from iwd import IWD +from iwd import NetworkType + +from hostapd import HostapdCLI + +class Test(unittest.TestCase): + def initial_connection(self): + ordered_network = self.device.get_ordered_network('TestAPRoam') + + self.assertEqual(ordered_network.type, NetworkType.psk) + + condition = 'not obj.connected' + self.wd.wait_for_object_condition(ordered_network.network_object, condition) + + self.device.connect_bssid(self.bss_hostapd[0].bssid) + + condition = 'obj.state == DeviceState.connected' + self.wd.wait_for_object_condition(self.device, condition) + + self.bss_hostapd[0].wait_for_event('AP-STA-CONNECTED') + + self.assertFalse(self.bss_hostapd[1].list_sta()) + + def test_full_scan(self): + """ + Tests that IWD first tries a limited scan, then a full scan after + an AP directed roam. After the full scan yields no results IWD + should stop trying to roam. + """ + self.initial_connection() + + # Disable other APs, so the scans come up empty + self.bss_hostapd[1].disable() + self.bss_hostapd[2].disable() + + # Send a bad candidate list with the BSS TM request which contains a + # channel with no AP operating on it. + self.bss_hostapd[0].send_bss_transition( + self.device.address, + [(self.bss_hostapd[1].bssid, "8f0000005105060603000000")] + ) + self.device.wait_for_event("roam-scan-triggered") + self.device.wait_for_event("no-roam-candidates") + # IWD should then trigger a full scan + self.device.wait_for_event("full-roam-scan") + self.device.wait_for_event("no-roam-candidates", timeout=30) + + # IWD should not trigger a roam again after the above 2 failures. + with self.assertRaises(TimeoutError): + self.device.wait_for_event("roam-scan-triggered", timeout=60) + + def test_bad_candidate_list(self): + """ + Tests behavior when the AP sends a candidate list but the scan + finds no BSS's. IWD should fall back to a full scan after. + """ + self.initial_connection() + + # Send a bad candidate list with the BSS TM request which contains a + # channel with no AP operating on it. + self.bss_hostapd[0].send_bss_transition( + self.device.address, + [(self.bss_hostapd[1].bssid, "8f0000005105060603000000")] + ) + self.device.wait_for_event("roam-scan-triggered") + self.device.wait_for_event("no-roam-candidates") + # IWD should then trigger a full scan + self.device.wait_for_event("full-roam-scan") + self.device.wait_for_event("roaming", timeout=30) + self.device.wait_for_event("connected") + + def test_bad_neighbor_report(self): + """ + Tests behavior when the AP sends no candidate list. IWD should + request a neighbor report. If the limited scan yields no BSS's IWD + should fall back to a full scan. + """ + + # Set a bad neighbor (channel that no AP is on) to force the limited + # roam scan to fail + self.bss_hostapd[0].set_neighbor( + self.bss_hostapd[1].bssid, + "TestAPRoam", + '%s8f000000%s%s060603000000' % (self.bss_hostapd[1].bssid.replace(':', ''), "51", "0b") + ) + + self.initial_connection() + + self.bss_hostapd[0].send_bss_transition(self.device.address, []) + self.device.wait_for_event("roam-scan-triggered") + # The AP will have sent a neighbor report with a single BSS but on + # channel 11 which no AP is on. This should result in a limited scan + # picking up no candidates. + self.device.wait_for_event("no-roam-candidates", timeout=30) + # IWD should then trigger a full scan + self.device.wait_for_event("full-roam-scan") + self.device.wait_for_event("roaming", timeout=30) + self.device.wait_for_event("connected") + + def setUp(self): + self.wd = IWD(True) + + devices = self.wd.list_devices(1) + self.device = devices[0] + + def tearDown(self): + self.wd = None + self.device = None + + for hapd in self.bss_hostapd: + hapd.reload() + + @classmethod + def setUpClass(cls): + IWD.copy_to_storage('TestAPRoam.psk') + + cls.bss_hostapd = [ HostapdCLI(config='ssid1.conf'), + HostapdCLI(config='ssid2.conf'), + HostapdCLI(config='ssid3.conf') ] + + @classmethod + def tearDownClass(cls): + IWD.clear_storage() + +if __name__ == '__main__': + unittest.main(exit=True) diff --git a/monitor/nlmon.c b/monitor/nlmon.c index 7924f6f2..65437dc8 100644 --- a/monitor/nlmon.c +++ b/monitor/nlmon.c @@ -404,6 +404,7 @@ static const struct { { { 0x00, 0x50, 0xf2 }, "Microsoft" }, { { 0x00, 0x90, 0x4c }, "Epigram" }, { { 0x50, 0x6f, 0x9a }, "Wi-Fi Alliance" }, + { { 0x00, 0x18, 0x0a }, "Cisco Meraki" }, { } }; diff --git a/src/eapol.c b/src/eapol.c index 6e37a54a..ab77746f 100644 --- a/src/eapol.c +++ b/src/eapol.c @@ -1810,7 +1810,7 @@ static void eapol_handle_ptk_3_of_4(struct eapol_sm *sm, if ((rsne[1] != hs->authenticator_ie[1] || memcmp(rsne + 2, hs->authenticator_ie + 2, rsne[1])) && - !handshake_util_ap_ie_matches(&rsn_info, + !handshake_util_ap_ie_matches(hs, &rsn_info, hs->authenticator_ie, hs->wpa_ie)) goto error_ie_different; diff --git a/src/ft.c b/src/ft.c index d8bee74c..0d6be4d4 100644 --- a/src/ft.c +++ b/src/ft.c @@ -223,7 +223,8 @@ static bool ft_parse_associate_resp_frame(const uint8_t *frame, size_t frame_len return true; } -static bool ft_verify_rsne(const uint8_t *rsne, const uint8_t *pmk_r0_name, +static bool ft_verify_rsne(struct handshake_state *hs, + const uint8_t *rsne, const uint8_t *pmk_r0_name, const uint8_t *authenticator_ie) { /* @@ -253,7 +254,7 @@ static bool ft_verify_rsne(const uint8_t *rsne, const uint8_t *pmk_r0_name, memcmp(msg2_rsne.pmkids, pmk_r0_name, 16)) return false; - if (!handshake_util_ap_ie_matches(&msg2_rsne, authenticator_ie, false)) + if (!handshake_util_ap_ie_matches(hs, &msg2_rsne, authenticator_ie, false)) return false; return true; @@ -301,7 +302,8 @@ static int parse_ies(struct handshake_state *hs, is_rsn = hs->supplicant_ie != NULL; if (is_rsn) { - if (!ft_verify_rsne(rsne, hs->pmk_r0_name, authenticator_ie)) + if (!ft_verify_rsne(hs, rsne, hs->pmk_r0_name, + authenticator_ie)) goto ft_error; } else if (rsne) goto ft_error; @@ -480,7 +482,7 @@ int __ft_rx_associate(uint32_t ifindex, const uint8_t *frame, size_t frame_len) memcmp(msg4_rsne.pmkids, hs->pmk_r1_name, 16)) return -EBADMSG; - if (!handshake_util_ap_ie_matches(&msg4_rsne, + if (!handshake_util_ap_ie_matches(hs, &msg4_rsne, hs->authenticator_ie, false)) return -EBADMSG; diff --git a/src/handshake.c b/src/handshake.c index c469e6fa..6db897bf 100644 --- a/src/handshake.c +++ b/src/handshake.c @@ -44,6 +44,7 @@ #include "src/erp.h" #include "src/band.h" #include "src/pmksa.h" +#include "src/vendor_quirks.h" static inline unsigned int n_ecc_groups(void) { @@ -368,6 +369,12 @@ void handshake_state_set_vendor_ies(struct handshake_state *s, } } +void handshake_state_set_vendor_quirks(struct handshake_state *s, + uint32_t quirks_mask) +{ + s->vendor_quirks = quirks_mask; +} + void handshake_state_set_kh_ids(struct handshake_state *s, const uint8_t *r0khid, size_t r0khid_len, const uint8_t *r1khid) @@ -877,7 +884,8 @@ void handshake_state_set_igtk(struct handshake_state *s, const uint8_t *key, * results vs the RSN/WPA IE obtained as part of the 4-way handshake. If they * don't match, the EAPoL packet must be silently discarded. */ -bool handshake_util_ap_ie_matches(const struct ie_rsn_info *msg_info, +bool handshake_util_ap_ie_matches(struct handshake_state *s, + const struct ie_rsn_info *msg_info, const uint8_t *scan_ie, bool is_wpa) { struct ie_rsn_info scan_info; @@ -907,11 +915,15 @@ bool handshake_util_ap_ie_matches(const struct ie_rsn_info *msg_info, if (msg_info->no_pairwise != scan_info.no_pairwise) return false; - if (msg_info->ptksa_replay_counter != scan_info.ptksa_replay_counter) - return false; + if (!(s->vendor_quirks & VENDOR_QUIRK_REPLAY_COUNTER_MISMATCH)) { + if (msg_info->ptksa_replay_counter != + scan_info.ptksa_replay_counter) + return false; - if (msg_info->gtksa_replay_counter != scan_info.gtksa_replay_counter) - return false; + if (msg_info->gtksa_replay_counter != + scan_info.gtksa_replay_counter) + return false; + } if (msg_info->mfpr != scan_info.mfpr) return false; diff --git a/src/handshake.h b/src/handshake.h index c6e3c10b..df0f8315 100644 --- a/src/handshake.h +++ b/src/handshake.h @@ -107,6 +107,7 @@ struct handshake_state { uint8_t *authenticator_fte; uint8_t *supplicant_fte; uint8_t *vendor_ies; + uint32_t vendor_quirks; size_t vendor_ies_len; enum ie_rsn_cipher_suite pairwise_cipher; enum ie_rsn_cipher_suite group_cipher; @@ -237,6 +238,9 @@ void handshake_state_set_vendor_ies(struct handshake_state *s, const struct iovec *iov, size_t n_iovs); +void handshake_state_set_vendor_quirks(struct handshake_state *s, + uint32_t quirks_mask); + void handshake_state_set_kh_ids(struct handshake_state *s, const uint8_t *r0khid, size_t r0khid_len, const uint8_t *r1khid); @@ -312,7 +316,8 @@ bool handshake_state_set_pmksa(struct handshake_state *s, struct pmksa *pmksa); void handshake_state_cache_pmksa(struct handshake_state *s); bool handshake_state_remove_pmksa(struct handshake_state *s); -bool handshake_util_ap_ie_matches(const struct ie_rsn_info *msg_info, +bool handshake_util_ap_ie_matches(struct handshake_state *s, + const struct ie_rsn_info *msg_info, const uint8_t *scan_ie, bool is_wpa); const uint8_t *handshake_util_find_kde(enum handshake_kde selector, diff --git a/src/scan.c b/src/scan.c index dfd667bb..b87c4621 100644 --- a/src/scan.c +++ b/src/scan.c @@ -51,6 +51,7 @@ #include "src/mpdu.h" #include "src/band.h" #include "src/scan.h" +#include "src/vendor_quirks.h" /* User configurable options */ static double RANK_2G_FACTOR; @@ -414,7 +415,8 @@ static struct l_genl_msg *scan_build_cmd(struct scan_context *sc, if (params->ap_scan) flags |= NL80211_SCAN_FLAG_AP; - flags |= NL80211_SCAN_FLAG_COLOCATED_6GHZ; + if (wiphy_supports_colocated_flag(sc->wiphy)) + flags |= NL80211_SCAN_FLAG_COLOCATED_6GHZ; if (flags) l_genl_msg_append_attr(msg, NL80211_ATTR_SCAN_FLAGS, 4, &flags); @@ -1220,6 +1222,11 @@ static void scan_parse_vendor_specific(struct scan_bss *bss, const void *data, uint16_t cost_flags; bool dgaf_disable; + if (L_WARN_ON(len < 3)) + return; + + bss->vendor_quirks |= vendor_quirks(data); + if (!bss->wpa && is_ie_wpa_ie(data, len)) { bss->wpa = l_memdup(data - 2, len + 2); return; diff --git a/src/scan.h b/src/scan.h index 4c1ebc21..b2a63505 100644 --- a/src/scan.h +++ b/src/scan.h @@ -79,6 +79,7 @@ struct scan_bss { uint8_t *wfd; /* Concatenated WFD IEs */ ssize_t wfd_size; /* Size of Concatenated WFD IEs */ int8_t snr; + uint32_t vendor_quirks; bool mde_present : 1; bool cc_present : 1; bool cap_rm_neighbor_report : 1; diff --git a/src/station.c b/src/station.c index a4c3e7d1..968aafb0 100644 --- a/src/station.c +++ b/src/station.c @@ -64,6 +64,7 @@ #include "src/eap-tls-common.h" #include "src/storage.h" #include "src/pmksa.h" +#include "src/vendor_quirks.h" #define STATION_RECENT_NETWORK_LIMIT 5 #define STATION_RECENT_FREQS_LIMIT 5 @@ -1446,6 +1447,8 @@ static struct handshake_state *station_handshake_setup(struct station *station, vendor_ies = network_info_get_extra_ies(info, bss, &iov_elems); handshake_state_set_vendor_ies(hs, vendor_ies, iov_elems); + handshake_state_set_vendor_quirks(hs, bss->vendor_quirks); + /* * It can't hurt to try the FILS IP Address Assignment independent of * which auth-proto is actually used. @@ -2404,6 +2407,11 @@ static void station_roam_retry(struct station *station) station->roam_scan_full = false; station->ap_directed_roaming = false; + if (station->roam_freqs) { + scan_freq_set_free(station->roam_freqs); + station->roam_freqs = NULL; + } + if (station->signal_low) station_roam_timeout_rearm(station, roam_retry_interval); } @@ -2433,8 +2441,16 @@ static void station_roam_failed(struct station *station) * We were told by the AP to roam, but failed. Try ourselves or * wait for the AP to tell us to roam again */ - if (station->ap_directed_roaming) + if (station->ap_directed_roaming) { + /* + * The candidate list from the AP (or neighbor report) found + * no BSS's. Force a full scan + */ + if (!station->roam_scan_full) + goto full_scan; + goto delayed_retry; + } /* * If we tried a limited scan, failed and the signal is still low, @@ -2446,6 +2462,8 @@ static void station_roam_failed(struct station *station) * the scan here, so that the destroy callback is not called * after the return of this function */ +full_scan: + station_debug_event(station, "full-roam-scan"); scan_cancel(netdev_get_wdev_id(station->netdev), station->roam_scan_id); @@ -3366,12 +3384,22 @@ static void station_ap_directed_roam(struct station *station, l_timeout_remove(station->roam_trigger_timeout); station->roam_trigger_timeout = NULL; - if (req_mode & WNM_REQUEST_MODE_PREFERRED_CANDIDATE_LIST) { + if ((req_mode & WNM_REQUEST_MODE_PREFERRED_CANDIDATE_LIST) && + !(station->connected_bss->vendor_quirks & + VENDOR_QUIRK_BAD_BSS_TM_CANDIDATE_LIST)) { l_debug("roam: AP sent a preferred candidate list"); station_neighbor_report_cb(station->netdev, 0, body + pos, body_len - pos, station); } else { - l_debug("roam: AP did not include a preferred candidate list"); + if (station->connected_bss->cap_rm_neighbor_report) { + if (!netdev_neighbor_report_req(station->netdev, + station_neighbor_report_cb)) + return; + + l_warn("failed to request neighbor report!"); + } + + l_debug("full scan after BSS transition request"); if (station_roam_scan(station, NULL) < 0) station_roam_failed(station); } diff --git a/src/vendor_quirks.c b/src/vendor_quirks.c new file mode 100644 index 00000000..e005ba3b --- /dev/null +++ b/src/vendor_quirks.c @@ -0,0 +1,56 @@ +/* + * + * Wireless daemon for Linux + * + * Copyright (C) 2025 Locus Robotics Corporation. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include + +#include + +#include "src/vendor_quirks.h" + +static const struct { + uint8_t oui[3]; + uint32_t quirks; +} quirk_db[] = { + /* Cisco Meraki */ + { { 0x00, 0x18, 0x0a }, VENDOR_QUIRK_BAD_BSS_TM_CANDIDATE_LIST }, + /* Hewlitt Packard, owns Aruba */ + { { 0x00, 0x0b, 0x86 }, VENDOR_QUIRK_REPLAY_COUNTER_MISMATCH }, +}; + +uint32_t vendor_quirks(const uint8_t *oui) +{ + size_t i; + uint32_t ret = 0; + + for (i = 0; i < L_ARRAY_SIZE(quirk_db); i++) { + if (memcmp(quirk_db[i].oui, oui, 3)) + continue; + + ret |= quirk_db[i].quirks; + } + + return ret; +} diff --git a/src/vendor_quirks.h b/src/vendor_quirks.h new file mode 100644 index 00000000..6c587d45 --- /dev/null +++ b/src/vendor_quirks.h @@ -0,0 +1,40 @@ +/* + * + * Wireless daemon for Linux + * + * Copyright (C) 2025 Locus Robotics Corporation. All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +enum vendor_quirk { + /* + * The neighbor list in a BSS Transition Management request from an AP + * contains a very sparse BSS list which generally leads to poor roaming + * decisions. + */ + VENDOR_QUIRK_BAD_BSS_TM_CANDIDATE_LIST = 1 << 0, + /* + * The PTK/GTK replay counter differs between a scan and FT + * authentication. This is not allowable in the spec, but seen with + * certain vendors. + */ + VENDOR_QUIRK_REPLAY_COUNTER_MISMATCH = 1 << 1, +}; + +uint32_t vendor_quirks(const uint8_t *oui); diff --git a/src/wiphy.c b/src/wiphy.c index fb544fe6..b6774f69 100644 --- a/src/wiphy.c +++ b/src/wiphy.c @@ -69,12 +69,26 @@ static uint32_t work_ids; static unsigned int wiphy_dump_id; enum driver_flag { + /* Force the use of the default interface created by the kernel */ DEFAULT_IF = 0x1, + /* + * Force the use of the PAE socket rather than control port, even if + * control port is supported + */ FORCE_PAE = 0x2, + /* Disable power save on the adapter during initialization */ POWER_SAVE_DISABLE = 0x4, + /* Don't use OWE when connecting to open networks */ OWE_DISABLE = 0x8, + /* Disables multicast RX frame registration */ MULTICAST_RX_DISABLE = 0x10, + /* + * Don't use SAE (WPA3) when connecting to hybrid networks. This will + * prevent IWD from connecting to WPA3-only networks + */ SAE_DISABLE = 0x20, + /* Disables use of the NL80211_SCAN_FLAG_COLOCATED_6GHZ flag in scans */ + COLOCATED_SCAN_DISABLE = 0x40, }; struct driver_flag_name { @@ -103,12 +117,13 @@ static const struct driver_info driver_infos[] = { }; static const struct driver_flag_name driver_flag_names[] = { - { "DefaultInterface", DEFAULT_IF }, - { "ForcePae", FORCE_PAE }, - { "PowerSaveDisable", POWER_SAVE_DISABLE }, - { "OweDisable", OWE_DISABLE }, - { "MulticastRxDisable", MULTICAST_RX_DISABLE }, - { "SaeDisable", SAE_DISABLE }, + { "DefaultInterface", DEFAULT_IF }, + { "ForcePae", FORCE_PAE }, + { "PowerSaveDisable", POWER_SAVE_DISABLE }, + { "OweDisable", OWE_DISABLE }, + { "MulticastRxDisable", MULTICAST_RX_DISABLE }, + { "SaeDisable", SAE_DISABLE }, + { "ColocatedScanDisable", COLOCATED_SCAN_DISABLE }, }; struct wiphy { @@ -963,6 +978,11 @@ bool wiphy_supports_multicast_rx(const struct wiphy *wiphy) !(wiphy->driver_flags & MULTICAST_RX_DISABLE); } +bool wiphy_supports_colocated_flag(const struct wiphy *wiphy) +{ + return !(wiphy->driver_flags & COLOCATED_SCAN_DISABLE); +} + const uint8_t *wiphy_get_ht_capabilities(const struct wiphy *wiphy, enum band_freq band, size_t *size) @@ -1382,6 +1402,9 @@ static void wiphy_print_basic_info(struct wiphy *wiphy) if (wiphy->driver_flags & SAE_DISABLE) flags = l_strv_append(flags, "SaeDisable"); + if (wiphy->driver_flags & COLOCATED_SCAN_DISABLE) + flags = l_strv_append(flags, "ColocatedScanDisable"); + joined = l_strjoinv(flags, ' '); l_info("\tDriver Flags: %s", joined); diff --git a/src/wiphy.h b/src/wiphy.h index 9fcbdcd2..19d79405 100644 --- a/src/wiphy.h +++ b/src/wiphy.h @@ -144,6 +144,7 @@ bool wiphy_country_is_unknown(struct wiphy *wiphy); bool wiphy_supports_uapsd(const struct wiphy *wiphy); bool wiphy_supports_cmd_offchannel(const struct wiphy *wiphy); bool wiphy_supports_multicast_rx(const struct wiphy *wiphy); +bool wiphy_supports_colocated_flag(const struct wiphy *wiphy); const uint8_t *wiphy_get_ht_capabilities(const struct wiphy *wiphy, enum band_freq band,