[GH-ISSUE #486] mDNS binds to all interfaces #306

Closed
opened 2026-02-27 19:29:55 +03:00 by kerem · 21 comments
Owner

Originally created by @frafall on GitHub (May 31, 2020).
Original GitHub issue: https://github.com/librespot-org/librespot/issues/486

Originally assigned to: @Johannesd3 on GitHub.

I run librespot on a Raspian which also runs a series of docker containers, which implies a shitload of networking interfaces, not all of them routable on the local network. BUT, the mdns responder in librespot includes all of these in the published record. This results in inconsistent connectivity/detection for Spotify clients.

# sudo tcpdump -n host 224.0.0.251 and port 5353
08:59:03.662690 IP 192.133.64.20.5353 > 224.0.0.251.5353: 0*- [0q] 11/0/0 PTR Stue HiFi._spotify-connect._tcp.local., SRV stue.local.:33099 0 0, TXT "VERSION=1.0" "CPath=/", A 192.133.64.20, A 192.133.64.75, A 172.18.0.1, A 172.17.0.1, A 172.19.0.1, A 169.254.205.221, A 169.254.249.175, A 169.254.112.169 (434)

Just to verify I built a macvlan setup with a separate network namespace including the macvlan interface only which resulted in the expected:

# sudo tcpdump -n host 224.0.0.251 and port 5353
09:09:38.525806 IP 192.133.64.100.5353 > 224.0.0.251.5353: 0*- [0q] 4/0/0 PTR Stue HiFi._spotify-connect._tcp.local., SRV stue.local.:39285 0 0, TXT "VERSION=1.0" "CPath=/", A 192.133.64.100 (252)

Now, I assume this is an oversight but we should have a config/commandline option to specify which network interface to bind to. As discovery is a core function in the librespot library we probably need to include it here and not push it to "future daemon".

For now I get around this with a network namespace limiting the visibility of interfaces for the daemon but this is not quite a well-known nor documented configuration nor should we expect every implementor such as spotifyd/raspotify/vollibrespot to known and implement.

Best regards
Frafall

Example of local interfaces when running on a host also running docker containers:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000
    link/ether b8:27:eb:4c:c5:e5 brd ff:ff:ff:ff:ff:ff
3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DORMANT group default qlen 1000
    link/ether b8:27:eb:19:90:b0 brd ff:ff:ff:ff:ff:ff
4: br-22f744702de6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:1c:cb:94:d6 brd ff:ff:ff:ff:ff:ff
5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default
    link/ether 02:42:db:f1:96:3e brd ff:ff:ff:ff:ff:ff
6: proxy_bridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default
    link/ether 02:42:f0:10:4f:43 brd ff:ff:ff:ff:ff:ff
8: vetha1ef36c@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master proxy_bridge state UP mode DEFAULT group default
    link/ether 4e:b2:e8:f0:d5:e6 brd ff:ff:ff:ff:ff:ff link-netnsid 0
10: veth6dd6abd@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master proxy_bridge state UP mode DEFAULT group default
    link/ether c2:a8:84:73:d8:b5 brd ff:ff:ff:ff:ff:ff link-netnsid 1
12: veth2e8b881@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master proxy_bridge state UP mode DEFAULT group default
    link/ether 9e:36:87:7d:ad:9c brd ff:ff:ff:ff:ff:ff link-netnsid 2

Macvlan config:

# Configure a macvlan interface in a separate namespace
ip link add mac1 link eth0 type macvlan mode bridge
ip netns add net1
ip link set mac1 netns net1
ip netns exec net1 ip link set lo up
ip netns exec net1 ip addr add 192.133.64.100/24 dev mac1
ip netns exec net1 ip link set mac1 up
ip netns exec net1 route add default gw 192.133.64.1

# macvlan 'hack' for host visibility, ip by dhcpcd
ip link add mac0 link eth0 type macvlan  mode bridge
ip link set mac0 up
ip route add 192.133.64.100/32 dev mac0

# Run librespot in network namespace
ip netns exec net1 /usr/bin/librespot --name "Stue HiFi" --backend alsa --bitrate 320 --disable-audio-cache --enable-volume-normalisation --linear-volume --initial-volume=100 --verbose
Originally created by @frafall on GitHub (May 31, 2020). Original GitHub issue: https://github.com/librespot-org/librespot/issues/486 Originally assigned to: @Johannesd3 on GitHub. I run librespot on a Raspian which also runs a series of docker containers, which implies a shitload of networking interfaces, not all of them routable on the local network. BUT, the mdns responder in librespot includes all of these in the published record. This results in inconsistent connectivity/detection for Spotify clients. ``` # sudo tcpdump -n host 224.0.0.251 and port 5353 08:59:03.662690 IP 192.133.64.20.5353 > 224.0.0.251.5353: 0*- [0q] 11/0/0 PTR Stue HiFi._spotify-connect._tcp.local., SRV stue.local.:33099 0 0, TXT "VERSION=1.0" "CPath=/", A 192.133.64.20, A 192.133.64.75, A 172.18.0.1, A 172.17.0.1, A 172.19.0.1, A 169.254.205.221, A 169.254.249.175, A 169.254.112.169 (434) ``` Just to verify I built a macvlan setup with a separate network namespace including the macvlan interface only which resulted in the expected: ``` # sudo tcpdump -n host 224.0.0.251 and port 5353 09:09:38.525806 IP 192.133.64.100.5353 > 224.0.0.251.5353: 0*- [0q] 4/0/0 PTR Stue HiFi._spotify-connect._tcp.local., SRV stue.local.:39285 0 0, TXT "VERSION=1.0" "CPath=/", A 192.133.64.100 (252) ``` Now, I assume this is an oversight but we should have a config/commandline option to specify which network interface to bind to. As discovery is a core function in the librespot library we probably need to include it here and not push it to "future daemon". For now I get around this with a network namespace limiting the visibility of interfaces for the daemon but this is not quite a well-known nor documented configuration nor should we expect every implementor such as spotifyd/raspotify/vollibrespot to known and implement. Best regards Frafall Example of local interfaces when running on a host also running docker containers: ``` 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DEFAULT group default qlen 1000 link/ether b8:27:eb:4c:c5:e5 brd ff:ff:ff:ff:ff:ff 3: wlan0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP mode DORMANT group default qlen 1000 link/ether b8:27:eb:19:90:b0 brd ff:ff:ff:ff:ff:ff 4: br-22f744702de6: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:1c:cb:94:d6 brd ff:ff:ff:ff:ff:ff 5: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN mode DEFAULT group default link/ether 02:42:db:f1:96:3e brd ff:ff:ff:ff:ff:ff 6: proxy_bridge: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default link/ether 02:42:f0:10:4f:43 brd ff:ff:ff:ff:ff:ff 8: vetha1ef36c@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master proxy_bridge state UP mode DEFAULT group default link/ether 4e:b2:e8:f0:d5:e6 brd ff:ff:ff:ff:ff:ff link-netnsid 0 10: veth6dd6abd@if9: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master proxy_bridge state UP mode DEFAULT group default link/ether c2:a8:84:73:d8:b5 brd ff:ff:ff:ff:ff:ff link-netnsid 1 12: veth2e8b881@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master proxy_bridge state UP mode DEFAULT group default link/ether 9e:36:87:7d:ad:9c brd ff:ff:ff:ff:ff:ff link-netnsid 2 ``` Macvlan config: ``` # Configure a macvlan interface in a separate namespace ip link add mac1 link eth0 type macvlan mode bridge ip netns add net1 ip link set mac1 netns net1 ip netns exec net1 ip link set lo up ip netns exec net1 ip addr add 192.133.64.100/24 dev mac1 ip netns exec net1 ip link set mac1 up ip netns exec net1 route add default gw 192.133.64.1 # macvlan 'hack' for host visibility, ip by dhcpcd ip link add mac0 link eth0 type macvlan mode bridge ip link set mac0 up ip route add 192.133.64.100/32 dev mac0 # Run librespot in network namespace ip netns exec net1 /usr/bin/librespot --name "Stue HiFi" --backend alsa --bitrate 320 --disable-audio-cache --enable-volume-normalisation --linear-volume --initial-volume=100 --verbose ```
kerem 2026-02-27 19:29:55 +03:00
Author
Owner

@ashthespy commented on GitHub (May 31, 2020):

Currently libmdns doesn't let us select an interface to listen on, and binds to all non loopback ones.
There is an issue upstream to enhance this, but needs work..

<!-- gh-comment-id:636449128 --> @ashthespy commented on GitHub (May 31, 2020): Currently `libmdns` doesn't let us select an interface to listen on, and [binds to all non loopback ones](https://github.com/librespot-org/libmdns/blob/2e30ce98bd2fc9f6f81394424a45d96b56548620/src/fsm.rs#L187-L214). There is an [issue upstream](https://github.com/librespot-org/libmdns/issues/9) to enhance this, but needs work..
Author
Owner

@frafall commented on GitHub (May 31, 2020):

Well, got Vollibrespot running in a network namespace with macvlan, publishes only what I want and get reliable discovery, only needed to make sure ipv6 and lo interface was properly set up.
But, I'd guess more ppl than me get unreliable discovery due to docker messing up network interfaces.

Nice work on the Vollibrespot btw, using it due to metadata and control functions :)

