2014-11-16 20:13:20 +00:00
// Copyright (C) 2014 The Syncthing Authors.
2014-09-29 19:43:32 +00:00
//
2014-04-18 11:20:42 +00:00
// Adapted from https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/IGD.go
// Copyright (c) 2010 Jack Palevich (https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/LICENSE)
2016-05-18 00:10:50 +00:00
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form 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.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER 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.
2014-06-01 20:50:14 +00:00
2014-10-18 02:01:25 +00:00
// Package upnp implements UPnP InternetGatewayDevice discovery, querying, and port mapping.
2014-06-01 20:50:14 +00:00
package upnp
2014-04-18 11:20:42 +00:00
import (
"bufio"
"bytes"
2019-11-26 07:39:51 +00:00
"context"
2014-04-18 11:20:42 +00:00
"encoding/xml"
2022-08-16 08:01:49 +00:00
"errors"
2014-04-18 11:20:42 +00:00
"fmt"
2021-11-22 07:59:47 +00:00
"io"
2014-04-18 11:20:42 +00:00
"net"
"net/http"
"net/url"
2023-12-11 06:36:18 +00:00
"runtime"
2014-04-18 11:20:42 +00:00
"strings"
2017-12-07 07:08:24 +00:00
"sync"
2014-04-18 11:20:42 +00:00
"time"
2015-04-22 22:54:31 +00:00
2023-12-11 06:36:18 +00:00
"github.com/syncthing/syncthing/lib/build"
2015-10-12 18:30:14 +00:00
"github.com/syncthing/syncthing/lib/dialer"
2016-04-10 19:36:38 +00:00
"github.com/syncthing/syncthing/lib/nat"
2022-09-14 06:44:46 +00:00
"github.com/syncthing/syncthing/lib/osutil"
2014-04-18 11:20:42 +00:00
)
2016-04-10 19:36:38 +00:00
func init ( ) {
nat . Register ( Discover )
}
2014-04-18 11:20:42 +00:00
type upnpService struct {
2015-12-23 15:31:12 +00:00
ID string ` xml:"serviceId" `
Type string ` xml:"serviceType" `
ControlURL string ` xml:"controlURL" `
2014-04-18 11:20:42 +00:00
}
type upnpDevice struct {
2023-12-11 06:36:18 +00:00
IsIPv6 bool
2014-09-26 04:28:53 +00:00
DeviceType string ` xml:"deviceType" `
FriendlyName string ` xml:"friendlyName" `
Devices [ ] upnpDevice ` xml:"deviceList>device" `
Services [ ] upnpService ` xml:"serviceList>service" `
2014-04-18 11:20:42 +00:00
}
type upnpRoot struct {
Device upnpDevice ` xml:"device" `
}
2018-07-30 14:34:35 +00:00
// UnsupportedDeviceTypeError for unsupported UPnP device types (i.e upnp:rootdevice)
type UnsupportedDeviceTypeError struct {
deviceType string
}
2020-06-16 07:27:34 +00:00
func ( e * UnsupportedDeviceTypeError ) Error ( ) string {
2018-07-30 14:34:35 +00:00
return fmt . Sprintf ( "Unsupported UPnP device of type %s" , e . deviceType )
}
2023-12-11 06:36:18 +00:00
const (
urnIgdV1 = "urn:schemas-upnp-org:device:InternetGatewayDevice:1"
urnIgdV2 = "urn:schemas-upnp-org:device:InternetGatewayDevice:2"
urnWANDeviceV1 = "urn:schemas-upnp-org:device:WANDevice:1"
urnWANDeviceV2 = "urn:schemas-upnp-org:device:WANDevice:2"
urnWANConnectionDeviceV1 = "urn:schemas-upnp-org:device:WANConnectionDevice:1"
urnWANConnectionDeviceV2 = "urn:schemas-upnp-org:device:WANConnectionDevice:2"
urnWANIPConnectionV1 = "urn:schemas-upnp-org:service:WANIPConnection:1"
urnWANIPConnectionV2 = "urn:schemas-upnp-org:service:WANIPConnection:2"
urnWANIPv6FirewallControlV1 = "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1"
urnWANPPPConnectionV1 = "urn:schemas-upnp-org:service:WANPPPConnection:1"
urnWANPPPConnectionV2 = "urn:schemas-upnp-org:service:WANPPPConnection:2"
)
2014-10-18 02:01:25 +00:00
// Discover discovers UPnP InternetGatewayDevices.
2015-04-16 09:26:09 +00:00
// The order in which the devices appear in the results list is not deterministic.
2022-07-28 15:17:29 +00:00
func Discover ( ctx context . Context , _ , timeout time . Duration ) [ ] nat . Device {
2016-04-10 19:36:38 +00:00
var results [ ] nat . Device
2014-04-18 11:20:42 +00:00
2015-04-16 09:26:09 +00:00
interfaces , err := net . Interfaces ( )
if err != nil {
l . Infoln ( "Listing network interfaces:" , err )
return results
}
2014-09-26 04:28:53 +00:00
2017-12-30 19:16:08 +00:00
resultChan := make ( chan nat . Device )
2015-04-16 09:26:09 +00:00
2017-12-07 07:08:24 +00:00
wg := & sync . WaitGroup { }
2015-05-23 21:27:02 +00:00
2015-04-16 09:26:09 +00:00
for _ , intf := range interfaces {
2023-08-21 14:49:28 +00:00
if intf . Flags & net . FlagRunning == 0 || intf . Flags & net . FlagMulticast == 0 {
2015-04-28 08:29:08 +00:00
continue
}
2023-12-11 06:36:18 +00:00
wg . Add ( 1 )
// Discovery is done sequentially per interface because we discovered that
// FritzBox routers return a broken result sometimes if the IPv4 and IPv6
// request arrive at the same time.
go func ( iface net . Interface ) {
defer wg . Done ( )
hasGUA , err := interfaceHasGUAIPv6 ( iface )
if err != nil {
l . Debugf ( "Couldn't check for IPv6 GUAs on %s: %s" , iface . Name , err )
} else if hasGUA {
// Discover IPv6 gateways on interface. Only discover IGDv2, since IGDv1
// + IPv6 is not standardized and will lead to duplicates on routers.
// Only do this when a non-link-local IPv6 is available. if we can't
// enumerate the interface, the IPv6 code will not work anyway
discover ( ctx , & iface , urnIgdV2 , timeout , resultChan , true )
}
// Discover IPv4 gateways on interface.
for _ , deviceType := range [ ] string { urnIgdV2 , urnIgdV1 } {
discover ( ctx , & iface , deviceType , timeout , resultChan , false )
}
} ( intf )
2014-04-18 11:20:42 +00:00
}
2015-05-23 21:27:02 +00:00
go func ( ) {
wg . Wait ( )
close ( resultChan )
} ( )
2016-09-24 07:33:56 +00:00
seenResults := make ( map [ string ] bool )
2020-06-22 08:01:57 +00:00
for {
select {
case result , ok := <- resultChan :
if ! ok {
return results
}
if seenResults [ result . ID ( ) ] {
l . Debugf ( "Skipping duplicate result %s" , result . ID ( ) )
continue
}
2016-09-24 07:33:56 +00:00
2020-06-22 08:01:57 +00:00
results = append ( results , result )
seenResults [ result . ID ( ) ] = true
2015-04-16 09:26:09 +00:00
2020-06-22 08:01:57 +00:00
l . Debugf ( "UPnP discovery result %s" , result . ID ( ) )
case <- ctx . Done ( ) :
return nil
}
}
2014-09-26 04:28:53 +00:00
}
2019-11-05 18:56:51 +00:00
// Search for UPnP InternetGatewayDevices for <timeout> seconds.
2014-09-26 04:28:53 +00:00
// The order in which the devices appear in the result list is not deterministic
2023-12-11 06:36:18 +00:00
func discover ( ctx context . Context , intf * net . Interface , deviceType string , timeout time . Duration , results chan <- nat . Device , ip6 bool ) {
var ssdp net . UDPAddr
var template string
if ip6 {
ssdp = net . UDPAddr { IP : [ ] byte { 0xFF , 0x05 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x0C } , Port : 1900 }
template = ` M - SEARCH * HTTP / 1.1
HOST : [ FF05 : : C ] : 1900
ST : % s
MAN : "ssdp:discover"
MX : % d
USER - AGENT : syncthing / % s
2014-09-26 04:28:53 +00:00
2023-12-11 06:36:18 +00:00
`
} else {
ssdp = net . UDPAddr { IP : [ ] byte { 239 , 255 , 255 , 250 } , Port : 1900 }
template = ` M - SEARCH * HTTP / 1.1
2015-10-31 15:36:08 +00:00
HOST : 239.255 .255 .250 : 1900
ST : % s
MAN : "ssdp:discover"
MX : % d
2023-12-11 06:36:18 +00:00
USER - AGENT : syncthing / % s
2014-04-18 11:20:42 +00:00
2014-05-28 14:04:20 +00:00
`
2023-12-11 06:36:18 +00:00
}
searchStr := fmt . Sprintf ( template , deviceType , timeout / time . Second , build . Version )
2014-09-26 04:28:53 +00:00
2021-03-17 22:12:26 +00:00
search := [ ] byte ( strings . ReplaceAll ( searchStr , "\n" , "\r\n" ) + "\r\n" )
2014-04-18 11:20:42 +00:00
2015-10-03 15:25:21 +00:00
l . Debugln ( "Starting discovery of device type" , deviceType , "on" , intf . Name )
2014-09-26 04:28:53 +00:00
2023-12-11 06:36:18 +00:00
proto := "udp4"
if ip6 {
proto = "udp6"
}
socket , err := net . ListenMulticastUDP ( proto , intf , & net . UDPAddr { IP : ssdp . IP } )
2014-04-18 11:20:42 +00:00
if err != nil {
2023-12-11 06:36:18 +00:00
if runtime . GOOS == "windows" && ip6 {
// Requires https://github.com/golang/go/issues/63529 to be fixed.
l . Infoln ( "Support for IPv6 UPnP is currently not available on Windows:" , err )
} else {
l . Debugln ( "UPnP discovery: listening to udp multicast:" , err )
}
2015-04-16 09:26:09 +00:00
return
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
defer socket . Close ( ) // Make sure our socket gets closed
2014-04-18 11:20:42 +00:00
2015-10-03 15:25:21 +00:00
l . Debugln ( "Sending search request for device type" , deviceType , "on" , intf . Name )
2014-04-18 11:20:42 +00:00
2023-12-11 06:36:18 +00:00
_ , err = socket . WriteTo ( search , & ssdp )
2014-09-26 04:28:53 +00:00
if err != nil {
2017-08-21 10:03:25 +00:00
if e , ok := err . ( net . Error ) ; ! ok || ! e . Timeout ( ) {
2017-11-14 21:49:36 +00:00
l . Debugln ( "UPnP discovery: sending search request:" , err )
2017-08-21 10:03:25 +00:00
}
2015-04-16 09:26:09 +00:00
return
2014-09-26 04:28:53 +00:00
}
2015-10-03 15:25:21 +00:00
l . Debugln ( "Listening for UPnP response for device type" , deviceType , "on" , intf . Name )
2014-09-26 04:28:53 +00:00
2020-02-13 14:39:36 +00:00
ctx , cancel := context . WithTimeout ( ctx , timeout )
defer cancel ( )
// Listen for responses until a timeout is reached or the context is
// cancelled
resp := make ( [ ] byte , 65536 )
loop :
2014-09-26 04:28:53 +00:00
for {
2020-02-13 14:39:36 +00:00
if err := socket . SetDeadline ( time . Now ( ) . Add ( 250 * time . Millisecond ) ) ; err != nil {
l . Infoln ( "UPnP socket:" , err )
break
}
2023-12-11 06:36:18 +00:00
n , udpAddr , err := socket . ReadFromUDP ( resp )
2014-09-26 04:28:53 +00:00
if err != nil {
2020-02-13 14:39:36 +00:00
select {
case <- ctx . Done ( ) :
break loop
default :
2014-09-26 04:28:53 +00:00
}
2020-02-13 14:39:36 +00:00
if e , ok := err . ( net . Error ) ; ok && e . Timeout ( ) {
continue // continue reading
}
2024-11-19 10:32:56 +00:00
l . Infoln ( "UPnP read:" , err ) // legitimate error, not a timeout.
2014-09-26 04:28:53 +00:00
break
}
2020-02-13 14:39:36 +00:00
2023-12-11 06:36:18 +00:00
igds , err := parseResponse ( ctx , deviceType , udpAddr , resp [ : n ] , intf )
2015-04-16 09:26:09 +00:00
if err != nil {
2018-07-30 14:34:35 +00:00
switch err . ( type ) {
case * UnsupportedDeviceTypeError :
l . Debugln ( err . Error ( ) )
default :
2022-03-26 10:05:57 +00:00
if ! errors . Is ( err , context . Canceled ) {
2019-11-26 07:39:51 +00:00
l . Infoln ( "UPnP parse:" , err )
}
2018-07-30 14:34:35 +00:00
}
2015-04-16 09:26:09 +00:00
continue
2014-10-23 02:09:17 +00:00
}
2017-12-30 19:16:08 +00:00
for _ , igd := range igds {
igd := igd // Copy before sending pointer to the channel.
2020-06-22 08:01:57 +00:00
select {
case results <- & igd :
case <- ctx . Done ( ) :
return
}
2017-12-30 19:16:08 +00:00
}
2014-09-26 04:28:53 +00:00
}
2015-10-03 15:25:21 +00:00
l . Debugln ( "Discovery for device type" , deviceType , "on" , intf . Name , "finished." )
2014-09-26 04:28:53 +00:00
}
2023-12-11 06:36:18 +00:00
func parseResponse ( ctx context . Context , deviceType string , addr * net . UDPAddr , resp [ ] byte , netInterface * net . Interface ) ( [ ] IGDService , error ) {
2015-10-03 15:25:21 +00:00
l . Debugln ( "Handling UPnP response:\n\n" + string ( resp ) )
2014-09-26 04:28:53 +00:00
2015-04-16 09:26:09 +00:00
reader := bufio . NewReader ( bytes . NewBuffer ( resp ) )
2014-04-18 11:20:42 +00:00
request := & http . Request { }
response , err := http . ReadResponse ( reader , request )
2014-05-16 17:04:32 +00:00
if err != nil {
2017-12-30 19:16:08 +00:00
return nil , err
2014-05-16 17:04:32 +00:00
}
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
respondingDeviceType := response . Header . Get ( "St" )
if respondingDeviceType != deviceType {
2018-07-30 14:34:35 +00:00
return nil , & UnsupportedDeviceTypeError { deviceType : respondingDeviceType }
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
deviceDescriptionLocation := response . Header . Get ( "Location" )
if deviceDescriptionLocation == "" {
2017-12-30 19:16:08 +00:00
return nil , errors . New ( "invalid IGD response: no location specified" )
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
deviceDescriptionURL , err := url . Parse ( deviceDescriptionLocation )
2014-04-18 11:20:42 +00:00
if err != nil {
2014-09-26 04:28:53 +00:00
l . Infoln ( "Invalid IGD location: " + err . Error ( ) )
2023-12-11 06:36:18 +00:00
return nil , err
}
if err != nil {
l . Infoln ( "Invalid source IP for IGD: " + err . Error ( ) )
return nil , err
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
deviceUSN := response . Header . Get ( "USN" )
if deviceUSN == "" {
2017-12-30 19:16:08 +00:00
return nil , errors . New ( "invalid IGD response: USN not specified" )
2014-09-26 04:28:53 +00:00
}
2023-12-11 06:36:18 +00:00
deviceIP := net . ParseIP ( deviceDescriptionURL . Hostname ( ) )
// If the hostname of the device parses as an IPv6 link-local address, we need
// to use the source IP address of the response as the hostname
// instead of the one given, since only the former contains the zone index,
// while the URL returned from the gateway cannot contain the zone index.
// (It can't know how interfaces are named/numbered on our machine)
if deviceIP != nil && deviceIP . To4 ( ) == nil && deviceIP . IsLinkLocalUnicast ( ) {
ipAddr := net . IPAddr {
IP : addr . IP ,
Zone : addr . Zone ,
}
deviceDescriptionPort := deviceDescriptionURL . Port ( )
deviceDescriptionURL . Host = "[" + ipAddr . String ( ) + "]"
if deviceDescriptionPort != "" {
deviceDescriptionURL . Host += ":" + deviceDescriptionPort
}
deviceDescriptionLocation = deviceDescriptionURL . String ( )
}
2015-05-05 11:53:11 +00:00
deviceUUID := strings . TrimPrefix ( strings . Split ( deviceUSN , "::" ) [ 0 ] , "uuid:" )
2014-09-26 04:28:53 +00:00
response , err = http . Get ( deviceDescriptionLocation )
if err != nil {
2017-12-30 19:16:08 +00:00
return nil , err
2014-09-26 04:28:53 +00:00
}
defer response . Body . Close ( )
if response . StatusCode >= 400 {
2017-12-30 19:16:08 +00:00
return nil , errors . New ( "bad status code:" + response . Status )
2014-09-26 04:28:53 +00:00
}
var upnpRoot upnpRoot
err = xml . NewDecoder ( response . Body ) . Decode ( & upnpRoot )
if err != nil {
2017-12-30 19:16:08 +00:00
return nil , err
2014-09-26 04:28:53 +00:00
}
2023-12-11 06:36:18 +00:00
// Figure out our IPv4 address on the interface used to reach the IGD.
localIPv4Address , err := localIPv4 ( netInterface )
2014-04-18 11:20:42 +00:00
if err != nil {
2023-12-11 06:36:18 +00:00
// On Android, we cannot enumerate IP addresses on interfaces directly.
// Therefore, we just try to connect to the IGD and look at which source IP
// address was used. This is not ideal, but it's the best we can do. Maybe
// we are on an IPv6-only network though, so don't error out in case pinholing is available.
localIPv4Address , err = localIPv4Fallback ( ctx , deviceDescriptionURL )
if err != nil {
l . Infoln ( "Unable to determine local IPv4 address for IGD: " + err . Error ( ) )
}
2017-12-30 19:16:08 +00:00
}
2023-12-11 06:36:18 +00:00
// This differs from IGDService.SupportsIPVersion(). While that method
// determines whether an already completely discovered device uses the IPv6
// firewall protocol, this just checks if the gateway's is IPv6. Currently we
// only want to discover IPv6 UPnP endpoints on IPv6 gateways and vice versa,
// which is why this needs to be stored but technically we could forgo this check
// and try WANIPv6FirewallControl via IPv4. This leads to errors though so we don't do it.
upnpRoot . Device . IsIPv6 = addr . IP . To4 ( ) == nil
services , err := getServiceDescriptions ( deviceUUID , localIPv4Address , deviceDescriptionLocation , upnpRoot . Device , netInterface )
2017-12-30 19:16:08 +00:00
if err != nil {
return nil , err
2014-04-18 11:20:42 +00:00
}
2017-12-30 19:16:08 +00:00
return services , nil
2014-09-26 04:28:53 +00:00
}
2014-04-18 11:20:42 +00:00
2023-12-11 06:36:18 +00:00
func localIPv4 ( netInterface * net . Interface ) ( net . IP , error ) {
addrs , err := netInterface . Addrs ( )
if err != nil {
return nil , err
}
for _ , addr := range addrs {
ip , _ , err := net . ParseCIDR ( addr . String ( ) )
if err != nil {
continue
}
if ip . To4 ( ) != nil {
return ip , nil
}
}
return nil , errors . New ( "no IPv4 address found for interface " + netInterface . Name )
}
func localIPv4Fallback ( ctx context . Context , url * url . URL ) ( net . IP , error ) {
2019-11-26 07:39:51 +00:00
timeoutCtx , cancel := context . WithTimeout ( ctx , time . Second )
defer cancel ( )
2023-12-11 06:36:18 +00:00
conn , err := dialer . DialContext ( timeoutCtx , "udp4" , url . Host )
2014-04-18 11:20:42 +00:00
if err != nil {
2016-03-25 20:22:29 +00:00
return nil , err
2014-04-18 11:20:42 +00:00
}
2023-12-11 06:36:18 +00:00
2014-04-18 11:20:42 +00:00
defer conn . Close ( )
2023-12-11 06:36:18 +00:00
ip , err := osutil . IPFromAddr ( conn . LocalAddr ( ) )
if err != nil {
return nil , err
}
if ip . To4 ( ) == nil {
return nil , errors . New ( "tried to obtain IPv4 through fallback but got IPv6 address" )
}
return ip , nil
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
func getChildDevices ( d upnpDevice , deviceType string ) [ ] upnpDevice {
2014-12-08 16:07:55 +00:00
var result [ ] upnpDevice
2014-04-18 11:20:42 +00:00
for _ , dev := range d . Devices {
if dev . DeviceType == deviceType {
2014-09-26 04:28:53 +00:00
result = append ( result , dev )
2014-04-18 11:20:42 +00:00
}
}
2014-09-26 04:28:53 +00:00
return result
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
func getChildServices ( d upnpDevice , serviceType string ) [ ] upnpService {
2014-12-08 16:07:55 +00:00
var result [ ] upnpService
2015-12-23 15:31:12 +00:00
for _ , service := range d . Services {
if service . Type == serviceType {
result = append ( result , service )
2014-04-18 11:20:42 +00:00
}
}
2014-09-26 04:28:53 +00:00
return result
2014-04-18 11:20:42 +00:00
}
2023-12-11 06:36:18 +00:00
func getServiceDescriptions ( deviceUUID string , localIPAddress net . IP , rootURL string , device upnpDevice , netInterface * net . Interface ) ( [ ] IGDService , error ) {
2014-12-08 16:07:55 +00:00
var result [ ] IGDService
2014-09-26 04:28:53 +00:00
2023-12-11 06:36:18 +00:00
if device . IsIPv6 && device . DeviceType == urnIgdV1 {
// IPv6 UPnP is only standardized for IGDv2. Furthermore, any WANIPConn services for IPv4 that
// we may discover here are likely to be broken because many routers make the choice to not allow
// port mappings for IPs differing from the source IP of the device making the request (which would be v6 here)
return nil , nil
} else if device . IsIPv6 && device . DeviceType == urnIgdV2 {
descriptions := getIGDServices ( deviceUUID , localIPAddress , rootURL , device ,
urnWANDeviceV2 ,
urnWANConnectionDeviceV2 ,
[ ] string { urnWANIPv6FirewallControlV1 } ,
netInterface )
result = append ( result , descriptions ... )
} else if device . DeviceType == urnIgdV1 {
2017-12-30 19:16:08 +00:00
descriptions := getIGDServices ( deviceUUID , localIPAddress , rootURL , device ,
2023-12-11 06:36:18 +00:00
urnWANDeviceV1 ,
urnWANConnectionDeviceV1 ,
[ ] string { urnWANIPConnectionV1 , urnWANPPPConnectionV1 } ,
netInterface )
2014-09-26 04:28:53 +00:00
result = append ( result , descriptions ... )
2023-12-11 06:36:18 +00:00
} else if device . DeviceType == urnIgdV2 {
2017-12-30 19:16:08 +00:00
descriptions := getIGDServices ( deviceUUID , localIPAddress , rootURL , device ,
2023-12-11 06:36:18 +00:00
urnWANDeviceV2 ,
urnWANConnectionDeviceV2 ,
[ ] string { urnWANIPConnectionV2 , urnWANPPPConnectionV2 } ,
netInterface )
2014-09-26 04:28:53 +00:00
result = append ( result , descriptions ... )
} else {
return result , errors . New ( "[" + rootURL + "] Malformed root device description: not an InternetGatewayDevice." )
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
if len ( result ) < 1 {
return result , errors . New ( "[" + rootURL + "] Malformed device description: no compatible service descriptions found." )
2014-04-18 11:20:42 +00:00
}
2015-04-28 20:32:10 +00:00
return result , nil
2014-07-22 17:23:43 +00:00
}
2014-04-18 11:20:42 +00:00
2023-12-11 06:36:18 +00:00
func getIGDServices ( deviceUUID string , localIPAddress net . IP , rootURL string , device upnpDevice , wanDeviceURN string , wanConnectionURN string , URNs [ ] string , netInterface * net . Interface ) [ ] IGDService {
2014-12-08 16:07:55 +00:00
var result [ ] IGDService
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
devices := getChildDevices ( device , wanDeviceURN )
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
if len ( devices ) < 1 {
2015-10-03 15:25:21 +00:00
l . Infoln ( rootURL , "- malformed InternetGatewayDevice description: no WANDevices specified." )
2014-09-26 04:28:53 +00:00
return result
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
for _ , device := range devices {
connections := getChildDevices ( device , wanConnectionURN )
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
if len ( connections ) < 1 {
2015-10-03 15:25:21 +00:00
l . Infoln ( rootURL , "- malformed " , wanDeviceURN , "description: no WANConnectionDevices specified." )
2014-09-26 04:28:53 +00:00
}
2014-04-18 11:20:42 +00:00
2014-09-26 04:28:53 +00:00
for _ , connection := range connections {
2015-12-23 15:31:12 +00:00
for _ , URN := range URNs {
services := getChildServices ( connection , URN )
2014-09-26 04:28:53 +00:00
2023-12-11 06:36:18 +00:00
if len ( services ) == 0 {
l . Debugln ( rootURL , "- no services of type" , URN , " found on connection." )
}
2014-09-26 04:28:53 +00:00
for _ , service := range services {
2022-07-28 14:51:03 +00:00
if service . ControlURL == "" {
2015-12-23 15:31:12 +00:00
l . Infoln ( rootURL + "- malformed" , service . Type , "description: no control URL." )
2014-09-26 04:28:53 +00:00
} else {
u , _ := url . Parse ( rootURL )
replaceRawPath ( u , service . ControlURL )
2015-12-23 15:31:12 +00:00
l . Debugln ( rootURL , "- found" , service . Type , "with URL" , u )
2014-09-26 04:28:53 +00:00
2017-12-30 19:16:08 +00:00
service := IGDService {
UUID : deviceUUID ,
Device : device ,
ServiceID : service . ID ,
URL : u . String ( ) ,
URN : service . Type ,
2023-12-11 06:36:18 +00:00
Interface : netInterface ,
LocalIPv4 : localIPAddress ,
2017-12-30 19:16:08 +00:00
}
2014-10-18 03:30:47 +00:00
result = append ( result , service )
2014-09-26 04:28:53 +00:00
}
}
}
}
2014-04-18 11:20:42 +00:00
}
2014-09-26 04:28:53 +00:00
return result
2014-07-02 18:28:03 +00:00
}
func replaceRawPath ( u * url . URL , rp string ) {
2015-01-08 05:23:20 +00:00
asURL , err := url . Parse ( rp )
if err != nil {
return
} else if asURL . IsAbs ( ) {
u . Path = asURL . Path
u . RawQuery = asURL . RawQuery
2014-04-18 11:20:42 +00:00
} else {
2015-01-08 05:23:20 +00:00
var p , q string
fs := strings . Split ( rp , "?" )
p = fs [ 0 ]
if len ( fs ) > 1 {
q = fs [ 1 ]
}
if p [ 0 ] == '/' {
u . Path = p
} else {
u . Path += p
}
u . RawQuery = q
2014-04-18 11:20:42 +00:00
}
}
2020-02-24 20:57:15 +00:00
func soapRequest ( ctx context . Context , url , service , function , message string ) ( [ ] byte , error ) {
2023-12-11 06:36:18 +00:00
return soapRequestWithIP ( ctx , url , service , function , message , nil )
}
func soapRequestWithIP ( ctx context . Context , url , service , function , message string , localIP * net . TCPAddr ) ( [ ] byte , error ) {
const template = ` < ? xml version = "1.0" ? >
2014-04-18 11:20:42 +00:00
< s : Envelope xmlns : s = "http://schemas.xmlsoap.org/soap/envelope/" s : encodingStyle = "http://schemas.xmlsoap.org/soap/encoding/" >
< s : Body > % s < / s : Body >
< / s : Envelope >
`
2014-10-18 03:30:47 +00:00
var resp [ ] byte
2023-12-11 06:36:18 +00:00
body := fmt . Sprintf ( template , message )
2014-04-18 11:20:42 +00:00
2022-07-28 15:14:49 +00:00
req , err := http . NewRequestWithContext ( ctx , "POST" , url , strings . NewReader ( body ) )
2014-04-18 11:20:42 +00:00
if err != nil {
2014-10-18 03:30:47 +00:00
return resp , err
2014-04-18 11:20:42 +00:00
}
2015-04-26 20:31:43 +00:00
req . Close = true
2014-04-18 11:20:42 +00:00
req . Header . Set ( "Content-Type" , ` text/xml; charset="utf-8" ` )
req . Header . Set ( "User-Agent" , "syncthing/1.0" )
2015-04-26 10:37:37 +00:00
req . Header [ "SOAPAction" ] = [ ] string { fmt . Sprintf ( ` "%s#%s" ` , service , function ) } // Enforce capitalization in header-entry for sensitive routers. See issue #1696
2014-04-18 11:20:42 +00:00
req . Header . Set ( "Connection" , "Close" )
req . Header . Set ( "Cache-Control" , "no-cache" )
req . Header . Set ( "Pragma" , "no-cache" )
2015-10-03 15:25:21 +00:00
l . Debugln ( "SOAP Request URL: " + url )
l . Debugln ( "SOAP Action: " + req . Header . Get ( "SOAPAction" ) )
l . Debugln ( "SOAP Request:\n\n" + body )
2014-04-18 11:20:42 +00:00
2023-12-11 06:36:18 +00:00
dialer := net . Dialer {
LocalAddr : localIP ,
}
transport := & http . Transport {
DialContext : dialer . DialContext ,
}
httpClient := & http . Client {
Transport : transport ,
}
r , err := httpClient . Do ( req )
2014-04-18 11:20:42 +00:00
if err != nil {
2017-11-14 21:49:36 +00:00
l . Debugln ( "SOAP do:" , err )
2014-10-18 03:30:47 +00:00
return resp , err
2014-04-18 11:20:42 +00:00
}
2023-12-11 06:36:18 +00:00
resp , err = io . ReadAll ( r . Body )
if err != nil {
l . Debugf ( "Error reading SOAP response: %s, partial response (if present):\n\n%s" , resp )
return resp , err
}
2015-10-03 15:25:21 +00:00
l . Debugf ( "SOAP Response: %s\n\n%s\n\n" , r . Status , resp )
2014-04-18 11:20:42 +00:00
r . Body . Close ( )
if r . StatusCode >= 400 {
2014-10-18 03:30:47 +00:00
return resp , errors . New ( function + ": " + r . Status )
2014-04-18 11:20:42 +00:00
}
2014-10-18 03:30:47 +00:00
return resp , nil
2014-04-18 11:20:42 +00:00
}
2023-12-11 06:36:18 +00:00
func interfaceHasGUAIPv6 ( intf net . Interface ) ( bool , error ) {
addrs , err := intf . Addrs ( )
if err != nil {
return false , err
}
for _ , addr := range addrs {
ip , _ , err := net . ParseCIDR ( addr . String ( ) )
if err != nil {
return false , err
}
// IsGlobalUnicast returns true for ULAs, so check for those separately.
if ip . To4 ( ) == nil && ip . IsGlobalUnicast ( ) && ! ip . IsPrivate ( ) {
return true , nil
}
}
return false , nil
}
2014-10-18 03:30:47 +00:00
type soapGetExternalIPAddressResponseEnvelope struct {
XMLName xml . Name
Body soapGetExternalIPAddressResponseBody ` xml:"Body" `
}
type soapGetExternalIPAddressResponseBody struct {
XMLName xml . Name
GetExternalIPAddressResponse getExternalIPAddressResponse ` xml:"GetExternalIPAddressResponse" `
}
type getExternalIPAddressResponse struct {
NewExternalIPAddress string ` xml:"NewExternalIPAddress" `
}
2015-05-21 07:55:55 +00:00
type soapErrorResponse struct {
ErrorCode int ` xml:"Body>Fault>detail>UPnPError>errorCode" `
ErrorDescription string ` xml:"Body>Fault>detail>UPnPError>errorDescription" `
}
2023-12-11 06:36:18 +00:00
type soapAddPinholeResponse struct {
UniqueID int ` xml:"Body>AddPinholeResponse>UniqueID" `
}