Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mDNS binds to all interfaces #486

Closed
frafall opened this issue May 31, 2020 · 21 comments
Closed

mDNS binds to all interfaces #486

frafall opened this issue May 31, 2020 · 21 comments

Comments

@frafall
Copy link

frafall commented May 31, 2020

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
@ashthespy
Copy link
Member

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..

@frafall
Copy link
Author

frafall commented 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 :)

@ashthespy
Copy link
Member

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! :-)

@roderickvd roderickvd changed the title mDns issue mDNS binds to all interfaces May 24, 2021
@roderickvd
Copy link
Member

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

@schnabel
Copy link

schnabel commented Jan 3, 2022

I have the same problem and reported upstream.

ayushnix added a commit to ayushnix/dotfiles that referenced this issue Feb 6, 2022
I won't use spotifyd or librespot anymore unless this gets resolved

Spotifyd/spotifyd#990
librespot-org/librespot#486
@willstott101
Copy link
Contributor

librespot-org/libmdns#35 released as 0.7

@NePoCz
Copy link

NePoCz commented 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

@JasonLG1979
Copy link
Contributor

JasonLG1979 commented 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.

@JasonLG1979
Copy link
Contributor

JasonLG1979 commented 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?).

@roderickvd
Copy link
Member

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.

@setime
Copy link
Contributor

setime commented 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),

@ashthespy
Copy link
Member

ashthespy commented 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 :-)

@setime
Copy link
Contributor

setime commented 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();

@roderickvd
Copy link
Member

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 👍

@ashthespy
Copy link
Member

ashthespy commented Oct 31, 2022

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

@setime
Copy link
Contributor

setime commented Nov 1, 2022

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

@setime
Copy link
Contributor

setime commented 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![]
    };

@ashthespy
Copy link
Member

ashthespy commented 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 :-)

@setime
Copy link
Contributor

setime commented Nov 5, 2022

I have created a PR.

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

@ashthespy
Copy link
Member

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

@setime
Copy link
Contributor

setime commented Nov 25, 2022

I updated the wiki :).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants