From fc8ab454b7b8e5b33062c2f04abd6b9e580334d1 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 10 Oct 2025 11:02:09 +0200 Subject: [PATCH 01/22] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'gomod2nix': 'github:nix-community/gomod2nix/47d628dc3b506bd28632e47280c6b89d3496909d?narHash=sha256-JeuGh9kA1SPL70fnvpLxkIkCWpTjtoPaus3jzvdna0k%3D' (2025-08-24) → 'github:nix-community/gomod2nix/7f8d7438f5870eb167abaf2c39eea3d2302019d1?narHash=sha256-pDyrtUQyeP1lVTMIYqJtftzDtsXEZaJjYy9ZQ/SGhL8%3D' (2025-10-09) • Updated input 'nixos-hardware': 'github:nixos/nixos-hardware/3441b5242af7577230a78ffb03542add264179ab?narHash=sha256-spZegilADH0q5OngM86u6NmXxduCNv5eX9vCiUPhOYc%3D' (2025-10-04) → 'github:nixos/nixos-hardware/d232c6f9ccad3af6d1b66f7feccece66f9aec61a?narHash=sha256-KwyyrQAdX1wD9HVhxotGxbSNdrVm4RGxAkbMKP6uSvE%3D' (2025-10-09) • Updated input 'nixpkgs': 'github:nixos/nixpkgs/3bcc93c5f7a4b30335d31f21e2f1281cba68c318?narHash=sha256-YWo57PL7mGZU7D4WeKFMiW4ex/O6ZolUS6UNBHTZfkI%3D' (2025-10-04) → 'github:nixos/nixpkgs/20c4598c84a671783f741e02bf05cbfaf4907cff?narHash=sha256-a0%2Bh02lyP2KwSNrZz4wLJTu9ikujNsTWIC874Bv7IJ0%3D' (2025-10-06) • Updated input 'nixpkgsMaster': 'github:NixOS/nixpkgs/f0a05ce889294b1c1e02377b086b9931554f5bf3?narHash=sha256-Fvh/nZ7x/msSPPPJXfEoCqgW9NIUQLSCJCCLr8bVMos%3D' (2025-10-05) → 'github:NixOS/nixpkgs/5b3275f79e9e0d8c29a7e2819ab6eddbe5cca66b?narHash=sha256-S%2BeueXZ1NyYh1JbM1OsPXl8lSkhldEj2EBuoJtjq%2BU8%3D' (2025-10-10) • Updated input 'nur': 'github:nix-community/NUR/dce08ba6904fcaad93c17ab65cf6b3e5dfc2d301?narHash=sha256-GkGJdNkR9gnVQt9OXwhGrD72EpK185jNVT7qoCh/3q4%3D' (2025-10-05) → 'github:nix-community/NUR/c84cfd2c38e5802149b7dd619f2d0eba522d0bc3?narHash=sha256-9IcKohgoH2WXYBcccqjTO0BnKMsHlDiEphyg7IQPaWg%3D' (2025-10-10) • Updated input 'nur/nixpkgs': 'github:nixos/nixpkgs/7df7ff7d8e00218376575f0acdcc5d66741351ee?narHash=sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs%3D' (2025-10-02) → 'github:nixos/nixpkgs/c9b6fb798541223bbb396d287d16f43520250518?narHash=sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH%2B47XEBo%3D' (2025-10-07) • Updated input 'vesc-tool': 'github:vedderb/vesc_tool/1f32af09ac21bd2bbb2e3a8b4148d1892636f8d9?narHash=sha256-v7P%2B2TVd0ZU6LFlBM0hg75bSRvnneefJZJ%2BAmzCf4Uk%3D' (2025-10-05) → 'github:vedderb/vesc_tool/cc9e1b48aa80628c06f5008727c3b6d6ea8fa93e?narHash=sha256-tdljAU7bb3/P4mEa6ezS7htB1U8O4%2BTAMU4KB2JEWUY%3D' (2025-10-08) --- flake.lock | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/flake.lock b/flake.lock index 29dc015..d032928 100644 --- a/flake.lock +++ b/flake.lock @@ -243,11 +243,11 @@ ] }, "locked": { - "lastModified": 1756047880, - "narHash": "sha256-JeuGh9kA1SPL70fnvpLxkIkCWpTjtoPaus3jzvdna0k=", + "lastModified": 1759991118, + "narHash": "sha256-pDyrtUQyeP1lVTMIYqJtftzDtsXEZaJjYy9ZQ/SGhL8=", "owner": "nix-community", "repo": "gomod2nix", - "rev": "47d628dc3b506bd28632e47280c6b89d3496909d", + "rev": "7f8d7438f5870eb167abaf2c39eea3d2302019d1", "type": "github" }, "original": { @@ -402,11 +402,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1759582739, - "narHash": "sha256-spZegilADH0q5OngM86u6NmXxduCNv5eX9vCiUPhOYc=", + "lastModified": 1760053573, + "narHash": "sha256-KwyyrQAdX1wD9HVhxotGxbSNdrVm4RGxAkbMKP6uSvE=", "owner": "nixos", "repo": "nixos-hardware", - "rev": "3441b5242af7577230a78ffb03542add264179ab", + "rev": "d232c6f9ccad3af6d1b66f7feccece66f9aec61a", "type": "github" }, "original": { @@ -418,11 +418,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1759580034, - "narHash": "sha256-YWo57PL7mGZU7D4WeKFMiW4ex/O6ZolUS6UNBHTZfkI=", + "lastModified": 1759735786, + "narHash": "sha256-a0+h02lyP2KwSNrZz4wLJTu9ikujNsTWIC874Bv7IJ0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "3bcc93c5f7a4b30335d31f21e2f1281cba68c318", + "rev": "20c4598c84a671783f741e02bf05cbfaf4907cff", "type": "github" }, "original": { @@ -450,11 +450,11 @@ }, "nixpkgsMaster": { "locked": { - "lastModified": 1759699079, - "narHash": "sha256-Fvh/nZ7x/msSPPPJXfEoCqgW9NIUQLSCJCCLr8bVMos=", + "lastModified": 1760086576, + "narHash": "sha256-S+eueXZ1NyYh1JbM1OsPXl8lSkhldEj2EBuoJtjq+U8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "f0a05ce889294b1c1e02377b086b9931554f5bf3", + "rev": "5b3275f79e9e0d8c29a7e2819ab6eddbe5cca66b", "type": "github" }, "original": { @@ -482,11 +482,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1759381078, - "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", + "lastModified": 1759831965, + "narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", + "rev": "c9b6fb798541223bbb396d287d16f43520250518", "type": "github" }, "original": { @@ -518,11 +518,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1759696599, - "narHash": "sha256-GkGJdNkR9gnVQt9OXwhGrD72EpK185jNVT7qoCh/3q4=", + "lastModified": 1760083541, + "narHash": "sha256-9IcKohgoH2WXYBcccqjTO0BnKMsHlDiEphyg7IQPaWg=", "owner": "nix-community", "repo": "NUR", - "rev": "dce08ba6904fcaad93c17ab65cf6b3e5dfc2d301", + "rev": "c84cfd2c38e5802149b7dd619f2d0eba522d0bc3", "type": "github" }, "original": { @@ -729,11 +729,11 @@ "treefmt-nix": "treefmt-nix_2" }, "locked": { - "lastModified": 1759670864, - "narHash": "sha256-v7P+2TVd0ZU6LFlBM0hg75bSRvnneefJZJ+AmzCf4Uk=", + "lastModified": 1759909745, + "narHash": "sha256-tdljAU7bb3/P4mEa6ezS7htB1U8O4+TAMU4KB2JEWUY=", "owner": "vedderb", "repo": "vesc_tool", - "rev": "1f32af09ac21bd2bbb2e3a8b4148d1892636f8d9", + "rev": "cc9e1b48aa80628c06f5008727c3b6d6ea8fa93e", "type": "github" }, "original": { From 81724a93e9892325077b13a0afbe47d02d917c9c Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Sun, 12 Oct 2025 03:19:03 +0200 Subject: [PATCH 02/22] Add BROFLIX logo --- hosts/iron/services/jellyfin/broflix.svg | 57 ++++++++++++++++++++++++ hosts/iron/services/jellyfin/default.nix | 25 ++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 hosts/iron/services/jellyfin/broflix.svg diff --git a/hosts/iron/services/jellyfin/broflix.svg b/hosts/iron/services/jellyfin/broflix.svg new file mode 100644 index 0000000..7653de9 --- /dev/null +++ b/hosts/iron/services/jellyfin/broflix.svg @@ -0,0 +1,57 @@ + + + + + + + + + unrar and chill + + diff --git a/hosts/iron/services/jellyfin/default.nix b/hosts/iron/services/jellyfin/default.nix index 1da1106..f5c31ab 100644 --- a/hosts/iron/services/jellyfin/default.nix +++ b/hosts/iron/services/jellyfin/default.nix @@ -1,6 +1,20 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: let inherit (config.networking) ports; + logoPng = pkgs.stdenvNoCC.mkDerivation { + name = "broflix.png"; + src = ./broflix.svg; + dontBuild = true; + dontUnpack = true; + installPhase = '' + export PATH="$PATH:${pkgs.lib.makeBinPath [pkgs.imagemagick]}" + convert \ + -background transparent \ + $src \ + -resize 1302x \ + $out + ''; + }; in { imports = [ @@ -76,6 +90,15 @@ in proxy_set_header X-Forwarded-Host $http_host; proxy_buffering off; } + location = /web/broflix.svg { + alias ${./broflix.svg}; + } + location = /web/assets/img/banner-light.png { + alias ${logoPng}; + } + location = /web/assets/img/banner-dark.png { + alias ${logoPng}; + } location = /web/ { proxy_pass http://127.0.0.1:${toString ports.jellyfin.tcp}/web/index.html; proxy_set_header Host $host; From c23cd959e322c684fc42eb5a3da7da2b55e1178d Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Thu, 2 Oct 2025 11:50:06 +0200 Subject: [PATCH 03/22] Add pipewire to iron --- hosts/iron/services/snapcast/alsa.nix | 6 ---- .../iron/services/snapcast/bluetooth-sink.nix | 24 +++++++++++---- hosts/iron/services/snapcast/default.nix | 15 +++++++++- hosts/iron/services/snapcast/ledfx.nix | 29 +++++++++++++++++-- hosts/iron/services/snapcast/snapclient.nix | 5 ++-- 5 files changed, 61 insertions(+), 18 deletions(-) delete mode 100644 hosts/iron/services/snapcast/alsa.nix diff --git a/hosts/iron/services/snapcast/alsa.nix b/hosts/iron/services/snapcast/alsa.nix deleted file mode 100644 index 1b8566e..0000000 --- a/hosts/iron/services/snapcast/alsa.nix +++ /dev/null @@ -1,6 +0,0 @@ -{ - boot.kernelModules = [ "snd-aloop" ]; - boot.extraModprobeConfig = '' - options snd-aloop id=ledfx,bluetooth enable=1,1 pcm_substreams=2,2 - ''; -} diff --git a/hosts/iron/services/snapcast/bluetooth-sink.nix b/hosts/iron/services/snapcast/bluetooth-sink.nix index 38cf8e3..43db142 100644 --- a/hosts/iron/services/snapcast/bluetooth-sink.nix +++ b/hosts/iron/services/snapcast/bluetooth-sink.nix @@ -1,7 +1,11 @@ { pkgs, ... }: { environment.systemPackages = with pkgs; [ - bluez-alsa + #bluez-alsa + bluez + bluez-tools + pipewire + wireplumber ]; hardware.bluetooth = { @@ -18,11 +22,17 @@ FastConnectable = "true"; # Allow repairing of existing devices JustWorksRepairing = "always"; + # to show battery state + Experimental = true; }; }; }; - services.blueman.enable = true; + services = { + blueman.enable = true; + ofono.enable = true; + upower.enable = true; + }; systemd.services = { bluetooth-auto-pair = { @@ -45,7 +55,8 @@ Restart = "on-failure"; }; }; - bluealsa-aplay = { + /* + bluealsa-aplay = { wantedBy = [ "multi-user.target" ]; serviceConfig = { DynamicUser = true; @@ -54,14 +65,15 @@ Restart = "on-failure"; SupplementaryGroups = [ "audio" ]; }; - }; - bluealsa-a2dp = { + }; + bluealsa-a2dp = { wantedBy = [ "multi-user.target" ]; serviceConfig = { Type = "simple"; ExecStart = "${pkgs.bluez-alsa}/bin/bluealsa -p a2dp-sink"; Restart = "on-failure"; }; - }; + }; + */ }; } diff --git a/hosts/iron/services/snapcast/default.nix b/hosts/iron/services/snapcast/default.nix index 803531e..496bb63 100644 --- a/hosts/iron/services/snapcast/default.nix +++ b/hosts/iron/services/snapcast/default.nix @@ -1,10 +1,23 @@ +{ pkgs, ... }: + { imports = [ - ./alsa.nix ./bluetooth-sink.nix ./ledfx.nix ./mopidy.nix ./snapclient.nix ./snapserver.nix ]; + + services.pipewire = { + enable = true; + systemWide = true; + audio.enable = true; + pulse.enable = true; + alsa.enable = true; + }; + + environment.systemPackages = [ pkgs.pulseaudio ]; + + users.users.jalr.extraGroups = [ "pipewire" ]; } diff --git a/hosts/iron/services/snapcast/ledfx.nix b/hosts/iron/services/snapcast/ledfx.nix index 4a4a3d6..d7db19d 100644 --- a/hosts/iron/services/snapcast/ledfx.nix +++ b/hosts/iron/services/snapcast/ledfx.nix @@ -7,6 +7,29 @@ in 8888 ]; + + services.pipewire.extraConfig.pipewire."10-ledfx-loop" = { + "context.modules" = [ + { + name = "libpipewire-module-loopback"; + args = { + "audio.position" = [ "FL,FR" ]; + "capture.props" = { + "media.class" = "Audio/Sink"; + "node.name" = "ledfx.input"; + "node.description" = "ledfx capture"; + }; + "playback.props" = { + "media.class" = "Audio/Source"; + "node.name" = "ledfx.output"; + "node.passive" = true; + }; + }; + } + ]; + }; + + systemd.services.snapclient-ledfx = { enable = true; description = "Snapcast client"; @@ -15,8 +38,8 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { DynamicUser = "yes"; - ExecStart = "${pkgs.snapcast}/bin/snapclient --host 127.0.0.1 --hostID ledfx -i 2 --player alsa -s ledfx"; - Group = "audio"; + ExecStart = "${pkgs.snapcast}/bin/snapclient --host 127.0.0.1 --hostID ledfx -i 2 --player pulse -s ledfx.input"; + Group = "pipewire"; NoNewPrivileges = true; ProtectControlGroups = true; ProtectHome = true; @@ -37,7 +60,7 @@ in serviceConfig = { DynamicUser = "yes"; ExecStart = "${pkgs.ledfx}/bin/ledfx --host 0.0.0.0 -p 8888 -c %S/ledfx"; - Group = "audio"; + Group = "pipewire"; NoNewPrivileges = true; ProtectControlGroups = true; ProtectHome = true; diff --git a/hosts/iron/services/snapcast/snapclient.nix b/hosts/iron/services/snapcast/snapclient.nix index 27bb202..b0c4687 100644 --- a/hosts/iron/services/snapcast/snapclient.nix +++ b/hosts/iron/services/snapcast/snapclient.nix @@ -7,10 +7,11 @@ wants = [ "network.target" "sound.target" ]; after = [ "network.target" "sound.target" ]; wantedBy = [ "multi-user.target" ]; + environment.PULSE_SERVER = "unix:/run/pulse/native"; serviceConfig = { DynamicUser = "yes"; - ExecStart = "${pkgs.snapcast}/bin/snapclient --host 127.0.0.1 -s DAC"; - Group = "audio"; + ExecStart = "${pkgs.snapcast}/bin/snapclient --host 127.0.0.1 --player pulse -s alsa_output.usb-Burr-Brown_from_TI_USB_Audio_DAC-00.analog-stereo"; + Group = "pipewire"; NoNewPrivileges = true; ProtectControlGroups = true; ProtectHome = true; From 1baad7a328619447b4689d2e3de6a0dc1a7ec82c Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Thu, 23 Oct 2025 10:00:02 +0200 Subject: [PATCH 04/22] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'disko': 'github:nix-community/disko/67ff9807dd148e704baadbd4fd783b54282ca627?narHash=sha256-IGmaEf3Do8o5Cwp1kXBN1wQmZwQN3NLfq5t4nHtVtcU%3D' (2025-09-19) → 'github:nix-community/disko/3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5?narHash=sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw%3D' (2025-10-17) • Updated input 'nix-pre-commit-hooks': 'github:cachix/git-hooks.nix/cfc9f7bb163ad8542029d303e599c0f7eee09835?narHash=sha256-PTod9NG%2Bi3XbbnBKMl/e5uHDBYpwIWivQ3gOWSEuIEM%3D' (2025-10-03) → 'github:cachix/git-hooks.nix/ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37?narHash=sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc%3D' (2025-10-17) • Updated input 'nixos-hardware': 'github:nixos/nixos-hardware/d232c6f9ccad3af6d1b66f7feccece66f9aec61a?narHash=sha256-KwyyrQAdX1wD9HVhxotGxbSNdrVm4RGxAkbMKP6uSvE%3D' (2025-10-09) → 'github:nixos/nixos-hardware/d6645c340ef7d821602fd2cd199e8d1eed10afbc?narHash=sha256-2m1S4jl%2BGEDtlt2QqeHil8Ny456dcGSKJAM7q3j/BFU%3D' (2025-10-20) • Updated input 'nixpkgs': 'github:nixos/nixpkgs/20c4598c84a671783f741e02bf05cbfaf4907cff?narHash=sha256-a0%2Bh02lyP2KwSNrZz4wLJTu9ikujNsTWIC874Bv7IJ0%3D' (2025-10-06) → 'github:nixos/nixpkgs/481cf557888e05d3128a76f14c76397b7d7cc869?narHash=sha256-G/iC4t/9j/52i/nm%2B0/4ybBmAF4hzR8CNHC75qEhjHo%3D' (2025-10-21) • Updated input 'nixpkgsMaster': 'github:NixOS/nixpkgs/5b3275f79e9e0d8c29a7e2819ab6eddbe5cca66b?narHash=sha256-S%2BeueXZ1NyYh1JbM1OsPXl8lSkhldEj2EBuoJtjq%2BU8%3D' (2025-10-10) → 'github:NixOS/nixpkgs/d05426b82736631cf0a4dc8bceef1e52a4326dd7?narHash=sha256-XS7PTk22esg%2BHHg5OSF6%2BjgR6X/2WbzHj1CC8aS0x9E%3D' (2025-10-23) • Updated input 'nur': 'github:nix-community/NUR/c84cfd2c38e5802149b7dd619f2d0eba522d0bc3?narHash=sha256-9IcKohgoH2WXYBcccqjTO0BnKMsHlDiEphyg7IQPaWg%3D' (2025-10-10) → 'github:nix-community/NUR/1d182e3ee8a3a55dc50ff907877294652606a152?narHash=sha256-YF83M84JXliUtQjVsOeXMOtZNnhmTRd9YwH8aVUu1Io%3D' (2025-10-23) • Updated input 'nur/nixpkgs': 'github:nixos/nixpkgs/c9b6fb798541223bbb396d287d16f43520250518?narHash=sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH%2B47XEBo%3D' (2025-10-07) → 'github:nixos/nixpkgs/01f116e4df6a15f4ccdffb1bcd41096869fb385c?narHash=sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d%2BdAiC3H%2BCDle4%3D' (2025-10-22) • Updated input 'sops-nix': 'github:Mic92/sops-nix/6e5a38e08a2c31ae687504196a230ae00ea95133?narHash=sha256-UvzKi02LMFP74csFfwLPAZ0mrE7k6EiYaKecplyX9Qk%3D' (2025-10-05) → 'github:Mic92/sops-nix/5a7d18b5c55642df5c432aadb757140edfeb70b3?narHash=sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY%3D' (2025-10-20) • Updated input 'vesc-tool': 'github:vedderb/vesc_tool/cc9e1b48aa80628c06f5008727c3b6d6ea8fa93e?narHash=sha256-tdljAU7bb3/P4mEa6ezS7htB1U8O4%2BTAMU4KB2JEWUY%3D' (2025-10-08) → 'github:vedderb/vesc_tool/8a6de0dda75e62681e7252a1512f34a5f4e70640?narHash=sha256-%2Bvk8SRGknwg5Q8y/R4oHkiLxe2SDQDYGj8jK7Xaeu6c%3D' (2025-10-17) --- flake.lock | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/flake.lock b/flake.lock index d032928..a706fcc 100644 --- a/flake.lock +++ b/flake.lock @@ -65,11 +65,11 @@ ] }, "locked": { - "lastModified": 1758287904, - "narHash": "sha256-IGmaEf3Do8o5Cwp1kXBN1wQmZwQN3NLfq5t4nHtVtcU=", + "lastModified": 1760701190, + "narHash": "sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw=", "owner": "nix-community", "repo": "disko", - "rev": "67ff9807dd148e704baadbd4fd783b54282ca627", + "rev": "3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5", "type": "github" }, "original": { @@ -386,11 +386,11 @@ ] }, "locked": { - "lastModified": 1759523803, - "narHash": "sha256-PTod9NG+i3XbbnBKMl/e5uHDBYpwIWivQ3gOWSEuIEM=", + "lastModified": 1760663237, + "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "cfc9f7bb163ad8542029d303e599c0f7eee09835", + "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", "type": "github" }, "original": { @@ -402,11 +402,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1760053573, - "narHash": "sha256-KwyyrQAdX1wD9HVhxotGxbSNdrVm4RGxAkbMKP6uSvE=", + "lastModified": 1760958188, + "narHash": "sha256-2m1S4jl+GEDtlt2QqeHil8Ny456dcGSKJAM7q3j/BFU=", "owner": "nixos", "repo": "nixos-hardware", - "rev": "d232c6f9ccad3af6d1b66f7feccece66f9aec61a", + "rev": "d6645c340ef7d821602fd2cd199e8d1eed10afbc", "type": "github" }, "original": { @@ -418,11 +418,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1759735786, - "narHash": "sha256-a0+h02lyP2KwSNrZz4wLJTu9ikujNsTWIC874Bv7IJ0=", + "lastModified": 1761016216, + "narHash": "sha256-G/iC4t/9j/52i/nm+0/4ybBmAF4hzR8CNHC75qEhjHo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "20c4598c84a671783f741e02bf05cbfaf4907cff", + "rev": "481cf557888e05d3128a76f14c76397b7d7cc869", "type": "github" }, "original": { @@ -450,11 +450,11 @@ }, "nixpkgsMaster": { "locked": { - "lastModified": 1760086576, - "narHash": "sha256-S+eueXZ1NyYh1JbM1OsPXl8lSkhldEj2EBuoJtjq+U8=", + "lastModified": 1761206094, + "narHash": "sha256-XS7PTk22esg+HHg5OSF6+jgR6X/2WbzHj1CC8aS0x9E=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "5b3275f79e9e0d8c29a7e2819ab6eddbe5cca66b", + "rev": "d05426b82736631cf0a4dc8bceef1e52a4326dd7", "type": "github" }, "original": { @@ -482,11 +482,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1759831965, - "narHash": "sha256-vgPm2xjOmKdZ0xKA6yLXPJpjOtQPHfaZDRtH+47XEBo=", + "lastModified": 1761114652, + "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "c9b6fb798541223bbb396d287d16f43520250518", + "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", "type": "github" }, "original": { @@ -518,11 +518,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1760083541, - "narHash": "sha256-9IcKohgoH2WXYBcccqjTO0BnKMsHlDiEphyg7IQPaWg=", + "lastModified": 1761205534, + "narHash": "sha256-YF83M84JXliUtQjVsOeXMOtZNnhmTRd9YwH8aVUu1Io=", "owner": "nix-community", "repo": "NUR", - "rev": "c84cfd2c38e5802149b7dd619f2d0eba522d0bc3", + "rev": "1d182e3ee8a3a55dc50ff907877294652606a152", "type": "github" }, "original": { @@ -634,11 +634,11 @@ ] }, "locked": { - "lastModified": 1759635238, - "narHash": "sha256-UvzKi02LMFP74csFfwLPAZ0mrE7k6EiYaKecplyX9Qk=", + "lastModified": 1760998189, + "narHash": "sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY=", "owner": "Mic92", "repo": "sops-nix", - "rev": "6e5a38e08a2c31ae687504196a230ae00ea95133", + "rev": "5a7d18b5c55642df5c432aadb757140edfeb70b3", "type": "github" }, "original": { @@ -729,11 +729,11 @@ "treefmt-nix": "treefmt-nix_2" }, "locked": { - "lastModified": 1759909745, - "narHash": "sha256-tdljAU7bb3/P4mEa6ezS7htB1U8O4+TAMU4KB2JEWUY=", + "lastModified": 1760697314, + "narHash": "sha256-+vk8SRGknwg5Q8y/R4oHkiLxe2SDQDYGj8jK7Xaeu6c=", "owner": "vedderb", "repo": "vesc_tool", - "rev": "cc9e1b48aa80628c06f5008727c3b6d6ea8fa93e", + "rev": "8a6de0dda75e62681e7252a1512f34a5f4e70640", "type": "github" }, "original": { From 0a2a569682b7ed3c0f6c31121cc8281221b79160 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 24 Oct 2025 12:01:58 +0200 Subject: [PATCH 05/22] Remove gnused it is not required --- pkgs/pomodoro-timer/default.nix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkgs/pomodoro-timer/default.nix b/pkgs/pomodoro-timer/default.nix index 4af648b..a3638f8 100644 --- a/pkgs/pomodoro-timer/default.nix +++ b/pkgs/pomodoro-timer/default.nix @@ -2,7 +2,6 @@ , stdenv , yad , uair -, gnused , writeShellScript , makeDesktopItem , imagemagick @@ -10,7 +9,7 @@ let pomodoroTimer = writeShellScript "pomodoro-timer" '' - export PATH=${lib.makeBinPath [yad uair gnused]} + export PATH=${lib.makeBinPath [yad uair]} uairctl listen -o yad \ | yad \ --title="Pomodoro" \ From c9a956bc6331257768473f7e8426472fa7822bb0 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 24 Oct 2025 17:38:34 +0200 Subject: [PATCH 06/22] Rework solarized --- users/jalr/modules/solarized.nix | 87 +++++++++++---- users/jalr/modules/sway/default.nix | 69 ++++++------ users/jalr/modules/sway/waybar.nix | 44 ++------ users/jalr/modules/sway/wofi.nix | 157 ++++++++++++++-------------- 4 files changed, 186 insertions(+), 171 deletions(-) diff --git a/users/jalr/modules/solarized.nix b/users/jalr/modules/solarized.nix index 5390a31..baa2547 100644 --- a/users/jalr/modules/solarized.nix +++ b/users/jalr/modules/solarized.nix @@ -1,23 +1,68 @@ -builtins.mapAttrs - (_: hex: { - inherit hex; - rgb = builtins.concatStringsSep "," (map (f: toString (builtins.fromTOML "i = 0x${f hex}").i) (map (pos: builtins.substring pos 2) [ 1 3 5 ])); - }) +let + colors = { + base00 = "#657b83"; + base01 = "#586e75"; + base02 = "#073642"; + base03 = "#002b36"; + base0 = "#839496"; + base1 = "#93a1a1"; + base2 = "#eee8d5"; + base3 = "#fdf6e3"; + blue = "#268bd2"; + cyan = "#2aa198"; + green = "#859900"; + magenta = "#d33682"; + orange = "#cb4b16"; + red = "#dc322f"; + violet = "#6c71c4"; + yellow = "#b58900"; + }; + common = { + base08 = colors.red; + base09 = colors.orange; + base0A = colors.yellow; + base0B = colors.green; + base0C = colors.cyan; + base0D = colors.blue; + base0E = colors.violet; + base0F = colors.magenta; + }; + light = common // { + base00 = colors.base3; + base01 = colors.base2; + base02 = colors.base1; + base03 = colors.base0; + base04 = colors.base00; + base05 = colors.base01; + base06 = colors.base02; + base07 = colors.base03; + }; + dark = common // { + base00 = colors.base03; + base01 = colors.base02; + base02 = colors.base01; + base03 = colors.base00; + base04 = colors.base0; + base05 = colors.base1; + base06 = colors.base2; + base07 = colors.base3; + }; + toRgb = hex: builtins.concatStringsSep "," ( + map + ( + f: toString (builtins.fromTOML "i = 0x${f hex}").i + ) + ( + map (pos: builtins.substring pos 2) [ 1 3 5 ] + ) + ); + makeScheme = colors: { + hex = colors; + rgb = builtins.mapAttrs (_: hex: (toRgb hex)) colors; + }; +in { - base00 = "#657b83"; - base01 = "#586e75"; - base02 = "#073642"; - base03 = "#002b36"; - base0 = "#839496"; - base1 = "#93a1a1"; - base2 = "#eee8d5"; - base3 = "#fdf6e3"; - blue = "#268bd2"; - cyan = "#2aa198"; - green = "#859900"; - magenta = "#d33682"; - orange = "#cb4b16"; - red = "#dc322f"; - violet = "#6c71c4"; - yellow = "#b58900"; + inherit colors; + light = makeScheme light; + dark = makeScheme dark; } diff --git a/users/jalr/modules/sway/default.nix b/users/jalr/modules/sway/default.nix index 2202b71..2d7bb71 100644 --- a/users/jalr/modules/sway/default.nix +++ b/users/jalr/modules/sway/default.nix @@ -363,40 +363,39 @@ in }; }; - xdg.configFile = { - "sway/light-theme".text = with solarized; '' - client.focused ${base01.hex} ${blue.hex} ${base3.hex} ${blue.hex} ${blue.hex} - client.focused_inactive ${base2.hex} ${base2.hex} ${base01.hex} ${base0.hex} ${base2.hex} - client.unfocused ${base2.hex} ${base3.hex} ${base01.hex} ${base2.hex} ${base2.hex} - client.urgent ${red.hex} ${red.hex} ${base3.hex} ${red.hex} ${red.hex} - ''; - "sway/dark-theme".text = with solarized; '' - client.focused ${base1.hex} ${blue.hex} ${base03.hex} ${blue.hex} ${blue.hex} - client.focused_inactive ${base02.hex} ${base02.hex} ${base1.hex} ${base03.hex} ${base02.hex} - client.unfocused ${base02.hex} ${base03.hex} ${base1.hex} ${base02.hex} ${base02.hex} - client.urgent ${red.hex} ${red.hex} ${base03.hex} ${red.hex} ${red.hex} - ''; - "swaynag/config".text = - let - # adding it to the header doesn’t work since the defaults overwrite it - commonConfig = /* ini */ '' - background=${lib.substring 1 6 solarized.base3.hex} - border-bottom=${lib.substring 1 6 solarized.base2.hex} - border=${lib.substring 1 6 solarized.base2.hex} - button-background=${lib.substring 1 6 solarized.base3.hex} - button-text=${lib.substring 1 6 solarized.base00.hex} - ''; - in - /* ini */ '' - font=Monospace 12 - - [warning] - text=${lib.substring 1 6 solarized.yellow.hex} - ${commonConfig} - - [error] - text=${lib.substring 1 6 solarized.red.hex} - ${commonConfig} + xdg.configFile = + let + makeTheme = scheme: '' + client.focused ${scheme.base05} ${scheme.base0D} ${scheme.base00} ${scheme.base0D} ${scheme.base0D} + client.focused_inactive ${scheme.base01} ${scheme.base01} ${scheme.base05} ${scheme.base03} ${scheme.base01} + client.unfocused ${scheme.base01} ${scheme.base00} ${scheme.base05} ${scheme.base01} ${scheme.base01} + client.urgent ${scheme.base08} ${scheme.base08} ${scheme.base00} ${scheme.base08} ${scheme.base08} ''; - }; + in + { + "sway/light-theme".text = makeTheme solarized.light.hex; + "sway/dark-theme".text = makeTheme solarized.dark.hex; + "swaynag/config".text = + let + # adding it to the header doesn’t work since the defaults overwrite it + commonConfig = /* ini */ '' + background=${lib.substring 1 6 solarized.colors.base3} + border-bottom=${lib.substring 1 6 solarized.colors.base2} + border=${lib.substring 1 6 solarized.colors.base2} + button-background=${lib.substring 1 6 solarized.colors.base3} + button-text=${lib.substring 1 6 solarized.colors.base00} + ''; + in + /* ini */ '' + font=Monospace 12 + + [warning] + text=${lib.substring 1 6 solarized.colors.yellow} + ${commonConfig} + + [error] + text=${lib.substring 1 6 solarized.colors.red} + ${commonConfig} + ''; + }; }) diff --git a/users/jalr/modules/sway/waybar.nix b/users/jalr/modules/sway/waybar.nix index 628e3e8..b30d1ae 100644 --- a/users/jalr/modules/sway/waybar.nix +++ b/users/jalr/modules/sway/waybar.nix @@ -18,7 +18,11 @@ let thinsp = " "; solarized = import ../solarized.nix; - solarizedColors = as: lib.strings.concatLines (lib.attrsets.mapAttrsToList (name: value: let color = solarized."${value}".hex; in "@define-color ${name} ${color};") as); + solarizedColors = lib.attrsets.mapAttrsToList (name: color: "@define-color ${name} ${color};"); + themeCss = { + light = lib.strings.concatLines (solarizedColors solarized.light.hex); + dark = lib.strings.concatLines (solarizedColors solarized.dark.hex); + }; in { # home-manager’s waybar module performs additional checks that are overly strict @@ -253,42 +257,8 @@ in }; xdg.configFile = { - "waybar/theme-light.css".text = solarizedColors { - base00 = "base3"; - base01 = "base2"; - base02 = "base1"; - base03 = "base0"; - base04 = "base00"; - base05 = "base01"; - base06 = "base02"; - base07 = "base03"; - base08 = "red"; - base09 = "orange"; - base0A = "yellow"; - base0B = "green"; - base0C = "cyan"; - base0D = "blue"; - base0E = "violet"; - base0F = "magenta"; - }; - "waybar/theme-dark.css".text = solarizedColors { - base00 = "base03"; - base01 = "base02"; - base02 = "base01"; - base03 = "base00"; - base04 = "base0"; - base05 = "base1"; - base06 = "base2"; - base07 = "base3"; - base08 = "red"; - base09 = "orange"; - base0A = "yellow"; - base0B = "green"; - base0C = "cyan"; - base0D = "blue"; - base0E = "violet"; - base0F = "magenta"; - }; + "waybar/theme-light.css".text = themeCss.light; + "waybar/theme-dark.css".text = themeCss.dark; "waybar/style.css".text = '' @import "theme.css"; diff --git a/users/jalr/modules/sway/wofi.nix b/users/jalr/modules/sway/wofi.nix index ca8eb9a..44d2914 100644 --- a/users/jalr/modules/sway/wofi.nix +++ b/users/jalr/modules/sway/wofi.nix @@ -1,91 +1,92 @@ { lib, ... }: let - solarized = import ../solarized.nix; + inherit (import ../solarized.nix) colors; in { - xdg.configFile = { - "wofi/color-light".text = lib.strings.concatLines (map (c: solarized."${c}".hex) [ - "base3" - "base2" - "base1" - "base0" - "base00" - "base01" - "base02" - "base03" - "red" - "orange" - "yellow" - "green" - "cyan" - "blue" - "violet" - "magenta" - ]); - "wofi/color-dark".text = lib.strings.concatLines (map (c: solarized."${c}".hex) [ - "base03" - "base02" - "base01" - "base00" - "base0" - "base1" - "base2" - "base3" - "red" - "orange" - "yellow" - "green" - "cyan" - "blue" - "violet" - "magenta" - ]); - "wofi/style.css".text = '' - window { - margin: 0px; - border: 3px solid --wofi-color1; - border-radius: 8px; - background-color: rgba(--wofi-rgb-color0,0.8); - } + xdg.configFile = + let + commonColors = with colors; [ + red + orange + yellow + green + cyan + blue + violet + magenta + ]; + in + { + "wofi/color-light".text = with colors; lib.strings.concatLines ( + [ + base3 + base2 + base1 + base0 + base00 + base01 + base02 + base03 + ] ++ commonColors + ); + "wofi/color-dark".text = with colors; lib.strings.concatLines ( + with colors; [ + base03 + base02 + base01 + base00 + base0 + base1 + base2 + base3 + ] ++ commonColors + ); + "wofi/style.css".text = '' + window { + margin: 0px; + border: 3px solid --wofi-color1; + border-radius: 8px; + background-color: rgba(--wofi-rgb-color0,0.8); + } - #input { - margin: 5px; - border: none; - color: --wofi-color4; - background-color: rgba(--wofi-rgb-color1,0.8); - } + #input { + margin: 5px; + border: none; + color: --wofi-color4; + background-color: rgba(--wofi-rgb-color1,0.8); + } - #inner-box { - margin: 5px; - border: none; - background: none; - } + #inner-box { + margin: 5px; + border: none; + background: none; + } - #outer-box { - margin: 5px; - border: none; - background: none; - } + #outer-box { + margin: 5px; + border: none; + background: none; + } - #scroll { - margin: 0px; - border: none; - } + #scroll { + margin: 0px; + border: none; + } - #text { - margin: 5px; - border: none; - color: --wofi-color4; - } + #text { + margin: 5px; + border: none; + color: --wofi-color4; + } - #entry:selected { - background-color: rgba(--wofi-rgb-color1,0.8); - } + #entry:selected { + background-color: rgba(--wofi-rgb-color1,0.8); + } - #entry:selected #text{ - color: --wofi-color11; - } - ''; - }; + #entry:selected #text{ + color: --wofi-color11; + } + ''; + }; } From be924e6c873c00c73b5baf86bfefe16ce96b0f93 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 24 Oct 2025 20:57:34 +0200 Subject: [PATCH 07/22] Replace pavucontrol with mixxc --- users/jalr/modules/default.nix | 1 + users/jalr/modules/mixxc/default.nix | 10 ++ users/jalr/modules/mixxc/style.css | 224 +++++++++++++++++++++++++++ users/jalr/modules/sway/default.nix | 1 + users/jalr/modules/sway/waybar.nix | 2 +- 5 files changed, 237 insertions(+), 1 deletion(-) create mode 100644 users/jalr/modules/mixxc/default.nix create mode 100644 users/jalr/modules/mixxc/style.css diff --git a/users/jalr/modules/default.nix b/users/jalr/modules/default.nix index 30ed808..0b32c3e 100644 --- a/users/jalr/modules/default.nix +++ b/users/jalr/modules/default.nix @@ -21,6 +21,7 @@ ./jameica.nix ./kicad.nix ./lsd + ./mixxc ./mpv.nix ./mute-indicator.nix ./mycli diff --git a/users/jalr/modules/mixxc/default.nix b/users/jalr/modules/mixxc/default.nix new file mode 100644 index 0000000..efde8d1 --- /dev/null +++ b/users/jalr/modules/mixxc/default.nix @@ -0,0 +1,10 @@ +{ nixosConfig, lib, pkgs, ... }: + +lib.mkIf nixosConfig.jalr.gui.enable { + home.packages = with pkgs; [ + mixxc + ]; + xdg.configFile = { + "mixxc/style.css".source = ./style.css; + }; +} diff --git a/users/jalr/modules/mixxc/style.css b/users/jalr/modules/mixxc/style.css new file mode 100644 index 0000000..33dbf39 --- /dev/null +++ b/users/jalr/modules/mixxc/style.css @@ -0,0 +1,224 @@ +.side { + $hide: false; + + .output { + transition: background 750ms; + padding: 5px; + + &.master { + transition: background 0ms; + } + + &.master:hover { + } + + .icon { + -gtk-icon-style: symbolic; + -gtk-icon-size: 16px; + } + } + + @if $hide { + min-height: 0; + min-width: 0; + + .output { + padding: 0; + + .icon { + -gtk-icon-style: symbolic; + -gtk-icon-size: 0; + } + } + } +} + +.main { + margin: 20px; +} + +.client { + $hide-name: false; + $hide-description: false; + + font-family: 'Iosevka Nerd Font'; + font-size: 1.2em; + + .icon { + -gtk-icon-style: symbolic; + } + + @if $hide-name { + .name { + font-size: 0; + } + } + + @if $hide-description { + .description { + font-size: 0; + } + } + + scale { + trough { + /* Slider Bar */ + border-radius: 10px; + + slider { + /* Slider Knob */ + padding: 0; + + border: none; + border-radius: 2px; + + transition-duration: 400ms; + } + + highlight { + /* Slider Bar Filled */ + border: none; + border-radius: 10px; + + margin: 2px; + + transition: background-image 300ms; + } + + fill { + /* Slider Peak */ + background: none; + + border-radius: 10px; + + margin: 0px; + } + } + } + + scale:active { + trough slider { + /* Slider Knob */ + transform: scale(1.1); + } + } +} + +.client.horizontal { + &.new { + animation: client-add-horizontal 300ms ease; + } + + &.removed { + animation: client-remove-horizontal 300ms ease; + } + + .icon { + padding-right: 13px; + + -gtk-icon-size: 16px; + } + + .volume { + /* Numeric Volume Level */ + padding-left: 22px; + padding-bottom: 3px; + } + + scale { + trough { + /* Slider Bar */ + min-height: 6px; + + slider { + /* Slider Knob */ + min-height: 21px; + min-width: 9px; + + margin-top: -7px; + margin-bottom: -7px; + } + } + } + +} + +@keyframes client-add-horizontal { + from { + transform: translateX(-200px); + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes client-remove-horizontal { + from { + opacity: 1; + } + to { + transform: translateX(-200px); + opacity: 0; + } +} + +.client.vertical { + &.new { + animation: client-add-vertical 300ms ease; + } + + &.removed { + animation: client-remove-vertical 300ms ease; + } + + .icon { + padding-bottom: 5px; + + -gtk-icon-size: 20px; + } + + .volume { + /* Numeric Volume Level */ + padding-top: 10px; + } + + scale { + trough { + /* Slider Bar */ + min-width: 4px; + + margin-top: 10px; + + slider { + /* Slider Knob */ + margin-left: -7px; + margin-right: -7px; + + min-height: 6px; + min-width: 14px; + } + } + } +} + +@keyframes client-add-vertical { + from { + transform: translateY(200px); + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes client-remove-vertical { + from { + opacity: 1; + } + to { + transform: translateY(200px); + opacity: 0; + } +} + diff --git a/users/jalr/modules/sway/default.nix b/users/jalr/modules/sway/default.nix index 2d7bb71..7e4e764 100644 --- a/users/jalr/modules/sway/default.nix +++ b/users/jalr/modules/sway/default.nix @@ -234,6 +234,7 @@ in "XF86AudioMute" = "exec pactl set-source-mute alsa_input.usb-BEHRINGER_UMC202HD_192k-00.HiFi__umc202hd_mono_in_U192k_0_1__source toggle"; "${cfg.modifier}+l" = "exec ${lockScreen}"; + "${cfg.modifier}+v" = "exec GSK_RENDERER=cairo GTK_USE_PORTAL=0 ${pkgs.mixxc}/bin/mixxc -A"; }; bars = [ ]; # managed as systemd user unit diff --git a/users/jalr/modules/sway/waybar.nix b/users/jalr/modules/sway/waybar.nix index b30d1ae..a275d36 100644 --- a/users/jalr/modules/sway/waybar.nix +++ b/users/jalr/modules/sway/waybar.nix @@ -171,7 +171,7 @@ in phone = "󰏲"; portable = "󰏲"; }; - on-click-right = "${pkgs.pavucontrol}/bin/pavucontrol"; + on-click-right = "GSK_RENDERER=cairo GTK_USE_PORTAL=0 ${pkgs.mixxc}/bin/mixxc -A -a t -a r"; }; network = { format-wifi = "{essid} ({signalStrength}%) 󰖩 "; From 8c72e2ed3ab9fe49947a0f8c4f75bd3ba957e34f Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 24 Oct 2025 23:00:06 +0200 Subject: [PATCH 08/22] fixup! Rework solarized --- users/jalr/modules/mycli/default.nix | 68 +++++++++++++++------------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/users/jalr/modules/mycli/default.nix b/users/jalr/modules/mycli/default.nix index e023039..844102b 100644 --- a/users/jalr/modules/mycli/default.nix +++ b/users/jalr/modules/mycli/default.nix @@ -105,39 +105,43 @@ let }; }; - colors = { - common = { - "output.header" = "bold ${solarized.green.hex}"; - "sql.datatype" = "nobold ${solarized.yellow.hex}"; - "sql.function" = "bold ${solarized.violet.hex}"; - "sql.keyword" = solarized.green.hex; - "sql.literal" = solarized.green.hex; - "sql.number" = solarized.cyan.hex; - "sql.string" = solarized.cyan.hex; - "sql.variable" = solarized.red.hex; - "sql.quoted-schema-object" = solarized.blue.hex; + colors = + let + c = solarized.colors; + in + { + common = { + "output.header" = "bold ${c.green}"; + "sql.datatype" = "nobold ${c.yellow}"; + "sql.function" = "bold ${c.violet}"; + "sql.keyword" = c.green; + "sql.literal" = c.green; + "sql.number" = c.cyan; + "sql.string" = c.cyan; + "sql.variable" = c.red; + "sql.quoted-schema-object" = c.blue; + }; + light = { + "prompt" = "bg:${c.blue} ${c.base02}"; + "selected" = "bg:${c.base2} ${c.base00}"; + "output.odd-row" = "${c.base01}"; + "output.even-row" = "${c.base01} bg:${c.base2}"; + "sql.comment" = "italic ${c.base1}"; + "sql.operator" = "bold ${c.base02}"; + "sql.punctuation" = "bold ${c.base01}"; + "sql.symbol" = "${c.base01}"; + }; + dark = { + "prompt" = "bg:${c.blue} ${c.base2}"; + "selected" = "bg:${c.base02} ${c.base0}"; + "output.odd-row" = "${c.base1}"; + "output.even-row" = "${c.base1} bg:${c.base02}"; + "sql.comment" = "italic ${c.base01}"; + "sql.operator" = "bold ${c.base2}"; + "sql.punctuation" = "bold ${c.base1}"; + "sql.symbol" = "${c.base1}"; + }; }; - light = { - "prompt" = "bg:${solarized.blue.hex} ${solarized.base02.hex}"; - "selected" = "bg:${solarized.base2.hex} ${solarized.base00.hex}"; - "output.odd-row" = "${solarized.base01.hex}"; - "output.even-row" = "${solarized.base01.hex} bg:${solarized.base2.hex}"; - "sql.comment" = "italic ${solarized.base1.hex}"; - "sql.operator" = "bold ${solarized.base02.hex}"; - "sql.punctuation" = "bold ${solarized.base01.hex}"; - "sql.symbol" = "${solarized.base01.hex}"; - }; - dark = { - "prompt" = "bg:${solarized.blue.hex} ${solarized.base2.hex}"; - "selected" = "bg:${solarized.base02.hex} ${solarized.base0.hex}"; - "output.odd-row" = "${solarized.base1.hex}"; - "output.even-row" = "${solarized.base1.hex} bg:${solarized.base02.hex}"; - "sql.comment" = "italic ${solarized.base01.hex}"; - "sql.operator" = "bold ${solarized.base2.hex}"; - "sql.punctuation" = "bold ${solarized.base1.hex}"; - "sql.symbol" = "${solarized.base1.hex}"; - }; - }; in { home.packages = [ From d4a2f6df35118ccdb8ed9249c5c5cdf528a22ce4 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 24 Oct 2025 23:03:29 +0200 Subject: [PATCH 09/22] Add freetube --- users/jalr/modules/default.nix | 1 + users/jalr/modules/freetube.nix | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 users/jalr/modules/freetube.nix diff --git a/users/jalr/modules/default.nix b/users/jalr/modules/default.nix index 0b32c3e..f97916b 100644 --- a/users/jalr/modules/default.nix +++ b/users/jalr/modules/default.nix @@ -14,6 +14,7 @@ ./firefox ./fish.nix ./fpv.nix + ./freetube.nix ./git.nix ./gnuradio.nix ./graphics diff --git a/users/jalr/modules/freetube.nix b/users/jalr/modules/freetube.nix new file mode 100644 index 0000000..3c45d96 --- /dev/null +++ b/users/jalr/modules/freetube.nix @@ -0,0 +1,7 @@ +{ nixosConfig, ... }: + +{ + programs.freetube = { + inherit (nixosConfig.jalr.gui) enable; + }; +} From 30c2bb63a07f894be1c9451862561f58cb69fee4 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 27 Oct 2025 23:04:54 +0100 Subject: [PATCH 10/22] Decrease brightness control steps to 1% --- users/jalr/modules/sway/waybar.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/users/jalr/modules/sway/waybar.nix b/users/jalr/modules/sway/waybar.nix index a275d36..c507ce9 100644 --- a/users/jalr/modules/sway/waybar.nix +++ b/users/jalr/modules/sway/waybar.nix @@ -152,8 +152,8 @@ in backlight = { format = "{percent}% {icon}"; format-icons = [ "󰛩" "󱩎" "󱩏" "󱩐" "󱩑" "󱩒" "󱩓" "󱩔" "󱩕" "󱩖" "󰛨" ]; - on-scroll-up = "${pkgs.brightnessctl}/bin/brightnessctl -q set +5%"; - on-scroll-down = "${pkgs.brightnessctl}/bin/brightnessctl -q set 5%-"; + on-scroll-up = "${pkgs.brightnessctl}/bin/brightnessctl -q set +1%"; + on-scroll-down = "${pkgs.brightnessctl}/bin/brightnessctl -q set 1%-"; }; pulseaudio = { format = "{volume}% {icon} {format_source}"; From bd4ee7054ed5b15ab7b072de2c5a171ab740addf Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 27 Oct 2025 23:06:02 +0100 Subject: [PATCH 11/22] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'nixpkgs': 'github:nixos/nixpkgs/481cf557888e05d3128a76f14c76397b7d7cc869?narHash=sha256-G/iC4t/9j/52i/nm%2B0/4ybBmAF4hzR8CNHC75qEhjHo%3D' (2025-10-21) → 'github:nixos/nixpkgs/78e34d1667d32d8a0ffc3eba4591ff256e80576e?narHash=sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8%3D' (2025-10-26) • Updated input 'nixpkgsMaster': 'github:NixOS/nixpkgs/d05426b82736631cf0a4dc8bceef1e52a4326dd7?narHash=sha256-XS7PTk22esg%2BHHg5OSF6%2BjgR6X/2WbzHj1CC8aS0x9E%3D' (2025-10-23) → 'github:NixOS/nixpkgs/8865b77677eb576ce1dbcb90b7a1ae95a774a6cd?narHash=sha256-rIne9pcxSoaLCxcyICguhH3SUzE9lep464L7zRGBbZk%3D' (2025-10-27) • Updated input 'nur': 'github:nix-community/NUR/1d182e3ee8a3a55dc50ff907877294652606a152?narHash=sha256-YF83M84JXliUtQjVsOeXMOtZNnhmTRd9YwH8aVUu1Io%3D' (2025-10-23) → 'github:nix-community/NUR/378c5c7b0b2471b59b71e42b229ea5e68050235d?narHash=sha256-0UtnyehKLys0HWhctZEjKN7zDe%2BML0HCDdqdfHk221o%3D' (2025-10-27) • Updated input 'nur/nixpkgs': 'github:nixos/nixpkgs/01f116e4df6a15f4ccdffb1bcd41096869fb385c?narHash=sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d%2BdAiC3H%2BCDle4%3D' (2025-10-22) → 'github:nixos/nixpkgs/6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce?narHash=sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c%3D' (2025-10-25) --- flake.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/flake.lock b/flake.lock index a706fcc..6771af6 100644 --- a/flake.lock +++ b/flake.lock @@ -418,11 +418,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1761016216, - "narHash": "sha256-G/iC4t/9j/52i/nm+0/4ybBmAF4hzR8CNHC75qEhjHo=", + "lastModified": 1761468971, + "narHash": "sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "481cf557888e05d3128a76f14c76397b7d7cc869", + "rev": "78e34d1667d32d8a0ffc3eba4591ff256e80576e", "type": "github" }, "original": { @@ -450,11 +450,11 @@ }, "nixpkgsMaster": { "locked": { - "lastModified": 1761206094, - "narHash": "sha256-XS7PTk22esg+HHg5OSF6+jgR6X/2WbzHj1CC8aS0x9E=", + "lastModified": 1761601585, + "narHash": "sha256-rIne9pcxSoaLCxcyICguhH3SUzE9lep464L7zRGBbZk=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "d05426b82736631cf0a4dc8bceef1e52a4326dd7", + "rev": "8865b77677eb576ce1dbcb90b7a1ae95a774a6cd", "type": "github" }, "original": { @@ -482,11 +482,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1761114652, - "narHash": "sha256-f/QCJM/YhrV/lavyCVz8iU3rlZun6d+dAiC3H+CDle4=", + "lastModified": 1761373498, + "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", "owner": "nixos", "repo": "nixpkgs", - "rev": "01f116e4df6a15f4ccdffb1bcd41096869fb385c", + "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", "type": "github" }, "original": { @@ -518,11 +518,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1761205534, - "narHash": "sha256-YF83M84JXliUtQjVsOeXMOtZNnhmTRd9YwH8aVUu1Io=", + "lastModified": 1761601325, + "narHash": "sha256-0UtnyehKLys0HWhctZEjKN7zDe+ML0HCDdqdfHk221o=", "owner": "nix-community", "repo": "NUR", - "rev": "1d182e3ee8a3a55dc50ff907877294652606a152", + "rev": "378c5c7b0b2471b59b71e42b229ea5e68050235d", "type": "github" }, "original": { From e199dc603b56d010162f5dc2a214ca90381842e8 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Tue, 28 Oct 2025 04:04:44 +0100 Subject: [PATCH 12/22] Enable hardware-accelerated transcoding --- hosts/iron/configuration.nix | 10 +++++++++- hosts/iron/services/jellyfin/default.nix | 6 +++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/hosts/iron/configuration.nix b/hosts/iron/configuration.nix index cdf307b..57a422a 100644 --- a/hosts/iron/configuration.nix +++ b/hosts/iron/configuration.nix @@ -218,7 +218,15 @@ with lib; { }; }; - hardware.enableRedistributableFirmware = true; + hardware = { + enableRedistributableFirmware = true; + graphics = { + enable = true; + extraPackages = [ + pkgs.intel-vaapi-driver + ]; + }; + }; virtualisation.containers.storage.settings = { storage = { diff --git a/hosts/iron/services/jellyfin/default.nix b/hosts/iron/services/jellyfin/default.nix index f5c31ab..eb4e918 100644 --- a/hosts/iron/services/jellyfin/default.nix +++ b/hosts/iron/services/jellyfin/default.nix @@ -24,10 +24,12 @@ in services.jellyfin = { enable = true; }; + systemd.services.jellyfin = { serviceConfig = { ###MemoryDenyWriteExecute = true; BindPaths = [ + "/dev/dri/renderD128" "/var/cache/jellyfin" "/var/lib/jellyfin" ]; @@ -38,13 +40,15 @@ in "/filebitch/pub/Filme" "/filebitch/pub/Serien" "/nix/store" + "/run/opengl-driver" "/var/lib/qBittorrent/downloads" ]; CapabilityBoundingSet = ""; + DeviceAllow = "/dev/dri/renderD128 rw"; #IPAddressAllow = "localhost"; #IPAddressDeny = "any"; LockPersonality = true; - PrivateDevices = lib.mkForce true; + PrivateDevices = false; PrivateUsers = true; ProtectClock = true; ProtectControlGroups = true; From d139988fa9d8526a2c653f860dc14fbf56e555ac Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Tue, 28 Oct 2025 21:06:14 +0100 Subject: [PATCH 13/22] Use nightly build of Jameica as VoP is not available in a stable release yet. --- users/jalr/modules/jameica.nix | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/users/jalr/modules/jameica.nix b/users/jalr/modules/jameica.nix index d9472e0..e50c86e 100644 --- a/users/jalr/modules/jameica.nix +++ b/users/jalr/modules/jameica.nix @@ -1,6 +1,16 @@ { nixosConfig, lib, pkgs, ... }: lib.mkIf nixosConfig.jalr.gui.enable { - home.packages = with pkgs; [ - jameica + home.packages = [ + ( + pkgs.jameica.overrideAttrs (_: { + version = "2.11.0-nightly"; + src = pkgs.fetchFromGitHub { + owner = "willuhn"; + repo = "jameica"; + rev = "e51bffc0e42907cbd802a644ab52810e0a36fff8"; + hash = "sha256-0KcT52dh/tJSX6q+uKkRybz33jKnYRTNDo1BftwJLAc="; + }; + }) + ) ]; } From 5cb7783a167c359f8f4becb50c54a2bf1e67ea38 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Wed, 29 Oct 2025 14:07:06 +0100 Subject: [PATCH 14/22] Use avahi-daemon release candidate Let's see if this fixes the high CPU load issues. --- modules/avahi.nix | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/modules/avahi.nix b/modules/avahi.nix index d0653bf..c12bd75 100644 --- a/modules/avahi.nix +++ b/modules/avahi.nix @@ -1,8 +1,35 @@ -{ config, lib, ... }: +{ config, lib, pkgs, ... }: lib.mkIf config.jalr.gui.enable { services.avahi = { enable = true; + package = + let + xmltoman = pkgs.xmltoman.overrideAttrs (_: { + nativeBuildInputs = [ + pkgs.installShellFiles + ]; + buildInputs = [ + (pkgs.perl.withPackages (pl: [ + pl.XMLParser + ])) + ]; + }); + in + pkgs.avahi.overrideAttrs (o: rec { + version = "0.9-rc2"; + src = pkgs.fetchurl { + url = "https://github.com/avahi/avahi/archive/refs/tags/v${version}.tar.gz"; + sha256 = "sha256-9k7+1qlyz5LLLfs1q/aqkXPWK4Q7FYUML0CvdqQjj4o="; + }; + patches = [ ]; + buildInputs = o.buildInputs ++ [ pkgs.systemdLibs ]; + nativeBuildInputs = o.nativeBuildInputs ++ [ xmltoman ]; + installFlags = [ + "runstatedir=${placeholder "out"}/run" + "sysconfdir=${placeholder "out"}/etc" + ]; + }); nssmdns4 = true; extraConfig = '' [server] From 5de772dbd5a782279016db41dce38e9ad2b97819 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 3 Nov 2025 20:13:04 +0100 Subject: [PATCH 15/22] Use package from master --- modules/esphome/default.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/esphome/default.nix b/modules/esphome/default.nix index 9bf4609..4edf71e 100644 --- a/modules/esphome/default.nix +++ b/modules/esphome/default.nix @@ -32,7 +32,7 @@ in enable = true; address = "127.0.0.1"; inherit (cfg) port; - package = pkgs.esphome; + package = pkgs.master.esphome; }; systemd.services.esphome = { From d2e08e999c4c22ba1306a6c53e605b77473eba70 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 3 Nov 2025 20:13:18 +0100 Subject: [PATCH 16/22] Use package from master --- hosts/iron/services/snapcast/ledfx.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/iron/services/snapcast/ledfx.nix b/hosts/iron/services/snapcast/ledfx.nix index d7db19d..99ee625 100644 --- a/hosts/iron/services/snapcast/ledfx.nix +++ b/hosts/iron/services/snapcast/ledfx.nix @@ -59,7 +59,7 @@ in wantedBy = [ "multi-user.target" ]; serviceConfig = { DynamicUser = "yes"; - ExecStart = "${pkgs.ledfx}/bin/ledfx --host 0.0.0.0 -p 8888 -c %S/ledfx"; + ExecStart = "${pkgs.master.ledfx}/bin/ledfx --host 0.0.0.0 -p 8888 -c %S/ledfx"; Group = "pipewire"; NoNewPrivileges = true; ProtectControlGroups = true; From 4bf2bd792559878c9d42de4fe1568ad60b76f3d9 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Mon, 3 Nov 2025 20:13:37 +0100 Subject: [PATCH 17/22] Use package from master --- modules/pipewire.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/pipewire.nix b/modules/pipewire.nix index 29e1845..ea97fb6 100644 --- a/modules/pipewire.nix +++ b/modules/pipewire.nix @@ -8,6 +8,7 @@ lib.mkIf config.jalr.gui.enable { services.pipewire = { enable = true; + package = pkgs.master.pipewire; pulse = { enable = true; }; From c7222d950098a8eb00f51ab58805b9b2d575742b Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Fri, 7 Nov 2025 15:10:36 +0100 Subject: [PATCH 18/22] Add Grafana to iron --- hosts/iron/ports.nix | 1 + hosts/iron/secrets.yaml | 8 +- hosts/iron/services/default.nix | 1 + hosts/iron/services/grafana.nix | 146 ++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 hosts/iron/services/grafana.nix diff --git a/hosts/iron/ports.nix b/hosts/iron/ports.nix index 2b2b495..7a60b1f 100644 --- a/hosts/iron/ports.nix +++ b/hosts/iron/ports.nix @@ -5,6 +5,7 @@ calibre-server.tcp = 8081; calibre-web.tcp = 8082; esphome.tcp = 6052; + grafana.tcp = 3001; home-assistant.tcp = 8123; jellyfin.tcp = 8096; matrix-synapse.tcp = 8008; diff --git a/hosts/iron/secrets.yaml b/hosts/iron/secrets.yaml index 90e5c61..3a51f2f 100644 --- a/hosts/iron/secrets.yaml +++ b/hosts/iron/secrets.yaml @@ -23,6 +23,8 @@ prometheus: vodafone-station: ENC[AES256_GCM,data:eaFqYEuK3UU=,iv:BauymCkvj33TmZLyii367uVEc4Iq4GGcik4nbyT9Fpk=,tag:poB+qh5tAdv/dEt3WN6yVw==,type:str] unpoller: ENC[AES256_GCM,data:WI1oUKHW4ef4pBk+mGM=,iv:C1LykPf1/ypUmy3ZCQzjfSjkpxhUukDNnfJnZLp2CJg=,tag:mSnZJKl9IHcx7I7GpFherw==,type:str] tvproxy: ENC[AES256_GCM,data:MbXEmgerpUiwDgcUKF2y1+Cc+d43sKPfGGTEkvNoZFFS4rzDWw4Udg==,iv:ZDsfSb3HK008e7/J/61iqVRafIzKbtPEdhH7ixo9lSY=,tag:3JbJ+2DJKQ9G2ui6VuWbOw==,type:str] +grafana: + secret-key: ENC[AES256_GCM,data:RX0ox0r3Jwm9DMIfBnsL7ydarlrYSVBjbVXbooHR1Ms=,iv:l8Aud8VyGtz3dNARh6s8/Y6MBtc4xj1Wu/LLJv1e+KA=,tag:+7TFyRPhBS1Tvn2JLBEeAQ==,type:str] sops: age: - recipient: age1hx7fdu4mcha7kkxe7yevtvs6xgzgaafgenm3drhvr609wlj94sgqm497je @@ -34,8 +36,8 @@ sops: SU1USkxFUUY2NVhmUHBhZkdrNDR1Q0kKiXIicInELRjDR3tuyA+lnXeCcd9lYvbV GnBRGPM7BNO/6AA7HhAei48Kt+XE6+jQX66yTXyviKhK7Lpjrlb2YQ== -----END AGE ENCRYPTED FILE----- - lastmodified: "2025-09-23T17:32:09Z" - mac: ENC[AES256_GCM,data:D4I6ayuLKBIxvEQopWXYHB/2fojEPn3oARU+9AiOrqX35Ue5BXZc08dmz+0J+RHjX5dA78xG6FGI3x4TtGeBwkMHeXwwwzRBuLDoGBGVngwLKvf21To6051A201EFqV7RKa5L8WruaJoeNLylH1K1B4nOrpv7G/81yCdpjmctQ0=,iv:QChpkOanDVj0PHykmA1MvDAYwiWawEmZ3h0s40U9joA=,tag:WluhQpBonORXgA9i01kTpw==,type:str] + lastmodified: "2025-11-04T17:52:25Z" + mac: ENC[AES256_GCM,data:/q98uwoYJsPRLlWxxDn7gJQ0jRxlAfVxEmUw8ayP8gIkWzGN1DCR0jx2LFlSlWEuaPScThw5IhGxbBlBxX2wV952MC7tEoHAAMvMJberG1a6do8zSvotDHocdXVlyj4jJZhQvjUVAmeVsYBY3oRwOHdzis0JO5IW0hxgs3x+xoQ=,iv:9BR0ws9ZzukjxLpPjvl73B3RmLA+c9e7F3AVk5l0SGc=,tag:OJ/iGy/Umlj/82EtZxjLSg==,type:str] pgp: - created_at: "2024-01-31T01:20:30Z" enc: |- @@ -49,4 +51,4 @@ sops: -----END PGP MESSAGE----- fp: 3044E71E3DEFF49B586CF5809BF4FCCB90854DA9 unencrypted_suffix: _unencrypted - version: 3.10.2 + version: 3.11.0 diff --git a/hosts/iron/services/default.nix b/hosts/iron/services/default.nix index 6c0e206..29cd267 100644 --- a/hosts/iron/services/default.nix +++ b/hosts/iron/services/default.nix @@ -5,6 +5,7 @@ ./dnsmasq.nix ./dyndns.nix ./esphome + ./grafana.nix ./home-assistant.nix ./jellyfin ./mail.nix diff --git a/hosts/iron/services/grafana.nix b/hosts/iron/services/grafana.nix new file mode 100644 index 0000000..15e1571 --- /dev/null +++ b/hosts/iron/services/grafana.nix @@ -0,0 +1,146 @@ +{ config +, lib +, pkgs +, ... +}: +let + inherit (config.networking) ports; + domain = "grafana.jalr.de"; + cfg = config.services.grafana; +in +{ + sops.secrets = { + "grafana/secret-key" = { + sopsFile = ../secrets.yaml; + owner = config.systemd.services.grafana.serviceConfig.User; + }; + }; + + services.grafana = { + enable = true; + settings = { + server = { + inherit domain; + root_url = "https://%(domain)s"; + http_addr = "127.0.0.1"; + http_port = ports.grafana.tcp; + }; + security = { + content_security_policy = true; + cookie_samesite = "strict"; + cookie_secure = true; + secret_key = "$__file{${config.sops.secrets."grafana/secret-key".path}}"; + strict_transport_security = true; + strict_transport_security_preload = true; + strict_transport_security_subdomains = true; + }; + analytics = { + reporting_enabled = false; + check_for_updates = false; + check_for_plugin_updates = false; + }; + }; + provision = { + datasources.settings = { + apiVersion = 1; + datasources = with config.services.prometheus; + ( + lib.lists.optional enable { + name = "Prometheus"; + type = "prometheus"; + url = "http://${listenAddress}:${toString port}"; + orgId = 1; + } + ) + ++ (with config.services.prometheus.alertmanager; ( + lib.lists.optional enable { + name = "Alertmanager"; + type = "alertmanager"; + url = "http://${listenAddress}:${toString port}"; + orgId = 1; + } + )); + deleteDatasources = [ + { + name = "Prometheus"; + orgId = 1; + } + { + name = "Alertmanager"; + orgId = 1; + } + ]; + }; + + dashboards.settings.providers = + let + # https://grafana.com/grafana/dashboards/ + fetchDashboard = + { name + , hash + , id + , version + , + }: + pkgs.fetchurl { + inherit name hash; + url = "https://grafana.com/api/dashboards/${toString id}/revisions/${toString version}/download"; + recursiveHash = true; + postFetch = '' + mv "$out" temp + mkdir -p "$out" + mv temp "$out/${name}.json"; + ''; + }; + dashboard = name: fetchArgs: { + inherit name; + options.path = fetchDashboard fetchArgs; + }; + in + [ + (dashboard "Node Exporter Full" + { + name = "node-exporter-full"; + hash = "sha256-QTHG9ioy7E8U8O8x/qFabOxK2qBjlGlzuEvwYKug0CQ="; + id = 1860; + version = 36; + }) + (dashboard "Node Exporter" + { + name = "node-exporter"; + hash = "sha256-2xgE0m3SUFiux501uCVb4aH3zGfapW/SmfxRsFC/514="; + id = 13978; + version = 2; + }) + (dashboard "AlertManager" + { + name = "alertmanager"; + hash = "sha256-Yvw0DGQJpqBYNzE4ES/x7ZAYF7iJ4SUNBKB+sJRuGBw="; + id = 9578; + version = 4; + }) + ]; + }; + }; + services.nginx.virtualHosts = { + "${domain}" = { + enableACME = true; + forceSSL = true; + + locations."/" = { + proxyPass = "http://${cfg.settings.server.http_addr}:${toString cfg.settings.server.http_port}"; + proxyWebsockets = true; + recommendedProxySettings = true; + }; + }; + }; + + environment.persistence."/persist".directories = [ + { + directory = "/var/lib/grafana"; + user = "grafana"; + group = "grafana"; + mode = "u=rwx,g=,o="; + } + ]; +} From 9dafacebda649da986892c6adca31a907b6138bd Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Tue, 11 Nov 2025 19:34:02 +0100 Subject: [PATCH 19/22] Add BLE sniffer to Wireshark --- modules/default.nix | 2 +- modules/wireshark.nix | 7 - modules/wireshark/default.nix | 23 + .../extcap/SnifferAPI/CaptureFiles.py | 91 ++ .../wireshark/extcap/SnifferAPI/Devices.py | 143 +++ .../wireshark/extcap/SnifferAPI/Exceptions.py | 60 ++ .../wireshark/extcap/SnifferAPI/Filelock.py | 69 ++ modules/wireshark/extcap/SnifferAPI/Logger.py | 207 +++++ .../extcap/SnifferAPI/Notifications.py | 90 ++ modules/wireshark/extcap/SnifferAPI/Packet.py | 583 ++++++++++++ modules/wireshark/extcap/SnifferAPI/Pcap.py | 79 ++ .../wireshark/extcap/SnifferAPI/Sniffer.py | 248 +++++ .../extcap/SnifferAPI/SnifferCollector.py | 283 ++++++ modules/wireshark/extcap/SnifferAPI/Types.py | 90 ++ modules/wireshark/extcap/SnifferAPI/UART.py | 233 +++++ .../wireshark/extcap/SnifferAPI/__init__.py | 0 .../wireshark/extcap/SnifferAPI/version.py | 38 + modules/wireshark/extcap/nrf_sniffer_ble.py | 854 ++++++++++++++++++ 18 files changed, 3092 insertions(+), 8 deletions(-) delete mode 100644 modules/wireshark.nix create mode 100644 modules/wireshark/default.nix create mode 100644 modules/wireshark/extcap/SnifferAPI/CaptureFiles.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Devices.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Exceptions.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Filelock.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Logger.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Notifications.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Packet.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Pcap.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Sniffer.py create mode 100644 modules/wireshark/extcap/SnifferAPI/SnifferCollector.py create mode 100644 modules/wireshark/extcap/SnifferAPI/Types.py create mode 100644 modules/wireshark/extcap/SnifferAPI/UART.py create mode 100644 modules/wireshark/extcap/SnifferAPI/__init__.py create mode 100644 modules/wireshark/extcap/SnifferAPI/version.py create mode 100644 modules/wireshark/extcap/nrf_sniffer_ble.py diff --git a/modules/default.nix b/modules/default.nix index ac459f1..cd6a1cf 100644 --- a/modules/default.nix +++ b/modules/default.nix @@ -50,7 +50,7 @@ ./uefi.nix ./unfree.nix ./upgrade-diff.nix - ./wireshark.nix + ./wireshark ./yubikey-gpg.nix ]; diff --git a/modules/wireshark.nix b/modules/wireshark.nix deleted file mode 100644 index 878f649..0000000 --- a/modules/wireshark.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ config, lib, pkgs, ... }: -lib.mkIf config.jalr.gui.enable { - programs.wireshark = { - enable = true; - package = pkgs.wireshark; - }; -} diff --git a/modules/wireshark/default.nix b/modules/wireshark/default.nix new file mode 100644 index 0000000..6c1b0c0 --- /dev/null +++ b/modules/wireshark/default.nix @@ -0,0 +1,23 @@ +{ config, lib, pkgs, ... }: +let + extcap = ./extcap; + pythonWithPackages = pkgs.python3.withPackages (pp: with pp; [ + pyserial + psutil + ]); + nrf_sniffer_ble = pkgs.writeShellScript "nrf_sniffer_ble" '' + script_path=$(dirname `which $0`) + + exec ${pythonWithPackages}/bin/python3 $script_path/nrf_sniffer_ble.py "$@" + ''; +in +lib.mkIf config.jalr.gui.enable { + programs.wireshark = { + enable = true; + package = pkgs.wireshark.overrideAttrs (o: { + postInstall = '' + cp -r ${extcap}/* ${nrf_sniffer_ble} $out/lib/wireshark/extcap + '' + o.postInstall; + }); + }; +} diff --git a/modules/wireshark/extcap/SnifferAPI/CaptureFiles.py b/modules/wireshark/extcap/SnifferAPI/CaptureFiles.py new file mode 100644 index 0000000..f5cf6cb --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/CaptureFiles.py @@ -0,0 +1,91 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import time, os, logging +from . import Logger +from . import Pcap + + +DEFAULT_CAPTURE_FILE_DIR = Logger.DEFAULT_LOG_FILE_DIR +DEFAULT_CAPTURE_FILE_NAME = "capture.pcap" + + +def get_capture_file_path(capture_file_path=None): + default_path = os.path.join(DEFAULT_CAPTURE_FILE_DIR, DEFAULT_CAPTURE_FILE_NAME) + if capture_file_path is None: + return default_path + if os.path.splitext(capture_file_path)[1] != ".pcap": + return default_path + return os.path.abspath(capture_file_path) + + +class CaptureFileHandler: + def __init__(self, capture_file_path=None, clear=False): + filename = get_capture_file_path(capture_file_path) + if not os.path.isdir(os.path.dirname(filename)): + os.makedirs(os.path.dirname(filename)) + self.filename = filename + self.backupFilename = self.filename+".1" + if not os.path.isfile(self.filename): + self.startNewFile() + elif os.path.getsize(self.filename) > 20000000: + self.doRollover() + if clear: + #clear file + self.startNewFile() + + def startNewFile(self): + with open(self.filename, "wb") as f: + f.write(Pcap.get_global_header()) + + def doRollover(self): + try: + os.remove(self.backupFilename) + except: + logging.exception("capture file rollover remove backup failed") + try: + os.rename(self.filename, self.backupFilename) + self.startNewFile() + except: + logging.exception("capture file rollover failed") + + def writePacket(self, packet): + with open(self.filename, "ab") as f: + packet = Pcap.create_packet( + bytes([packet.boardId] + packet.getList()), + packet.time) + f.write(packet) diff --git a/modules/wireshark/extcap/SnifferAPI/Devices.py b/modules/wireshark/extcap/SnifferAPI/Devices.py new file mode 100644 index 0000000..8ea3c0e --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Devices.py @@ -0,0 +1,143 @@ +# Copyright (c) 2017, Nordic Semiconductor ASA +# +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from . import Notifications +import logging, threading + +class DeviceList(Notifications.Notifier): + def __init__(self, *args, **kwargs): + Notifications.Notifier.__init__(self, *args, **kwargs) + logging.info("args: " + str(args)) + logging.info("kwargs: " + str(kwargs)) + self._deviceListLock = threading.RLock() + with self._deviceListLock: + self.devices = [] + + def __len__(self): + return len(self.devices) + + def __repr__(self): + return "Sniffer Device List: "+str(self.asList()) + + def clear(self): + logging.info("Clearing") + with self._deviceListLock: + self.devices = [] + self.notify("DEVICES_CLEARED") + + def appendOrUpdate(self, newDevice): + with self._deviceListLock: + existingDevice = self.find(newDevice) + + # Add device to the list of devices being displayed, but only if CRC is OK + if existingDevice == None: + self.append(newDevice) + else: + updated = False + if (newDevice.name != "\"\"") and (existingDevice.name == "\"\""): + existingDevice.name = newDevice.name + updated = True + + if (newDevice.RSSI != 0 and (existingDevice.RSSI < (newDevice.RSSI - 5)) or (existingDevice.RSSI > (newDevice.RSSI+2))): + existingDevice.RSSI = newDevice.RSSI + updated = True + + if updated: + self.notify("DEVICE_UPDATED", existingDevice) + + def append(self, device): + self.devices.append(device) + self.notify("DEVICE_ADDED", device) + + def find(self, id): + if type(id) == list: + for dev in self.devices: + if dev.address == id: + return dev + elif type(id) == int: + return self.devices[id] + elif type(id) == str: + for dev in self.devices: + if dev.name in [id, '"'+id+'"']: + return dev + elif id.__class__.__name__ == "Device": + return self.find(id.address) + return None + + def remove(self, id): + if type(id) == list: #address + device = self.devices.pop(self.devices.index(self.find(id))) + elif type(id) == int: + device = self.devices.pop(id) + elif type(id) == Device: + device = self.devices.pop(self.devices.index(self.find(id.address))) + self.notify("DEVICE_REMOVED", device) + + def index(self, device): + index = 0 + for dev in self.devices: + if dev.address == device.address: + return index + index += 1 + return None + + def setFollowed(self, device): + if device in self.devices: + for dev in self.devices: + dev.followed = False + device.followed = True + self.notify("DEVICE_FOLLOWED", device) + + def asList(self): + return self.devices[:] + +class Device: + def __init__(self, address, name, RSSI): + self.address = address + self.name = name + self.RSSI = RSSI + self.followed = False + + def __repr__(self): + return 'Bluetooth LE device "'+self.name+'" ('+str(self.address)+')' + +def listToString(list): + str = "" + for i in list: + str+=chr(i) + return str diff --git a/modules/wireshark/extcap/SnifferAPI/Exceptions.py b/modules/wireshark/extcap/SnifferAPI/Exceptions.py new file mode 100644 index 0000000..f3ab20c --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Exceptions.py @@ -0,0 +1,60 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +class SnifferTimeout(Exception): + pass + +class UARTPacketError(Exception): + pass + +class LockedException(Exception): + def __init__(self, message): + self.message = message + +class InvalidPacketException(Exception): + pass + +class InvalidAdvChannel(Exception): + pass + +# Internal Use +class SnifferWatchDogTimeout(SnifferTimeout): + pass + +# Internal Use +class ExitCodeException(Exception): + pass diff --git a/modules/wireshark/extcap/SnifferAPI/Filelock.py b/modules/wireshark/extcap/SnifferAPI/Filelock.py new file mode 100644 index 0000000..5570942 --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Filelock.py @@ -0,0 +1,69 @@ +import os +import logging +from sys import platform + +if platform == 'linux': + import psutil + +from . import Exceptions + +# Lock file management. +# ref: https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch05s09.html +# +# Stored in /var/lock: +# The naming convention which must be used is "LCK.." followed by the base name of the device. +# For example, to lock /dev/ttyS0 the file "LCK..ttyS0" would be created. +# HDB UUCP lock file format: +# process identifier (PID) as a ten byte ASCII decimal number, with a trailing newline + +def lockpid(lockfile): + if (os.path.isfile(lockfile)): + with open(lockfile) as fd: + lockpid = fd.read() + + try: + return int(lockpid) + except: + logging.info("Lockfile is invalid. Overriding it..") + os.remove(lockfile) + return 0 + + return 0 + +def lock(port): + if platform != 'linux': + return + + tty = os.path.basename(port) + lockfile = os.path.join( + '/run', + 'user', + f'{os.getuid()}', + f'{tty}.lock' + ) + + lockedpid = lockpid(lockfile) + if lockedpid: + if lockedpid == os.getpid(): + return + + if psutil.pid_exists(lockedpid): + raise Exceptions.LockedException(f"Device {port} is locked") + else: + logging.info("Lockfile is stale. Overriding it..") + os.remove(lockfile) + + fd = open(lockfile, 'w') + with open(lockfile, 'w') as fd: + fd.write(f'{os.getpid():10}') + +def unlock(port): + if platform != 'linux': + return + + tty = os.path.basename(port) + lockfile = f'/var/lock/LCK..{tty}' + + lockedpid = lockpid(lockfile) + if lockedpid == os.getpid(): + os.remove(lockfile) diff --git a/modules/wireshark/extcap/SnifferAPI/Logger.py b/modules/wireshark/extcap/SnifferAPI/Logger.py new file mode 100644 index 0000000..cbb1e7c --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Logger.py @@ -0,0 +1,207 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import time, os, logging, traceback, threading +import logging.handlers as logHandlers + +################################################################# +# This file contains the logger. To log a line, simply write # +# 'logging.[level]("whatever you want to log")' # +# [level] is one of {info, debug, warning, error, critical, # +# exception} # +# See python logging documentation # +# As long as Logger.initLogger has been called beforehand, this # +# will result in the line being appended to the log file # +################################################################# + +appdata = os.getenv('appdata') +if appdata: + DEFAULT_LOG_FILE_DIR = os.path.join(appdata, 'Nordic Semiconductor', 'Sniffer', 'logs') +else: + DEFAULT_LOG_FILE_DIR = "/tmp/logs" + +DEFAULT_LOG_FILE_NAME = "log.txt" + +logFileName = None +logHandler = None +logHandlerArray = [] +logFlusher = None + +myMaxBytes = 1000000 + + +def setLogFileName(log_file_path): + global logFileName + logFileName = os.path.abspath(log_file_path) + + +# Ensure that the directory we are writing the log file to exists. +# Create our logfile, and write the timestamp in the first line. +def initLogger(): + try: + global logFileName + if logFileName is None: + logFileName = os.path.join(DEFAULT_LOG_FILE_DIR, DEFAULT_LOG_FILE_NAME) + + # First, make sure that the directory exists + if not os.path.isdir(os.path.dirname(logFileName)): + os.makedirs(os.path.dirname(logFileName)) + + # If the file does not exist, create it, and save the timestamp + if not os.path.isfile(logFileName): + with open(logFileName, "w") as f: + f.write(str(time.time()) + str(os.linesep)) + + global logFlusher + global logHandlerArray + + logHandler = MyRotatingFileHandler(logFileName, mode='a', maxBytes=myMaxBytes, backupCount=3) + logFormatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%d-%b-%Y %H:%M:%S (%z)') + logHandler.setFormatter(logFormatter) + logger = logging.getLogger() + logger.addHandler(logHandler) + logger.setLevel(logging.INFO) + logFlusher = LogFlusher(logHandler) + logHandlerArray.append(logHandler) + except: + print("LOGGING FAILED") + print(traceback.format_exc()) + raise + + +def shutdownLogger(): + if logFlusher is not None: + logFlusher.stop() + logging.shutdown() + + +# Clear the log (typically after it has been sent on email) +def clearLog(): + try: + logHandler.doRollover() + except: + print("LOGGING FAILED") + raise + + +# Returns the timestamp residing on the first line of the logfile. Used for checking the time of creation +def getTimestamp(): + try: + with open(logFileName, "r") as f: + f.seek(0) + return f.readline() + except: + print("LOGGING FAILED") + + +def addTimestamp(): + try: + with open(logFileName, "a") as f: + f.write(str(time.time()) + os.linesep) + except: + print("LOGGING FAILED") + + +# Returns the entire content of the logfile. Used when sending emails +def readAll(): + try: + text = "" + with open(logFileName, "r") as f: + text = f.read() + return text + except: + print("LOGGING FAILED") + + +def addLogHandler(logHandler): + global logHandlerArray + logger = logging.getLogger() + logger.addHandler(logHandler) + logger.setLevel(logging.INFO) + logHandlerArray.append(logHandler) + +def removeLogHandler(logHandler): + global logHandlerArray + logger = logging.getLogger() + logger.removeHandler(logHandler) + logHandlerArray.remove(logHandler) + + +class MyRotatingFileHandler(logHandlers.RotatingFileHandler): + def doRollover(self): + try: + logHandlers.RotatingFileHandler.doRollover(self) + addTimestamp() + self.maxBytes = myMaxBytes + except: + # There have been permissions issues with the log files. + self.maxBytes += int(myMaxBytes / 2) + + +class LogFlusher(threading.Thread): + def __init__(self, logHandler): + threading.Thread.__init__(self) + + self.daemon = True + self.handler = logHandler + self.exit = threading.Event() + + self.start() + + def run(self): + while True: + if self.exit.wait(10): + try: + self.doFlush() + except AttributeError as e: + print(e) + break + self.doFlush() + + def doFlush(self): + self.handler.flush() + os.fsync(self.handler.stream.fileno()) + + def stop(self): + self.exit.set() + + +if __name__ == '__main__': + initLogger() + for i in range(50): + logging.info("test log no. " + str(i)) + print("test log no. ", i) diff --git a/modules/wireshark/extcap/SnifferAPI/Notifications.py b/modules/wireshark/extcap/SnifferAPI/Notifications.py new file mode 100644 index 0000000..d9fd8e4 --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Notifications.py @@ -0,0 +1,90 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import threading, logging + +class Notification(): + def __init__(self, key, msg = None): + if type(key) is not str: + raise TypeError("Invalid notification key: "+str(key)) + self.key = key + self.msg = msg + + def __repr__(self): + return "Notification (key: %s, msg: %s)" % (str(self.key), str(self.msg)) + +class Notifier(): + def __init__(self, callbacks = [], **kwargs): + self.callbacks = {} + self.callbackLock = threading.RLock() + + for callback in callbacks: + self.subscribe(*callback) + + def clearCallbacks(self): + with self.callbackLock: + self.callbacks.clear() + + def subscribe(self, key, callback): + with self.callbackLock: + if callback not in self.getCallbacks(key): + self.getCallbacks(key).append(callback) + + def unSubscribe(self, key, callback): + with self.callbackLock: + if callback in self.getCallbacks(key): + self.getCallbacks(key).remove(callback) + + def getCallbacks(self, key): + with self.callbackLock: + if key not in self.callbacks: + self.callbacks[key] = [] + return self.callbacks[key] + + def notify(self, key = None, msg = None, notification = None): + with self.callbackLock: + if notification == None: + notification = Notification(key,msg) + + for callback in self.getCallbacks(notification.key): + callback(notification) + + for callback in self.getCallbacks("*"): + callback(notification) + + def passOnNotification(self, notification): + self.notify(notification = notification) diff --git a/modules/wireshark/extcap/SnifferAPI/Packet.py b/modules/wireshark/extcap/SnifferAPI/Packet.py new file mode 100644 index 0000000..ddac78d --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Packet.py @@ -0,0 +1,583 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +from . import UART, Exceptions, Notifications +import time, logging, os, sys, serial +from .Types import * + +ADV_ACCESS_ADDRESS = [0xD6, 0xBE, 0x89, 0x8E] + +SYNCWORD_POS = 0 +PAYLOAD_LEN_POS_V1 = 1 +PAYLOAD_LEN_POS = 0 +PROTOVER_POS = PAYLOAD_LEN_POS+2 +PACKETCOUNTER_POS = PROTOVER_POS+1 +ID_POS = PACKETCOUNTER_POS+2 + +BLE_HEADER_LEN_POS = ID_POS+1 +FLAGS_POS = BLE_HEADER_LEN_POS+1 +CHANNEL_POS = FLAGS_POS+1 +RSSI_POS = CHANNEL_POS+1 +EVENTCOUNTER_POS = RSSI_POS+1 +TIMESTAMP_POS = EVENTCOUNTER_POS+2 +BLEPACKET_POS = TIMESTAMP_POS+4 +TXADD_POS = BLEPACKET_POS + 4 +TXADD_MSK = 0x40 +PAYLOAD_POS = BLE_HEADER_LEN_POS + +HEADER_LENGTH = 6 +BLE_HEADER_LENGTH = 10 + +VALID_ADV_CHANS = [37, 38, 39] + +PACKET_COUNTER_CAP = 2**16 + + +class PacketReader(Notifications.Notifier): + def __init__(self, portnum=None, callbacks=[], baudrate=None): + Notifications.Notifier.__init__(self, callbacks) + self.portnum = portnum + try: + self.uart = UART.Uart(portnum, baudrate) + except serial.SerialException as e: + logging.exception("Error opening UART %s" % str(e)) + self.uart = UART.Uart() + self.packetCounter = 0 + self.lastReceivedPacketCounter = 0 + self.lastReceivedPacket = None + self.lastReceivedTimestampPacket = None + self.supportedProtocolVersion = PROTOVER_V3 + + def setup(self): + pass + + def doExit(self): + # This method will always join the Uart worker thread + self.uart.close() + # Clear method references to avoid uncollectable cyclic references + self.clearCallbacks() + + # This function takes a byte list, encode it in SLIP protocol and return the encoded byte list + def encodeToSLIP(self, byteList): + tempSLIPBuffer = [] + tempSLIPBuffer.append(SLIP_START) + for i in byteList: + if i == SLIP_START: + tempSLIPBuffer.append(SLIP_ESC) + tempSLIPBuffer.append(SLIP_ESC_START) + elif i == SLIP_END: + tempSLIPBuffer.append(SLIP_ESC) + tempSLIPBuffer.append(SLIP_ESC_END) + elif i == SLIP_ESC: + tempSLIPBuffer.append(SLIP_ESC) + tempSLIPBuffer.append(SLIP_ESC_ESC) + else: + tempSLIPBuffer.append(i) + tempSLIPBuffer.append(SLIP_END) + return tempSLIPBuffer + + # This function uses getSerialByte() function to get SLIP encoded bytes from the serial port and return a decoded byte list + # Based on https://github.com/mehdix/pyslip/ + def decodeFromSLIP(self, timeout=None, complete_timeout=None): + dataBuffer = [] + startOfPacket = False + endOfPacket = False + + if complete_timeout is not None: + time_start = time.time() + + while not startOfPacket and (complete_timeout is None or (time.time() - time_start < complete_timeout)): + res = self.getSerialByte(timeout) + startOfPacket = (res == SLIP_START) + + while not endOfPacket and (complete_timeout is None or (time.time() - time_start < complete_timeout)): + serialByte = self.getSerialByte(timeout) + if serialByte == SLIP_END: + endOfPacket = True + elif serialByte == SLIP_ESC: + serialByte = self.getSerialByte(timeout) + if serialByte == SLIP_ESC_START: + dataBuffer.append(SLIP_START) + elif serialByte == SLIP_ESC_END: + dataBuffer.append(SLIP_END) + elif serialByte == SLIP_ESC_ESC: + dataBuffer.append(SLIP_ESC) + else: + dataBuffer.append(SLIP_END) + else: + dataBuffer.append(serialByte) + if not endOfPacket: + raise Exceptions.UARTPacketError("Exceeded max timeout of %f seconds." % complete_timeout) + return dataBuffer + + # This function read byte chuncks from the serial port and return one byte at a time + # Based on https://github.com/mehdix/pyslip/ + def getSerialByte(self, timeout=None): + serialByte = self.uart.readByte(timeout) + if serialByte is None: + raise Exceptions.SnifferTimeout("Packet read timed out.") + return serialByte + + def handlePacketHistory(self, packet): + # Reads and validates packet counter + if self.lastReceivedPacket is not None \ + and packet.packetCounter != (self.lastReceivedPacket.packetCounter + 1) % PACKET_COUNTER_CAP \ + and self.lastReceivedPacket.packetCounter != 0: + + logging.info("gap in packets, between " + str(self.lastReceivedPacket.packetCounter) + " and " + + str(packet.packetCounter) + " packet before: " + str(self.lastReceivedPacket.packetList) + + " packet after: " + str(packet.packetList)) + + self.lastReceivedPacket = packet + if packet.id in [EVENT_PACKET_DATA_PDU, EVENT_PACKET_ADV_PDU]: + self.lastReceivedTimestampPacket = packet + + def getPacketTime(self, packet): + ble_payload_length = self.lastReceivedPacket.payloadLength - BLE_HEADER_LENGTH + + if packet.phy == PHY_1M: + return 8 * (1 + ble_payload_length) + elif packet.phy == PHY_2M: + return 4 * (2 + ble_payload_length) + elif packet.phy == PHY_CODED: + # blePacket is not assigned if not packet is "OK" (CRC error) + ci = packet.packetList[BLEPACKET_POS + 4] + fec2_block_len = ble_payload_length - 4 - 1 + fec1_block_us = 80 + 256 + 16 + 24 + if ci == PHY_CODED_CI_S8: + return fec1_block_us + 64 * fec2_block_len + 24 + elif ci == PHY_CODED_CI_S2: + return fec1_block_us + 16 * fec2_block_len + 6 + # Unknown PHY or Coding Indicator + return 0 + + def convertPacketListProtoVer2(self, packet): + # Convert to version 2 + packet.packetList[PROTOVER_POS] = 2 + + # Convert to common packet ID + if packet.packetList[ID_POS] == EVENT_PACKET_ADV_PDU: + packet.packetList[ID_POS] = EVENT_PACKET_DATA_PDU + + if packet.packetList[ID_POS] != EVENT_PACKET_DATA_PDU: + # These types do not have a timestamp + return + + # Convert time-stamp to End to Start delta + time_delta = 0 + if self.lastReceivedTimestampPacket is not None and self.lastReceivedTimestampPacket.valid: + time_delta = (packet.timestamp - + (self.lastReceivedTimestampPacket.timestamp + + self.getPacketTime(self.lastReceivedTimestampPacket))) + + time_delta = toLittleEndian(time_delta, 4) + packet.packetList[TIMESTAMP_POS ] = time_delta[0] + packet.packetList[TIMESTAMP_POS+1] = time_delta[1] + packet.packetList[TIMESTAMP_POS+2] = time_delta[2] + packet.packetList[TIMESTAMP_POS+3] = time_delta[3] + + + def handlePacketCompatibility(self, packet): + if self.supportedProtocolVersion == PROTOVER_V2 and packet.packetList[PROTOVER_POS] > PROTOVER_V2: + self.convertPacketListProtoVer2(packet) + + def setSupportedProtocolVersion(self, supportedProtocolVersion): + if (supportedProtocolVersion != PROTOVER_V3): + logging.info("Using packet compatibility, converting packets to protocol version %d", supportedProtocolVersion) + self.supportedProtocolVersion = supportedProtocolVersion + + def getPacket(self, timeout=None): + packetList = [] + try: + packetList = self.decodeFromSLIP(timeout) + except Exceptions.UARTPacketError: # FIXME: This is never thrown... + logging.exception("") + return None + else: + packet = Packet(packetList) + if packet.valid: + self.handlePacketCompatibility(packet) + self.handlePacketHistory(packet) + return packet + + def sendPacket(self, id, payload): + packetList = [HEADER_LENGTH] + [len(payload)] + [PROTOVER_V1] + toLittleEndian(self.packetCounter, 2) + [id] + payload + packetList = self.encodeToSLIP(packetList) + self.packetCounter += 1 + self.uart.writeList(packetList) + + def sendScan(self, findScanRsp = False, findAux = False, scanCoded = False): + flags0 = findScanRsp | (findAux << 1) | (scanCoded << 2) + self.sendPacket(REQ_SCAN_CONT, [flags0]) + logging.info("Scan flags: %s" % bin(flags0)) + + def sendFollow(self, addr, followOnlyAdvertisements = False, followOnlyLegacy = False, followCoded = False): + flags0 = followOnlyAdvertisements | (followOnlyLegacy << 1) | (followCoded << 2) + logging.info("Follow flags: %s" % bin(flags0)) + self.sendPacket(REQ_FOLLOW, addr + [flags0]) + + def sendPingReq(self): + self.sendPacket(PING_REQ, []) + + def getBytes(self, value, size): + if (len(value) < size): + value = [0] * (size - len(value)) + value + else: + value = value[:size] + + return value + + def sendTK(self, TK): + TK = self.getBytes(TK, 16) + self.sendPacket(SET_TEMPORARY_KEY, TK) + logging.info("Sent TK to sniffer: " + str(TK)) + + def sendPrivateKey(self, pk): + pk = self.getBytes(pk, 32) + self.sendPacket(SET_PRIVATE_KEY, pk) + logging.info("Sent private key to sniffer: " + str(pk)) + + def sendLegacyLTK(self, ltk): + ltk = self.getBytes(ltk, 16) + self.sendPacket(SET_LEGACY_LONG_TERM_KEY, ltk) + logging.info("Sent Legacy LTK to sniffer: " + str(ltk)) + + def sendSCLTK(self, ltk): + ltk = self.getBytes(ltk, 16) + self.sendPacket(SET_SC_LONG_TERM_KEY, ltk) + logging.info("Sent SC LTK to sniffer: " + str(ltk)) + + def sendIRK(self, irk): + irk = self.getBytes(irk, 16) + self.sendPacket(SET_IDENTITY_RESOLVING_KEY, irk) + logging.info("Sent IRK to sniffer: " + str(irk)) + + def sendSwitchBaudRate(self, newBaudRate): + self.sendPacket(SWITCH_BAUD_RATE_REQ, toLittleEndian(newBaudRate, 4)) + + def switchBaudRate(self, newBaudRate): + self.uart.switchBaudRate(newBaudRate) + + def sendHopSequence(self, hopSequence): + for chan in hopSequence: + if chan not in VALID_ADV_CHANS: + raise Exceptions.InvalidAdvChannel("%s is not an adv channel" % str(chan)) + payload = [len(hopSequence)] + hopSequence + [37]*(3-len(hopSequence)) + self.sendPacket(SET_ADV_CHANNEL_HOP_SEQ, payload) + self.notify("NEW_ADV_HOP_SEQ", {"hopSequence":hopSequence}) + + def sendVersionReq(self): + self.sendPacket(REQ_VERSION, []) + + def sendTimestampReq(self): + self.sendPacket(REQ_TIMESTAMP, []) + + def sendGoIdle(self): + self.sendPacket(GO_IDLE, []) + + +class Packet: + def __init__(self, packetList): + try: + if not packetList: + raise Exceptions.InvalidPacketException("packet list not valid: %s" % str(packetList)) + + self.protover = packetList[PROTOVER_POS] + + if self.protover > PROTOVER_V3: + logging.exception("Unsupported protocol version %s" % str(self.protover)) + raise RuntimeError("Unsupported protocol version %s" % str(self.protover)) + + self.packetCounter = parseLittleEndian(packetList[PACKETCOUNTER_POS:PACKETCOUNTER_POS + 2]) + self.id = packetList[ID_POS] + + if int(self.protover) == PROTOVER_V1: + self.payloadLength = packetList[PAYLOAD_LEN_POS_V1] + else: + self.payloadLength = parseLittleEndian(packetList[PAYLOAD_LEN_POS:PAYLOAD_LEN_POS + 2]) + + self.packetList = packetList + self.readPayload(packetList) + + except Exceptions.InvalidPacketException as e: + logging.error("Invalid packet: %s" % str(e)) + self.OK = False + self.valid = False + except Exception as e: + logging.exception("packet creation error %s" %str(e)) + logging.info("packetList: " + str(packetList)) + self.OK = False + self.valid = False + + def __repr__(self): + return "UART packet, type: "+str(self.id)+", PC: "+str(self.packetCounter) + + def readPayload(self, packetList): + self.blePacket = None + self.OK = False + + if not self.validatePacketList(packetList): + raise Exceptions.InvalidPacketException("packet list not valid: %s" % str(packetList)) + else: + self.valid = True + + self.payload = packetList[PAYLOAD_POS:PAYLOAD_POS+self.payloadLength] + + if self.id == EVENT_PACKET_ADV_PDU or self.id == EVENT_PACKET_DATA_PDU: + try: + self.bleHeaderLength = packetList[BLE_HEADER_LEN_POS] + if self.bleHeaderLength == BLE_HEADER_LENGTH: + self.flags = packetList[FLAGS_POS] + self.readFlags() + self.channel = packetList[CHANNEL_POS] + self.rawRSSI = packetList[RSSI_POS] + self.RSSI = -self.rawRSSI + self.eventCounter = parseLittleEndian(packetList[EVENTCOUNTER_POS:EVENTCOUNTER_POS+2]) + + self.timestamp = parseLittleEndian(packetList[TIMESTAMP_POS:TIMESTAMP_POS+4]) + + # The hardware adds a padding byte which isn't sent on air. + # We remove it, and update the payload length in the packet list. + if self.phy == PHY_CODED: + self.packetList.pop(BLEPACKET_POS+6+1) + else: + self.packetList.pop(BLEPACKET_POS+6) + self.payloadLength -= 1 + if self.protover >= PROTOVER_V2: + # Write updated payload length back to the packet list. + payloadLength = toLittleEndian(self.payloadLength, 2) + packetList[PAYLOAD_LEN_POS ] = payloadLength[0] + packetList[PAYLOAD_LEN_POS+1] = payloadLength[1] + else: # PROTOVER_V1 + packetList[PAYLOAD_LEN_POS_V1] = self.payloadLength + else: + logging.info("Invalid BLE Header Length " + str(packetList)) + self.valid = False + + if self.OK: + try: + if self.protover >= PROTOVER_V3: + packet_type = (PACKET_TYPE_ADVERTISING + if self.id == EVENT_PACKET_ADV_PDU else + PACKET_TYPE_DATA) + else: + packet_type = (PACKET_TYPE_ADVERTISING + if packetList[BLEPACKET_POS : BLEPACKET_POS + 4] == ADV_ACCESS_ADDRESS else + PACKET_TYPE_DATA) + + self.blePacket = BlePacket(packet_type, packetList[BLEPACKET_POS:], self.phy) + except Exception as e: + logging.exception("blePacket error %s" % str(e)) + except Exception as e: + # malformed packet + logging.exception("packet error %s" % str(e)) + self.OK = False + elif self.id == PING_RESP: + if self.protover < PROTOVER_V3: + self.version = parseLittleEndian(packetList[PAYLOAD_POS:PAYLOAD_POS+2]) + elif self.id == RESP_VERSION: + self.version = ''.join([chr(i) for i in packetList[PAYLOAD_POS:]]) + elif self.id == RESP_TIMESTAMP: + self.timestamp = parseLittleEndian(packetList[PAYLOAD_POS:PAYLOAD_POS+4]) + elif self.id == SWITCH_BAUD_RATE_RESP or self.id == SWITCH_BAUD_RATE_REQ: + self.baudRate = parseLittleEndian(packetList[PAYLOAD_POS:PAYLOAD_POS+4]) + else: + logging.info("Unknown packet ID") + + def readFlags(self): + self.crcOK = not not (self.flags & 1) + self.direction = not not (self.flags & 2) + self.encrypted = not not (self.flags & 4) + self.micOK = not not (self.flags & 8) + self.phy = (self.flags >> 4) & 7 + self.OK = self.crcOK and (self.micOK or not self.encrypted) + + def getList(self): + return self.packetList + + def validatePacketList(self, packetList): + try: + if (self.payloadLength + HEADER_LENGTH) == len(packetList): + return True + else: + return False + except: + logging.exception("Invalid packet: %s" % str(packetList)) + return False + +class BlePacket(): + def __init__(self, type, packetList, phy): + self.type = type + + offset = 0 + offset = self.extractAccessAddress(packetList, offset) + offset = self.extractFormat(packetList, phy, offset) + + if self.type == PACKET_TYPE_ADVERTISING: + offset = self.extractAdvHeader(packetList, offset) + else: + offset = self.extractConnHeader(packetList, offset) + + offset = self.extractLength(packetList, offset) + self.payload = packetList[offset:] + + if self.type == PACKET_TYPE_ADVERTISING: + offset = self.extractAddresses(packetList, offset) + self.extractName(packetList, offset) + + + def __repr__(self): + return "BLE packet, AAddr: "+str(self.accessAddress) + + def extractAccessAddress(self, packetList, offset): + self.accessAddress = packetList[offset:offset+4] + return offset + 4 + + def extractFormat(self, packetList, phy, offset): + self.coded = phy == PHY_CODED + if self.coded: + self.codingIndicator = packetList[offset] & 3 + return offset + 1 + + return offset + + def extractAdvHeader(self, packetList, offset): + self.advType = packetList[offset] & 15 + self.txAddrType = (packetList[offset] >> 6) & 1 + if self.advType in [1, 3, 5]: + self.rxAddrType = (packetList[offset] << 7) & 1 + elif self.advType == 7: + flags = packetList[offset + 2] + if flags & 0x02: + self.rxAddrType = (packetList[offset] << 7) & 1 + return offset + 1 + + def extractConnHeader(self, packetList, offset): + self.llid = packetList[offset] & 3 + self.sn = (packetList[offset] >> 2) & 1 + self.nesn = (packetList[offset] >> 3) & 1 + self.md = (packetList[offset] >> 4) & 1 + return offset + 1 + + def extractAddresses(self, packetList, offset): + addr = None + scanAddr = None + + if self.advType in [0, 1, 2, 4, 6]: + addr = packetList[offset:offset+6] + addr.reverse() + addr += [self.txAddrType] + offset += 6 + + if self.advType in [3, 5]: + scanAddr = packetList[offset:offset+6] + scanAddr.reverse() + scanAddr += [self.txAddrType] + offset += 6 + addr = packetList[offset:offset+6] + addr.reverse() + addr += [self.rxAddrType] + offset += 6 + + if self.advType == 1: + scanAddr = packetList[offset:offset+6] + scanAddr.reverse() + scanAddr += [self.rxAddrType] + offset += 6 + + if self.advType == 7: + ext_header_len = packetList[offset] & 0x3f + offset += 1 + + ext_header_offset = offset + flags = packetList[offset] + ext_header_offset += 1 + + if flags & 0x01: + addr = packetList[ext_header_offset:ext_header_offset+6] + addr.reverse() + addr += [self.txAddrType] + ext_header_offset += 6 + + if flags & 0x02: + scanAddr = packetList[ext_header_offset:ext_header_offset+6] + scanAddr.reverse() + scanAddr += [self.rxAddrType] + ext_header_offset += 6 + + offset += ext_header_len + + self.advAddress = addr + self.scanAddress = scanAddr + return offset + + def extractName(self, packetList, offset): + name = "" + if self.advType in [0, 2, 4, 6, 7]: + i = offset + while i < len(packetList): + length = packetList[i] + if (i+length+1) > len(packetList) or length == 0: + break + type = packetList[i+1] + if type == 8 or type == 9: + nameList = packetList[i+2:i+length+1] + name = "" + for j in nameList: + name += chr(j) + i += (length+1) + name = '"'+name+'"' + elif (self.advType == 1): + name = "[ADV_DIRECT_IND]" + + self.name = name + + def extractLength(self, packetList, offset): + self.length = packetList[offset] + return offset + 1 + +def parseLittleEndian(list): + total = 0 + for i in range(len(list)): + total+=(list[i] << (8*i)) + return total + +def toLittleEndian(value, size): + list = [0]*size + for i in range(size): + list[i] = (value >> (i*8)) % 256 + return list + diff --git a/modules/wireshark/extcap/SnifferAPI/Pcap.py b/modules/wireshark/extcap/SnifferAPI/Pcap.py new file mode 100644 index 0000000..63cbe41 --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Pcap.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +import struct + + +# See: +# - https://github.com/pcapng/pcapng +# - https://www.tcpdump.org/linktypes/LINKTYPE_NORDIC_BLE.html +PACKET_HEADER = struct.Struct("= PROTOVER_V3: + if self._last_time is None: + # Timestamp from Host + packet.time = time.time() + else: + # Timestamp using reference and packet timestamp diff + if packet.timestamp < self._last_timestamp: + time_diff = (1 << 32) - (self._last_timestamp - packet.timestamp) + else: + time_diff = (packet.timestamp - self._last_timestamp) + + packet.time = self._last_time + (time_diff / 1_000_000) + + self._last_time = packet.time + self._last_timestamp = packet.timestamp + else: + # Timestamp from Host + packet.time = time.time() + + self._appendPacket(packet) + + self.notify("NEW_BLE_PACKET", {"packet": packet}) + self._captureHandler.writePacket(packet) + + self._nProcessedPackets += 1 + if packet.OK: + try: + if packet.blePacket.type == PACKET_TYPE_ADVERTISING: + + if self.state == STATE_FOLLOWING and packet.blePacket.advType == 5: + self._connectionAccessAddress = packet.blePacket.accessAddress + + if self.state == STATE_FOLLOWING and packet.blePacket.advType == 4: + newDevice = Devices.Device(address=packet.blePacket.advAddress, name=packet.blePacket.name, RSSI=packet.RSSI) + self._devices.appendOrUpdate(newDevice) + + if self.state == STATE_SCANNING: + if (packet.blePacket.advType in [0, 1, 2, 4, 6, 7] and + packet.blePacket.advAddress != None and + packet.crcOK and + not packet.direction + ): + newDevice = Devices.Device(address=packet.blePacket.advAddress, name=packet.blePacket.name, RSSI=packet.RSSI) + self._devices.appendOrUpdate(newDevice) + + except Exception as e: + logging.exception("packet processing error %s" % str(e)) + self.notify("PACKET_PROCESSING_ERROR", {"errorString": str(e)}) + + def _continuouslyPipe(self): + while not self._exit: + try: + packet = self._packetReader.getPacket(timeout=12) + if packet == None or not packet.valid: + raise Exceptions.InvalidPacketException("") + except Exceptions.SnifferTimeout as e: + logging.info(str(e)) + packet = None + except (SerialException, ValueError): + logging.exception("UART read error") + logging.error("Lost contact with sniffer hardware.") + self._doExit() + except Exceptions.InvalidPacketException: + pass + else: + if packet.id == EVENT_PACKET_DATA_PDU or packet.id == EVENT_PACKET_ADV_PDU: + self._processBLEPacket(packet) + elif packet.id == EVENT_FOLLOW: + # This packet has no value for the user. + pass + elif packet.id == EVENT_CONNECT: + self._connectEventPacketCounterValue = packet.packetCounter + self._inConnection = True + # copy it because packets are eventually deleted + self._currentConnectRequest = copy.copy(self._findPacketByPacketCounter(self._connectEventPacketCounterValue-1)) + elif packet.id == EVENT_DISCONNECT: + if self._inConnection: + self._packetsInLastConnection = packet.packetCounter - self._connectEventPacketCounterValue + self._inConnection = False + elif packet.id == SWITCH_BAUD_RATE_RESP and self._switchingBaudRate: + self._switchingBaudRate = False + if (packet.baudRate == self._proposedBaudRate): + self._packetReader.switchBaudRate(self._proposedBaudRate) + else: + self._switchBaudRate(packet.baudRate) + elif packet.id == PING_RESP: + if hasattr(packet, 'version'): + versions = { 1116: '3.1.0', + 1115: '3.0.0', + 1114: '2.0.0', + 1113: '2.0.0-beta-3', + 1112: '2.0.0-beta-1' } + self._fwversion = versions.get(packet.version, 'SVN rev: %d' % packet.version) + logging.info("Firmware version %s" % self._fwversion) + elif packet.id == RESP_VERSION: + self._fwversion = packet.version + logging.info("Firmware version %s" % self._fwversion) + elif packet.id == RESP_TIMESTAMP: + # Use current time as timestamp reference + self._last_time = time.time() + self._last_timestamp = packet.timestamp + + lt = time.localtime(self._last_time) + usecs = int((self._last_time - int(self._last_time)) * 1_000_000) + logging.info(f'Firmware timestamp {self._last_timestamp} reference: ' + f'{time.strftime("%b %d %Y %X", lt)}.{usecs} {time.strftime("%Z", lt)}') + else: + logging.info("Unknown packet ID") + + def _findPacketByPacketCounter(self, packetCounterValue): + with self._packetListLock: + for i in range(-1, -1-len(self._packets), -1): + # iterate backwards through packets + if self._packets[i].packetCounter == packetCounterValue: + return self._packets[i] + return None + + def _startScanning(self, findScanRsp = False, findAux = False, scanCoded = False): + logging.info("starting scan") + + if self.state == STATE_FOLLOWING: + logging.info("Stopped sniffing device") + + self._setState(STATE_SCANNING) + self._packetReader.sendScan(findScanRsp, findAux, scanCoded) + self._packetReader.sendTK([0]) + + def _doExit(self): + self._exit = True + self.notify("APP_EXIT") + self._packetReader.doExit() + # Clear method references to avoid uncollectable cyclic references + self.clearCallbacks() + self._devices.clearCallbacks() + + def _startFollowing(self, device, followOnlyAdvertisements = False, followOnlyLegacy = False, followCoded = False): + self._devices.setFollowed(device) + logging.info("Sniffing device " + str(self._devices.index(device)) + ' - "'+device.name+'"') + self._packetReader.sendFollow(device.address, followOnlyAdvertisements, followOnlyLegacy, followCoded) + self._setState(STATE_FOLLOWING) + + def _clearDevices(self): + self._devices.clear() + + def _appendPacket(self, packet): + with self._packetListLock: + if len(self._packets) > 100000: + self._packets = self._packets[20000:] + self._packets.append(packet) + + def _getPackets(self, number = -1): + with self._packetListLock: + returnList = self._packets[0:number] + self._packets = self._packets[number:] + return returnList + + def _clearPackets(self): + with self._packetListLock: + del self._packets[:] diff --git a/modules/wireshark/extcap/SnifferAPI/Types.py b/modules/wireshark/extcap/SnifferAPI/Types.py new file mode 100644 index 0000000..d8a9b1e --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/Types.py @@ -0,0 +1,90 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +SLIP_START = 0xAB +SLIP_END = 0xBC +SLIP_ESC = 0xCD +SLIP_ESC_START = SLIP_START + 1 +SLIP_ESC_END = SLIP_END + 1 +SLIP_ESC_ESC = SLIP_ESC + 1 + +PROTOVER_V3 = 3 +PROTOVER_V2 = 2 +PROTOVER_V1 = 1 + +# UART protocol packet codes (see sniffer_uart_protocol.pdf) +REQ_FOLLOW = 0x00 +EVENT_FOLLOW = 0x01 +EVENT_PACKET_ADV_PDU = 0x02 +EVENT_CONNECT = 0x05 +EVENT_PACKET_DATA_PDU = 0x06 +REQ_SCAN_CONT = 0x07 +EVENT_DISCONNECT = 0x09 +SET_TEMPORARY_KEY = 0x0C +PING_REQ = 0x0D +PING_RESP = 0x0E +SWITCH_BAUD_RATE_REQ = 0x13 +SWITCH_BAUD_RATE_RESP = 0x14 +SET_ADV_CHANNEL_HOP_SEQ = 0x17 +SET_PRIVATE_KEY = 0x18 +SET_LEGACY_LONG_TERM_KEY = 0x19 +SET_SC_LONG_TERM_KEY = 0x1A +REQ_VERSION = 0x1B +RESP_VERSION = 0x1C +REQ_TIMESTAMP = 0x1D +RESP_TIMESTAMP = 0x1E +SET_IDENTITY_RESOLVING_KEY= 0x1F +GO_IDLE = 0xFE + +PACKET_TYPE_UNKNOWN = 0x00 +PACKET_TYPE_ADVERTISING = 0x01 +PACKET_TYPE_DATA = 0x02 + +ADV_TYPE_ADV_IND = 0x0 +ADV_TYPE_ADV_DIRECT_IND = 0x1 +ADV_TYPE_ADV_NONCONN_IND = 0x2 +ADV_TYPE_ADV_SCAN_IND = 0x6 +ADV_TYPE_SCAN_REQ = 0x3 +ADV_TYPE_SCAN_RSP = 0x4 +ADV_TYPE_CONNECT_REQ = 0x5 +ADV_TYPE_ADV_EXT_IND = 0x7 + +PHY_1M = 0 +PHY_2M = 1 +PHY_CODED = 2 + +PHY_CODED_CI_S8 = 0 +PHY_CODED_CI_S2 = 1 diff --git a/modules/wireshark/extcap/SnifferAPI/UART.py b/modules/wireshark/extcap/SnifferAPI/UART.py new file mode 100644 index 0000000..76038f6 --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/UART.py @@ -0,0 +1,233 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import collections +import logging +import serial +from threading import Thread, Event + +import serial.tools.list_ports as list_ports + +from . import Exceptions +from . import Packet +from . import Filelock + +import os +if os.name == "posix": + import termios + +SNIFFER_OLD_DEFAULT_BAUDRATE = 460800 +# Baudrates that should be tried (add more if required) +SNIFFER_BAUDRATES = [1000000, 460800] + + +def find_sniffer(write_data=False): + open_ports = list_ports.comports() + + sniffers = [] + for port in [x.device for x in open_ports]: + for rate in SNIFFER_BAUDRATES: + reader = None + l_errors = [serial.SerialException, ValueError, Exceptions.LockedException, OSError] + if os.name == 'posix': + l_errors.append(termios.error) + try: + reader = Packet.PacketReader(portnum=port, baudrate=rate) + try: + if write_data: + reader.sendPingReq() + _ = reader.decodeFromSLIP(0.1, complete_timeout=0.1) + else: + _ = reader.decodeFromSLIP(0.3, complete_timeout=0.3) + + # FIXME: Should add the baud rate here, but that will be a breaking change + sniffers.append(port) + break + except (Exceptions.SnifferTimeout, Exceptions.UARTPacketError): + pass + except tuple(l_errors): + continue + finally: + if reader is not None: + reader.doExit() + return sniffers + + +def find_sniffer_baudrates(port, write_data=False): + for rate in SNIFFER_BAUDRATES: + reader = None + try: + reader = Packet.PacketReader(portnum=port, baudrate=rate) + try: + if write_data: + reader.sendPingReq() + _ = reader.decodeFromSLIP(0.1, complete_timeout=0.1) + else: + _ = reader.decodeFromSLIP(0.3, complete_timeout=0.3) + + # TODO: possibly include additional rates based on protocol version + return {"default": rate, "other": []} + except (Exceptions.SnifferTimeout, Exceptions.UARTPacketError): + pass + finally: + if reader is not None: + reader.doExit() + return None + + +class Uart: + def __init__(self, portnum=None, baudrate=None): + self.ser = None + try: + if baudrate is not None and baudrate not in SNIFFER_BAUDRATES: + raise Exception("Invalid baudrate: " + str(baudrate)) + + logging.info('Opening serial port {}'.format(portnum)) + + self.portnum = portnum + if self.portnum: + Filelock.lock(portnum) + + self.ser = serial.Serial( + port=portnum, + baudrate=9600, + rtscts=True, + exclusive=True + ) + self.ser.baudrate = baudrate + + except Exception: + if self.ser: + self.ser.close() + self.ser = None + raise + + self.read_queue = collections.deque() + self.read_queue_has_data = Event() + + self.worker_thread = Thread(target=self._read_worker) + self.reading = True + self.worker_thread.setDaemon(True) + self.worker_thread.start() + + def _read_worker(self): + self.ser.reset_input_buffer() + while self.reading: + try: + # Read any data available, or wait for at least one byte + data_read = self.ser.read(self.ser.in_waiting or 1) + #logging.info('type: {}'.format(data_read.__class__)) + self._read_queue_extend(data_read) + except serial.SerialException as e: + logging.info("Unable to read UART: %s" % e) + self.reading = False + return + + def close(self): + if self.ser: + logging.info("closing UART") + self.reading = False + # Wake any threads waiting on the queue + self.read_queue_has_data.set() + if hasattr(self.ser, "cancel_read"): + self.ser.cancel_read() + self.worker_thread.join() + self.ser.close() + else: + self.ser.close() + self.worker_thread.join() + self.ser = None + + if self.portnum: + Filelock.unlock(self.portnum) + + def __del__(self): + self.close() + + def switchBaudRate(self, newBaudRate): + self.ser.baudrate = newBaudRate + + def readByte(self, timeout=None): + r = self._read_queue_get(timeout) + return r + + def writeList(self, array): + try: + self.ser.write(array) + except serial.SerialTimeoutException: + logging.info("Got write timeout, ignoring error") + + except serial.SerialException as e: + self.ser.close() + raise e + + def _read_queue_extend(self, data): + if len(data) > 0: + self.read_queue.extend(data) + self.read_queue_has_data.set() + + def _read_queue_get(self, timeout=None): + data = None + if self.read_queue_has_data.wait(timeout): + self.read_queue_has_data.clear() + try: + data = self.read_queue.popleft() + except IndexError: + # This will happen when the class is destroyed + return None + if len(self.read_queue) > 0: + self.read_queue_has_data.set() + return data + + +def list_serial_ports(): + # Scan for available ports. + return list_ports.comports() + +if __name__ == "__main__": + import time + t_start = time.time() + s = find_sniffer() + tn = time.time() + print(s) + print("find_sniffer took %f seconds" % (tn - t_start)) + for p in s: + t = time.time() + print(find_sniffer_baudrates(p)) + tn = time.time() + print("find_sniffer_baudrate took %f seconds" % (tn - t)) + tn = time.time() + print("total runtime %f" % (tn - t_start)) diff --git a/modules/wireshark/extcap/SnifferAPI/__init__.py b/modules/wireshark/extcap/SnifferAPI/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/wireshark/extcap/SnifferAPI/version.py b/modules/wireshark/extcap/SnifferAPI/version.py new file mode 100644 index 0000000..75cccf5 --- /dev/null +++ b/modules/wireshark/extcap/SnifferAPI/version.py @@ -0,0 +1,38 @@ +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +VERSION_STRING = "4.1.1" + diff --git a/modules/wireshark/extcap/nrf_sniffer_ble.py b/modules/wireshark/extcap/nrf_sniffer_ble.py new file mode 100644 index 0000000..f8212a1 --- /dev/null +++ b/modules/wireshark/extcap/nrf_sniffer_ble.py @@ -0,0 +1,854 @@ +#!/usr/bin/env python3 + +# Copyright (c) Nordic Semiconductor ASA +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form, except as embedded into a Nordic +# Semiconductor ASA integrated circuit in a product or a software update for +# such product, must reproduce the above copyright notice, this list of +# conditions and the following disclaimer in the documentation and/or other +# materials provided with the distribution. +# +# 3. Neither the name of Nordic Semiconductor ASA nor the names of its +# contributors may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# 4. This software, with or without modification, must only be used with a +# Nordic Semiconductor ASA integrated circuit. +# +# 5. Any software provided in binary form under this license must not be reverse +# engineered, decompiled, modified and/or disassembled. +# +# THIS SOFTWARE IS PROVIDED BY NORDIC SEMICONDUCTOR ASA "AS IS" AND ANY EXPRESS +# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY, NONINFRINGEMENT, AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL NORDIC SEMICONDUCTOR ASA OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +# GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +""" +Wireshark extcap wrapper for the nRF Sniffer for Bluetooth LE by Nordic Semiconductor. +""" + +import os +import sys +import argparse +import re +import time +import struct +import logging + +from SnifferAPI import Logger +try: + import serial +except ImportError: + Logger.initLogger() + logging.error(f'pyserial not found, please run: "{sys.executable} -m pip install -r requirements.txt" and retry') + sys.exit(f'pyserial not found, please run: "{sys.executable} -m pip install -r requirements.txt" and retry') + +from SnifferAPI import Sniffer, UART, Devices, Pcap, Exceptions + +ERROR_USAGE = 0 +ERROR_ARG = 1 +ERROR_INTERFACE = 2 +ERROR_FIFO = 3 +ERROR_INTERNAL = 4 + +CTRL_CMD_INIT = 0 +CTRL_CMD_SET = 1 +CTRL_CMD_ADD = 2 +CTRL_CMD_REMOVE = 3 +CTRL_CMD_ENABLE = 4 +CTRL_CMD_DISABLE = 5 +CTRL_CMD_STATUSBAR = 6 +CTRL_CMD_INFO_MSG = 7 +CTRL_CMD_WARN_MSG = 8 +CTRL_CMD_ERROR_MSG = 9 + +CTRL_ARG_DEVICE = 0 +CTRL_ARG_KEY_TYPE = 1 +CTRL_ARG_KEY_VAL = 2 +CTRL_ARG_ADVHOP = 3 +CTRL_ARG_HELP = 4 +CTRL_ARG_RESTORE = 5 +CTRL_ARG_LOG = 6 +CTRL_ARG_DEVICE_CLEAR = 7 +CTRL_ARG_NONE = 255 + +CTRL_KEY_TYPE_PASSKEY = 0 +CTRL_KEY_TYPE_OOB = 1 +CTRL_KEY_TYPE_LEGACY_LTK = 2 +CTRL_KEY_TYPE_SC_LTK = 3 +CTRL_KEY_TYPE_DH_PRIVATE_KEY = 4 +CTRL_KEY_TYPE_IRK = 5 +CTRL_KEY_TYPE_ADD_ADDR = 6 +CTRL_KEY_TYPE_FOLLOW_ADDR = 7 + +fn_capture = None +fn_ctrl_in = None +fn_ctrl_out = None + +extcap_log_handler = None +extcap_version = None + +# Wireshark nRF Sniffer for Bluetooth LE Toolbar will always cache the last used key and adv hop and send +# this when starting a capture. To ensure that the key and adv hop is always shown correctly +# in the Toolbar, even if the user has changed it but not applied it, we send the last used +# key and adv hop back as a default value. +last_used_key_type = CTRL_KEY_TYPE_PASSKEY +last_used_key_val = "" +last_used_advhop = "37,38,39" + +zero_addr = "[00,00,00,00,00,00,0]" + +# While searching for a selected Device we must not write packets to the pipe until +# the device is found to avoid getting advertising packets from other devices. +write_new_packets = False + +# The RSSI capture filter value given from Wireshark. +rssi_filter = 0 + +# The RSSI filtering is not on when in follow mode. +in_follow_mode = False + +# nRF Sniffer for Bluetooth LE interface option to only capture advertising packets +capture_only_advertising = False +capture_only_legacy_advertising = False +capture_scan_response = True +capture_scan_aux_pointer = True +capture_coded = False + +def extcap_config(interface): + """List configuration for the given interface""" + print("arg {number=0}{call=--only-advertising}{display=Only advertising packets}" + "{tooltip=The sniffer will only capture advertising packets from the selected device}{type=boolflag}{save=true}") + print("arg {number=1}{call=--only-legacy-advertising}{display=Only legacy advertising packets}" + "{tooltip=The sniffer will only capture legacy advertising packets from the selected device}{type=boolflag}{save=true}") + print("arg {number=2}{call=--scan-follow-rsp}{display=Find scan response data}" + "{tooltip=The sniffer will follow scan requests and scan responses in scan mode}{type=boolflag}{default=true}{save=true}") + print("arg {number=3}{call=--scan-follow-aux}{display=Find auxiliary pointer data}" + "{tooltip=The sniffer will follow aux pointers in scan mode}{type=boolflag}{default=true}{save=true}") + print("arg {number=3}{call=--coded}{display=Scan and follow devices on LE Coded PHY}" + "{tooltip=Scan for devices and follow advertiser on LE Coded PHY}{type=boolflag}{default=false}{save=true}") + +def extcap_dlts(interface): + """List DLTs for the given interface""" + print("dlt {number=272}{name=NORDIC_BLE}{display=nRF Sniffer for Bluetooth LE}") + + +def get_baud_rates(interface): + if not hasattr(serial, "__version__") or not serial.__version__.startswith('3.'): + raise RuntimeError("Too old version of python 'serial' Library. Version 3 required.") + return UART.find_sniffer_baudrates(interface) + + +def get_interfaces(): + if not hasattr(serial, "__version__") or not serial.__version__.startswith('3.'): + raise RuntimeError("Too old version of python 'serial' Library. Version 3 required.") + + devices = UART.find_sniffer() + return devices + + +def extcap_interfaces(): + """List available interfaces to capture from""" + print("extcap {version=%s}{display=nRF Sniffer for Bluetooth LE}" + "{help=https://www.nordicsemi.com/Software-and-Tools/Development-Tools/nRF-Sniffer-for-Bluetooth-LE}" + % Sniffer.VERSION_STRING) + + for interface_port in get_interfaces(): + if sys.platform == 'win32': + print("interface {value=%s-%s}{display=nRF Sniffer for Bluetooth LE %s}" % (interface_port, extcap_version, interface_port)) + else: + print("interface {value=%s-%s}{display=nRF Sniffer for Bluetooth LE}" % (interface_port, extcap_version)) + + print("control {number=%d}{type=selector}{display=Device}{tooltip=Device list}" % CTRL_ARG_DEVICE) + print("control {number=%d}{type=selector}{display=Key}{tooltip=}" % CTRL_ARG_KEY_TYPE) + print("control {number=%d}{type=string}{display=Value}" + "{tooltip=6 digit passkey or 16 or 32 bytes encryption key in hexadecimal starting with '0x', big endian format." + "If the entered key is shorter than 16 or 32 bytes, it will be zero-padded in front'}" + "{validation=\\b^(([0-9]{6})|(0x[0-9a-fA-F]{1,64})|([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2}) (public|random))$\\b}" % CTRL_ARG_KEY_VAL) + print("control {number=%d}{type=string}{display=Adv Hop}" + "{default=37,38,39}" + "{tooltip=Advertising channel hop sequence. " + "Change the order in which the sniffer switches advertising channels. " + "Valid channels are 37, 38 and 39 separated by comma.}" + r"{validation=^\s*((37|38|39)\s*,\s*){0,2}(37|38|39){1}\s*$}{required=true}" % CTRL_ARG_ADVHOP) + print("control {number=%d}{type=button}{display=Clear}{tooltop=Clear or remove device from Device list}" % CTRL_ARG_DEVICE_CLEAR) + print("control {number=%d}{type=button}{role=help}{display=Help}{tooltip=Access user guide (launches browser)}" % CTRL_ARG_HELP) + print("control {number=%d}{type=button}{role=restore}{display=Defaults}{tooltip=Resets the user interface and clears the log file}" % CTRL_ARG_RESTORE) + print("control {number=%d}{type=button}{role=logger}{display=Log}{tooltip=Log per interface}" % CTRL_ARG_LOG) + + print("value {control=%d}{value= }{display=All advertising devices}{default=true}" % CTRL_ARG_DEVICE) + print("value {control=%d}{value=%s}{display=Follow IRK}" % (CTRL_ARG_DEVICE, zero_addr)) + + print("value {control=%d}{value=%d}{display=Legacy Passkey}{default=true}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_PASSKEY)) + print("value {control=%d}{value=%d}{display=Legacy OOB data}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_OOB)) + print("value {control=%d}{value=%d}{display=Legacy LTK}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_LEGACY_LTK)) + print("value {control=%d}{value=%d}{display=SC LTK}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_SC_LTK)) + print("value {control=%d}{value=%d}{display=SC Private Key}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_DH_PRIVATE_KEY)) + print("value {control=%d}{value=%d}{display=IRK}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_IRK)) + print("value {control=%d}{value=%d}{display=Add LE address}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_ADD_ADDR)) + print("value {control=%d}{value=%d}{display=Follow LE address}" % (CTRL_ARG_KEY_TYPE, CTRL_KEY_TYPE_FOLLOW_ADDR)) + + +def string_address(address): + """Make a string representation of the address""" + if len(address) < 7: + return None + + addr_string = '' + + for i in range(5): + addr_string += (format(address[i], '02x') + ':') + addr_string += format(address[5], '02x') + ' ' + + if address[6]: + addr_string += ' random ' + else: + addr_string += ' public ' + + return addr_string + + +def control_read(): + """Read a message from the control channel""" + header = fn_ctrl_in.read(6) + if not header: + # Ref. https://docs.python.org/3/tutorial/inputoutput.html#methods-of-file-objects: + # > If the end of the file has been reached, f.read() will return an + # > empty string ('') + return None, None, None + + _, _, length, arg, typ = struct.unpack('>sBHBB', header) + + payload = bytearray() + if length > 2: + payload = fn_ctrl_in.read(length - 2) + + return arg, typ, payload + +def control_write(arg, typ, message): + """Write the message to the control channel""" + + if not fn_ctrl_out: + # No control out has been opened + return + + packet = bytearray() + packet += struct.pack('>BBHBB', ord('T'), 0, len(message) + 2, arg, typ) + packet += message.encode('utf-8') + + fn_ctrl_out.write(packet) + + +def capture_write(message): + """Write the message to the capture pipe""" + fn_capture.write(message) + fn_capture.flush() + + +def new_packet(notification): + """A new Bluetooth LE packet has arrived""" + if write_new_packets == True: + packet = notification.msg["packet"] + + if rssi_filter == 0 or in_follow_mode == True or packet.RSSI > rssi_filter: + p = bytes([packet.boardId] + packet.getList()) + capture_write(Pcap.create_packet(p, packet.time)) + + +def device_added(notification): + """A device is added or updated""" + device = notification.msg + + # Only add devices matching RSSI filter + if rssi_filter == 0 or device.RSSI > rssi_filter: + # Extcap selector uses \0 character to separate value and display value, + # therefore the display value cannot contain the \0 character as this + # would lead to truncation of the display value. + display = (device.name.replace('\0', '\\0') + + (" " + str(device.RSSI) + " dBm " if device.RSSI != 0 else " ") + + string_address(device.address)) + + message = str(device.address) + '\0' + display + + control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, message) + + +def device_removed(notification): + """A device is removed""" + device = notification.msg + display = device.name + " " + string_address(device.address) + + message = "" + message += str(device.address) + + control_write(CTRL_ARG_DEVICE, CTRL_CMD_REMOVE, message) + logging.info("Removed: " + display) + +def devices_cleared(notification): + """Devices have been cleared""" + message = "" + control_write(CTRL_ARG_DEVICE, CTRL_CMD_REMOVE, message) + + control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, " " + '\0' + "All advertising devices") + control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, zero_addr + '\0' + "Follow IRK") + control_write(CTRL_ARG_DEVICE, CTRL_CMD_SET, " ") + +def handle_control_command(sniffer, arg, typ, payload): + """Handle command from control channel""" + global last_used_key_type + + if arg == CTRL_ARG_DEVICE: + if payload == b' ': + scan_for_devices(sniffer) + else: + values = payload + values = values.replace(b'[', b'') + values = values.replace(b']', b'') + device_address = values.split(b',') + + logging.info('follow_device: {}'.format(device_address)) + for i in range(6): + device_address[i] = int(device_address[i]) + + device_address[6] = 1 if device_address[6] == b' 1' else 0 + + device = Devices.Device(address=device_address, name='""', RSSI=0) + + follow_device(sniffer, device) + + elif arg == CTRL_ARG_DEVICE_CLEAR: + clear_devices(sniffer) + elif arg == CTRL_ARG_KEY_TYPE: + last_used_key_type = int(payload.decode('utf-8')) + elif arg == CTRL_ARG_KEY_VAL: + set_key_value(sniffer, payload) + elif arg == CTRL_ARG_ADVHOP: + set_advhop(sniffer, payload) + + +def control_read_initial_values(sniffer): + """Read initial control values""" + initialized = False + + while not initialized: + arg, typ, payload = control_read() + if typ == CTRL_CMD_INIT: + initialized = True + else: + handle_control_command(sniffer, arg, typ, payload) + + +def control_write_defaults(): + """Write default control values""" + control_write(CTRL_ARG_KEY_TYPE, CTRL_CMD_SET, str(last_used_key_type)) + control_write(CTRL_ARG_KEY_VAL, CTRL_CMD_SET, last_used_key_val) + control_write(CTRL_ARG_ADVHOP, CTRL_CMD_SET, last_used_advhop) + + +def scan_for_devices(sniffer): + """Start scanning for advertising devices""" + global in_follow_mode + if sniffer.state == 2: + log = "Scanning all advertising devices" + logging.info(log) + sniffer.scan(capture_scan_response, capture_scan_aux_pointer, capture_coded) + + in_follow_mode = False + + +def clear_devices(sniffer): + """Clear the advertising devices list""" + global in_follow_mode + + sniffer.clearDevices() + scan_for_devices(sniffer) + + in_follow_mode = False + + +def follow_device(sniffer, device): + """Follow the selected device""" + global write_new_packets, in_follow_mode + + sniffer.follow(device, capture_only_advertising, capture_only_legacy_advertising, capture_coded) + time.sleep(.1) + + in_follow_mode = True + logging.info("Following " + string_address(device.address)) + + +def set_key_value(sniffer, payload): + """Send key value to device""" + global last_used_key_val + + payload = payload.decode('utf-8') + last_used_key_val = payload + + if (last_used_key_type == CTRL_KEY_TYPE_PASSKEY): + if re.match("^[0-9]{6}$", payload): + set_passkey(sniffer, payload) + else: + logging.info("Invalid key value: " + str(payload)) + elif (last_used_key_type == CTRL_KEY_TYPE_OOB): + if re.match("^0[xX][0-9A-Za-z]{1,32}$", payload): + set_OOB(sniffer, payload[2:]) + else: + logging.info("Invalid key value: " + str(payload)) + elif (last_used_key_type == CTRL_KEY_TYPE_DH_PRIVATE_KEY): + if (re.match("^0[xX][0-9A-Za-z]{1,64}$", payload)): + set_dh_private_key(sniffer, payload[2:]) + else: + logging.info("Invalid key value: " + str(payload)) + elif (last_used_key_type == CTRL_KEY_TYPE_LEGACY_LTK): + if (re.match("^0[xX][0-9A-Za-z]{1,32}$", payload)): + set_legacy_ltk(sniffer, payload[2:]) + else: + logging.info("Invalid key value: " + str(payload)) + elif (last_used_key_type == CTRL_KEY_TYPE_SC_LTK): + if (re.match("^0[xX][0-9A-Za-z]{1,32}$", payload)): + set_sc_ltk(sniffer, payload[2:]) + else: + logging.info("Invalid key value: " + str(payload)) + elif (last_used_key_type == CTRL_KEY_TYPE_IRK): + if (re.match("^0[xX][0-9A-Za-z]{1,32}$", payload)): + set_irk(sniffer, payload[2:]) + else: + logging.info("Invalid key value: " + str(payload)) + elif (last_used_key_type == CTRL_KEY_TYPE_ADD_ADDR): + if (re.match("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2}) (public|random)$", payload)): + add_address(sniffer, payload) + else: + logging.info("Invalid key value: " + str(payload)) + elif (last_used_key_type == CTRL_KEY_TYPE_FOLLOW_ADDR): + if (re.match("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2}) (public|random)$", payload)): + follow_address(sniffer, payload) + else: + logging.info("Invalid key value: " + str(payload)) + else: + logging.info("Invalid key type: " + str(last_used_key_type)) + +def parse_hex(value): + if len(value) % 2 != 0: + value = '0' + value + + a = list(value) + return [int(x + y, 16) for x,y in zip(a[::2], a[1::2])] + +def set_passkey(sniffer, payload): + """Send passkey to device""" + passkey = [] + logging.info("Setting Passkey: " + payload) + init_payload = int(payload, 10) + if len(payload) >= 6: + passkey = [] + passkey += [(init_payload >> 16) & 0xFF] + passkey += [(init_payload >> 8) & 0xFF] + passkey += [(init_payload >> 0) & 0xFF] + + sniffer.sendTK(passkey) + +def set_OOB(sniffer, payload): + """Send OOB to device""" + logging.info("Setting OOB data: " + payload) + sniffer.sendTK(parse_hex(payload)) + +def set_dh_private_key(sniffer, payload): + """Send Diffie-Hellman private key to device""" + logging.info("Setting DH private key: " + payload) + sniffer.sendPrivateKey(parse_hex(payload)) + +def set_legacy_ltk(sniffer, payload): + """Send Legacy Long Term Key (LTK) to device""" + logging.info("Setting Legacy LTK: " + payload) + sniffer.sendLegacyLTK(parse_hex(payload)) + +def set_sc_ltk(sniffer, payload): + """Send LE secure connections Long Term Key (LTK) to device""" + logging.info("Setting SC LTK: " + payload) + sniffer.sendSCLTK(parse_hex(payload)) + +def set_irk(sniffer, payload): + """Send Identity Resolving Key (IRK) to device""" + logging.info("Setting IRK: " + payload) + sniffer.sendIRK(parse_hex(payload)) + +def add_address(sniffer, payload): + """Add LE address to device list""" + logging.info("Adding LE address: " + payload) + + (addr,addr_type) = payload.split(' ') + device = [int(a, 16) for a in addr.split(":")] + + device.append(1 if addr_type == "random" else 0) + + new_device = Devices.Device(address=device, name='""', RSSI=0) + sniffer.addDevice(new_device) + +def follow_address(sniffer, payload): + """Add LE address to device list""" + logging.info("Adding LE address: " + payload) + + (addr,addr_type) = payload.split(' ') + device = [int(a, 16) for a in addr.split(":")] + + device.append(1 if addr_type == "random" else 0) + + new_device = Devices.Device(address=device, name='""', RSSI=0) + sniffer.addDevice(new_device) + + control_write(CTRL_ARG_DEVICE, CTRL_CMD_SET, f"{new_device.address}") + follow_device(sniffer, new_device) + +def set_advhop(sniffer, payload): + """Set advertising channel hop sequence""" + global last_used_advhop + + payload = payload.decode('utf-8') + + last_used_advhop = payload + + hops = [int(channel) for channel in payload.split(',')] + + sniffer.setAdvHopSequence(hops) + + log = "AdvHopSequence: " + str(hops) + logging.info(log) + + +def control_loop(sniffer): + """Main loop reading control messages""" + arg_read = CTRL_ARG_NONE + while arg_read is not None: + arg_read, typ, payload = control_read() + handle_control_command(sniffer, arg_read, typ, payload) + + +def error_interface_not_found(interface, fifo): + log = "nRF Sniffer for Bluetooth LE could not find interface: " + interface + control_write(CTRL_ARG_NONE, CTRL_CMD_ERROR_MSG, log) + extcap_close_fifo(fifo) + sys.exit(ERROR_INTERFACE) + + +def validate_interface(interface, fifo): + """Check if interface exists""" + if sys.platform != 'win32' and not os.path.exists(interface): + error_interface_not_found(interface, fifo) + + +def get_default_baudrate(interface, fifo): + """Return the baud rate that interface is running at, or exit if the board is not found""" + rates = get_baud_rates(interface) + if rates is None: + error_interface_not_found(interface, fifo) + return rates["default"] + +def get_supported_protocol_version(extcap_version): + """Return the maximum supported Packet Protocol Version""" + if extcap_version == 'None': + return 2 + + (major, minor) = extcap_version.split('.') + + major = int(major) + minor = int(minor) + + if major > 3 or (major == 3 and minor >= 4): + return 3 + else: + return 2 + +def setup_extcap_log_handler(): + """Add the a handler that emits log messages through the extcap control out channel""" + global extcap_log_handler + extcap_log_handler = ExtcapLoggerHandler() + Logger.addLogHandler(extcap_log_handler) + control_write(CTRL_ARG_LOG, CTRL_CMD_SET, "") + + +def teardown_extcap_log_handler(): + """Remove and reset the extcap log handler""" + global extcap_log_handler + if extcap_log_handler: + Logger.removeLogHandler(extcap_log_handler) + extcap_log_handler = None + + +def sniffer_capture(interface, baudrate, fifo, control_in, control_out): + """Start the sniffer to capture packets""" + global fn_capture, fn_ctrl_in, fn_ctrl_out, write_new_packets, extcap_log_handler + + try: + fn_capture = open(fifo, 'wb', 0) + + if control_out is not None: + fn_ctrl_out = open(control_out, 'wb', 0) + setup_extcap_log_handler() + + if control_in is not None: + fn_ctrl_in = open(control_in, 'rb', 0) + + logging.info("Log started at %s", time.strftime("%c")) + + interface, extcap_version = interface.split('-') + logging.info("Extcap version %s", str(extcap_version)) + + capture_write(Pcap.get_global_header()) + validate_interface(interface, fifo) + if baudrate is None: + baudrate = get_default_baudrate(interface, fifo) + + sniffer = Sniffer.Sniffer(interface, baudrate) + sniffer.subscribe("NEW_BLE_PACKET", new_packet) + sniffer.subscribe("DEVICE_ADDED", device_added) + sniffer.subscribe("DEVICE_UPDATED", device_added) + sniffer.subscribe("DEVICE_REMOVED", device_removed) + sniffer.subscribe("DEVICES_CLEARED", devices_cleared) + sniffer.setAdvHopSequence([37, 38, 39]) + sniffer.setSupportedProtocolVersion(get_supported_protocol_version(extcap_version)) + logging.info("Sniffer created") + + logging.info("Software version: %s" % sniffer.swversion) + sniffer.getFirmwareVersion() + sniffer.getTimestamp() + sniffer.start() + logging.info("sniffer started") + sniffer.scan(capture_scan_response, capture_scan_aux_pointer, capture_coded) + logging.info("scanning started") + + if fn_ctrl_in is not None and fn_ctrl_out is not None: + # First read initial control values + control_read_initial_values(sniffer) + + # Then write default values + control_write_defaults() + logging.info("defaults written") + + # Start receiving packets + write_new_packets = True + + # Start the control loop + logging.info("control loop") + control_loop(sniffer) + logging.info("exiting control loop") + + else: + logging.info("") + # Start receiving packets + write_new_packets = True + while True: + # Wait for keyboardinterrupt + pass + + except Exceptions.LockedException as e: + logging.info('{}'.format(e.message)) + + except OSError: + # We'll get OSError=22 when/if wireshark kills the pipe(s) on capture + # stop. + pass + + finally: + # The first thing we should do is to tear down the extcap log handler. + # This might already have triggered an OSError, or we will trigger one + # by attempting to log at this point. + teardown_extcap_log_handler() + + # Safe to use logging again. + logging.info("Tearing down") + + sniffer.doExit() + if fn_capture is not None and not fn_capture.closed: + fn_capture.close() + + if fn_ctrl_in is not None and not fn_ctrl_in.closed: + fn_ctrl_in.close() + + if fn_ctrl_out is not None and not fn_ctrl_out.closed: + fn_ctrl_out.close() + + fn_capture = None + fn_ctrl_out = None + fn_ctrl_in = None + + logging.info("Exiting") + + +def extcap_close_fifo(fifo): + """"Close extcap fifo""" + if not os.path.exists(fifo): + print("FIFO does not exist!", file=sys.stderr) + return + + # This is apparently needed to workaround an issue on Windows/macOS + # where the message cannot be read. (really?) + fh = open(fifo, 'wb', 0) + fh.close() + + +class ExtcapLoggerHandler(logging.Handler): + """Handler used to display all logging messages in extcap""" + + def emit(self, record): + """Send log message to extcap""" + message = record.message.replace('\0', '\\0') + log_message = f"{record.levelname}: {message}\n" + control_write(CTRL_ARG_LOG, CTRL_CMD_ADD, log_message) + + +def parse_capture_filter(capture_filter): + """"Parse given capture filter""" + global rssi_filter + m = re.search(r"^\s*rssi\s*(>=?)\s*(-?[0-9]+)\s*$", capture_filter, re.IGNORECASE) + if m: + rssi_filter = int(m.group(2)) + if rssi_filter > -10 or rssi_filter < -256: + print("Illegal RSSI value, must be between -10 and -256") + # Handle >= by modifying the threshold, since comparisons are always done with + # the > operator + if m.group(1) == '>=': + rssi_filter = rssi_filter - 1 + else: + print("Filter syntax: \"RSSI >= -value\"") + +import atexit + +@atexit.register +def goodbye(): + logging.info("Exiting PID {}".format(os.getpid())) + + +if __name__ == '__main__': + + # Capture options + parser = argparse.ArgumentParser(description="Nordic Semiconductor nRF Sniffer for Bluetooth LE extcap plugin") + + # Extcap Arguments + parser.add_argument("--capture", + help="Start the capture", + action="store_true") + + parser.add_argument("--extcap-interfaces", + help="List available interfaces to capture from", + action="store_true") + + parser.add_argument("--extcap-interface", + help="The interface to capture from") + + parser.add_argument("--extcap-dlts", + help="List DLTs for the given interface", + action="store_true") + + parser.add_argument("--extcap-config", + help="List configurations for the given interface", + action="store_true") + + parser.add_argument("--extcap-capture-filter", + help="Used together with capture to provide a capture filter") + + parser.add_argument("--fifo", + help="Use together with capture to provide the fifo to dump data to") + + parser.add_argument("--extcap-control-in", + help="Used together with capture to get control messages from toolbar") + + parser.add_argument("--extcap-control-out", + help="Used together with capture to send control messages to toolbar") + + parser.add_argument("--extcap-version", + help="Set extcap supported version") + + # Interface Arguments + parser.add_argument("--device", help="Device", default="") + parser.add_argument("--baudrate", type=int, help="The sniffer baud rate") + parser.add_argument("--only-advertising", help="Only advertising packets", action="store_true") + parser.add_argument("--only-legacy-advertising", help="Only legacy advertising packets", action="store_true") + parser.add_argument("--scan-follow-rsp", help="Find scan response data ", action="store_true") + parser.add_argument("--scan-follow-aux", help="Find auxiliary pointer data", action="store_true") + parser.add_argument("--coded", help="Scan and follow on LE Coded PHY", action="store_true") + + logging.info("Started PID {}".format(os.getpid())) + + try: + args, unknown = parser.parse_known_args() + logging.info(args) + + except argparse.ArgumentError as exc: + print("%s" % exc, file=sys.stderr) + fifo_found = False + fifo = "" + for arg in sys.argv: + if arg == "--fifo" or arg == "--extcap-fifo": + fifo_found = True + elif fifo_found: + fifo = arg + break + extcap_close_fifo(fifo) + sys.exit(ERROR_ARG) + + if len(sys.argv) <= 1: + parser.exit("No arguments given!") + + if args.extcap_version: + extcap_version = args.extcap_version + + if args.extcap_capture_filter: + parse_capture_filter(args.extcap_capture_filter) + if args.extcap_interface and len(sys.argv) == 5: + sys.exit(0) + + if not args.extcap_interfaces and args.extcap_interface is None: + parser.exit("An interface must be provided or the selection must be displayed") + + if args.extcap_interfaces or args.extcap_interface is None: + extcap_interfaces() + sys.exit(0) + + if len(unknown) > 0: + print("Sniffer %d unknown arguments given" % len(unknown)) + logging.info("Sniffer %d unknown arguments given" % len(unknown)) + + interface = args.extcap_interface + + capture_only_advertising = args.only_advertising + capture_only_legacy_advertising = args.only_legacy_advertising + capture_scan_response = args.scan_follow_rsp + capture_scan_aux_pointer = args.scan_follow_aux + capture_coded = args.coded + + if args.extcap_config: + extcap_config(interface) + elif args.extcap_dlts: + extcap_dlts(interface) + elif args.capture: + if args.fifo is None: + parser.print_help() + sys.exit(ERROR_FIFO) + try: + logging.info('sniffer capture') + sniffer_capture(interface, args.baudrate, args.fifo, args.extcap_control_in, args.extcap_control_out) + except KeyboardInterrupt: + pass + except Exception as e: + import traceback + logging.info(traceback.format_exc()) + logging.info('internal error: {}'.format(repr(e))) + sys.exit(ERROR_INTERNAL) + else: + parser.print_help() + sys.exit(ERROR_USAGE) + logging.info('main exit PID {}'.format(os.getpid())) From cc301951fca3034993f374aac8b3505336002dfa Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Tue, 11 Nov 2025 19:34:40 +0100 Subject: [PATCH 20/22] Apply black --- .../extcap/SnifferAPI/CaptureFiles.py | 8 +- .../wireshark/extcap/SnifferAPI/Devices.py | 21 +- .../wireshark/extcap/SnifferAPI/Exceptions.py | 6 + .../wireshark/extcap/SnifferAPI/Filelock.py | 26 +- modules/wireshark/extcap/SnifferAPI/Logger.py | 17 +- .../extcap/SnifferAPI/Notifications.py | 18 +- modules/wireshark/extcap/SnifferAPI/Packet.py | 250 ++++++---- modules/wireshark/extcap/SnifferAPI/Pcap.py | 29 +- .../wireshark/extcap/SnifferAPI/Sniffer.py | 42 +- .../extcap/SnifferAPI/SnifferCollector.py | 113 +++-- modules/wireshark/extcap/SnifferAPI/Types.py | 90 ++-- modules/wireshark/extcap/SnifferAPI/UART.py | 21 +- .../wireshark/extcap/SnifferAPI/version.py | 1 - modules/wireshark/extcap/nrf_sniffer_ble.py | 429 ++++++++++++------ 14 files changed, 684 insertions(+), 387 deletions(-) diff --git a/modules/wireshark/extcap/SnifferAPI/CaptureFiles.py b/modules/wireshark/extcap/SnifferAPI/CaptureFiles.py index f5cf6cb..8c218e5 100644 --- a/modules/wireshark/extcap/SnifferAPI/CaptureFiles.py +++ b/modules/wireshark/extcap/SnifferAPI/CaptureFiles.py @@ -59,13 +59,13 @@ class CaptureFileHandler: if not os.path.isdir(os.path.dirname(filename)): os.makedirs(os.path.dirname(filename)) self.filename = filename - self.backupFilename = self.filename+".1" + self.backupFilename = self.filename + ".1" if not os.path.isfile(self.filename): self.startNewFile() elif os.path.getsize(self.filename) > 20000000: self.doRollover() if clear: - #clear file + # clear file self.startNewFile() def startNewFile(self): @@ -86,6 +86,6 @@ class CaptureFileHandler: def writePacket(self, packet): with open(self.filename, "ab") as f: packet = Pcap.create_packet( - bytes([packet.boardId] + packet.getList()), - packet.time) + bytes([packet.boardId] + packet.getList()), packet.time + ) f.write(packet) diff --git a/modules/wireshark/extcap/SnifferAPI/Devices.py b/modules/wireshark/extcap/SnifferAPI/Devices.py index 8ea3c0e..61ac961 100644 --- a/modules/wireshark/extcap/SnifferAPI/Devices.py +++ b/modules/wireshark/extcap/SnifferAPI/Devices.py @@ -39,6 +39,7 @@ from . import Notifications import logging, threading + class DeviceList(Notifications.Notifier): def __init__(self, *args, **kwargs): Notifications.Notifier.__init__(self, *args, **kwargs) @@ -52,7 +53,7 @@ class DeviceList(Notifications.Notifier): return len(self.devices) def __repr__(self): - return "Sniffer Device List: "+str(self.asList()) + return "Sniffer Device List: " + str(self.asList()) def clear(self): logging.info("Clearing") @@ -69,11 +70,15 @@ class DeviceList(Notifications.Notifier): self.append(newDevice) else: updated = False - if (newDevice.name != "\"\"") and (existingDevice.name == "\"\""): + if (newDevice.name != '""') and (existingDevice.name == '""'): existingDevice.name = newDevice.name updated = True - if (newDevice.RSSI != 0 and (existingDevice.RSSI < (newDevice.RSSI - 5)) or (existingDevice.RSSI > (newDevice.RSSI+2))): + if ( + newDevice.RSSI != 0 + and (existingDevice.RSSI < (newDevice.RSSI - 5)) + or (existingDevice.RSSI > (newDevice.RSSI + 2)) + ): existingDevice.RSSI = newDevice.RSSI updated = True @@ -93,14 +98,14 @@ class DeviceList(Notifications.Notifier): return self.devices[id] elif type(id) == str: for dev in self.devices: - if dev.name in [id, '"'+id+'"']: + if dev.name in [id, '"' + id + '"']: return dev elif id.__class__.__name__ == "Device": return self.find(id.address) return None def remove(self, id): - if type(id) == list: #address + if type(id) == list: # address device = self.devices.pop(self.devices.index(self.find(id))) elif type(id) == int: device = self.devices.pop(id) @@ -126,6 +131,7 @@ class DeviceList(Notifications.Notifier): def asList(self): return self.devices[:] + class Device: def __init__(self, address, name, RSSI): self.address = address @@ -134,10 +140,11 @@ class Device: self.followed = False def __repr__(self): - return 'Bluetooth LE device "'+self.name+'" ('+str(self.address)+')' + return 'Bluetooth LE device "' + self.name + '" (' + str(self.address) + ")" + def listToString(list): str = "" for i in list: - str+=chr(i) + str += chr(i) return str diff --git a/modules/wireshark/extcap/SnifferAPI/Exceptions.py b/modules/wireshark/extcap/SnifferAPI/Exceptions.py index f3ab20c..86f356a 100644 --- a/modules/wireshark/extcap/SnifferAPI/Exceptions.py +++ b/modules/wireshark/extcap/SnifferAPI/Exceptions.py @@ -38,23 +38,29 @@ class SnifferTimeout(Exception): pass + class UARTPacketError(Exception): pass + class LockedException(Exception): def __init__(self, message): self.message = message + class InvalidPacketException(Exception): pass + class InvalidAdvChannel(Exception): pass + # Internal Use class SnifferWatchDogTimeout(SnifferTimeout): pass + # Internal Use class ExitCodeException(Exception): pass diff --git a/modules/wireshark/extcap/SnifferAPI/Filelock.py b/modules/wireshark/extcap/SnifferAPI/Filelock.py index 5570942..7bf21b5 100644 --- a/modules/wireshark/extcap/SnifferAPI/Filelock.py +++ b/modules/wireshark/extcap/SnifferAPI/Filelock.py @@ -2,7 +2,7 @@ import os import logging from sys import platform -if platform == 'linux': +if platform == "linux": import psutil from . import Exceptions @@ -16,8 +16,9 @@ from . import Exceptions # HDB UUCP lock file format: # process identifier (PID) as a ten byte ASCII decimal number, with a trailing newline + def lockpid(lockfile): - if (os.path.isfile(lockfile)): + if os.path.isfile(lockfile): with open(lockfile) as fd: lockpid = fd.read() @@ -30,17 +31,13 @@ def lockpid(lockfile): return 0 + def lock(port): - if platform != 'linux': + if platform != "linux": return tty = os.path.basename(port) - lockfile = os.path.join( - '/run', - 'user', - f'{os.getuid()}', - f'{tty}.lock' - ) + lockfile = os.path.join("/run", "user", f"{os.getuid()}", f"{tty}.lock") lockedpid = lockpid(lockfile) if lockedpid: @@ -53,16 +50,17 @@ def lock(port): logging.info("Lockfile is stale. Overriding it..") os.remove(lockfile) - fd = open(lockfile, 'w') - with open(lockfile, 'w') as fd: - fd.write(f'{os.getpid():10}') + fd = open(lockfile, "w") + with open(lockfile, "w") as fd: + fd.write(f"{os.getpid():10}") + def unlock(port): - if platform != 'linux': + if platform != "linux": return tty = os.path.basename(port) - lockfile = f'/var/lock/LCK..{tty}' + lockfile = f"/var/lock/LCK..{tty}" lockedpid = lockpid(lockfile) if lockedpid == os.getpid(): diff --git a/modules/wireshark/extcap/SnifferAPI/Logger.py b/modules/wireshark/extcap/SnifferAPI/Logger.py index cbb1e7c..228a0f1 100644 --- a/modules/wireshark/extcap/SnifferAPI/Logger.py +++ b/modules/wireshark/extcap/SnifferAPI/Logger.py @@ -48,9 +48,11 @@ import logging.handlers as logHandlers # will result in the line being appended to the log file # ################################################################# -appdata = os.getenv('appdata') +appdata = os.getenv("appdata") if appdata: - DEFAULT_LOG_FILE_DIR = os.path.join(appdata, 'Nordic Semiconductor', 'Sniffer', 'logs') + DEFAULT_LOG_FILE_DIR = os.path.join( + appdata, "Nordic Semiconductor", "Sniffer", "logs" + ) else: DEFAULT_LOG_FILE_DIR = "/tmp/logs" @@ -89,8 +91,12 @@ def initLogger(): global logFlusher global logHandlerArray - logHandler = MyRotatingFileHandler(logFileName, mode='a', maxBytes=myMaxBytes, backupCount=3) - logFormatter = logging.Formatter('%(asctime)s %(levelname)s: %(message)s', datefmt='%d-%b-%Y %H:%M:%S (%z)') + logHandler = MyRotatingFileHandler( + logFileName, mode="a", maxBytes=myMaxBytes, backupCount=3 + ) + logFormatter = logging.Formatter( + "%(asctime)s %(levelname)s: %(message)s", datefmt="%d-%b-%Y %H:%M:%S (%z)" + ) logHandler.setFormatter(logFormatter) logger = logging.getLogger() logger.addHandler(logHandler) @@ -154,6 +160,7 @@ def addLogHandler(logHandler): logger.setLevel(logging.INFO) logHandlerArray.append(logHandler) + def removeLogHandler(logHandler): global logHandlerArray logger = logging.getLogger() @@ -200,7 +207,7 @@ class LogFlusher(threading.Thread): self.exit.set() -if __name__ == '__main__': +if __name__ == "__main__": initLogger() for i in range(50): logging.info("test log no. " + str(i)) diff --git a/modules/wireshark/extcap/SnifferAPI/Notifications.py b/modules/wireshark/extcap/SnifferAPI/Notifications.py index d9fd8e4..b7cba37 100644 --- a/modules/wireshark/extcap/SnifferAPI/Notifications.py +++ b/modules/wireshark/extcap/SnifferAPI/Notifications.py @@ -37,18 +37,20 @@ import threading, logging -class Notification(): - def __init__(self, key, msg = None): + +class Notification: + def __init__(self, key, msg=None): if type(key) is not str: - raise TypeError("Invalid notification key: "+str(key)) + raise TypeError("Invalid notification key: " + str(key)) self.key = key self.msg = msg def __repr__(self): return "Notification (key: %s, msg: %s)" % (str(self.key), str(self.msg)) -class Notifier(): - def __init__(self, callbacks = [], **kwargs): + +class Notifier: + def __init__(self, callbacks=[], **kwargs): self.callbacks = {} self.callbackLock = threading.RLock() @@ -75,10 +77,10 @@ class Notifier(): self.callbacks[key] = [] return self.callbacks[key] - def notify(self, key = None, msg = None, notification = None): + def notify(self, key=None, msg=None, notification=None): with self.callbackLock: if notification == None: - notification = Notification(key,msg) + notification = Notification(key, msg) for callback in self.getCallbacks(notification.key): callback(notification) @@ -87,4 +89,4 @@ class Notifier(): callback(notification) def passOnNotification(self, notification): - self.notify(notification = notification) + self.notify(notification=notification) diff --git a/modules/wireshark/extcap/SnifferAPI/Packet.py b/modules/wireshark/extcap/SnifferAPI/Packet.py index ddac78d..bc4abd9 100644 --- a/modules/wireshark/extcap/SnifferAPI/Packet.py +++ b/modules/wireshark/extcap/SnifferAPI/Packet.py @@ -43,17 +43,17 @@ ADV_ACCESS_ADDRESS = [0xD6, 0xBE, 0x89, 0x8E] SYNCWORD_POS = 0 PAYLOAD_LEN_POS_V1 = 1 PAYLOAD_LEN_POS = 0 -PROTOVER_POS = PAYLOAD_LEN_POS+2 -PACKETCOUNTER_POS = PROTOVER_POS+1 -ID_POS = PACKETCOUNTER_POS+2 +PROTOVER_POS = PAYLOAD_LEN_POS + 2 +PACKETCOUNTER_POS = PROTOVER_POS + 1 +ID_POS = PACKETCOUNTER_POS + 2 -BLE_HEADER_LEN_POS = ID_POS+1 -FLAGS_POS = BLE_HEADER_LEN_POS+1 -CHANNEL_POS = FLAGS_POS+1 -RSSI_POS = CHANNEL_POS+1 -EVENTCOUNTER_POS = RSSI_POS+1 -TIMESTAMP_POS = EVENTCOUNTER_POS+2 -BLEPACKET_POS = TIMESTAMP_POS+4 +BLE_HEADER_LEN_POS = ID_POS + 1 +FLAGS_POS = BLE_HEADER_LEN_POS + 1 +CHANNEL_POS = FLAGS_POS + 1 +RSSI_POS = CHANNEL_POS + 1 +EVENTCOUNTER_POS = RSSI_POS + 1 +TIMESTAMP_POS = EVENTCOUNTER_POS + 2 +BLEPACKET_POS = TIMESTAMP_POS + 4 TXADD_POS = BLEPACKET_POS + 4 TXADD_MSK = 0x40 PAYLOAD_POS = BLE_HEADER_LEN_POS @@ -109,7 +109,7 @@ class PacketReader(Notifications.Notifier): tempSLIPBuffer.append(SLIP_END) return tempSLIPBuffer - # This function uses getSerialByte() function to get SLIP encoded bytes from the serial port and return a decoded byte list + # This function uses getSerialByte() function to get SLIP encoded bytes from the serial port and return a decoded byte list # Based on https://github.com/mehdix/pyslip/ def decodeFromSLIP(self, timeout=None, complete_timeout=None): dataBuffer = [] @@ -119,11 +119,15 @@ class PacketReader(Notifications.Notifier): if complete_timeout is not None: time_start = time.time() - while not startOfPacket and (complete_timeout is None or (time.time() - time_start < complete_timeout)): + while not startOfPacket and ( + complete_timeout is None or (time.time() - time_start < complete_timeout) + ): res = self.getSerialByte(timeout) - startOfPacket = (res == SLIP_START) + startOfPacket = res == SLIP_START - while not endOfPacket and (complete_timeout is None or (time.time() - time_start < complete_timeout)): + while not endOfPacket and ( + complete_timeout is None or (time.time() - time_start < complete_timeout) + ): serialByte = self.getSerialByte(timeout) if serialByte == SLIP_END: endOfPacket = True @@ -138,9 +142,11 @@ class PacketReader(Notifications.Notifier): else: dataBuffer.append(SLIP_END) else: - dataBuffer.append(serialByte) + dataBuffer.append(serialByte) if not endOfPacket: - raise Exceptions.UARTPacketError("Exceeded max timeout of %f seconds." % complete_timeout) + raise Exceptions.UARTPacketError( + "Exceeded max timeout of %f seconds." % complete_timeout + ) return dataBuffer # This function read byte chuncks from the serial port and return one byte at a time @@ -153,13 +159,23 @@ class PacketReader(Notifications.Notifier): def handlePacketHistory(self, packet): # Reads and validates packet counter - if self.lastReceivedPacket is not None \ - and packet.packetCounter != (self.lastReceivedPacket.packetCounter + 1) % PACKET_COUNTER_CAP \ - and self.lastReceivedPacket.packetCounter != 0: + if ( + self.lastReceivedPacket is not None + and packet.packetCounter + != (self.lastReceivedPacket.packetCounter + 1) % PACKET_COUNTER_CAP + and self.lastReceivedPacket.packetCounter != 0 + ): - logging.info("gap in packets, between " + str(self.lastReceivedPacket.packetCounter) + " and " - + str(packet.packetCounter) + " packet before: " + str(self.lastReceivedPacket.packetList) - + " packet after: " + str(packet.packetList)) + logging.info( + "gap in packets, between " + + str(self.lastReceivedPacket.packetCounter) + + " and " + + str(packet.packetCounter) + + " packet before: " + + str(self.lastReceivedPacket.packetList) + + " packet after: " + + str(packet.packetList) + ) self.lastReceivedPacket = packet if packet.id in [EVENT_PACKET_DATA_PDU, EVENT_PACKET_ADV_PDU]: @@ -198,25 +214,34 @@ class PacketReader(Notifications.Notifier): # Convert time-stamp to End to Start delta time_delta = 0 - if self.lastReceivedTimestampPacket is not None and self.lastReceivedTimestampPacket.valid: - time_delta = (packet.timestamp - - (self.lastReceivedTimestampPacket.timestamp + - self.getPacketTime(self.lastReceivedTimestampPacket))) + if ( + self.lastReceivedTimestampPacket is not None + and self.lastReceivedTimestampPacket.valid + ): + time_delta = packet.timestamp - ( + self.lastReceivedTimestampPacket.timestamp + + self.getPacketTime(self.lastReceivedTimestampPacket) + ) time_delta = toLittleEndian(time_delta, 4) - packet.packetList[TIMESTAMP_POS ] = time_delta[0] - packet.packetList[TIMESTAMP_POS+1] = time_delta[1] - packet.packetList[TIMESTAMP_POS+2] = time_delta[2] - packet.packetList[TIMESTAMP_POS+3] = time_delta[3] - + packet.packetList[TIMESTAMP_POS] = time_delta[0] + packet.packetList[TIMESTAMP_POS + 1] = time_delta[1] + packet.packetList[TIMESTAMP_POS + 2] = time_delta[2] + packet.packetList[TIMESTAMP_POS + 3] = time_delta[3] def handlePacketCompatibility(self, packet): - if self.supportedProtocolVersion == PROTOVER_V2 and packet.packetList[PROTOVER_POS] > PROTOVER_V2: + if ( + self.supportedProtocolVersion == PROTOVER_V2 + and packet.packetList[PROTOVER_POS] > PROTOVER_V2 + ): self.convertPacketListProtoVer2(packet) def setSupportedProtocolVersion(self, supportedProtocolVersion): - if (supportedProtocolVersion != PROTOVER_V3): - logging.info("Using packet compatibility, converting packets to protocol version %d", supportedProtocolVersion) + if supportedProtocolVersion != PROTOVER_V3: + logging.info( + "Using packet compatibility, converting packets to protocol version %d", + supportedProtocolVersion, + ) self.supportedProtocolVersion = supportedProtocolVersion def getPacket(self, timeout=None): @@ -234,17 +259,30 @@ class PacketReader(Notifications.Notifier): return packet def sendPacket(self, id, payload): - packetList = [HEADER_LENGTH] + [len(payload)] + [PROTOVER_V1] + toLittleEndian(self.packetCounter, 2) + [id] + payload + packetList = ( + [HEADER_LENGTH] + + [len(payload)] + + [PROTOVER_V1] + + toLittleEndian(self.packetCounter, 2) + + [id] + + payload + ) packetList = self.encodeToSLIP(packetList) self.packetCounter += 1 self.uart.writeList(packetList) - def sendScan(self, findScanRsp = False, findAux = False, scanCoded = False): + def sendScan(self, findScanRsp=False, findAux=False, scanCoded=False): flags0 = findScanRsp | (findAux << 1) | (scanCoded << 2) self.sendPacket(REQ_SCAN_CONT, [flags0]) logging.info("Scan flags: %s" % bin(flags0)) - def sendFollow(self, addr, followOnlyAdvertisements = False, followOnlyLegacy = False, followCoded = False): + def sendFollow( + self, + addr, + followOnlyAdvertisements=False, + followOnlyLegacy=False, + followCoded=False, + ): flags0 = followOnlyAdvertisements | (followOnlyLegacy << 1) | (followCoded << 2) logging.info("Follow flags: %s" % bin(flags0)) self.sendPacket(REQ_FOLLOW, addr + [flags0]) @@ -253,7 +291,7 @@ class PacketReader(Notifications.Notifier): self.sendPacket(PING_REQ, []) def getBytes(self, value, size): - if (len(value) < size): + if len(value) < size: value = [0] * (size - len(value)) + value else: value = value[:size] @@ -294,10 +332,12 @@ class PacketReader(Notifications.Notifier): def sendHopSequence(self, hopSequence): for chan in hopSequence: if chan not in VALID_ADV_CHANS: - raise Exceptions.InvalidAdvChannel("%s is not an adv channel" % str(chan)) - payload = [len(hopSequence)] + hopSequence + [37]*(3-len(hopSequence)) + raise Exceptions.InvalidAdvChannel( + "%s is not an adv channel" % str(chan) + ) + payload = [len(hopSequence)] + hopSequence + [37] * (3 - len(hopSequence)) self.sendPacket(SET_ADV_CHANNEL_HOP_SEQ, payload) - self.notify("NEW_ADV_HOP_SEQ", {"hopSequence":hopSequence}) + self.notify("NEW_ADV_HOP_SEQ", {"hopSequence": hopSequence}) def sendVersionReq(self): self.sendPacket(REQ_VERSION, []) @@ -313,21 +353,31 @@ class Packet: def __init__(self, packetList): try: if not packetList: - raise Exceptions.InvalidPacketException("packet list not valid: %s" % str(packetList)) + raise Exceptions.InvalidPacketException( + "packet list not valid: %s" % str(packetList) + ) self.protover = packetList[PROTOVER_POS] if self.protover > PROTOVER_V3: - logging.exception("Unsupported protocol version %s" % str(self.protover)) - raise RuntimeError("Unsupported protocol version %s" % str(self.protover)) + logging.exception( + "Unsupported protocol version %s" % str(self.protover) + ) + raise RuntimeError( + "Unsupported protocol version %s" % str(self.protover) + ) - self.packetCounter = parseLittleEndian(packetList[PACKETCOUNTER_POS:PACKETCOUNTER_POS + 2]) + self.packetCounter = parseLittleEndian( + packetList[PACKETCOUNTER_POS : PACKETCOUNTER_POS + 2] + ) self.id = packetList[ID_POS] if int(self.protover) == PROTOVER_V1: self.payloadLength = packetList[PAYLOAD_LEN_POS_V1] else: - self.payloadLength = parseLittleEndian(packetList[PAYLOAD_LEN_POS:PAYLOAD_LEN_POS + 2]) + self.payloadLength = parseLittleEndian( + packetList[PAYLOAD_LEN_POS : PAYLOAD_LEN_POS + 2] + ) self.packetList = packetList self.readPayload(packetList) @@ -337,24 +387,26 @@ class Packet: self.OK = False self.valid = False except Exception as e: - logging.exception("packet creation error %s" %str(e)) + logging.exception("packet creation error %s" % str(e)) logging.info("packetList: " + str(packetList)) self.OK = False self.valid = False def __repr__(self): - return "UART packet, type: "+str(self.id)+", PC: "+str(self.packetCounter) + return "UART packet, type: " + str(self.id) + ", PC: " + str(self.packetCounter) def readPayload(self, packetList): self.blePacket = None self.OK = False if not self.validatePacketList(packetList): - raise Exceptions.InvalidPacketException("packet list not valid: %s" % str(packetList)) + raise Exceptions.InvalidPacketException( + "packet list not valid: %s" % str(packetList) + ) else: self.valid = True - self.payload = packetList[PAYLOAD_POS:PAYLOAD_POS+self.payloadLength] + self.payload = packetList[PAYLOAD_POS : PAYLOAD_POS + self.payloadLength] if self.id == EVENT_PACKET_ADV_PDU or self.id == EVENT_PACKET_DATA_PDU: try: @@ -365,23 +417,27 @@ class Packet: self.channel = packetList[CHANNEL_POS] self.rawRSSI = packetList[RSSI_POS] self.RSSI = -self.rawRSSI - self.eventCounter = parseLittleEndian(packetList[EVENTCOUNTER_POS:EVENTCOUNTER_POS+2]) + self.eventCounter = parseLittleEndian( + packetList[EVENTCOUNTER_POS : EVENTCOUNTER_POS + 2] + ) - self.timestamp = parseLittleEndian(packetList[TIMESTAMP_POS:TIMESTAMP_POS+4]) + self.timestamp = parseLittleEndian( + packetList[TIMESTAMP_POS : TIMESTAMP_POS + 4] + ) # The hardware adds a padding byte which isn't sent on air. # We remove it, and update the payload length in the packet list. if self.phy == PHY_CODED: - self.packetList.pop(BLEPACKET_POS+6+1) + self.packetList.pop(BLEPACKET_POS + 6 + 1) else: - self.packetList.pop(BLEPACKET_POS+6) + self.packetList.pop(BLEPACKET_POS + 6) self.payloadLength -= 1 if self.protover >= PROTOVER_V2: # Write updated payload length back to the packet list. payloadLength = toLittleEndian(self.payloadLength, 2) - packetList[PAYLOAD_LEN_POS ] = payloadLength[0] - packetList[PAYLOAD_LEN_POS+1] = payloadLength[1] - else: # PROTOVER_V1 + packetList[PAYLOAD_LEN_POS] = payloadLength[0] + packetList[PAYLOAD_LEN_POS + 1] = payloadLength[1] + else: # PROTOVER_V1 packetList[PAYLOAD_LEN_POS_V1] = self.payloadLength else: logging.info("Invalid BLE Header Length " + str(packetList)) @@ -390,15 +446,22 @@ class Packet: if self.OK: try: if self.protover >= PROTOVER_V3: - packet_type = (PACKET_TYPE_ADVERTISING - if self.id == EVENT_PACKET_ADV_PDU else - PACKET_TYPE_DATA) + packet_type = ( + PACKET_TYPE_ADVERTISING + if self.id == EVENT_PACKET_ADV_PDU + else PACKET_TYPE_DATA + ) else: - packet_type = (PACKET_TYPE_ADVERTISING - if packetList[BLEPACKET_POS : BLEPACKET_POS + 4] == ADV_ACCESS_ADDRESS else - PACKET_TYPE_DATA) + packet_type = ( + PACKET_TYPE_ADVERTISING + if packetList[BLEPACKET_POS : BLEPACKET_POS + 4] + == ADV_ACCESS_ADDRESS + else PACKET_TYPE_DATA + ) - self.blePacket = BlePacket(packet_type, packetList[BLEPACKET_POS:], self.phy) + self.blePacket = BlePacket( + packet_type, packetList[BLEPACKET_POS:], self.phy + ) except Exception as e: logging.exception("blePacket error %s" % str(e)) except Exception as e: @@ -407,13 +470,17 @@ class Packet: self.OK = False elif self.id == PING_RESP: if self.protover < PROTOVER_V3: - self.version = parseLittleEndian(packetList[PAYLOAD_POS:PAYLOAD_POS+2]) + self.version = parseLittleEndian( + packetList[PAYLOAD_POS : PAYLOAD_POS + 2] + ) elif self.id == RESP_VERSION: - self.version = ''.join([chr(i) for i in packetList[PAYLOAD_POS:]]) + self.version = "".join([chr(i) for i in packetList[PAYLOAD_POS:]]) elif self.id == RESP_TIMESTAMP: - self.timestamp = parseLittleEndian(packetList[PAYLOAD_POS:PAYLOAD_POS+4]) + self.timestamp = parseLittleEndian( + packetList[PAYLOAD_POS : PAYLOAD_POS + 4] + ) elif self.id == SWITCH_BAUD_RATE_RESP or self.id == SWITCH_BAUD_RATE_REQ: - self.baudRate = parseLittleEndian(packetList[PAYLOAD_POS:PAYLOAD_POS+4]) + self.baudRate = parseLittleEndian(packetList[PAYLOAD_POS : PAYLOAD_POS + 4]) else: logging.info("Unknown packet ID") @@ -438,7 +505,8 @@ class Packet: logging.exception("Invalid packet: %s" % str(packetList)) return False -class BlePacket(): + +class BlePacket: def __init__(self, type, packetList, phy): self.type = type @@ -458,12 +526,11 @@ class BlePacket(): offset = self.extractAddresses(packetList, offset) self.extractName(packetList, offset) - def __repr__(self): - return "BLE packet, AAddr: "+str(self.accessAddress) + return "BLE packet, AAddr: " + str(self.accessAddress) def extractAccessAddress(self, packetList, offset): - self.accessAddress = packetList[offset:offset+4] + self.accessAddress = packetList[offset : offset + 4] return offset + 4 def extractFormat(self, packetList, phy, offset): @@ -497,29 +564,29 @@ class BlePacket(): scanAddr = None if self.advType in [0, 1, 2, 4, 6]: - addr = packetList[offset:offset+6] + addr = packetList[offset : offset + 6] addr.reverse() addr += [self.txAddrType] offset += 6 if self.advType in [3, 5]: - scanAddr = packetList[offset:offset+6] + scanAddr = packetList[offset : offset + 6] scanAddr.reverse() scanAddr += [self.txAddrType] offset += 6 - addr = packetList[offset:offset+6] + addr = packetList[offset : offset + 6] addr.reverse() addr += [self.rxAddrType] offset += 6 if self.advType == 1: - scanAddr = packetList[offset:offset+6] + scanAddr = packetList[offset : offset + 6] scanAddr.reverse() scanAddr += [self.rxAddrType] offset += 6 if self.advType == 7: - ext_header_len = packetList[offset] & 0x3f + ext_header_len = packetList[offset] & 0x3F offset += 1 ext_header_offset = offset @@ -527,13 +594,13 @@ class BlePacket(): ext_header_offset += 1 if flags & 0x01: - addr = packetList[ext_header_offset:ext_header_offset+6] + addr = packetList[ext_header_offset : ext_header_offset + 6] addr.reverse() addr += [self.txAddrType] ext_header_offset += 6 if flags & 0x02: - scanAddr = packetList[ext_header_offset:ext_header_offset+6] + scanAddr = packetList[ext_header_offset : ext_header_offset + 6] scanAddr.reverse() scanAddr += [self.rxAddrType] ext_header_offset += 6 @@ -550,17 +617,17 @@ class BlePacket(): i = offset while i < len(packetList): length = packetList[i] - if (i+length+1) > len(packetList) or length == 0: + if (i + length + 1) > len(packetList) or length == 0: break - type = packetList[i+1] + type = packetList[i + 1] if type == 8 or type == 9: - nameList = packetList[i+2:i+length+1] + nameList = packetList[i + 2 : i + length + 1] name = "" for j in nameList: name += chr(j) - i += (length+1) - name = '"'+name+'"' - elif (self.advType == 1): + i += length + 1 + name = '"' + name + '"' + elif self.advType == 1: name = "[ADV_DIRECT_IND]" self.name = name @@ -569,15 +636,16 @@ class BlePacket(): self.length = packetList[offset] return offset + 1 + def parseLittleEndian(list): total = 0 for i in range(len(list)): - total+=(list[i] << (8*i)) + total += list[i] << (8 * i) return total -def toLittleEndian(value, size): - list = [0]*size - for i in range(size): - list[i] = (value >> (i*8)) % 256 - return list +def toLittleEndian(value, size): + list = [0] * size + for i in range(size): + list[i] = (value >> (i * 8)) % 256 + return list diff --git a/modules/wireshark/extcap/SnifferAPI/Pcap.py b/modules/wireshark/extcap/SnifferAPI/Pcap.py index 63cbe41..8b0445a 100644 --- a/modules/wireshark/extcap/SnifferAPI/Pcap.py +++ b/modules/wireshark/extcap/SnifferAPI/Pcap.py @@ -44,14 +44,16 @@ import struct # - https://github.com/pcapng/pcapng # - https://www.tcpdump.org/linktypes/LINKTYPE_NORDIC_BLE.html PACKET_HEADER = struct.Struct(" empty string ('') return None, None, None - _, _, length, arg, typ = struct.unpack('>sBHBB', header) + _, _, length, arg, typ = struct.unpack(">sBHBB", header) payload = bytearray() if length > 2: @@ -239,6 +321,7 @@ def control_read(): return arg, typ, payload + def control_write(arg, typ, message): """Write the message to the control channel""" @@ -247,8 +330,8 @@ def control_write(arg, typ, message): return packet = bytearray() - packet += struct.pack('>BBHBB', ord('T'), 0, len(message) + 2, arg, typ) - packet += message.encode('utf-8') + packet += struct.pack(">BBHBB", ord("T"), 0, len(message) + 2, arg, typ) + packet += message.encode("utf-8") fn_ctrl_out.write(packet) @@ -278,11 +361,13 @@ def device_added(notification): # Extcap selector uses \0 character to separate value and display value, # therefore the display value cannot contain the \0 character as this # would lead to truncation of the display value. - display = (device.name.replace('\0', '\\0') + - (" " + str(device.RSSI) + " dBm " if device.RSSI != 0 else " ") + - string_address(device.address)) + display = ( + device.name.replace("\0", "\\0") + + (" " + str(device.RSSI) + " dBm " if device.RSSI != 0 else " ") + + string_address(device.address) + ) - message = str(device.address) + '\0' + display + message = str(device.address) + "\0" + display control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, message) @@ -298,33 +383,35 @@ def device_removed(notification): control_write(CTRL_ARG_DEVICE, CTRL_CMD_REMOVE, message) logging.info("Removed: " + display) + def devices_cleared(notification): """Devices have been cleared""" message = "" control_write(CTRL_ARG_DEVICE, CTRL_CMD_REMOVE, message) - control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, " " + '\0' + "All advertising devices") - control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, zero_addr + '\0' + "Follow IRK") + control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, " " + "\0" + "All advertising devices") + control_write(CTRL_ARG_DEVICE, CTRL_CMD_ADD, zero_addr + "\0" + "Follow IRK") control_write(CTRL_ARG_DEVICE, CTRL_CMD_SET, " ") + def handle_control_command(sniffer, arg, typ, payload): """Handle command from control channel""" global last_used_key_type if arg == CTRL_ARG_DEVICE: - if payload == b' ': + if payload == b" ": scan_for_devices(sniffer) else: values = payload - values = values.replace(b'[', b'') - values = values.replace(b']', b'') - device_address = values.split(b',') + values = values.replace(b"[", b"") + values = values.replace(b"]", b"") + device_address = values.split(b",") - logging.info('follow_device: {}'.format(device_address)) + logging.info("follow_device: {}".format(device_address)) for i in range(6): device_address[i] = int(device_address[i]) - device_address[6] = 1 if device_address[6] == b' 1' else 0 + device_address[6] = 1 if device_address[6] == b" 1" else 0 device = Devices.Device(address=device_address, name='""', RSSI=0) @@ -333,7 +420,7 @@ def handle_control_command(sniffer, arg, typ, payload): elif arg == CTRL_ARG_DEVICE_CLEAR: clear_devices(sniffer) elif arg == CTRL_ARG_KEY_TYPE: - last_used_key_type = int(payload.decode('utf-8')) + last_used_key_type = int(payload.decode("utf-8")) elif arg == CTRL_ARG_KEY_VAL: set_key_value(sniffer, payload) elif arg == CTRL_ARG_ADVHOP: @@ -384,8 +471,10 @@ def follow_device(sniffer, device): """Follow the selected device""" global write_new_packets, in_follow_mode - sniffer.follow(device, capture_only_advertising, capture_only_legacy_advertising, capture_coded) - time.sleep(.1) + sniffer.follow( + device, capture_only_advertising, capture_only_legacy_advertising, capture_coded + ) + time.sleep(0.1) in_follow_mode = True logging.info("Following " + string_address(device.address)) @@ -395,58 +484,64 @@ def set_key_value(sniffer, payload): """Send key value to device""" global last_used_key_val - payload = payload.decode('utf-8') + payload = payload.decode("utf-8") last_used_key_val = payload - if (last_used_key_type == CTRL_KEY_TYPE_PASSKEY): + if last_used_key_type == CTRL_KEY_TYPE_PASSKEY: if re.match("^[0-9]{6}$", payload): set_passkey(sniffer, payload) else: logging.info("Invalid key value: " + str(payload)) - elif (last_used_key_type == CTRL_KEY_TYPE_OOB): + elif last_used_key_type == CTRL_KEY_TYPE_OOB: if re.match("^0[xX][0-9A-Za-z]{1,32}$", payload): set_OOB(sniffer, payload[2:]) else: logging.info("Invalid key value: " + str(payload)) - elif (last_used_key_type == CTRL_KEY_TYPE_DH_PRIVATE_KEY): - if (re.match("^0[xX][0-9A-Za-z]{1,64}$", payload)): + elif last_used_key_type == CTRL_KEY_TYPE_DH_PRIVATE_KEY: + if re.match("^0[xX][0-9A-Za-z]{1,64}$", payload): set_dh_private_key(sniffer, payload[2:]) else: logging.info("Invalid key value: " + str(payload)) - elif (last_used_key_type == CTRL_KEY_TYPE_LEGACY_LTK): - if (re.match("^0[xX][0-9A-Za-z]{1,32}$", payload)): + elif last_used_key_type == CTRL_KEY_TYPE_LEGACY_LTK: + if re.match("^0[xX][0-9A-Za-z]{1,32}$", payload): set_legacy_ltk(sniffer, payload[2:]) else: logging.info("Invalid key value: " + str(payload)) - elif (last_used_key_type == CTRL_KEY_TYPE_SC_LTK): - if (re.match("^0[xX][0-9A-Za-z]{1,32}$", payload)): + elif last_used_key_type == CTRL_KEY_TYPE_SC_LTK: + if re.match("^0[xX][0-9A-Za-z]{1,32}$", payload): set_sc_ltk(sniffer, payload[2:]) else: logging.info("Invalid key value: " + str(payload)) - elif (last_used_key_type == CTRL_KEY_TYPE_IRK): - if (re.match("^0[xX][0-9A-Za-z]{1,32}$", payload)): + elif last_used_key_type == CTRL_KEY_TYPE_IRK: + if re.match("^0[xX][0-9A-Za-z]{1,32}$", payload): set_irk(sniffer, payload[2:]) else: logging.info("Invalid key value: " + str(payload)) - elif (last_used_key_type == CTRL_KEY_TYPE_ADD_ADDR): - if (re.match("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2}) (public|random)$", payload)): + elif last_used_key_type == CTRL_KEY_TYPE_ADD_ADDR: + if re.match( + "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2}) (public|random)$", payload + ): add_address(sniffer, payload) else: logging.info("Invalid key value: " + str(payload)) - elif (last_used_key_type == CTRL_KEY_TYPE_FOLLOW_ADDR): - if (re.match("^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2}) (public|random)$", payload)): + elif last_used_key_type == CTRL_KEY_TYPE_FOLLOW_ADDR: + if re.match( + "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2}) (public|random)$", payload + ): follow_address(sniffer, payload) else: logging.info("Invalid key value: " + str(payload)) else: logging.info("Invalid key type: " + str(last_used_key_type)) -def parse_hex(value): - if len(value) % 2 != 0: - value = '0' + value - a = list(value) - return [int(x + y, 16) for x,y in zip(a[::2], a[1::2])] +def parse_hex(value): + if len(value) % 2 != 0: + value = "0" + value + + a = list(value) + return [int(x + y, 16) for x, y in zip(a[::2], a[1::2])] + def set_passkey(sniffer, payload): """Send passkey to device""" @@ -461,36 +556,42 @@ def set_passkey(sniffer, payload): sniffer.sendTK(passkey) + def set_OOB(sniffer, payload): """Send OOB to device""" logging.info("Setting OOB data: " + payload) sniffer.sendTK(parse_hex(payload)) + def set_dh_private_key(sniffer, payload): """Send Diffie-Hellman private key to device""" logging.info("Setting DH private key: " + payload) sniffer.sendPrivateKey(parse_hex(payload)) + def set_legacy_ltk(sniffer, payload): """Send Legacy Long Term Key (LTK) to device""" logging.info("Setting Legacy LTK: " + payload) sniffer.sendLegacyLTK(parse_hex(payload)) + def set_sc_ltk(sniffer, payload): """Send LE secure connections Long Term Key (LTK) to device""" logging.info("Setting SC LTK: " + payload) sniffer.sendSCLTK(parse_hex(payload)) + def set_irk(sniffer, payload): """Send Identity Resolving Key (IRK) to device""" logging.info("Setting IRK: " + payload) sniffer.sendIRK(parse_hex(payload)) + def add_address(sniffer, payload): """Add LE address to device list""" logging.info("Adding LE address: " + payload) - (addr,addr_type) = payload.split(' ') + (addr, addr_type) = payload.split(" ") device = [int(a, 16) for a in addr.split(":")] device.append(1 if addr_type == "random" else 0) @@ -498,11 +599,12 @@ def add_address(sniffer, payload): new_device = Devices.Device(address=device, name='""', RSSI=0) sniffer.addDevice(new_device) + def follow_address(sniffer, payload): """Add LE address to device list""" logging.info("Adding LE address: " + payload) - (addr,addr_type) = payload.split(' ') + (addr, addr_type) = payload.split(" ") device = [int(a, 16) for a in addr.split(":")] device.append(1 if addr_type == "random" else 0) @@ -513,15 +615,16 @@ def follow_address(sniffer, payload): control_write(CTRL_ARG_DEVICE, CTRL_CMD_SET, f"{new_device.address}") follow_device(sniffer, new_device) + def set_advhop(sniffer, payload): """Set advertising channel hop sequence""" global last_used_advhop - payload = payload.decode('utf-8') + payload = payload.decode("utf-8") last_used_advhop = payload - hops = [int(channel) for channel in payload.split(',')] + hops = [int(channel) for channel in payload.split(",")] sniffer.setAdvHopSequence(hops) @@ -546,7 +649,7 @@ def error_interface_not_found(interface, fifo): def validate_interface(interface, fifo): """Check if interface exists""" - if sys.platform != 'win32' and not os.path.exists(interface): + if sys.platform != "win32" and not os.path.exists(interface): error_interface_not_found(interface, fifo) @@ -557,12 +660,13 @@ def get_default_baudrate(interface, fifo): error_interface_not_found(interface, fifo) return rates["default"] + def get_supported_protocol_version(extcap_version): """Return the maximum supported Packet Protocol Version""" - if extcap_version == 'None': + if extcap_version == "None": return 2 - (major, minor) = extcap_version.split('.') + (major, minor) = extcap_version.split(".") major = int(major) minor = int(minor) @@ -572,6 +676,7 @@ def get_supported_protocol_version(extcap_version): else: return 2 + def setup_extcap_log_handler(): """Add the a handler that emits log messages through the extcap control out channel""" global extcap_log_handler @@ -593,18 +698,18 @@ def sniffer_capture(interface, baudrate, fifo, control_in, control_out): global fn_capture, fn_ctrl_in, fn_ctrl_out, write_new_packets, extcap_log_handler try: - fn_capture = open(fifo, 'wb', 0) + fn_capture = open(fifo, "wb", 0) if control_out is not None: - fn_ctrl_out = open(control_out, 'wb', 0) + fn_ctrl_out = open(control_out, "wb", 0) setup_extcap_log_handler() if control_in is not None: - fn_ctrl_in = open(control_in, 'rb', 0) + fn_ctrl_in = open(control_in, "rb", 0) logging.info("Log started at %s", time.strftime("%c")) - interface, extcap_version = interface.split('-') + interface, extcap_version = interface.split("-") logging.info("Extcap version %s", str(extcap_version)) capture_write(Pcap.get_global_header()) @@ -619,7 +724,9 @@ def sniffer_capture(interface, baudrate, fifo, control_in, control_out): sniffer.subscribe("DEVICE_REMOVED", device_removed) sniffer.subscribe("DEVICES_CLEARED", devices_cleared) sniffer.setAdvHopSequence([37, 38, 39]) - sniffer.setSupportedProtocolVersion(get_supported_protocol_version(extcap_version)) + sniffer.setSupportedProtocolVersion( + get_supported_protocol_version(extcap_version) + ) logging.info("Sniffer created") logging.info("Software version: %s" % sniffer.swversion) @@ -655,7 +762,7 @@ def sniffer_capture(interface, baudrate, fifo, control_in, control_out): pass except Exceptions.LockedException as e: - logging.info('{}'.format(e.message)) + logging.info("{}".format(e.message)) except OSError: # We'll get OSError=22 when/if wireshark kills the pipe(s) on capture @@ -689,14 +796,14 @@ def sniffer_capture(interface, baudrate, fifo, control_in, control_out): def extcap_close_fifo(fifo): - """"Close extcap fifo""" + """ "Close extcap fifo""" if not os.path.exists(fifo): print("FIFO does not exist!", file=sys.stderr) return # This is apparently needed to workaround an issue on Windows/macOS # where the message cannot be read. (really?) - fh = open(fifo, 'wb', 0) + fh = open(fifo, "wb", 0) fh.close() @@ -705,13 +812,13 @@ class ExtcapLoggerHandler(logging.Handler): def emit(self, record): """Send log message to extcap""" - message = record.message.replace('\0', '\\0') + message = record.message.replace("\0", "\\0") log_message = f"{record.levelname}: {message}\n" control_write(CTRL_ARG_LOG, CTRL_CMD_ADD, log_message) def parse_capture_filter(capture_filter): - """"Parse given capture filter""" + """ "Parse given capture filter""" global rssi_filter m = re.search(r"^\s*rssi\s*(>=?)\s*(-?[0-9]+)\s*$", capture_filter, re.IGNORECASE) if m: @@ -720,66 +827,89 @@ def parse_capture_filter(capture_filter): print("Illegal RSSI value, must be between -10 and -256") # Handle >= by modifying the threshold, since comparisons are always done with # the > operator - if m.group(1) == '>=': + if m.group(1) == ">=": rssi_filter = rssi_filter - 1 else: - print("Filter syntax: \"RSSI >= -value\"") + print('Filter syntax: "RSSI >= -value"') + import atexit + @atexit.register def goodbye(): - logging.info("Exiting PID {}".format(os.getpid())) + logging.info("Exiting PID {}".format(os.getpid())) -if __name__ == '__main__': +if __name__ == "__main__": # Capture options - parser = argparse.ArgumentParser(description="Nordic Semiconductor nRF Sniffer for Bluetooth LE extcap plugin") + parser = argparse.ArgumentParser( + description="Nordic Semiconductor nRF Sniffer for Bluetooth LE extcap plugin" + ) # Extcap Arguments - parser.add_argument("--capture", - help="Start the capture", - action="store_true") + parser.add_argument("--capture", help="Start the capture", action="store_true") - parser.add_argument("--extcap-interfaces", - help="List available interfaces to capture from", - action="store_true") + parser.add_argument( + "--extcap-interfaces", + help="List available interfaces to capture from", + action="store_true", + ) - parser.add_argument("--extcap-interface", - help="The interface to capture from") + parser.add_argument("--extcap-interface", help="The interface to capture from") - parser.add_argument("--extcap-dlts", - help="List DLTs for the given interface", - action="store_true") + parser.add_argument( + "--extcap-dlts", help="List DLTs for the given interface", action="store_true" + ) - parser.add_argument("--extcap-config", - help="List configurations for the given interface", - action="store_true") + parser.add_argument( + "--extcap-config", + help="List configurations for the given interface", + action="store_true", + ) - parser.add_argument("--extcap-capture-filter", - help="Used together with capture to provide a capture filter") + parser.add_argument( + "--extcap-capture-filter", + help="Used together with capture to provide a capture filter", + ) - parser.add_argument("--fifo", - help="Use together with capture to provide the fifo to dump data to") + parser.add_argument( + "--fifo", help="Use together with capture to provide the fifo to dump data to" + ) - parser.add_argument("--extcap-control-in", - help="Used together with capture to get control messages from toolbar") + parser.add_argument( + "--extcap-control-in", + help="Used together with capture to get control messages from toolbar", + ) - parser.add_argument("--extcap-control-out", - help="Used together with capture to send control messages to toolbar") + parser.add_argument( + "--extcap-control-out", + help="Used together with capture to send control messages to toolbar", + ) - parser.add_argument("--extcap-version", - help="Set extcap supported version") + parser.add_argument("--extcap-version", help="Set extcap supported version") # Interface Arguments parser.add_argument("--device", help="Device", default="") parser.add_argument("--baudrate", type=int, help="The sniffer baud rate") - parser.add_argument("--only-advertising", help="Only advertising packets", action="store_true") - parser.add_argument("--only-legacy-advertising", help="Only legacy advertising packets", action="store_true") - parser.add_argument("--scan-follow-rsp", help="Find scan response data ", action="store_true") - parser.add_argument("--scan-follow-aux", help="Find auxiliary pointer data", action="store_true") - parser.add_argument("--coded", help="Scan and follow on LE Coded PHY", action="store_true") + parser.add_argument( + "--only-advertising", help="Only advertising packets", action="store_true" + ) + parser.add_argument( + "--only-legacy-advertising", + help="Only legacy advertising packets", + action="store_true", + ) + parser.add_argument( + "--scan-follow-rsp", help="Find scan response data ", action="store_true" + ) + parser.add_argument( + "--scan-follow-aux", help="Find auxiliary pointer data", action="store_true" + ) + parser.add_argument( + "--coded", help="Scan and follow on LE Coded PHY", action="store_true" + ) logging.info("Started PID {}".format(os.getpid())) @@ -839,16 +969,23 @@ if __name__ == '__main__': parser.print_help() sys.exit(ERROR_FIFO) try: - logging.info('sniffer capture') - sniffer_capture(interface, args.baudrate, args.fifo, args.extcap_control_in, args.extcap_control_out) + logging.info("sniffer capture") + sniffer_capture( + interface, + args.baudrate, + args.fifo, + args.extcap_control_in, + args.extcap_control_out, + ) except KeyboardInterrupt: pass except Exception as e: import traceback + logging.info(traceback.format_exc()) - logging.info('internal error: {}'.format(repr(e))) + logging.info("internal error: {}".format(repr(e))) sys.exit(ERROR_INTERNAL) else: parser.print_help() sys.exit(ERROR_USAGE) - logging.info('main exit PID {}'.format(os.getpid())) + logging.info("main exit PID {}".format(os.getpid())) From 9f528e2f6f4a6abed89e3847d5f98a71d1e92b56 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Tue, 18 Nov 2025 14:49:18 +0100 Subject: [PATCH 21/22] Fix rule to send mails from ipv4 only --- hosts/iron/services/mail.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hosts/iron/services/mail.nix b/hosts/iron/services/mail.nix index fbe802e..c5d855e 100644 --- a/hosts/iron/services/mail.nix +++ b/hosts/iron/services/mail.nix @@ -29,7 +29,7 @@ in smtp_bind_address = "159.69.103.126"; smtp_bind_address_enforce = true; }; - masterConfig.smtp_inet.args = [ + masterConfig.smtp.args = [ "-o" "inet_protocols=ipv4" ]; From 8a0b56982a7e5782b34a868a26093370c3529ef4 Mon Sep 17 00:00:00 2001 From: Jakob Lechner Date: Tue, 18 Nov 2025 14:59:08 +0100 Subject: [PATCH 22/22] flake.lock: Update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flake lock file updates: • Updated input 'disko': 'github:nix-community/disko/3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5?narHash=sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw%3D' (2025-10-17) → 'github:nix-community/disko/af087d076d3860760b3323f6b583f4d828c1ac17?narHash=sha256-TtcPgPmp2f0FAnc%2BDMEw4ardEgv1SGNR3/WFGH0N19M%3D' (2025-11-04) • Updated input 'nix-pre-commit-hooks': 'github:cachix/git-hooks.nix/ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37?narHash=sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc%3D' (2025-10-17) → 'github:cachix/git-hooks.nix/7275fa67fbbb75891c16d9dee7d88e58aea2d761?narHash=sha256-YG19IyrTdnVn0l3DvcUYm85u3PaqBt6tI6VvolcuHnA%3D' (2025-11-16) • Updated input 'nixos-hardware': 'github:nixos/nixos-hardware/d6645c340ef7d821602fd2cd199e8d1eed10afbc?narHash=sha256-2m1S4jl%2BGEDtlt2QqeHil8Ny456dcGSKJAM7q3j/BFU%3D' (2025-10-20) → 'github:nixos/nixos-hardware/899dc449bc6428b9ee6b3b8f771ca2b0ef945ab9?narHash=sha256-BWWnUUT01lPwCWUvS0p6Px5UOBFeXJ8jR%2BZdLX8IbrU%3D' (2025-11-11) • Updated input 'nixpkgs': 'github:nixos/nixpkgs/78e34d1667d32d8a0ffc3eba4591ff256e80576e?narHash=sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8%3D' (2025-10-26) → 'github:nixos/nixpkgs/4c8cdd5b1a630e8f72c9dd9bf582b1afb3127d2c?narHash=sha256-LBVOyaH6NFzQ3X/c6vfMZ9k4SV2ofhpxeL9YnhHNJQQ%3D' (2025-11-16) • Updated input 'nixpkgsMaster': 'github:NixOS/nixpkgs/8865b77677eb576ce1dbcb90b7a1ae95a774a6cd?narHash=sha256-rIne9pcxSoaLCxcyICguhH3SUzE9lep464L7zRGBbZk%3D' (2025-10-27) → 'github:NixOS/nixpkgs/15901670689a6f338ebd2a9436b947ec189463a3?narHash=sha256-NzmsN8hRIn/9rJvZH3vPirBrOJJfeSfvPr4%2BfeeK7LY%3D' (2025-11-18) • Updated input 'nur': 'github:nix-community/NUR/378c5c7b0b2471b59b71e42b229ea5e68050235d?narHash=sha256-0UtnyehKLys0HWhctZEjKN7zDe%2BML0HCDdqdfHk221o%3D' (2025-10-27) → 'github:nix-community/NUR/4c584dcedf9aa3394e9730e62693515a0e47674b?narHash=sha256-B1ua1UtkPuMwT8o4nOR7yNP5yz10usMcNnxwHpGtLck%3D' (2025-11-18) • Updated input 'nur/nixpkgs': 'github:nixos/nixpkgs/6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce?narHash=sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c%3D' (2025-10-25) → 'github:nixos/nixpkgs/50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a?narHash=sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c%2Bi7novT85Uk%3D' (2025-11-16) • Updated input 'sops-nix': 'github:Mic92/sops-nix/5a7d18b5c55642df5c432aadb757140edfeb70b3?narHash=sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY%3D' (2025-10-20) → 'github:Mic92/sops-nix/3f66a7fb9626a9a9c077612ef10a0ce396286c7d?narHash=sha256-n5xDOeNN%2BsmocQp3EMIc11IzBlR9wvvTIJZeL0g33Fs%3D' (2025-11-17) • Updated input 'vesc-tool': 'github:vedderb/vesc_tool/8a6de0dda75e62681e7252a1512f34a5f4e70640?narHash=sha256-%2Bvk8SRGknwg5Q8y/R4oHkiLxe2SDQDYGj8jK7Xaeu6c%3D' (2025-10-17) → 'github:vedderb/vesc_tool/6a75051ce9742d97f14addd5d175ac516effb3c6?narHash=sha256-j%2BAZQYOuZ0X33p76LsZu4/NZl1Ccu6kkwPKC5HpIn1Y%3D' (2025-11-12) --- flake.lock | 54 +++++++++++++++++++++++++++--------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/flake.lock b/flake.lock index 6771af6..7c92712 100644 --- a/flake.lock +++ b/flake.lock @@ -65,11 +65,11 @@ ] }, "locked": { - "lastModified": 1760701190, - "narHash": "sha256-y7UhnWlER8r776JsySqsbTUh2Txf7K30smfHlqdaIQw=", + "lastModified": 1762276996, + "narHash": "sha256-TtcPgPmp2f0FAnc+DMEw4ardEgv1SGNR3/WFGH0N19M=", "owner": "nix-community", "repo": "disko", - "rev": "3a9450b26e69dcb6f8de6e2b07b3fc1c288d85f5", + "rev": "af087d076d3860760b3323f6b583f4d828c1ac17", "type": "github" }, "original": { @@ -386,11 +386,11 @@ ] }, "locked": { - "lastModified": 1760663237, - "narHash": "sha256-BflA6U4AM1bzuRMR8QqzPXqh8sWVCNDzOdsxXEguJIc=", + "lastModified": 1763319842, + "narHash": "sha256-YG19IyrTdnVn0l3DvcUYm85u3PaqBt6tI6VvolcuHnA=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37", + "rev": "7275fa67fbbb75891c16d9dee7d88e58aea2d761", "type": "github" }, "original": { @@ -402,11 +402,11 @@ }, "nixos-hardware": { "locked": { - "lastModified": 1760958188, - "narHash": "sha256-2m1S4jl+GEDtlt2QqeHil8Ny456dcGSKJAM7q3j/BFU=", + "lastModified": 1762847253, + "narHash": "sha256-BWWnUUT01lPwCWUvS0p6Px5UOBFeXJ8jR+ZdLX8IbrU=", "owner": "nixos", "repo": "nixos-hardware", - "rev": "d6645c340ef7d821602fd2cd199e8d1eed10afbc", + "rev": "899dc449bc6428b9ee6b3b8f771ca2b0ef945ab9", "type": "github" }, "original": { @@ -418,11 +418,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1761468971, - "narHash": "sha256-vY2OLVg5ZTobdroQKQQSipSIkHlxOTrIF1fsMzPh8w8=", + "lastModified": 1763334038, + "narHash": "sha256-LBVOyaH6NFzQ3X/c6vfMZ9k4SV2ofhpxeL9YnhHNJQQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "78e34d1667d32d8a0ffc3eba4591ff256e80576e", + "rev": "4c8cdd5b1a630e8f72c9dd9bf582b1afb3127d2c", "type": "github" }, "original": { @@ -450,11 +450,11 @@ }, "nixpkgsMaster": { "locked": { - "lastModified": 1761601585, - "narHash": "sha256-rIne9pcxSoaLCxcyICguhH3SUzE9lep464L7zRGBbZk=", + "lastModified": 1763473525, + "narHash": "sha256-NzmsN8hRIn/9rJvZH3vPirBrOJJfeSfvPr4+feeK7LY=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "8865b77677eb576ce1dbcb90b7a1ae95a774a6cd", + "rev": "15901670689a6f338ebd2a9436b947ec189463a3", "type": "github" }, "original": { @@ -482,11 +482,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1761373498, - "narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=", + "lastModified": 1763283776, + "narHash": "sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c+i7novT85Uk=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce", + "rev": "50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a", "type": "github" }, "original": { @@ -518,11 +518,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1761601325, - "narHash": "sha256-0UtnyehKLys0HWhctZEjKN7zDe+ML0HCDdqdfHk221o=", + "lastModified": 1763471545, + "narHash": "sha256-B1ua1UtkPuMwT8o4nOR7yNP5yz10usMcNnxwHpGtLck=", "owner": "nix-community", "repo": "NUR", - "rev": "378c5c7b0b2471b59b71e42b229ea5e68050235d", + "rev": "4c584dcedf9aa3394e9730e62693515a0e47674b", "type": "github" }, "original": { @@ -634,11 +634,11 @@ ] }, "locked": { - "lastModified": 1760998189, - "narHash": "sha256-ee2e1/AeGL5X8oy/HXsZQvZnae6XfEVdstGopKucYLY=", + "lastModified": 1763417348, + "narHash": "sha256-n5xDOeNN+smocQp3EMIc11IzBlR9wvvTIJZeL0g33Fs=", "owner": "Mic92", "repo": "sops-nix", - "rev": "5a7d18b5c55642df5c432aadb757140edfeb70b3", + "rev": "3f66a7fb9626a9a9c077612ef10a0ce396286c7d", "type": "github" }, "original": { @@ -729,11 +729,11 @@ "treefmt-nix": "treefmt-nix_2" }, "locked": { - "lastModified": 1760697314, - "narHash": "sha256-+vk8SRGknwg5Q8y/R4oHkiLxe2SDQDYGj8jK7Xaeu6c=", + "lastModified": 1762968599, + "narHash": "sha256-j+AZQYOuZ0X33p76LsZu4/NZl1Ccu6kkwPKC5HpIn1Y=", "owner": "vedderb", "repo": "vesc_tool", - "rev": "8a6de0dda75e62681e7252a1512f34a5f4e70640", + "rev": "6a75051ce9742d97f14addd5d175ac516effb3c6", "type": "github" }, "original": {