<!-- gh-comment-id:636472984 --> @frafall commented on GitHub (May 31, 2020): Well, got Vollibrespot running in a network namespace with macvlan, publishes only what I want and get reliable discovery, only needed to make sure ipv6 and lo interface was properly set up. But, I'd guess more ppl than me get unreliable discovery due to docker messing up network interfaces. Nice work on the Vollibrespot btw, using it due to metadata and control functions :)
Author
Owner

@ashthespy commented on GitHub (May 31, 2020):

Well, got Vollibrespot running in a network namespace with macvlan, publishes only what I want and get reliable discovery, only needed to make sure ipv6 and lo interface was properly set up.
But, I'd guess more ppl than me get unreliable discovery due to docker messing up network interfaces.

Interesting workaround, like you said probably helpful in other situations as well!

Nice work on the Vollibrespot btw, using it due to metadata and control functions :)

I should really clean it up, it started of as a quick PoC for Volumo, but nice to see that it's useful to others as well! :-)

<!-- gh-comment-id:636489390 --> @ashthespy commented on GitHub (May 31, 2020): > Well, got Vollibrespot running in a network namespace with macvlan, publishes only what I want and get reliable discovery, only needed to make sure ipv6 and lo interface was properly set up. But, I'd guess more ppl than me get unreliable discovery due to docker messing up network interfaces. Interesting workaround, like you said probably helpful in other situations as well! >Nice work on the Vollibrespot btw, using it due to metadata and control functions :) I should really clean it up, it started of as a quick PoC for Volumo, but nice to see that it's useful to others as well! :-)
Author
Owner

