diff --git a/flake.lock b/flake.lock
index c5852ed..7c92712 100644
--- a/flake.lock
+++ b/flake.lock
@@ -45,11 +45,11 @@
},
"crane": {
"locked": {
- "lastModified": 1754269165,
- "narHash": "sha256-0tcS8FHd4QjbCVoxN9jI+PjHgA4vc/IjkUSp+N3zy0U=",
+ "lastModified": 1731098351,
+ "narHash": "sha256-HQkYvKvaLQqNa10KEFGgWHfMAbWBfFp+4cAgkut+NNE=",
"owner": "ipetkov",
"repo": "crane",
- "rev": "444e81206df3f7d92780680e45858e31d2f07a08",
+ "rev": "ef80ead953c1b28316cc3f8613904edc2eb90c28",
"type": "github"
},
"original": {
@@ -65,11 +65,11 @@
]
},
"locked": {
- "lastModified": 1765326679,
- "narHash": "sha256-fTLX9kDwLr9Y0rH/nG+h1XG5UU+jBcy0PFYn5eneRX8=",
+ "lastModified": 1762276996,
+ "narHash": "sha256-TtcPgPmp2f0FAnc+DMEw4ardEgv1SGNR3/WFGH0N19M=",
"owner": "nix-community",
"repo": "disko",
- "rev": "d64e5cdca35b5fad7c504f615357a7afe6d9c49e",
+ "rev": "af087d076d3860760b3323f6b583f4d828c1ac17",
"type": "github"
},
"original": {
@@ -81,11 +81,11 @@
"flake-compat": {
"flake": false,
"locked": {
- "lastModified": 1747046372,
- "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
+ "lastModified": 1696426674,
+ "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
+ "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
@@ -97,11 +97,11 @@
"flake-compat_2": {
"flake": false,
"locked": {
- "lastModified": 1761588595,
- "narHash": "sha256-XKUZz9zewJNUj46b4AJdiRZJAvSZ0Dqj2BNfXvFlJC4=",
+ "lastModified": 1747046372,
+ "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=",
"owner": "edolstra",
"repo": "flake-compat",
- "rev": "f387cd2afec9419c8ee37694406ca490c3f34ee5",
+ "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
"type": "github"
},
"original": {
@@ -118,11 +118,11 @@
]
},
"locked": {
- "lastModified": 1754091436,
- "narHash": "sha256-XKqDMN1/Qj1DKivQvscI4vmHfDfvYR2pfuFOJiCeewM=",
+ "lastModified": 1730504689,
+ "narHash": "sha256-hgmguH29K2fvs9szpq2r3pz2/8cJd2LPS+b4tfNFCwE=",
"owner": "hercules-ci",
"repo": "flake-parts",
- "rev": "67df8c627c2c39c41dbec76a1f201929929ab0bd",
+ "rev": "506278e768c2a08bec68eb62932193e341f55c90",
"type": "github"
},
"original": {
@@ -243,11 +243,11 @@
]
},
"locked": {
- "lastModified": 1763982521,
- "narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=",
+ "lastModified": 1759991118,
+ "narHash": "sha256-pDyrtUQyeP1lVTMIYqJtftzDtsXEZaJjYy9ZQ/SGhL8=",
"owner": "nix-community",
"repo": "gomod2nix",
- "rev": "02e63a239d6eabd595db56852535992c898eba72",
+ "rev": "7f8d7438f5870eb167abaf2c39eea3d2302019d1",
"type": "github"
},
"original": {
@@ -263,16 +263,16 @@
]
},
"locked": {
- "lastModified": 1765384171,
- "narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
+ "lastModified": 1758463745,
+ "narHash": "sha256-uhzsV0Q0I9j2y/rfweWeGif5AWe0MGrgZ/3TjpDYdGA=",
"owner": "nix-community",
"repo": "home-manager",
- "rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
+ "rev": "3b955f5f0a942f9f60cdc9cacb7844335d0f21c3",
"type": "github"
},
"original": {
"owner": "nix-community",
- "ref": "release-25.11",
+ "ref": "release-25.05",
"repo": "home-manager",
"type": "github"
}
@@ -327,16 +327,16 @@
"rust-overlay": "rust-overlay"
},
"locked": {
- "lastModified": 1762205063,
- "narHash": "sha256-If6vQ+KvtKs3ARBO9G3l+4wFSCYtRBrwX1z+I+B61wQ=",
+ "lastModified": 1737639419,
+ "narHash": "sha256-AEEDktApTEZ5PZXNDkry2YV2k6t0dTgLPEmAZbnigXU=",
"owner": "nix-community",
"repo": "lanzaboote",
- "rev": "88b8a563ff5704f4e8d8e5118fb911fa2110ca05",
+ "rev": "a65905a09e2c43ff63be8c0e86a93712361f871e",
"type": "github"
},
"original": {
"owner": "nix-community",
- "ref": "v0.4.3",
+ "ref": "v0.4.2",
"repo": "lanzaboote",
"type": "github"
}
@@ -386,11 +386,11 @@
]
},
"locked": {
- "lastModified": 1765464257,
- "narHash": "sha256-dixPWKiHzh80PtD0aLuxYNQ0xP+843dfXG/yM3OzaYQ=",
+ "lastModified": 1763319842,
+ "narHash": "sha256-YG19IyrTdnVn0l3DvcUYm85u3PaqBt6tI6VvolcuHnA=",
"owner": "cachix",
"repo": "git-hooks.nix",
- "rev": "09e45f2598e1a8499c3594fe11ec2943f34fe509",
+ "rev": "7275fa67fbbb75891c16d9dee7d88e58aea2d761",
"type": "github"
},
"original": {
@@ -402,11 +402,11 @@
},
"nixos-hardware": {
"locked": {
- "lastModified": 1764440730,
- "narHash": "sha256-ZlJTNLUKQRANlLDomuRWLBCH5792x+6XUJ4YdFRjtO4=",
+ "lastModified": 1762847253,
+ "narHash": "sha256-BWWnUUT01lPwCWUvS0p6Px5UOBFeXJ8jR+ZdLX8IbrU=",
"owner": "nixos",
"repo": "nixos-hardware",
- "rev": "9154f4569b6cdfd3c595851a6ba51bfaa472d9f3",
+ "rev": "899dc449bc6428b9ee6b3b8f771ca2b0ef945ab9",
"type": "github"
},
"original": {
@@ -418,27 +418,43 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1765311797,
- "narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
+ "lastModified": 1763334038,
+ "narHash": "sha256-LBVOyaH6NFzQ3X/c6vfMZ9k4SV2ofhpxeL9YnhHNJQQ=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
+ "rev": "4c8cdd5b1a630e8f72c9dd9bf582b1afb3127d2c",
"type": "github"
},
"original": {
"owner": "nixos",
- "ref": "nixos-25.11",
+ "ref": "nixos-25.05",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs-stable": {
+ "locked": {
+ "lastModified": 1730741070,
+ "narHash": "sha256-edm8WG19kWozJ/GqyYx2VjW99EdhjKwbY3ZwdlPAAlo=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "d063c1dd113c91ab27959ba540c0d9753409edf3",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgsMaster": {
"locked": {
- "lastModified": 1765536405,
- "narHash": "sha256-HTHfcqG8WsrJG0aW3edXF5nQJK3VjPWcUTEi/r0LV7o=",
+ "lastModified": 1763473525,
+ "narHash": "sha256-NzmsN8hRIn/9rJvZH3vPirBrOJJfeSfvPr4+feeK7LY=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "27225de9f2030213246e0d8d62957c43d5229368",
+ "rev": "15901670689a6f338ebd2a9436b947ec189463a3",
"type": "github"
},
"original": {
@@ -466,11 +482,11 @@
},
"nixpkgs_2": {
"locked": {
- "lastModified": 1765186076,
- "narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
+ "lastModified": 1763283776,
+ "narHash": "sha256-Y7TDFPK4GlqrKrivOcsHG8xSGqQx3A6c+i7novT85Uk=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
+ "rev": "50a96edd8d0db6cc8db57dab6bb6d6ee1f3dc49a",
"type": "github"
},
"original": {
@@ -502,11 +518,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
- "lastModified": 1765540078,
- "narHash": "sha256-hqGAGgmlYxwQufnYSS8E8wH7xyqLoaSIWGqZgdROkZg=",
+ "lastModified": 1763471545,
+ "narHash": "sha256-B1ua1UtkPuMwT8o4nOR7yNP5yz10usMcNnxwHpGtLck=",
"owner": "nix-community",
"repo": "NUR",
- "rev": "b4d99f4da68e9ffd29862904825730ba31a79406",
+ "rev": "4c584dcedf9aa3394e9730e62693515a0e47674b",
"type": "github"
},
"original": {
@@ -551,14 +567,15 @@
"nixpkgs": [
"lanzaboote",
"nixpkgs"
- ]
+ ],
+ "nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
- "lastModified": 1750779888,
- "narHash": "sha256-wibppH3g/E2lxU43ZQHC5yA/7kIKLGxVEnsnVK1BtRg=",
+ "lastModified": 1731363552,
+ "narHash": "sha256-vFta1uHnD29VUY4HJOO/D6p6rxyObnf+InnSMT4jlMU=",
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
- "rev": "16ec914f6fb6f599ce988427d9d94efddf25fe6d",
+ "rev": "cd1af27aa85026ac759d5d3fccf650abe7e1bbf0",
"type": "github"
},
"original": {
@@ -597,11 +614,11 @@
]
},
"locked": {
- "lastModified": 1761791894,
- "narHash": "sha256-myRIDh+PxaREz+z9LzbqBJF+SnTFJwkthKDX9zMyddY=",
+ "lastModified": 1731897198,
+ "narHash": "sha256-Ou7vLETSKwmE/HRQz4cImXXJBr/k9gp4J4z/PF8LzTE=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "59c45eb69d9222a4362673141e00ff77842cd219",
+ "rev": "0be641045af6d8666c11c2c40e45ffc9667839b5",
"type": "github"
},
"original": {
@@ -617,11 +634,11 @@
]
},
"locked": {
- "lastModified": 1765231718,
- "narHash": "sha256-qdBzo6puTgG4G2RHG0PkADg22ZnQo1JmSVFRxrD4QM4=",
+ "lastModified": 1763417348,
+ "narHash": "sha256-n5xDOeNN+smocQp3EMIc11IzBlR9wvvTIJZeL0g33Fs=",
"owner": "Mic92",
"repo": "sops-nix",
- "rev": "7fd1416aba1865eddcdec5bb11339b7222c2363e",
+ "rev": "3f66a7fb9626a9a9c077612ef10a0ce396286c7d",
"type": "github"
},
"original": {
@@ -712,11 +729,11 @@
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
- "lastModified": 1764772762,
- "narHash": "sha256-/0i4g+kiq9jdczpxX9Wjj5PSBYxDM6nqqmLKcwvY2sg=",
+ "lastModified": 1762968599,
+ "narHash": "sha256-j+AZQYOuZ0X33p76LsZu4/NZl1Ccu6kkwPKC5HpIn1Y=",
"owner": "vedderb",
"repo": "vesc_tool",
- "rev": "7087fcabd5b0e193cb4a367477d352f0d51fdca0",
+ "rev": "6a75051ce9742d97f14addd5d175ac516effb3c6",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 56a931e..d56e73d 100644
--- a/flake.nix
+++ b/flake.nix
@@ -19,7 +19,7 @@
};
home-manager = {
- url = "github:nix-community/home-manager/release-25.11";
+ url = "github:nix-community/home-manager/release-25.05";
inputs.nixpkgs.follows = "nixpkgs";
};
@@ -32,7 +32,7 @@
};
lanzaboote = {
- url = "github:nix-community/lanzaboote/v0.4.3";
+ url = "github:nix-community/lanzaboote/v0.4.2";
inputs.nixpkgs.follows = "nixpkgs";
};
@@ -43,7 +43,7 @@
nixos-hardware.url = "github:nixos/nixos-hardware/master";
- nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
+ nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
nixpkgsMaster.url = "github:NixOS/nixpkgs/master";
@@ -225,7 +225,7 @@
sops.secrets =
let
secretFile = config.sops.defaultSopsFile;
- getSecrets = file: builtins.fromJSON (builtins.readFile (pkgs.runCommand "secretKeys" { } ''${pkgs.yq-go}/bin/yq -o json '[del .sops | .. | select(tag != "!!seq" and tag != "!!map") | path | join("/")]' ${file} > $out''));
+ getSecrets = file: builtins.fromJSON (builtins.readFile (pkgs.runCommandNoCC "secretKeys" { } ''${pkgs.yq-go}/bin/yq -o json '[del .sops | .. | select(tag != "!!seq" and tag != "!!map") | path | join("/")]' ${file} > $out''));
secretNames = getSecrets secretFile;
secrets =
if builtins.pathExists secretFile then
diff --git a/hosts/aluminium/configuration.nix b/hosts/aluminium/configuration.nix
index e2105f6..3dfd6a3 100644
--- a/hosts/aluminium/configuration.nix
+++ b/hosts/aluminium/configuration.nix
@@ -130,6 +130,13 @@
priority = 1;
};
- system.stateVersion = "25.11";
+ # This value determines the NixOS release from which the default
+ # settings for stateful data, like file locations and database versions
+ # on your system were taken. It‘s perfectly fine and recommended to leave
+ # this value at the release version of the first install of this system.
+ # Before changing this value read the documentation for this option
+ # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
+ system.stateVersion = "23.11"; # Did you read the comment?
+
}
diff --git a/hosts/cadmium/configuration.nix b/hosts/cadmium/configuration.nix
index 9d91380..32c1151 100644
--- a/hosts/cadmium/configuration.nix
+++ b/hosts/cadmium/configuration.nix
@@ -57,5 +57,12 @@
autologin.username = "jalr";
};
- system.stateVersion = "25.11";
+ # This value determines the NixOS release from which the default
+ # settings for stateful data, like file locations and database versions
+ # on your system were taken. It‘s perfectly fine and recommended to leave
+ # this value at the release version of the first install of this system.
+ # Before changing this value read the documentation for this option
+ # (e.g. man configuration.nix or on https://nixos.org/nixos/options.html).
+ system.stateVersion = "23.11"; # Did you read the comment?
+
}
diff --git a/hosts/copper/configuration.nix b/hosts/copper/configuration.nix
index bb38116..841d390 100644
--- a/hosts/copper/configuration.nix
+++ b/hosts/copper/configuration.nix
@@ -72,6 +72,6 @@
};
};
- system.stateVersion = "25.11";
+ system.stateVersion = "24.05";
}
diff --git a/hosts/iron/configuration.nix b/hosts/iron/configuration.nix
index 7b191f6..57a422a 100644
--- a/hosts/iron/configuration.nix
+++ b/hosts/iron/configuration.nix
@@ -37,7 +37,7 @@ with lib; {
./ports.nix
];
config = {
- system.stateVersion = "25.11";
+ system.stateVersion = "25.05";
security.sudo.wheelNeedsPassword = false;
@@ -114,7 +114,7 @@ with lib; {
interface ${interfaces.wan}
ipv6rs
ia_na 1
- ia_pd 2 ${interfaces.lan}/0
+ ia_pd 1/::/64 ${interfaces.lan}/0/64
'';
jalr.luksUsbUnlock = {
@@ -131,7 +131,6 @@ with lib; {
boot = {
kernel.sysctl = {
"net.ipv6.conf.all.forwarding" = 1;
- "net.ipv6.conf.enp0s25.accept_ra" = 1;
};
initrd = {
availableKernelModules = [
diff --git a/hosts/iron/services/mail.nix b/hosts/iron/services/mail.nix
index 9040449..c5d855e 100644
--- a/hosts/iron/services/mail.nix
+++ b/hosts/iron/services/mail.nix
@@ -1,9 +1,15 @@
+{ config, ... }:
+
+let
+ inherit (config.networking) ports;
+in
{
#sops.secrets."domain_key_jalr.de".owner = "rspamd";
jalr = {
mailserver = {
enable = true;
fqdn = "hha.jalr.de";
+ relayPort = ports.postfix-relay.tcp;
domains = [
{
domain = "jalr.de";
@@ -19,18 +25,14 @@
};
};
services.postfix = {
- settings = {
- main = {
- smtp_bind_address = "159.69.103.126";
- smtp_bind_address_enforce = true;
- };
- master = {
- smtp.args = [
- "-o"
- "inet_protocols=ipv4"
- ];
- };
+ config = {
+ smtp_bind_address = "159.69.103.126";
+ smtp_bind_address_enforce = true;
};
+ masterConfig.smtp.args = [
+ "-o"
+ "inet_protocols=ipv4"
+ ];
};
services.nginx.virtualHosts."hha.jalr.de" = {
enableACME = true;
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;
diff --git a/hosts/iron/services/snapcast/mopidy.nix b/hosts/iron/services/snapcast/mopidy.nix
index fead046..18be1a5 100644
--- a/hosts/iron/services/snapcast/mopidy.nix
+++ b/hosts/iron/services/snapcast/mopidy.nix
@@ -1,7 +1,35 @@
{ lib, pkgs, config, ... }:
let
interfaces = import ../../interfaces.nix;
- cfg = config.services.mopidy;
+ mopidyConfig = {
+ audio.output = "audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/run/snapserver/mopidy.fifo";
+ file.enabled = false;
+ local = {
+ library = "sqlite";
+ scan_flush_threshold = 100;
+ media_dir = "/var/lib/music";
+ included_file_extensions = lib.strings.concatStringsSep "," [
+ ".aac"
+ ".flac"
+ ".m4a"
+ ".mp3"
+ ".opus"
+ ];
+ };
+ m3u = {
+ playlists_dir = "$XDG_CONFIG_DIR/mopidy/playlists";
+ };
+ http = {
+ enabled = true;
+ hostname = "::";
+ port = 6680;
+ };
+ mpd = {
+ enabled = true;
+ hostname = "::";
+ port = 6600;
+ };
+ };
in
{
services.mopidy = {
@@ -15,40 +43,12 @@ in
mopidy-somafm
mopidy-ytmusic
];
- settings = {
- audio.output = "audioresample ! audioconvert ! audio/x-raw,rate=48000,channels=2,format=S16LE ! wavenc ! filesink location=/run/snapserver/mopidy.fifo";
- file.enabled = false;
- local = {
- library = "sqlite";
- scan_flush_threshold = 100;
- media_dir = "/var/lib/music";
- included_file_extensions = lib.strings.concatStringsSep "," [
- ".aac"
- ".flac"
- ".m4a"
- ".mp3"
- ".opus"
- ];
- };
- m3u = {
- playlists_dir = "$XDG_CONFIG_DIR/mopidy/playlists";
- };
- http = {
- enabled = true;
- hostname = "::";
- port = 6680;
- };
- mpd = {
- enabled = true;
- hostname = "::";
- port = 6600;
- };
- };
+ configuration = lib.generators.toINI { } mopidyConfig;
};
networking.firewall.interfaces."${interfaces.lan}".allowedTCPPorts = [
- cfg.settings.http.port
- cfg.settings.mpd.port
+ mopidyConfig.http.port
+ mopidyConfig.mpd.port
];
environment.systemPackages = [
diff --git a/hosts/iron/services/snapcast/snapserver.nix b/hosts/iron/services/snapcast/snapserver.nix
index 385c600..0d44d04 100644
--- a/hosts/iron/services/snapcast/snapserver.nix
+++ b/hosts/iron/services/snapcast/snapserver.nix
@@ -1,30 +1,46 @@
-{ pkgs, config, ... }:
+{ lib, pkgs, config, ... }:
let
inherit (config.networking) ports;
interfaces = import ../../interfaces.nix;
- cfg = config.services.snapserver;
- airplayPort1 = 5000;
in
{
services.snapserver = {
enable = true;
- settings = {
- tcp-streaming.port = ports.snapserver.tcp;
- tcp = {
- enabled = true;
- port = ports.snapserverTcp.tcp;
+ port = ports.snapserver.tcp;
+ tcp.port = ports.snapserverTcp.tcp;
+ http.port = ports.snapserverHttp.tcp;
+ streams = {
+ default = {
+ type = "meta";
+ location = "meta:///hass/bluetooth/airplay/mopidy";
+ query = {
+ chunk_ms = "30";
+ buffer = "690";
+ codec = "flac";
+ };
};
- http = {
- enabled = true;
- port = ports.snapserverHttp.tcp;
+ mopidy = {
+ type = "pipe";
+ location = "/run/snapserver/mopidy.fifo";
+ };
+ hass = {
+ type = "pipe";
+ location = "/run/snapserver/hass.fifo";
+ };
+ bluetooth = {
+ location = "";
+ type = "alsa";
+ query = {
+ device = "hw:bluetooth,1";
+ };
+ };
+ airplay = {
+ type = "airplay";
+ location = lib.getExe' pkgs.shairport-sync "shairport-sync";
+ query = {
+ devicename = "Snapcast";
+ };
};
- stream.source = [
- "airplay://${pkgs.shairport-sync}/bin/shairport-sync?name=airplay&devicename=Snapcast&port=${toString airplayPort1}"
- #"alsa://?name=bluetooth&device=hw:bluetooth,1"
- "pipe:///run/snapserver/hass.fifo?name=hass"
- "pipe:///run/snapserver/mopidy.fifo?name=mopidy"
- "meta:///hass/airplay/mopidy?name=default&buffer=690&chunk_ms=30&codec=flac"
- ];
};
};
@@ -39,10 +55,10 @@ in
networking.firewall.interfaces."${interfaces.lan}" = {
allowedTCPPorts = [
- cfg.settings.http.port
- cfg.settings.tcp.port
- cfg.settings.tcp-streaming.port
- airplayPort1
+ config.services.snapserver.http.port
+ config.services.snapserver.port
+ config.services.snapserver.tcp.port
+ 5000 # airplay
];
allowedUDPPortRanges = [
{ from = 6001; to = 6011; } # airplay
@@ -51,7 +67,7 @@ in
networking.firewall.interfaces.iot = {
allowedTCPPorts = [
- cfg.settings.stream.port
+ config.services.snapserver.port
];
};
}
diff --git a/hosts/jalr-t520/configuration.nix b/hosts/jalr-t520/configuration.nix
index c273842..e10957b 100644
--- a/hosts/jalr-t520/configuration.nix
+++ b/hosts/jalr-t520/configuration.nix
@@ -38,5 +38,5 @@
hardware.graphics.extraPackages = lib.singleton pkgs.vaapiIntel;
- system.stateVersion = "25.11";
+ system.stateVersion = "25.05";
}
diff --git a/hosts/magnesium/configuration.nix b/hosts/magnesium/configuration.nix
index 5b27a65..626f0b8 100644
--- a/hosts/magnesium/configuration.nix
+++ b/hosts/magnesium/configuration.nix
@@ -28,5 +28,5 @@
priority = 1;
};
- system.stateVersion = "25.11";
+ system.stateVersion = "24.11";
}
diff --git a/hosts/magnesium/services/default.nix b/hosts/magnesium/services/default.nix
index 6ee1681..c257730 100644
--- a/hosts/magnesium/services/default.nix
+++ b/hosts/magnesium/services/default.nix
@@ -4,7 +4,6 @@
./forgejo.nix
./gitlab-runner.nix
./hedgedoc.nix
- ./ip.nix
./it-tools.nix
./mealie.nix
./ntfy.nix
diff --git a/hosts/magnesium/services/ip.nix b/hosts/magnesium/services/ip.nix
deleted file mode 100644
index a18a531..0000000
--- a/hosts/magnesium/services/ip.nix
+++ /dev/null
@@ -1,64 +0,0 @@
-{ pkgs, lib, ... }:
-
-let
- baseDomain = "jalr.de";
- webDomain = "ip.${baseDomain}";
- ip4Domain = "ip4.${baseDomain}";
- ip6Domain = "ip6.${baseDomain}";
-in
-{
- services.nginx.virtualHosts = lib.attrsets.genAttrs [ ip4Domain ip6Domain ]
- (_: {
- enableACME = true;
- addSSL = true;
- locations."/" = {
- return = ''200 "$remote_addr\n"'';
- extraConfig = ''
- types { } default_type "text/plain; charset=utf-8";
- add_header Access-Control-Allow-Origin *;
- '';
- };
- }) // {
- "${webDomain}" = {
- enableACME = true;
- forceSSL = true;
- root = pkgs.writeTextDir "index.html" ''
-
-
-
- ${webDomain}
-
-
- ${webDomain}
-
- - IPv6: Loading...
- - IPv4: Loading...
-
-
-
-
- '';
- };
- };
-}
diff --git a/hosts/magnesium/services/webserver.nix b/hosts/magnesium/services/webserver.nix
index 480048f..a30a098 100644
--- a/hosts/magnesium/services/webserver.nix
+++ b/hosts/magnesium/services/webserver.nix
@@ -20,6 +20,10 @@ in
https "max-age=31536000";
}
add_header Strict-Transport-Security $hsts_header;
+
+ add_header Referrer-Policy strict-origin;
+ add_header X-Content-Type-Options nosniff;
+ add_header X-Frame-Options SAMEORIGIN;
'';
virtualHosts = {
"${domain}" = {
diff --git a/justfile b/justfile
index bc38276..d1d2a9a 100644
--- a/justfile
+++ b/justfile
@@ -2,11 +2,11 @@ usb_ram_disk := "/dev/disk/by-label/RAM_USB"
usb_ram_mountpoint := shell("findmnt -n -o TARGET $1 || true", usb_ram_disk)
boot:
- nixos-rebuild boot --flake . --sudo
+ nixos-rebuild boot --flake . --use-remote-sudo
which fwupdmgr >/dev/null 2>&1 && fwupdmgr update || true
switch:
- nixos-rebuild switch --flake . --sudo
+ nixos-rebuild switch --flake . --use-remote-sudo
which fwupdmgr >/dev/null 2>&1 && fwupdmgr update || true
build:
diff --git a/modules/autologin.nix b/modules/autologin.nix
index db0409f..b0b4576 100644
--- a/modules/autologin.nix
+++ b/modules/autologin.nix
@@ -27,7 +27,7 @@ in
serviceConfig = lib.mkForce {
ExecStart = [
"" # override upstream default with an empty ExecStart
- "@${pkgs.util-linux}/sbin/agetty agetty --login-program ${pkgs.shadow}/bin/login --autologin '${cfg.autologin.username}' --noclear %I $TERM"
+ "@${pkgs.utillinux}/sbin/agetty agetty --login-program ${pkgs.shadow}/bin/login --autologin '${cfg.autologin.username}' --noclear %I $TERM"
];
restartIfChanged = false;
};
diff --git a/modules/default.nix b/modules/default.nix
index d2f35d2..cd6a1cf 100644
--- a/modules/default.nix
+++ b/modules/default.nix
@@ -33,6 +33,7 @@
./mailserver
./matrix
./mobile-network.nix
+ ./mute-indicator.nix
./neo.nix
./networking
./nix.nix
@@ -49,7 +50,7 @@
./uefi.nix
./unfree.nix
./upgrade-diff.nix
- ./wireshark.nix
+ ./wireshark
./yubikey-gpg.nix
];
@@ -66,8 +67,6 @@
];
};
- system.rebuild.enableNg = true;
-
programs.nano.enable = false;
security.acme = {
diff --git a/modules/dns.nix b/modules/dns.nix
index 9acae6b..a15d066 100644
--- a/modules/dns.nix
+++ b/modules/dns.nix
@@ -6,7 +6,7 @@ let
in
{
config = lib.mkIf config.jalr.workstation.enable {
- services.dnscrypt-proxy = {
+ services.dnscrypt-proxy2 = {
enable = true;
settings = {
ipv6_servers = true;
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 = {
diff --git a/modules/libvirt.nix b/modules/libvirt.nix
index e398bfe..b93f8c8 100644
--- a/modules/libvirt.nix
+++ b/modules/libvirt.nix
@@ -17,6 +17,7 @@ in
virtualisation = {
libvirtd = {
enable = true;
+ qemu.ovmf.enable = true;
# start: starts all guests that were running prior to shutdown
# ignore: only start guests which are marked as autostart
diff --git a/modules/mailserver/default.nix b/modules/mailserver/default.nix
index a18dc19..59a8e2c 100644
--- a/modules/mailserver/default.nix
+++ b/modules/mailserver/default.nix
@@ -5,6 +5,11 @@ in
{
options.jalr.mailserver = with lib; with lib.types; {
enable = mkEnableOption "simple mail server";
+ relayPort = mkOption {
+ description = "SMTP port for relay mail relay.";
+ type = port;
+ default = 25;
+ };
fqdn = mkOption {
type = str;
description = ''
diff --git a/modules/mailserver/dovecot.nix b/modules/mailserver/dovecot.nix
index dbd6e71..a2485a5 100644
--- a/modules/mailserver/dovecot.nix
+++ b/modules/mailserver/dovecot.nix
@@ -8,26 +8,6 @@ let
"\n"
({ address, passwordHash, ... }: "${address}:${passwordHash}")
cfg.users);
-
- sieveScripts = {
- learn-spam = pkgs.writeText "learn-spam.sieve" ''
- require ["vnd.dovecot.pipe", "copy", "imapsieve"];
- pipe :copy "rspamc" ["learn_spam"];
- '';
- learn-ham = pkgs.writeText "learn-ham.sieve" ''
- require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
-
- if environment :matches "imap.mailbox" "*" {
- set "mailbox" "''${1}";
- }
-
- if string "''${mailbox}" "Trash" {
- stop;
- }
-
- pipe :copy "rspamc" ["learn_ham"];
- '';
- };
in
lib.mkIf cfg.enable {
services.dovecot2 = {
@@ -131,12 +111,12 @@ lib.mkIf cfg.enable {
${lib.optionalString cfg.spam.enable ''
imapsieve_mailbox1_name = Spam
imapsieve_mailbox1_causes = COPY
- imapsieve_mailbox1_before = file:${sieveScripts.learn-spam}
+ imapsieve_mailbox1_before = file:/var/lib/dovecot/sieve/learn-spam.sieve
imapsieve_mailbox2_name = *
imapsieve_mailbox2_from = Spam
imapsieve_mailbox2_causes = COPY
- imapsieve_mailbox2_before = file:${sieveScripts.learn-ham}
+ imapsieve_mailbox2_before = file:/var/lib/dovecot/sieve/learn-ham.sieve
sieve_pipe_bin_dir = ${pkgs.symlinkJoin { name = "sieve-pipe-bin-dir"; paths = with pkgs; [ rspamd ]; } }/bin
''}
}
@@ -145,12 +125,37 @@ lib.mkIf cfg.enable {
environment.systemPackages = [ pkgs.dovecot_pigeonhole ];
- /*
- systemd.services.dovecot2 = {
+ systemd.services.dovecot2 = {
wants = [ "acme-finished-${cfg.fqdn}.target" ];
after = [ "acme-finished-${cfg.fqdn}.target" ];
- };
- */
+
+ preStart = lib.mkIf cfg.spam.enable
+ (lib.mkAfter
+ (lib.concatStrings
+ (lib.mapAttrsToList
+ (name: content: ''
+ cp ${pkgs.writeText name content} /var/lib/dovecot/sieve/${name}
+ '')
+ {
+ "learn-spam.sieve" = ''
+ require ["vnd.dovecot.pipe", "copy", "imapsieve"];
+ pipe :copy "rspamc" ["learn_spam"];
+ '';
+ "learn-ham.sieve" = ''
+ require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"];
+
+ if environment :matches "imap.mailbox" "*" {
+ set "mailbox" "''${1}";
+ }
+
+ if string "''${mailbox}" "Trash" {
+ stop;
+ }
+
+ pipe :copy "rspamc" ["learn_ham"];
+ '';
+ })));
+ };
networking.firewall.allowedTCPPorts = [
143 # IMAP
diff --git a/modules/mailserver/postfix.nix b/modules/mailserver/postfix.nix
index 1209fb0..e813dcb 100644
--- a/modules/mailserver/postfix.nix
+++ b/modules/mailserver/postfix.nix
@@ -38,81 +38,72 @@ lib.mkIf cfg.enable {
services.postfix = {
enable = true;
+ inherit (cfg) relayPort;
+
enableSubmission = false; # plain/STARTTLS (latter is forced in submissionOptions)
enableSubmissions = true; # submission with implicit TLS (TCP/465)
- settings = {
- main = {
- smtpd_tls_chain_files = [
- "${cfg.certDir}/key.pem"
- "${cfg.certDir}/fullchain.pem"
- ];
- recipient_delimiter = "+";
- myhostname = cfg.fqdn;
- mynetworks_style = "host";
+ hostname = cfg.fqdn;
+ networksStyle = "host";
+ sslCert = "${cfg.certDir}/fullchain.pem";
+ sslKey = "${cfg.certDir}/key.pem";
- # General
- smtpd_banner = "${cfg.fqdn} ESMTP";
- disable_vrfy_command = true; # disable check if mailbox exists
- enable_long_queue_ids = true; # better for debugging
- strict_rfc821_envelopes = true; # only accept properly formatted envelope
- message_size_limit = cfg.messageSizeLimit;
-
- virtual_mailbox_domains = listToString (map (x: x.domain) cfg.domains);
- virtual_mailbox_maps = "hash:/var/lib/postfix/conf/valiases";
- virtual_alias_maps = "hash:/var/lib/postfix/conf/valiases";
- virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
-
- smtpd_recipient_restrictions = listToString [
- "reject_non_fqdn_recipient"
- "reject_unknown_recipient_domain"
- "reject_unverified_recipient"
- ];
-
- smtpd_client_restrictions = listToString [
- "reject_unknown_client_hostname"
- ];
-
- smtpd_sender_restrictions = listToString [
- "reject_non_fqdn_sender"
- "reject_unknown_sender_domain"
- ];
-
- # generated 2021-02-04, Mozilla Guideline v5.6, Postfix 3.5.6, OpenSSL 1.1.1i, intermediate configuration
- # https://ssl-config.mozilla.org/#server=postfix&version=3.5.6&config=intermediate&openssl=1.1.1i&guideline=5.6
- smtpd_tls_security_level = "may";
- smtpd_tls_auth_only = "yes";
- smtpd_tls_mandatory_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
- smtpd_tls_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
- smtpd_tls_mandatory_ciphers = "medium";
- smtpd_tls_loglevel = "1";
-
- tls_medium_cipherlist = listToString [
- "ECDHE-ECDSA-AES128-GCM-SHA256"
- "ECDHE-RSA-AES128-GCM-SHA256"
- "ECDHE-ECDSA-AES256-GCM-SHA384"
- "ECDHE-RSA-AES256-GCM-SHA384"
- "ECDHE-ECDSA-CHACHA20-POLY1305"
- "ECDHE-RSA-CHACHA20-POLY1305"
- "DHE-RSA-AES128-GCM-SHA256"
- "DHE-RSA-AES256-GCM-SHA384"
- ];
- tls_preempt_cipherlist = "no";
- };
- master = {
- submission-header-cleanup = {
- private = false;
- maxproc = 0;
- command = "cleanup";
- args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ];
- };
- };
- };
+ recipientDelimiter = "+";
mapFiles = {
inherit valiases;
};
+ config = {
+ # General
+ smtpd_banner = "${cfg.fqdn} ESMTP";
+ disable_vrfy_command = true; # disable check if mailbox exists
+ enable_long_queue_ids = true; # better for debugging
+ strict_rfc821_envelopes = true; # only accept properly formatted envelope
+ message_size_limit = toString cfg.messageSizeLimit;
+
+ virtual_mailbox_domains = listToString (map (x: x.domain) cfg.domains);
+ virtual_mailbox_maps = "hash:/var/lib/postfix/conf/valiases";
+ virtual_alias_maps = "hash:/var/lib/postfix/conf/valiases";
+ virtual_transport = "lmtp:unix:/run/dovecot2/dovecot-lmtp";
+
+ smtpd_recipient_restrictions = listToString [
+ "reject_non_fqdn_recipient"
+ "reject_unknown_recipient_domain"
+ "reject_unverified_recipient"
+ ];
+
+ smtpd_client_restrictions = listToString [
+ "reject_unknown_client_hostname"
+ ];
+
+ smtpd_sender_restrictions = listToString [
+ "reject_non_fqdn_sender"
+ "reject_unknown_sender_domain"
+ ];
+
+ # generated 2021-02-04, Mozilla Guideline v5.6, Postfix 3.5.6, OpenSSL 1.1.1i, intermediate configuration
+ # https://ssl-config.mozilla.org/#server=postfix&version=3.5.6&config=intermediate&openssl=1.1.1i&guideline=5.6
+ smtpd_tls_security_level = "may";
+ smtpd_tls_auth_only = "yes";
+ smtpd_tls_mandatory_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
+ smtpd_tls_protocols = "!SSLv2, !SSLv3, !TLSv1, !TLSv1.1";
+ smtpd_tls_mandatory_ciphers = "medium";
+ smtpd_tls_loglevel = "1";
+
+ tls_medium_cipherlist = listToString [
+ "ECDHE-ECDSA-AES128-GCM-SHA256"
+ "ECDHE-RSA-AES128-GCM-SHA256"
+ "ECDHE-ECDSA-AES256-GCM-SHA384"
+ "ECDHE-RSA-AES256-GCM-SHA384"
+ "ECDHE-ECDSA-CHACHA20-POLY1305"
+ "ECDHE-RSA-CHACHA20-POLY1305"
+ "DHE-RSA-AES128-GCM-SHA256"
+ "DHE-RSA-AES256-GCM-SHA384"
+ ];
+ tls_preempt_cipherlist = "no";
+ };
+
# plain/STARTTLS (forced with smtpd_tls_security_level)
submissionOptions = {
smtpd_tls_security_level = "encrypt";
@@ -138,6 +129,15 @@ lib.mkIf cfg.enable {
};
# implicit TLS
submissionsOptions = config.services.postfix.submissionOptions;
+
+ masterConfig = {
+ submission-header-cleanup = {
+ private = false;
+ maxproc = 0;
+ command = "cleanup";
+ args = [ "-o" "header_checks=pcre:${submissionHeaderCleanupRules}" ];
+ };
+ };
};
networking.firewall.allowedTCPPorts = [
diff --git a/modules/mute-indicator.nix b/modules/mute-indicator.nix
new file mode 100644
index 0000000..cd56106
--- /dev/null
+++ b/modules/mute-indicator.nix
@@ -0,0 +1,5 @@
+{
+ services.udev.extraRules = ''
+ SUBSYSTEM=="tty", ATTRS{idVendor}=="1eaf", ATTRS{idProduct}=="6d75", SYMLINK+="mute-indicator"
+ '';
+}
diff --git a/modules/pipewire.nix b/modules/pipewire.nix
index 51ebd94..ea97fb6 100644
--- a/modules/pipewire.nix
+++ b/modules/pipewire.nix
@@ -8,7 +8,7 @@ lib.mkIf config.jalr.gui.enable {
services.pipewire = {
enable = true;
- package = pkgs.pipewire;
+ package = pkgs.master.pipewire;
pulse = {
enable = true;
};
diff --git a/modules/sshd.nix b/modules/sshd.nix
index 0d02cb4..040ed09 100644
--- a/modules/sshd.nix
+++ b/modules/sshd.nix
@@ -13,8 +13,11 @@
];
# Use key exchange algorithms recommended by `nixpkgs#ssh-audit`
KexAlgorithms = [
+ "curve25519-sha256"
+ "curve25519-sha256@libssh.org"
+ "diffie-hellman-group16-sha512"
+ "diffie-hellman-group18-sha512"
"sntrup761x25519-sha512@openssh.com"
- "mlkem768x25519-sha256"
];
PasswordAuthentication = false;
StreamLocalBindUnlink = true; # unbind gnupg sockets if they exists
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..8c218e5
--- /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..61ac961
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/Devices.py
@@ -0,0 +1,150 @@
+# 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..86f356a
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/Exceptions.py
@@ -0,0 +1,66 @@
+# 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..7bf21b5
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/Filelock.py
@@ -0,0 +1,67 @@
+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..228a0f1
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/Logger.py
@@ -0,0 +1,214 @@
+# 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..b7cba37
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/Notifications.py
@@ -0,0 +1,92 @@
+# 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..bc4abd9
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/Packet.py
@@ -0,0 +1,651 @@
+# 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..8b0445a
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/Pcap.py
@@ -0,0 +1,82 @@
+#!/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..eac7609
--- /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..ecd16d2
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/UART.py
@@ -0,0 +1,238 @@
+# 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..5b94c32
--- /dev/null
+++ b/modules/wireshark/extcap/SnifferAPI/version.py
@@ -0,0 +1,37 @@
+# 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..1aa1380
--- /dev/null
+++ b/modules/wireshark/extcap/nrf_sniffer_ble.py
@@ -0,0 +1,991 @@
+#!/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(0.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()))
diff --git a/pkgs/default.nix b/pkgs/default.nix
index a1415f7..898812b 100644
--- a/pkgs/default.nix
+++ b/pkgs/default.nix
@@ -17,6 +17,7 @@ in
};
fpvout = callPackage ./fpvout { };
illuminanced = callPackage ./illuminanced { };
+ mute-indicator = callPackage ./mute-indicator { };
myintercom-doorbell = callPackage ./myintercom-doorbell {
inherit poetry2nix;
};
diff --git a/pkgs/docker-machine-driver-hetzner/default.nix b/pkgs/docker-machine-driver-hetzner/default.nix
index f3c9828..aa0efa1 100644
--- a/pkgs/docker-machine-driver-hetzner/default.nix
+++ b/pkgs/docker-machine-driver-hetzner/default.nix
@@ -2,12 +2,12 @@
buildGoApplication rec {
pname = "docker-machine-driver-hetzner";
- version = "5.0.2";
+ version = "5.0.1";
src = fetchFromGitHub {
rev = "${version}";
owner = "JonasProgrammer";
repo = "docker-machine-driver-hetzner";
- sha256 = "sha256-5mSlKedXSHNKnjfx+qVXplReSMZ5SKQBXt9Ct+ivgjk=";
+ sha256 = "sha256-JREn6AzayaHkyhdOTJ8P2H/s/5RaKLe+Qb8GV5dI2pA=";
};
modules = ./gomod2nix.toml;
#nativeBuildInputs = [ pkg-config ];
diff --git a/pkgs/docker-machine-driver-hetzner/gomod2nix.toml b/pkgs/docker-machine-driver-hetzner/gomod2nix.toml
index fc6567c..d38264c 100644
--- a/pkgs/docker-machine-driver-hetzner/gomod2nix.toml
+++ b/pkgs/docker-machine-driver-hetzner/gomod2nix.toml
@@ -11,8 +11,8 @@ schema = 3
version = "v2.2.0"
hash = "sha256-nPufwYQfTkyrEkbBrpqM3C2vnMxfIz6tAaBmiUP7vd4="
[mod."github.com/codegangsta/cli"]
- version = "v1.22.14"
- hash = "sha256-lpNDP0bM02JWeUjCOXU8HwHk3FT5zB4gIOO/EvaxRao="
+ version = "v1.22.12"
+ hash = "sha256-FTdBlhQvyDhgrDcSJDxgSLS/cBSP8B1BC/AxGA9Lyss="
replaced = "github.com/urfave/cli"
[mod."github.com/cpuguy83/go-md2man/v2"]
version = "v2.0.2"
@@ -27,44 +27,47 @@ schema = 3
version = "v1.5.3"
hash = "sha256-svogITcP4orUIsJFjMtp+Uv1+fKJv2Q5Zwf2dMqnpOQ="
[mod."github.com/hetznercloud/hcloud-go/v2"]
- version = "v2.5.1"
- hash = "sha256-SaYuQIdfI3S2+RM7bNE9wIPRnaKkv3giq8fOS671/KM="
+ version = "v2.2.0"
+ hash = "sha256-4sOfDyy/VP/LSoIm/ydtJKxKljtfLCC7ZzgWh9NPuAc="
[mod."github.com/matttproud/golang_protobuf_extensions"]
version = "v1.0.4"
hash = "sha256-uovu7OycdeZ2oYQ7FhVxLey5ZX3T0FzShaRldndyGvc="
[mod."github.com/moby/term"]
version = "v0.0.0-20221205130635-1aeaba878587"
hash = "sha256-wX2ftzjEHzltzN68CsYVXMiaLPNU7V2phVyyPKv3mn8="
+ [mod."github.com/pkg/errors"]
+ version = "v0.9.1"
+ hash = "sha256-mNfQtcrQmu3sNg/7IwiieKWOgFQOVVe2yXgKBpe/wZw="
[mod."github.com/prometheus/client_golang"]
- version = "v1.17.0"
- hash = "sha256-FIIzCuNqHdVzpbyH7yAp7Tcu+1tPxEMS5g6KfsGQBGE="
+ version = "v1.16.0"
+ hash = "sha256-P/b4/8m1ztF0fCLSJ+eRXN74Bncx2vjOJx7nFl2QEg4="
[mod."github.com/prometheus/client_model"]
- version = "v0.4.1-0.20230718164431-9a2bf3000d16"
- hash = "sha256-t9LgImRW4h0XMSxfAazrGHqyDljDyl0YC5r9cYuXcKc="
+ version = "v0.3.0"
+ hash = "sha256-vP+miJfsoK5UG9eug8z/bhAMj3bwg66T2vIh8WHoOKU="
[mod."github.com/prometheus/common"]
- version = "v0.44.0"
- hash = "sha256-8n3gSWKDSJtGfOQgxsiCGyTnUjb5hvSxJi/hPcrE5Oo="
+ version = "v0.42.0"
+ hash = "sha256-dJqoPZKtY2umWFWwMeRYY9I2JaFlpcMX4atkEcN5+hs="
[mod."github.com/prometheus/procfs"]
- version = "v0.11.1"
- hash = "sha256-yphZ7NZtYC/tb0HVag2T58SuN64Ial9sBo/TdCEQx6Q="
+ version = "v0.10.1"
+ hash = "sha256-EJ8q8wux4964WE4X7UkHb+MXjLhX4TROJaoLIQvD/eQ="
[mod."github.com/russross/blackfriday/v2"]
version = "v2.1.0"
hash = "sha256-R+84l1si8az5yDqd5CYcFrTyNZ1eSYlpXKq6nFt4OTQ="
[mod."golang.org/x/crypto"]
- version = "v0.16.0"
- hash = "sha256-DgSVOnXRK8GF01p5rLtq4qPBcglwEoOk8qhW2EGfJfA="
+ version = "v0.12.0"
+ hash = "sha256-Wes72EA9ICTG8o0nEYWZk9xjpqlniorFeY6o26GExns="
[mod."golang.org/x/net"]
- version = "v0.19.0"
- hash = "sha256-3M5rKEvJx4cO/q+06cGjR5sxF5JpnUWY0+fQttrWdT4="
+ version = "v0.12.0"
+ hash = "sha256-zQZBj42+wLLxXwS/e+KNbu8+SukMDxxW23WSi5XQXAA="
[mod."golang.org/x/sys"]
- version = "v0.15.0"
- hash = "sha256-n7TlABF6179RzGq3gctPDKDPRtDfnwPdjNCMm8ps2KY="
+ version = "v0.11.0"
+ hash = "sha256-g/LjhABK2c/u6v7M2aAIrHvZjmx/ikGHkef86775N38="
[mod."golang.org/x/term"]
- version = "v0.15.0"
- hash = "sha256-rsvtsE7sKmBwtR+mhJ8iUq93ZT8fV2LU+Pd69sh2es8="
+ version = "v0.11.0"
+ hash = "sha256-muSv/d8Qpl+NXiOB01n6LeFEzC+hrlGviDdfu+6QdQ4="
[mod."golang.org/x/text"]
- version = "v0.14.0"
- hash = "sha256-yh3B0tom1RfzQBf1RNmfdNWF1PtiqxV41jW1GVS6JAg="
+ version = "v0.12.0"
+ hash = "sha256-aNQaW3EgCK9ehpnBzIAkZX6TmiUU1S175YlJUH7P5Qg="
[mod."google.golang.org/protobuf"]
- version = "v1.31.0"
- hash = "sha256-UdIk+xRaMfdhVICvKRk1THe3R1VU+lWD8hqoW/y8jT0="
+ version = "v1.30.0"
+ hash = "sha256-Y07NKhSuJQ2w7F7MAINQyBf+/hdMHOrxwA3B4ljQQKs="
diff --git a/pkgs/docker-machine-gitlab/default.nix b/pkgs/docker-machine-gitlab/default.nix
index a6651ab..ca847bd 100644
--- a/pkgs/docker-machine-gitlab/default.nix
+++ b/pkgs/docker-machine-gitlab/default.nix
@@ -11,7 +11,7 @@
(
buildGoApplication rec {
pname = "docker-machine-gitlab";
- version = "0.16.2-gitlab.42";
+ version = "0.16.2-gitlab.32";
goPackagePath = "github.com/docker/machine";
modules = ./gomod2nix.toml;
@@ -20,7 +20,7 @@
group = "gitlab-org";
owner = "ci-cd";
repo = "docker-machine";
- sha256 = "sha256-cq36HK//sY7e+Vej7dbEXWymCzwMFXpztINJIqiiLyA=";
+ sha256 = "sha256-jipKo3LRTDUVKMkBK2qH/JIUcj3vJh7SdcQ8FMTr2Ok=";
};
nativeBuildInputs = [
diff --git a/pkgs/mute-indicator/.gitignore b/pkgs/mute-indicator/.gitignore
new file mode 100644
index 0000000..58e33b9
--- /dev/null
+++ b/pkgs/mute-indicator/.gitignore
@@ -0,0 +1,3 @@
+.pio
+.pioenvs
+.piolibdeps
diff --git a/pkgs/mute-indicator/README.md b/pkgs/mute-indicator/README.md
new file mode 100644
index 0000000..fd3273b
--- /dev/null
+++ b/pkgs/mute-indicator/README.md
@@ -0,0 +1,8 @@
+
+I recommend setting up a udev rule, so that the python script knows which serial port it should connect to:
+```bash
+echo 'SUBSYSTEM=="tty", ATTRS{idVendor}=="1eaf", ATTRS{idProduct}=="6d75", SYMLINK+="mute-indicator"' | sudo tee /etc/udev/rules.d/99-mute-indicator.rules
+```
+
+### Google Meet
+[Greasemonkey user script to automatically unmute](https://gist.github.com/jalr/ba132ed4a7133cf4fdbc98c97bf1a9e4)
diff --git a/pkgs/mute-indicator/default.nix b/pkgs/mute-indicator/default.nix
new file mode 100644
index 0000000..e80b205
--- /dev/null
+++ b/pkgs/mute-indicator/default.nix
@@ -0,0 +1,21 @@
+{ python310Packages }:
+
+python310Packages.buildPythonApplication rec {
+ pname = "mute-indicator";
+ version = "0.0.1";
+
+ src = ./.;
+
+ propagatedBuildInputs = with python310Packages; [
+ python
+ pulsectl
+ pyserial
+ ];
+
+ # installPhase = ''
+ # echo $src
+ # mkdir -p $out/bin
+ # cp pulseaudio-mute-indicator.py $out/bin/pulseaudio-mute-indicator
+ # chmod +x $out/bin/pulseaudio-mute-indicator
+ # '';
+}
diff --git a/pkgs/mute-indicator/platformio.ini b/pkgs/mute-indicator/platformio.ini
new file mode 100644
index 0000000..0585428
--- /dev/null
+++ b/pkgs/mute-indicator/platformio.ini
@@ -0,0 +1,24 @@
+[env:bluepill]
+framework = arduino
+platform = ststm32
+board = genericSTM32F103C8
+
+board_build.mcu = stm32f103c8t6
+board_build.f_cpu = 72000000L
+
+upload_protocol = dfu
+upload_port = anything
+
+lib_deps =
+ Adafruit NeoPixel
+
+build_flags =
+ -D USBCON
+ -D PIO_FRAMEWORK_ARDUINO_ENABLE_CDC
+ -D USBD_VID=0x1EAF
+ -D USBD_PID=0x6d75
+ -D USB_MANUFACTURER_STRING="\"github.com/jalr\""
+ -D USB_PRODUCT_STRING="\"mute-indicator\""
+
+# -D HAL_PCD_MODULE_ENABLED
+# -D USB_PRODUCT=bluepill
diff --git a/pkgs/mute-indicator/pulseaudio_mute_indicator/__init__.py b/pkgs/mute-indicator/pulseaudio_mute_indicator/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/pkgs/mute-indicator/pulseaudio_mute_indicator/__init__.py
@@ -0,0 +1 @@
+
diff --git a/pkgs/mute-indicator/pulseaudio_mute_indicator/service.py b/pkgs/mute-indicator/pulseaudio_mute_indicator/service.py
new file mode 100644
index 0000000..0ef8e51
--- /dev/null
+++ b/pkgs/mute-indicator/pulseaudio_mute_indicator/service.py
@@ -0,0 +1,28 @@
+import pulsectl
+import serial
+
+
+def run():
+ with pulsectl.Pulse("event-printer") as pulse:
+
+ def print_events(ev):
+ with pulsectl.Pulse("event-source-info") as pulse2:
+ source = pulse2.source_info(ev.index)
+ if source.name in [
+ "alsa_input.usb-BEHRINGER_UMC202HD_192k-00.analog-stereo",
+ "alsa_input.usb-BEHRINGER_UMC202HD_192k-00.analog-stereo-input",
+ ]:
+ muted = bool(source.mute)
+ with serial.Serial(port="/dev/mute-indicator", baudrate=115200) as ser:
+ if muted:
+ ser.write("L0:32,0,0\n".encode())
+ ser.write("L1:32,0,0\n".encode())
+ ser.write("S\n".encode())
+ else:
+ ser.write("L0:0,32,0\n".encode())
+ ser.write("L1:0,32,0\n".encode())
+ ser.write("S\n".encode())
+
+ pulse.event_mask_set("source")
+ pulse.event_callback_set(print_events)
+ pulse.event_listen(timeout=0)
diff --git a/pkgs/mute-indicator/setup.py b/pkgs/mute-indicator/setup.py
new file mode 100644
index 0000000..742c390
--- /dev/null
+++ b/pkgs/mute-indicator/setup.py
@@ -0,0 +1,20 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="pulseaudio_mute_indicator",
+ version="0.0.1",
+ url="https://github.com/jalr/mute-indicator.git",
+ author="jalr",
+ author_email="mail@jalr.de",
+ description="Microphone mute LED indicator",
+ packages=find_packages(),
+ install_requires=[
+ "pulsectl",
+ "pyserial",
+ ],
+ entry_points={
+ "console_scripts": [
+ "mute-indicator-service = pulseaudio_mute_indicator.service:run",
+ ],
+ },
+)
diff --git a/pkgs/mute-indicator/src/main.cpp b/pkgs/mute-indicator/src/main.cpp
new file mode 100644
index 0000000..5f9ec6b
--- /dev/null
+++ b/pkgs/mute-indicator/src/main.cpp
@@ -0,0 +1,60 @@
+#include
+#include
+
+#define NUM_LEDS 2
+#define DATA_PIN PB8
+
+Adafruit_NeoPixel pixels(NUM_LEDS, DATA_PIN, NEO_GRB + NEO_KHZ800);
+
+void setup() {
+ Serial.begin(115200);
+ delay(500);
+ pixels.begin();
+ Serial.println("Serial Neopixel interface ready.");
+ pixels.clear();
+}
+
+void loop() {
+ if (Serial.available() > 0) {
+ Serial.println("Received command");
+ char command = Serial.read();
+ switch(command) {
+ case 'L':
+ {
+ Serial.println("got L");
+ String ledStr = Serial.readStringUntil(':');
+ if (ledStr.length() > 0) {
+ long led = ledStr.toInt();
+ Serial.print("got led:");
+ Serial.println(led);
+ long red = Serial.readStringUntil(',').toInt();
+ Serial.print("got red:");
+ Serial.println(red);
+ long green = Serial.readStringUntil(',').toInt();
+ Serial.print("got green:");
+ Serial.println(green);
+ long blue = Serial.readStringUntil('\n').toInt();
+ Serial.print("got blue:");
+ Serial.println(blue);
+ pixels.setPixelColor(led, pixels.Color(red, green, blue));
+ Serial.println("pixel set.");
+ }
+ break;
+ }
+ case 'S':
+ {
+ Serial.readStringUntil('\n');
+ pixels.show();
+ Serial.println("pixel shown.");
+ break;
+ }
+ case 'C':
+ {
+ Serial.readStringUntil('\n');
+ pixels.clear();
+ Serial.println("pixels cleared.");
+ break;
+ }
+ }
+ }
+}
diff --git a/pkgs/myintercom-doorbell/default.nix b/pkgs/myintercom-doorbell/default.nix
index a333689..37b3711 100644
--- a/pkgs/myintercom-doorbell/default.nix
+++ b/pkgs/myintercom-doorbell/default.nix
@@ -1,10 +1,7 @@
-{ poetry2nix, python3 }:
+{ poetry2nix }:
poetry2nix.mkPoetryApplication {
pname = "myintercom-audiosocket";
version = "0.0.1";
projectDir = ./.;
- propagatedBuildInputs = [
- python3.pkgs.audioop-lts
- ];
}
diff --git a/pkgs/myintercom-doorbell/poetry.lock b/pkgs/myintercom-doorbell/poetry.lock
index 6c644ee..0ce0d9b 100644
--- a/pkgs/myintercom-doorbell/poetry.lock
+++ b/pkgs/myintercom-doorbell/poetry.lock
@@ -1,24 +1,24 @@
-# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "urllib3"
-version = "2.6.1"
+version = "2.5.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
- {file = "urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b"},
- {file = "urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f"},
+ {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"},
+ {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"},
]
[package.extras]
-brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""]
+brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
-zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""]
+zstd = ["zstandard (>=0.18.0)"]
[metadata]
lock-version = "2.1"
-python-versions = "^3.13"
-content-hash = "33f69fba51a1d96af3dfe0957433b3a4c2f5baa5064ada3a95a640d65230416a"
+python-versions = "^3.12"
+content-hash = "a2502d4bca34c8c9ddc7579666a62dc15d3573a0240075cb566922a1d031831e"
diff --git a/pkgs/myintercom-doorbell/pyproject.toml b/pkgs/myintercom-doorbell/pyproject.toml
index d4442a2..3a57dc6 100644
--- a/pkgs/myintercom-doorbell/pyproject.toml
+++ b/pkgs/myintercom-doorbell/pyproject.toml
@@ -7,7 +7,7 @@ readme = "README.md"
packages = [{include = "myintercom_doorbell"}]
[tool.poetry.dependencies]
-python = "^3.13"
+python = "^3.12"
urllib3 = "^2.5.0"
[tool.poetry.scripts]
diff --git a/pkgs/myintercom-doorbell/shell.nix b/pkgs/myintercom-doorbell/shell.nix
index 0ee7dda..cd52c53 100644
--- a/pkgs/myintercom-doorbell/shell.nix
+++ b/pkgs/myintercom-doorbell/shell.nix
@@ -4,7 +4,5 @@ mkShell {
buildInputs = [
poetry
];
- propagatedBuildInputs = [
- python3.pkgs.audioop-lts
- ];
+
}
diff --git a/pkgs/tabbed-box-maker/default.nix b/pkgs/tabbed-box-maker/default.nix
index e327776..dee949e 100644
--- a/pkgs/tabbed-box-maker/default.nix
+++ b/pkgs/tabbed-box-maker/default.nix
@@ -9,14 +9,14 @@ stdenvNoCC.mkDerivation {
src = fetchFromGitHub {
owner = "paulh-rnd";
repo = "TabbedBoxMaker";
- rev = "023dda4255b0a5b4e24196ed40fc956bd8394254";
+ rev = "7df0e6c711aeef36b6a89e3f3fe456b09225c836";
fetchSubmodules = true;
- sha256 = "sha256-wXZHg31HbOKHYxwgeoYUp2rMt4tdW4p9R3CEM69U1eU=";
+ sha256 = "8TNNVMSwbvcEwkvMHecHtGLEpiX3F0g0EGsgO1YKBGQ=";
};
dontBuild = true;
installPhase = ''
mkdir $out
- cp -r * $out
+ cp * $out
'';
}
diff --git a/pkgs/vim-typoscript/default.nix b/pkgs/vim-typoscript/default.nix
index dd90c89..ab85d79 100644
--- a/pkgs/vim-typoscript/default.nix
+++ b/pkgs/vim-typoscript/default.nix
@@ -1,11 +1,12 @@
-{ buildVimPlugin, fetchgit }:
+{ buildVimPlugin, fetchFromGitHub }:
buildVimPlugin rec {
pname = "vim-typoscript";
- version = "2.1.0";
- src = fetchgit {
- url = "https://git.daniel-siepmann.de/danielsiepmann/vim-syntax-typoscript";
+ version = "2.0.0";
+ src = fetchFromGitHub {
+ owner = "DanielSiepmann";
+ repo = "mirror-vim.typoscript";
rev = "v${version}";
- hash = "sha256-m2Gxycsrs9WbPydXCziWFsIYMJrDlfGF98SaamPBuuM=";
+ sha256 = "sha256-fCB+ikDmkfEP/W0pFYGrsZiH30vT0g3z6GZpRGk0Rhc=";
};
meta.homepage = "https://git.daniel-siepmann.de/danielsiepmann/vim-syntax-typoscript";
}
diff --git a/users/jalr/modules/ddev.nix b/users/jalr/modules/ddev.nix
index 2690c91..2e154d2 100644
--- a/users/jalr/modules/ddev.nix
+++ b/users/jalr/modules/ddev.nix
@@ -1,6 +1,6 @@
{ nixosConfig, lib, pkgs, ... }:
lib.mkIf nixosConfig.jalr.gui.enable {
home.packages = [
- pkgs.ddev
+ pkgs.master.ddev
];
}
diff --git a/users/jalr/modules/default.nix b/users/jalr/modules/default.nix
index 3b79c49..f97916b 100644
--- a/users/jalr/modules/default.nix
+++ b/users/jalr/modules/default.nix
@@ -24,6 +24,7 @@
./lsd
./mixxc
./mpv.nix
+ ./mute-indicator.nix
./mycli
./neovim
./nix-index.nix
diff --git a/users/jalr/modules/firefox/default.nix b/users/jalr/modules/firefox/default.nix
index 9f708c5..62b3eb5 100644
--- a/users/jalr/modules/firefox/default.nix
+++ b/users/jalr/modules/firefox/default.nix
@@ -277,9 +277,9 @@
darkreader
sponsorblock
(tree-style-tab.override {
- version = "4.1.11";
- url = "https://addons.mozilla.org/firefox/downloads/file/4502732/tree_style_tab-4.1.11.xpi";
- sha256 = "sha256-6TFKdnO8I5vls6b75Ig633ffabXIAbiVQa9QuPADfvU=";
+ version = "4.1.6";
+ url = "https://addons.mozilla.org/firefox/downloads/file/4488104/tree_style_tab-4.1.6.xpi";
+ sha256 = "sha256-X0HC6jzytjBsM+8HmbK48DUihtdN9oCsqLUJqp29csQ=";
})
ublock-origin
umatrix
diff --git a/users/jalr/modules/git.nix b/users/jalr/modules/git.nix
index 09d9c16..3cf4007 100644
--- a/users/jalr/modules/git.nix
+++ b/users/jalr/modules/git.nix
@@ -9,15 +9,17 @@ in
programs = {
git = {
enable = true;
+ userName = "Jakob Lechner";
+ userEmail = "mail@jalr.de";
signing = {
key = "3044E71E3DEFF49B586CF5809BF4FCCB90854DA9";
signByDefault = false;
};
- settings = {
- user = {
- name = "Jakob Lechner";
- email = "mail@jalr.de";
- };
+ diff-so-fancy = {
+ enable = true;
+ markEmptyLines = false;
+ };
+ extraConfig = {
init.defaultBranch = "main";
diff.sops.textconv = "${pkgs.sops}/bin/sops -d";
pull.ff = "only";
@@ -42,11 +44,6 @@ in
};
lfs.enable = true;
};
- diff-so-fancy = {
- enable = true;
- enableGitIntegration = true;
- settings.markEmptyLines = false;
- };
lazygit = {
enable = true;
settings = {
diff --git a/users/jalr/modules/graphics/gimp.nix b/users/jalr/modules/graphics/gimp.nix
index bb318b7..c64e108 100644
--- a/users/jalr/modules/graphics/gimp.nix
+++ b/users/jalr/modules/graphics/gimp.nix
@@ -1,13 +1,7 @@
{ nixosConfig, lib, pkgs, ... }:
lib.mkIf nixosConfig.jalr.gui.enable {
- home.packages = [
- (
- pkgs.gimp-with-plugins.override {
- plugins = with pkgs.gimpPlugins; [
- resynthesizer
- ];
- }
- )
+ home.packages = with pkgs; [
+ gimp
];
}
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=";
+ };
+ })
+ )
];
}
diff --git a/users/jalr/modules/mute-indicator.nix b/users/jalr/modules/mute-indicator.nix
new file mode 100644
index 0000000..ecde0cd
--- /dev/null
+++ b/users/jalr/modules/mute-indicator.nix
@@ -0,0 +1,18 @@
+{ nixosConfig, lib, pkgs, ... }:
+
+lib.mkIf nixosConfig.jalr.gui.enable {
+ home.packages = with pkgs; [
+ mute-indicator
+ ];
+
+ systemd.user.services.mute-indicator = {
+ Unit.Description = "Mute Indicator";
+ Service = {
+ Type = "simple";
+ ExecStart = "${pkgs.mute-indicator}/bin/mute-indicator-service";
+ RestartSec = 5;
+ Restart = "on-failure";
+ };
+ Install.WantedBy = [ "default.target" ];
+ };
+}
diff --git a/users/jalr/modules/neovim/default.nix b/users/jalr/modules/neovim/default.nix
index 34c091c..03b8b0b 100644
--- a/users/jalr/modules/neovim/default.nix
+++ b/users/jalr/modules/neovim/default.nix
@@ -1,4 +1,4 @@
-{ lib, nixosConfig, pkgs, ... }:
+{ lib, nixosConfig, config, pkgs, ... }:
let
fakePlugin = pkgs.runCommand "neovim-fake-plugin" { } "mkdir $out";
@@ -207,6 +207,7 @@ in
''
-- this configuration applies to workstations only
-- https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md
+ local lsp = require('lspconfig')
-- show linter messages
vim.diagnostic.config({ virtual_text = true })
@@ -214,10 +215,7 @@ in
builtins.concatStringsSep "\n" (
lib.mapAttrsToList
(
- lang: cfg: ''
- vim.lsp.config('${lang}', ${lib.generators.toLua { } cfg})
- vim.lsp.enable('${lang}', true)
- ''
+ lang: cfg: "lsp.${lang}.setup\n" + lib.generators.toLua { } cfg
)
{
# C and C++
diff --git a/users/jalr/modules/sway/waybar.nix b/users/jalr/modules/sway/waybar.nix
index 33607f5..c507ce9 100644
--- a/users/jalr/modules/sway/waybar.nix
+++ b/users/jalr/modules/sway/waybar.nix
@@ -430,7 +430,7 @@ in
# ensure sway is already started, otherwise workspaces will not work
ExecStartPre = "${config.wayland.windowManager.sway.package}/bin/swaymsg";
ExecStart = "${pkgs.waybar}/bin/waybar";
- ExecReload = "${pkgs.util-linux}/bin/kill -SIGUSR2 $MAINPID";
+ ExecReload = "${pkgs.utillinux}/bin/kill -SIGUSR2 $MAINPID";
Restart = "on-failure";
RestartSec = "1s";
};
diff --git a/users/jalr/modules/tor-browser.nix b/users/jalr/modules/tor-browser.nix
index 5e7521a..f0420ab 100644
--- a/users/jalr/modules/tor-browser.nix
+++ b/users/jalr/modules/tor-browser.nix
@@ -1,6 +1,6 @@
{ nixosConfig, lib, pkgs, ... }:
lib.mkIf nixosConfig.jalr.gui.enable {
home.packages = with pkgs; [
- tor-browser
+ tor-browser-bundle-bin
];
}