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}

- - - - - ''; - }; - }; -} 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 ]; }