@roderickvd commented on GitHub (Aug 7, 2021):

@Johannesd3 I thought about assigning this to you, as you have new mDNS in the works.

<!-- gh-comment-id:894702956 --> @roderickvd commented on GitHub (Aug 7, 2021): @Johannesd3 I thought about assigning this to you, as you have new mDNS in the works.
Author
Owner

@schnabel commented on GitHub (Jan 3, 2022):

I have the same problem and reported upstream.

<!-- gh-comment-id:1004050347 --> @schnabel commented on GitHub (Jan 3, 2022): I have the same problem and reported [upstream](https://github.com/librespot-org/libmdns/issues/23).
Author
Owner

@willstott101 commented on GitHub (May 8, 2022):

https://github.com/librespot-org/libmdns/pull/35 released as 0.7

<!-- gh-comment-id:1120434519 --> @willstott101 commented on GitHub (May 8, 2022): https://github.com/librespot-org/libmdns/pull/35 released as 0.7
Author
Owner

@NePoCz commented on GitHub (Sep 16, 2022):

Hi, since librespot now uses libmdns 0.7 which added support for binding to interface(s), would it be possible to implement this option?

I would try a PR myself, but unfortunately have no coding experience so stepping asside here

<!-- gh-comment-id:1249391914 --> @NePoCz commented on GitHub (Sep 16, 2022): Hi, since librespot now uses libmdns 0.7 which added support for binding to interface(s), would it be possible to implement this option? I would try a PR myself, but unfortunately have no coding experience so stepping asside here
Author
Owner

@JasonLG1979 commented on GitHub (Sep 16, 2022):

Hi, since librespot now uses libmdns 0.7 which added support for binding to interface(s), would it be possible to implement this option?

I would try a PR myself, but unfortunately have no coding experience so stepping asside here

Sure, if @roderickvd thinks it's a good idea I think I can do that pretty easily.

<!-- gh-comment-id:1249607835 --> @JasonLG1979 commented on GitHub (Sep 16, 2022): > Hi, since librespot now uses libmdns 0.7 which added support for binding to interface(s), would it be possible to implement this option? > > I would try a PR myself, but unfortunately have no coding experience so stepping asside here Sure, if @roderickvd thinks it's a good idea I think I can do that pretty easily.
Author
Owner

@JasonLG1979 commented on GitHub (Sep 16, 2022):

It would probably be something like discovery-interfaces that would be a comma separated list or a single interface, that defaults to "any" or "all" (I'll have to look at libmdns to see what our options are?).

<!-- gh-comment-id:1249612646 --> @JasonLG1979 commented on GitHub (Sep 16, 2022): It would probably be something like `discovery-interfaces` that would be a comma separated list or a single interface, that defaults to "any" or "all" (I'll have to look at libmdns to see what our options are?).
Author
Owner

@roderickvd commented on GitHub (Sep 16, 2022):

Sure why not? The "why not" of course is not to have too many command line options, but in this case I think it's in fair demand and a new option is warranted.

<!-- gh-comment-id:1249711564 --> @roderickvd commented on GitHub (Sep 16, 2022): Sure why not? The "why not" of course is not to have too many command line options, but in this case I think it's in fair demand and a new option is warranted.
Author
Owner

@setime commented on GitHub (Oct 31, 2022):

I have a proposal for implementing an option to specify to which interfaces the mDNS responder shall bind. I am a new to Rust so my code will be for sure not optimal. I am unsure how to implement parsing IPv4 and IPv6 addresses. I used the libmdns exmaple from here as a reference.

Please give feedback :).

diff --git a/Cargo.lock b/Cargo.lock
index fe16c16..c97cdbb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1365,6 +1365,7 @@ dependencies = [
  "librespot-playback",
  "librespot-protocol",
  "log",
+ "regex",
  "rpassword",
  "sha1",
  "thiserror",
diff --git a/Cargo.toml b/Cargo.toml
index 7a423d7..23f41cc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -60,6 +60,7 @@ sha1 = "0.10"
 thiserror = "1.0"
 tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] }
 url = "2.2"
+regex = "1.5"
 
 [features]
 alsa-backend = ["librespot-playback/alsa-backend"]
diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs
index 3c01003..4732230 100644
--- a/discovery/src/lib.rs
+++ b/discovery/src/lib.rs
@@ -47,6 +47,7 @@ pub struct Discovery {
 pub struct Builder {
     server_config: server::Config,
     port: u16,
+    bind_ips: Vec<std::net::IpAddr>,
 }
 
 /// Errors that can occur while setting up a [`Discovery`] instance.
@@ -87,6 +88,7 @@ impl Builder {
                 client_id: client_id.into(),
             },
             port: 0,
+            bind_ips: vec![],
         }
     }
 
