Repository: rclone/rclone
Pull request #6366 (2022-08-08 16:59:22)

dlna: specify SSDP interface names from command line
<!--
Thank you very much for contributing code or documentation to rclone! Please
fill out the following questions to make it easier for us to review your
changes.

You do not need to check all the boxes below all at once, feel free to take
your time and add more commits. If you're done and ready for review, please
check the last box.
-->

#### What is the purpose of this change?

<!--
Describe the changes here
-->
Enabling a user to specify the interfaces used for DLNA (SSDP). This is especially useful when serving DLNA on a machine with many virtual network interfaces (for example when also running Kubernetes) for which you don't wish to advertise DLNA.

#### Was the change discussed in an issue or in the forum before?

<!--
Link issues and relevant forum posts here.
-->
No.

#### Checklist

- [x] I have read the [contribution guidelines](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#submitting-a-new-feature-or-bug-fix).
- [x] I have added tests for all changes in this PR if appropriate.
- [x] I have added documentation for the changes if appropriate.
- [x] All commit messages are in [house style](https://github.com/rclone/rclone/blob/master/CONTRIBUTING.md#commit-messages).
- [x] I'm done, this Pull Request is ready for review :-)


simonbos: Ok, I don't have any more questions; thanks for all the usefull comments :smile: Merge?
ncw: Looks like the DLNA tests need updating
simonbos: Yes, I forgot to run the tests locally after changing the `newServer` output signature. With the latest change, `go test -v` (in the `cmd/serve/dlna` directory) runs succesfully.

Patch:
diff --git a/cmd/serve/dlna/dlna.go b/cmd/serve/dlna/dlna.go
index fc802226c..da6a08a19 100644
--- a/cmd/serve/dlna/dlna.go
+++ b/cmd/serve/dlna/dlna.go
@@ -51,7 +51,10 @@ files that they are not able to play back correctly.
 		f := cmd.NewFsSrc(args)
 
 		cmd.Run(false, false, command, func() error {
-			s := newServer(f, &dlnaflags.Opt)
+			s, err := newServer(f, &dlnaflags.Opt)
+			if err != nil {
+				return err
+			}
 			if err := s.Serve(); err != nil {
 				return err
 			}
@@ -92,17 +95,32 @@ type server struct {
 	vfs *vfs.VFS
 }
 
-func newServer(f fs.Fs, opt *dlnaflags.Options) *server {
+func newServer(f fs.Fs, opt *dlnaflags.Options) (*server, error) {
 	friendlyName := opt.FriendlyName
 	if friendlyName == "" {
 		friendlyName = makeDefaultFriendlyName()
 	}
+	interfaces := make([]net.Interface, 0, len(opt.InterfaceNames))
+	for _, interfaceName := range opt.InterfaceNames {
+		var err error
+		intf, err := net.InterfaceByName(interfaceName)
+		if err != nil {
+			return nil, fmt.Errorf("failed to resolve interface name '%s': %w", interfaceName, err)
+		}
+		if !isAppropriatelyConfigured(*intf) {
+			return nil, fmt.Errorf("interface '%s' is not appropriately configured (it should be UP, MULTICAST and MTU > 0)", interfaceName)
+		}
+		interfaces = append(interfaces, *intf)
+	}
+	if len(interfaces) == 0 {
+		interfaces = listInterfaces()
+	}
 
 	s := &server{
 		AnnounceInterval: 10 * time.Second,
 		FriendlyName:     friendlyName,
 		RootDeviceUUID:   makeDeviceUUID(friendlyName),
-		Interfaces:       listInterfaces(),
+		Interfaces:       interfaces,
 
 		httpListenAddr: opt.ListenAddr,
 
@@ -138,7 +156,7 @@ func newServer(f fs.Fs, opt *dlnaflags.Options) *server {
 			http.FileServer(data.Assets))))
 	s.handler = logging(withHeader("Server", serverField, r))
 
-	return s
+	return s, nil
 }
 
 // UPnPService is the interface for the SOAP service.
diff --git a/cmd/serve/dlna/dlna_util.go b/cmd/serve/dlna/dlna_util.go
index ad961e067..d54dfea5b 100644
--- a/cmd/serve/dlna/dlna_util.go
+++ b/cmd/serve/dlna/dlna_util.go
@@ -47,13 +47,17 @@ func listInterfaces() []net.Interface {
 
 	var active []net.Interface
 	for _, intf := range ifs {
-		if intf.Flags&net.FlagUp != 0 && intf.Flags&net.FlagMulticast != 0 && intf.MTU > 0 {
+		if isAppropriatelyConfigured(intf) {
 			active = append(active, intf)
 		}
 	}
 	return active
 }
 
+func isAppropriatelyConfigured(intf net.Interface) bool {
+	return intf.Flags&net.FlagUp != 0 && intf.Flags&net.FlagMulticast != 0 && intf.MTU > 0
+}
+
 func didlLite(chardata string) string {
 	return `<DIDL-Lite` +
 		` xmlns:dc="http://purl.org/dc/elements/1.1/"` +
diff --git a/cmd/serve/dlna/dlnaflags/dlnaflags.go b/cmd/serve/dlna/dlnaflags/dlnaflags.go
index c8c1e6f05..06ea27a19 100644
--- a/cmd/serve/dlna/dlnaflags/dlnaflags.go
+++ b/cmd/serve/dlna/dlnaflags/dlnaflags.go
@@ -23,16 +23,18 @@ logging of all UPNP traffic.
 
 // Options is the type for DLNA serving options.
 type Options struct {
-	ListenAddr   string
-	FriendlyName string
-	LogTrace     bool
+	ListenAddr     string
+	FriendlyName   string
+	LogTrace       bool
+	InterfaceNames []string
 }
 
 // DefaultOpt contains the defaults options for DLNA serving.
 var DefaultOpt = Options{
-	ListenAddr:   ":7879",
-	FriendlyName: "",
-	LogTrace:     false,
+	ListenAddr:     ":7879",
+	FriendlyName:   "",
+	LogTrace:       false,
+	InterfaceNames: []string{},
 }
 
 // Opt contains the options for DLNA serving.
@@ -45,6 +47,7 @@ func addFlagsPrefix(flagSet *pflag.FlagSet, prefix string, Opt *Options) {
 	flags.StringVarP(flagSet, &Opt.ListenAddr, prefix+"addr", "", Opt.ListenAddr, "The ip:port or :port to bind the DLNA http server to")
 	flags.StringVarP(flagSet, &Opt.FriendlyName, prefix+"name", "", Opt.FriendlyName, "Name of DLNA server")
 	flags.BoolVarP(flagSet, &Opt.LogTrace, prefix+"log-trace", "", Opt.LogTrace, "Enable trace logging of SOAP traffic")
+	flags.StringArrayVarP(flagSet, &Opt.InterfaceNames, prefix+"interface", "", Opt.InterfaceNames, "The interface to use for SSDP (repeat as necessary)")
 }
 
 // AddFlags add the command line flags for DLNA serving.


Test patch:
diff --git a/cmd/serve/dlna/dlna_test.go b/cmd/serve/dlna/dlna_test.go
index 8327ee054..3a3d2fa4b 100644
--- a/cmd/serve/dlna/dlna_test.go
+++ b/cmd/serve/dlna/dlna_test.go
@@ -35,7 +35,9 @@ const (
 func startServer(t *testing.T, f fs.Fs) {
 	opt := dlnaflags.DefaultOpt
 	opt.ListenAddr = testBindAddress
-	dlnaServer = newServer(f, &opt)
+	var err error
+	dlnaServer, err = newServer(f, &opt)
+	assert.NoError(t, err)
 	assert.NoError(t, dlnaServer.Serve())
 	baseURL = "http://" + dlnaServer.HTTPConn.Addr().String()
 }