@@ -102,6 +104,12 @@ impl Builder {
         self
     }
 
+    /// Set the ip addresses on which the mdns service should bind
+    pub fn bind_ips(mut self, bind_ips: Vec<std::net::IpAddr>) -> Self {
+        self.bind_ips = bind_ips;
+        self
+    }
+
     /// Sets the port on which it should listen to incoming connections.
     /// The default value `0` means any port.
     pub fn port(mut self, port: u16) -> Self {
@@ -117,6 +125,7 @@ impl Builder {
         let mut port = self.port;
         let name = self.server_config.name.clone().into_owned();
         let server = DiscoveryServer::new(self.server_config, &mut port)??;
+        let bind_ips = self.bind_ips;
 
         #[cfg(feature = "with-dns-sd")]
         let svc = dns_sd::DNSService::register(
@@ -127,14 +136,23 @@ impl Builder {
             port,
             &["VERSION=1.0", "CPath=/"],
         )?;
-
+         
         #[cfg(not(feature = "with-dns-sd"))]
-        let svc = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?.register(
+        let svc = if !bind_ips.is_empty() {
+            libmdns::Responder::spawn_with_ip_list(&tokio::runtime::Handle::current(), bind_ips)?.register(
             "_spotify-connect._tcp".to_owned(),
             name,
             port,
             &["VERSION=1.0", "CPath=/"],
-        );
+            )
+        } else {
+            libmdns::Responder::spawn(&tokio::runtime::Handle::current())?.register(
+                "_spotify-connect._tcp".to_owned(),
+                name,
+                port,
+                &["VERSION=1.0", "CPath=/"],
+            )
+        };
 
         Ok(Discovery { server, _svc: svc })
     }
diff --git a/src/main.rs b/src/main.rs
index d5c5392..e28f05d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -14,6 +14,7 @@ use log::{error, info, trace, warn};
 use sha1::{Digest, Sha1};
 use thiserror::Error;
 use url::Url;
+use regex::Regex;
 
 use librespot::{
     connect::{config::ConnectConfig, spirc::Spirc},
@@ -185,6 +186,7 @@ struct Setup {
     zeroconf_port: u16,
     player_event_program: Option<String>,
     emit_sink_events: bool,
+    bind_ips:  Vec<std::net::IpAddr>,
 }
 
 fn get_setup() -> Setup {
@@ -240,6 +242,7 @@ fn get_setup() -> Setup {
     const VOLUME_CTRL: &str = "volume-ctrl";
     const VOLUME_RANGE: &str = "volume-range";
     const ZEROCONF_PORT: &str = "zeroconf-port";
+    const BIND_IP: &str = "bind-ip";
 
     // Mostly arbitrary.
     const AP_PORT_SHORT: &str = "a";
@@ -258,6 +261,7 @@ fn get_setup() -> Setup {
     const DISABLE_GAPLESS_SHORT: &str = "g";
     const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H";
     const HELP_SHORT: &str = "h";
+    const BIND_IP_SHORT: &str = "i";
     const CACHE_SIZE_LIMIT_SHORT: &str = "M";
     const MIXER_TYPE_SHORT: &str = "m";
     const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N";
@@ -570,6 +574,12 @@ fn get_setup() -> Setup {
         AUTOPLAY,
         "Explicitly set autoplay {on|off}. Defaults to following the client setting.",
         "OVERRIDE",
+    )
+    .optopt(
+        BIND_IP_SHORT,
+        BIND_IP, 
+        "Interface IP address wo which mDNS will bind. Defaults to any interface",
+        "IP"
     );
 
     #[cfg(feature = "passthrough-decoder")]
@@ -1168,6 +1178,47 @@ fn get_setup() -> Setup {
         None => SessionConfig::default().autoplay,
     };
 
+    let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) {
+        if  let Some(bind_ip) = opt_str(BIND_IP) {
+            let mut ip_addresses: Vec<std::net::IpAddr> =  vec![];
+            let re_ipv4 = Regex::new(r"(\d+\.\d+\.\d+\.\d+)").unwrap();
+            for ip_addresse in bind_ip.split(",") {
+                if re_ipv4.is_match(ip_addresse){
+                    ip_addresses.push(ip_addresse.parse::<std::net::Ipv4Addr>().unwrap().into());
+                } else {
+                    let ip_v6: Vec<&str> = ip_addresse.split(":").collect();
+                    if 8 == ip_v6.len(){
+                        let new_ip_v6: Vec<u16> = ip_v6.iter().map(|&ip_part| 
+                            if ip_part.is_empty() {
+                                0
+                            } else {
+                                u16::from_str_radix(ip_part, 16).unwrap()
+                            }).collect();
+                        ip_addresses.push( std::net::Ipv6Addr::new(
+                            new_ip_v6[0], 
+                            new_ip_v6[1],
+                            new_ip_v6[2], 
+                            new_ip_v6[3], 
+                            new_ip_v6[4],
+                            new_ip_v6[5],
+                            new_ip_v6[6],
+                            new_ip_v6[7]
+                        ).into());
+                    } else {
+                        println!("Unable to match string {} to an IPv4 or IPv6 pattern", ip_addresse);
+                    }
+                }
+            }
+            ip_addresses
+        } else {
+            println!("Unable to bind-ip option, default to all interfaces.");
+            vec![]
+        }
+    } else {
+        vec![]
+    };
+    
+
     let connect_config = {
         let connect_default_config = ConnectConfig::default();
 
@@ -1608,6 +1659,7 @@ fn get_setup() -> Setup {
         zeroconf_port,
         player_event_program,
         emit_sink_events,
+        bind_ips,
     }
 }
 
@@ -1640,6 +1692,7 @@ async fn main() {
             .name(setup.connect_config.name.clone())
             .device_type(setup.connect_config.device_type)
             .port(setup.zeroconf_port)
+            .bind_ips(setup.bind_ips)
             .launch()
         {
             Ok(d) => discovery = Some(d),

<!-- gh-comment-id:1296759485 --> @setime commented on GitHub (Oct 31, 2022): I have a proposal for implementing an option to specify to which interfaces the mDNS responder shall bind. I am a new to Rust so my code will be for sure not optimal. I am unsure how to implement parsing IPv4 and IPv6 addresses. I used the libmdns exmaple from [here](https://github.com/librespot-org/libmdns/blob/main/examples/register_with_ip_list.rs) as a reference. Please give feedback :). ```rust diff --git a/Cargo.lock b/Cargo.lock index fe16c16..c97cdbb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1365,6 +1365,7 @@ dependencies = [ "librespot-playback", "librespot-protocol", "log", + "regex", "rpassword", "sha1", "thiserror", diff --git a/Cargo.toml b/Cargo.toml index 7a423d7..23f41cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ sha1 = "0.10" thiserror = "1.0" tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] } url = "2.2" +regex = "1.5" [features] alsa-backend = ["librespot-playback/alsa-backend"] diff --git a/discovery/src/lib.rs b/discovery/src/lib.rs index 3c01003..4732230 100644 --- a/discovery/src/lib.rs +++ b/discovery/src/lib.rs @@ -47,6 +47,7 @@ pub struct Discovery { pub struct Builder { server_config: server::Config, port: u16, + bind_ips: Vec<std::net::IpAddr>, } /// Errors that can occur while setting up a [`Discovery`] instance. @@ -87,6 +88,7 @@ impl Builder { client_id: client_id.into(), }, port: 0, + bind_ips: vec![], } } @@ -102,6 +104,12 @@ impl Builder { self } + /// Set the ip addresses on which the mdns service should bind + pub fn bind_ips(mut self, bind_ips: Vec<std::net::IpAddr>) -> Self { + self.bind_ips = bind_ips; + self + } + /// Sets the port on which it should listen to incoming connections. /// The default value `0` means any port. pub fn port(mut self, port: u16) -> Self { @@ -117,6 +125,7 @@ impl Builder { let mut port = self.port; let name = self.server_config.name.clone().into_owned(); let server = DiscoveryServer::new(self.server_config, &mut port)??; + let bind_ips = self.bind_ips; #[cfg(feature = "with-dns-sd")] let svc = dns_sd::DNSService::register( @@ -127,14 +136,23 @@ impl Builder { port, &["VERSION=1.0", "CPath=/"], )?; - + #[cfg(not(feature = "with-dns-sd"))] - let svc = libmdns::Responder::spawn(&tokio::runtime::Handle::current())?.register( + let svc = if !bind_ips.is_empty() { + libmdns::Responder::spawn_with_ip_list(&tokio::runtime::Handle::current(), bind_ips)?.register( "_spotify-connect._tcp".to_owned(), name, port, &["VERSION=1.0", "CPath=/"], - ); + ) + } else { + libmdns::Responder::spawn(&tokio::runtime::Handle::current())?.register( + "_spotify-connect._tcp".to_owned(), + name, + port, + &["VERSION=1.0", "CPath=/"], + ) + }; Ok(Discovery { server, _svc: svc }) } diff --git a/src/main.rs b/src/main.rs index d5c5392..e28f05d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; use thiserror::Error; use url::Url; +use regex::Regex; use librespot::{ connect::{config::ConnectConfig, spirc::Spirc}, @@ -185,6 +186,7 @@ struct Setup { zeroconf_port: u16, player_event_program: Option<String>, emit_sink_events: bool, + bind_ips: Vec<std::net::IpAddr>, } fn get_setup() -> Setup { @@ -240,6 +242,7 @@ fn get_setup() -> Setup { const VOLUME_CTRL: &str = "volume-ctrl"; const VOLUME_RANGE: &str = "volume-range"; const ZEROCONF_PORT: &str = "zeroconf-port"; + const BIND_IP: &str = "bind-ip"; // Mostly arbitrary. const AP_PORT_SHORT: &str = "a"; @@ -258,6 +261,7 @@ fn get_setup() -> Setup { const DISABLE_GAPLESS_SHORT: &str = "g"; const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H"; const HELP_SHORT: &str = "h"; + const BIND_IP_SHORT: &str = "i"; const CACHE_SIZE_LIMIT_SHORT: &str = "M"; const MIXER_TYPE_SHORT: &str = "m"; const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N"; @@ -570,6 +574,12 @@ fn get_setup() -> Setup { AUTOPLAY, "Explicitly set autoplay {on|off}. Defaults to following the client setting.", "OVERRIDE", + ) + .optopt( + BIND_IP_SHORT, + BIND_IP, + "Interface IP address wo which mDNS will bind. Defaults to any interface", + "IP" ); #[cfg(feature = "passthrough-decoder")] @@ -1168,6 +1178,47 @@ fn get_setup() -> Setup { None => SessionConfig::default().autoplay, }; + let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) { + if let Some(bind_ip) = opt_str(BIND_IP) { + let mut ip_addresses: Vec<std::net::IpAddr> = vec![]; + let re_ipv4 = Regex::new(r"(\d+\.\d+\.\d+\.\d+)").unwrap(); + for ip_addresse in bind_ip.split(",") { + if re_ipv4.is_match(ip_addresse){ + ip_addresses.push(ip_addresse.parse::<std::net::Ipv4Addr>().unwrap().into()); + } else { + let ip_v6: Vec<&str> = ip_addresse.split(":").collect(); + if 8 == ip_v6.len(){ + let new_ip_v6: Vec<u16> = ip_v6.iter().map(|&ip_part| + if ip_part.is_empty() { + 0 + } else { + u16::from_str_radix(ip_part, 16).unwrap() + }).collect(); + ip_addresses.push( std::net::Ipv6Addr::new( + new_ip_v6[0], + new_ip_v6[1], + new_ip_v6[2], + new_ip_v6[3], + new_ip_v6[4], + new_ip_v6[5], + new_ip_v6[6], + new_ip_v6[7] + ).into()); + } else { + println!("Unable to match string {} to an IPv4 or IPv6 pattern", ip_addresse); + } + } + } + ip_addresses + } else { + println!("Unable to bind-ip option, default to all interfaces."); + vec![] + } + } else { + vec![] + }; + + let connect_config = { let connect_default_config = ConnectConfig::default(); @@ -1608,6 +1659,7 @@ fn get_setup() -> Setup { zeroconf_port, player_event_program, emit_sink_events, + bind_ips, } } @@ -1640,6 +1692,7 @@ async fn main() { .name(setup.connect_config.name.clone()) .device_type(setup.connect_config.device_type) .port(setup.zeroconf_port) + .bind_ips(setup.bind_ips) .launch() { Ok(d) => discovery = Some(d), ```
Author
Owner

@ashthespy commented on GitHub (Oct 31, 2022):

You shouldn't need regex to go from string to IpAddr, you can use the convenient FromStr trait, so could use do something like this :-)

<!-- gh-comment-id:1296825572 --> @ashthespy commented on GitHub (Oct 31, 2022): You shouldn't need `regex` to go from string to `IpAddr`, you can use the convenient [`FromStr`](https://doc.rust-lang.org/std/str/trait.FromStr.html) trait, so could use do something like [this](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=de6d9045a7ead7206cc30bbdaa268a63) :-)
Author
Owner

@setime commented on GitHub (Oct 31, 2022):

Thanks for the hint. Avoid a regex seems like a good idea. The main issue I had with the propose approach is that I am unable to recover if the string cannot be parsed, e.g. 192.168.0.a. This ends in a panic. I think it would be better to print a corresponding message and then bind to all IP addresses, so it doesn't fail.
I tried to resolve this but I was not able to. There is no pattern like try & catch in Rust, which I probably would have used in python or so. I tried to look for something to replace it but I was unable to get assert() or parse(), expect() and unwrap() working. Does anybody has a good idea here?
Anyhow removing the regex yields much cleaner code:

@@ -1168,6 +1177,21 @@ fn get_setup() -> Setup {
         None => SessionConfig::default().autoplay,
     };
 
+    let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) {
+        if  let Some(bind_ip) = opt_str(BIND_IP) {
+            let string_ips:  Vec<&str> = bind_ip.split(",").collect();
+            string_ips.iter()
+                .map(|s| s.parse::<std::net::IpAddr>().unwrap())
+                .collect()
+        } else {
+            println!("Unable to bind-ip option, default to all interfaces.");
+            vec![]
+        }
+    } else {
+        vec![]
+    };
+    
+
     let connect_config = {
         let connect_default_config = ConnectConfig::default();
<!-- gh-comment-id:1297070808 --> @setime commented on GitHub (Oct 31, 2022): Thanks for the hint. Avoid a regex seems like a good idea. The main issue I had with the propose approach is that I am unable to recover if the string cannot be parsed, e.g. `192.168.0.a`. This ends in a panic. I think it would be better to print a corresponding message and then bind to all IP addresses, so it doesn't fail. I tried to resolve this but I was not able to. There is no pattern like `try & catch` in Rust, which I probably would have used in python or so. I tried to look for something to replace it but I was unable to get `assert()` or `parse()`, `expect()` and `unwrap()` working. Does anybody has a good idea here? Anyhow removing the regex yields much cleaner code: ```patch @@ -1168,6 +1177,21 @@ fn get_setup() -> Setup { None => SessionConfig::default().autoplay, }; + let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) { + if let Some(bind_ip) = opt_str(BIND_IP) { + let string_ips: Vec<&str> = bind_ip.split(",").collect(); + string_ips.iter() + .map(|s| s.parse::<std::net::IpAddr>().unwrap()) + .collect() + } else { + println!("Unable to bind-ip option, default to all interfaces."); + vec![] + } + } else { + vec![] + }; + + let connect_config = { let connect_default_config = ConnectConfig::default(); ```
Author
Owner

@roderickvd commented on GitHub (Oct 31, 2022):

You should look into IpAddr::from_str. The Rust idiom for success or failure is returning and doing something with a Result. Welcome to Rust 👍

<!-- gh-comment-id:1297109098 --> @roderickvd commented on GitHub (Oct 31, 2022): You should look into [`IpAddr::from_str`](https://doc.rust-lang.org/std/net/enum.IpAddr.html#impl-FromStr-for-IpAddr). The Rust idiom for success or failure is returning and doing something with a [`Result`](https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html). Welcome to Rust 👍
Author
Owner

@ashthespy commented on GitHub (Oct 31, 2022):

And and filter_map and map_err are your friends when playing with lists :-)

<!-- gh-comment-id:1297141354 --> @ashthespy commented on GitHub (Oct 31, 2022): And and [`filter_map`](https://doc.rust-lang.org/std/iter/struct.FilterMap.html) and [`map_err`](https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err) are your [friends](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=60f2e4edf29b984d1b7404d6b26a6a79) when playing with lists :-)
Author
Owner

@setime commented on GitHub (Nov 1, 2022):

Alright thanks for the hints. I will read up and try again ;)

<!-- gh-comment-id:1298731308 --> @setime commented on GitHub (Nov 1, 2022): Alright thanks for the hints. I will read up and try again ;)
Author
Owner

@setime commented on GitHub (Nov 2, 2022):

I was able to write something up with filter_map and map_err as well as unwrap_or_else(). Looking at the rest of the code unwrap_or_else() seems the way to go and then exiting the program. Hence I would propose:

    let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) {
        if  let Some(bind_ip) = opt_str(BIND_IP) {
            let string_ips:  Vec<&str> = bind_ip.split(",").collect();
            string_ips.iter()
                .map(|s| s.trim().parse::<std::net::IpAddr>().unwrap_or_else(|_| {
                    invalid_error_msg(
                        BIND_IP,
                        BIND_IP_SHORT,
                        s,
                        "IPv4 and IPv6 addresses",
                        ""
                    );
                    exit(1);
                }))
                .collect()
        } else {
            println!("Unable to bind-ip option, default to all interfaces.");
            vec![]
        }
    } else {
        vec![]
    };

Continuing in case of an error:

    let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) {
        if  let Some(bind_ip) = opt_str(BIND_IP) {
            let string_ips:  Vec<&str> = bind_ip.split(",").collect();
            let mut errors = vec![];
            let ip_addresses = string_ips.into_iter()
                .map(|s| s.trim().parse::<std::net::IpAddr>())
                .filter_map(|r| 
                    r.map_err(|e| errors.push(e.to_string())).ok())
                .collect();
            if !errors.is_empty(){
                println!("issue parsing the address errors {:?}, mapped IP addresses {:?} successfull", errors, ip_addresses);
            }
            ip_addresses
        } else {
            println!("Unable to bind-ip option, default to all interfaces.");
            vec![]
        }
    } else {
        vec![]
    };
<!-- gh-comment-id:1299957897 --> @setime commented on GitHub (Nov 2, 2022): I was able to write something up with `filter_map` and `map_err` as well as `unwrap_or_else()`. Looking at the rest of the code `unwrap_or_else()` seems the way to go and then exiting the program. Hence I would propose: ```rust let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) { if let Some(bind_ip) = opt_str(BIND_IP) { let string_ips: Vec<&str> = bind_ip.split(",").collect(); string_ips.iter() .map(|s| s.trim().parse::<std::net::IpAddr>().unwrap_or_else(|_| { invalid_error_msg( BIND_IP, BIND_IP_SHORT, s, "IPv4 and IPv6 addresses", "" ); exit(1); })) .collect() } else { println!("Unable to bind-ip option, default to all interfaces."); vec![] } } else { vec![] }; ``` Continuing in case of an error: ```rust let bind_ips: Vec<std::net::IpAddr> = if opt_present(BIND_IP) { if let Some(bind_ip) = opt_str(BIND_IP) { let string_ips: Vec<&str> = bind_ip.split(",").collect(); let mut errors = vec![]; let ip_addresses = string_ips.into_iter() .map(|s| s.trim().parse::<std::net::IpAddr>()) .filter_map(|r| r.map_err(|e| errors.push(e.to_string())).ok()) .collect(); if !errors.is_empty(){ println!("issue parsing the address errors {:?}, mapped IP addresses {:?} successfull", errors, ip_addresses); } ip_addresses } else { println!("Unable to bind-ip option, default to all interfaces."); vec![] } } else { vec![] }; ```
Author
Owner

@ashthespy commented on GitHub (Nov 2, 2022):

Looks good, please feel free to open a PR! :-)
There are some minor things you could tweak, e.g Split is an iterator already, so you don't need to collect() -> into_iter() it.. You could bind_ip.split(",").map(....) but the rest looks good

I hope you're enjoying Rust :-)

<!-- gh-comment-id:1300622960 --> @ashthespy commented on GitHub (Nov 2, 2022): Looks good, please feel free to open a PR! :-) There are some minor things you could tweak, e.g [`Split`](https://doc.rust-lang.org/std/primitive.str.html#method.split) is an iterator already, so you don't need to `collect() -> into_iter()` it.. You could `bind_ip.split(",").map(....)` but the rest looks good I hope you're enjoying Rust :-)
Author
Owner

@setime commented on GitHub (Nov 5, 2022):

I have created a PR.

Thanks for the help and advice. So far I like Rust.

<!-- gh-comment-id:1304432732 --> @setime commented on GitHub (Nov 5, 2022): I have created a [PR](https://github.com/librespot-org/librespot/pull/1071). Thanks for the help and advice. So far I like Rust.
Author
Owner

@ashthespy commented on GitHub (Nov 25, 2022):

Implemented with #1071, @setime please update the wiki and we can close this! :-)

<!-- gh-comment-id:1327178152 --> @ashthespy commented on GitHub (Nov 25, 2022): Implemented with #1071, @setime please update the wiki and we can close this! :-)
Author
Owner

@setime commented on GitHub (Nov 25, 2022):

I updated the wiki :).

<!-- gh-comment-id:1327702744 --> @setime commented on GitHub (Nov 25, 2022): I updated the wiki :).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/librespot#306
No description provided.