github.com/mysteriumnetwork/node@v0.0.0-20240516044423-365054f76801/nat/service_ics.go (about)

     1  /*
     2   * Copyright (C) 2019 The "MysteriumNetwork/node" Authors.
     3   *
     4   * This program is free software: you can redistribute it and/or modify
     5   * it under the terms of the GNU General Public License as published by
     6   * the Free Software Foundation, either version 3 of the License, or
     7   * (at your option) any later version.
     8   *
     9   * This program is distributed in the hope that it will be useful,
    10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12   * GNU General Public License for more details.
    13   *
    14   * You should have received a copy of the GNU General Public License
    15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16   */
    17  
    18  package nat
    19  
    20  import (
    21  	"net"
    22  	"strconv"
    23  	"strings"
    24  	"sync"
    25  
    26  	"github.com/mysteriumnetwork/node/utils"
    27  	"github.com/pkg/errors"
    28  )
    29  
    30  const (
    31  	enablePublicSharing  = "$config.EnableSharing(0)"
    32  	enablePrivateSharing = "$config.EnableSharing(1)"
    33  	disableSharing       = "$config.DisableSharing()"
    34  	propsForPublic       = `$props.IsIcsPublic = 1;$props.IsIcsPrivate = 0;`
    35  	propsForPrivate      = `$props.IsIcsPublic = 0;$props.IsIcsPrivate = 1;`
    36  	propsForDisable      = `$props.IsIcsPublic = 0;$props.IsIcsPrivate = 0;`
    37  )
    38  
    39  func getSharingScript(ifaceName, props, action string) string {
    40  	return `regsvr32 /s hnetcfg.dll;
    41  		$netShare = New-Object -ComObject HNetCfg.HNetShare;
    42  		$c = $netShare.EnumEveryConnection |? { $netshare.NetConnectionProps.Invoke($_).Name -eq "` + ifaceName + `" };
    43  		$config = $netShare.INetSharingConfigurationForINetConnection.Invoke($c);
    44  		Try {
    45  			` + action + `
    46  		} Catch {
    47  			$guid = $netShare.NetConnectionProps.Invoke($c).Guid;
    48  			$props = Get-WmiObject -Class HNet_ConnectionProperties -Namespace "ROOT\microsoft\homenet" -Filter "__PATH like '%$guid%'";` + props + `$props.Put();
    49  			` + action + `
    50  		}`
    51  }
    52  
    53  func getPublicSharingScript(ifaceName string) string {
    54  	return getSharingScript(ifaceName, propsForPublic, enablePublicSharing)
    55  }
    56  
    57  func getPrivateSharingScript(ifaceName string) string {
    58  	return getSharingScript(ifaceName, propsForPrivate, enablePrivateSharing)
    59  }
    60  
    61  func getDisableSharingScript(ifaceName string) string {
    62  	return getSharingScript(ifaceName, propsForDisable, disableSharing)
    63  }
    64  
    65  type serviceICS struct {
    66  	mu                  sync.Mutex
    67  	activeInternalIface string
    68  	remoteAccessStatus  string
    69  	powerShell          func(cmd string) ([]byte, error)
    70  	setICSAddresses     func(config map[string]string) (map[string]string, error)
    71  	oldICSConfig        map[string]string
    72  }
    73  
    74  func (ics *serviceICS) disableICSAllInterfaces() error {
    75  	// Filter out invalid media (where NETCON_MEDIATYPE = NCM_NONE) because it is not an instance of INetConnection
    76  	// and will throw a cast error when attempting to `INetSharingConfigurationForINetConnection($conn)`
    77  	// Enum: https://docs.microsoft.com/en-us/windows/desktop/api/netcon/ne-netcon-tagnetcon_mediatype
    78  	_, err := ics.powerShell(`regsvr32 /s hnetcfg.dll;
    79  		$mgr = New-Object -ComObject HnetCfg.HNetShare;
    80  		filter IsValidMediaType { if ($mgr.NetConnectionProps($_).MediaType -gt 0) { $_ } };
    81  		$connections = $mgr.EnumEveryConnection | IsValidMediaType;
    82  		foreach ($conn in $connections) {
    83  			$mgr.INetSharingConfigurationForINetConnection($conn).DisableSharing()
    84  		};
    85  	`)
    86  	return err
    87  }
    88  
    89  // Enable enables internet connection sharing for the public interface.
    90  func (ics *serviceICS) Enable() error {
    91  	// We have to clean up ICS configuration for all interfaces to apply our configuration.
    92  	// It is possible to have ICS configured only for single pair of interfaces.
    93  	if err := ics.disableICSAllInterfaces(); err != nil {
    94  		return errors.Wrap(err, "failed to cleanup ICS before Enabling")
    95  	}
    96  
    97  	if err := ics.enableRemoteAccessService(); err != nil {
    98  		return errors.Wrap(err, "failed to Enable RemoteAccess service")
    99  	}
   100  
   101  	ifaceName, err := ics.getPublicInterfaceName()
   102  	if err != nil {
   103  		return errors.Wrap(err, "failed to get public interface name")
   104  	}
   105  
   106  	_, err = ics.powerShell(getPublicSharingScript(ifaceName))
   107  	return errors.Wrap(err, "failed to enable internet connection sharing")
   108  }
   109  
   110  func (ics *serviceICS) enableRemoteAccessService() error {
   111  	status, err := ics.powerShell(`(Get-WmiObject Win32_Service -filter "Name='RemoteAccess'").StartMode`)
   112  	if err != nil {
   113  		return errors.Wrap(err, "failed to get RemoteAccess service startup type")
   114  	}
   115  
   116  	statusStringified := strings.ToLower(strings.TrimSpace(string(status)))
   117  	if statusStringified == "auto" {
   118  		ics.remoteAccessStatus = "automatic"
   119  	} else {
   120  		ics.remoteAccessStatus = statusStringified
   121  	}
   122  
   123  	if _, err := ics.powerShell("Set-Service -Name RemoteAccess -StartupType automatic"); err != nil {
   124  		return errors.Wrap(err, "failed to set RemoteAccess service startup type to automatic")
   125  	}
   126  
   127  	_, err = ics.powerShell("Start-Service -Name RemoteAccess")
   128  	return errors.Wrap(err, "failed to start RemoteAccess service")
   129  }
   130  
   131  // Setup enables internet connection sharing for the local interface.
   132  func (ics *serviceICS) Setup(opts Options) (rules []interface{}, err error) {
   133  	ics.mu.Lock()
   134  	defer ics.mu.Unlock()
   135  
   136  	ip := incrementIP(opts.VPNNetwork.IP)
   137  	ics.oldICSConfig, err = ics.setICSAddresses(map[string]string{
   138  		"ScopeAddress":          ip.String(),
   139  		"ScopeAddressBackup":    ip.String(),
   140  		"StandaloneDhcpAddress": ip.String()})
   141  	if err != nil {
   142  		return nil, errors.Wrap(err, "failed to set ICS IP-address range")
   143  	}
   144  
   145  	internalInterface, err := ics.getInternalInterfaceName()
   146  	if err != nil {
   147  		return nil, errors.Wrap(err, "failed to find suitable interface")
   148  	}
   149  
   150  	_, err = ics.powerShell(getPrivateSharingScript(internalInterface))
   151  	if err != nil {
   152  		return nil, errors.Wrap(err, "failed to enable internet connection sharing for internal interface")
   153  	}
   154  
   155  	ics.activeInternalIface = internalInterface
   156  	return nil, nil
   157  }
   158  
   159  // Del disables internet connection sharing for the local interface.
   160  func (ics *serviceICS) Del([]interface{}) error {
   161  	ics.mu.Lock()
   162  	defer ics.mu.Unlock()
   163  
   164  	ifaceName, err := ics.getInternalInterfaceName()
   165  	if err != nil {
   166  		return errors.Wrap(err, "failed to find suitable interface")
   167  	}
   168  
   169  	_, err = ics.powerShell(getDisableSharingScript(ifaceName))
   170  	if err != nil {
   171  		return errors.Wrap(err, "failed to disable internet connection sharing for internal interface")
   172  	}
   173  
   174  	ics.activeInternalIface = ""
   175  	return nil
   176  }
   177  
   178  // Disable disables internet connection sharing for the public interface.
   179  func (ics *serviceICS) Disable() error {
   180  	result := utils.ErrorCollection{}
   181  
   182  	if _, err := ics.setICSAddresses(ics.oldICSConfig); err != nil {
   183  		return errors.Wrap(err, "failed to revert ICS IP-address range")
   184  	}
   185  
   186  	if ics.activeInternalIface != "" {
   187  		if err := ics.Del(nil); err != nil {
   188  			result.Add(errors.Wrap(err, "Failed to cleanup internet connection sharing for internal interface"))
   189  		}
   190  	}
   191  
   192  	_, err := ics.powerShell("Set-Service -Name RemoteAccess -StartupType " + ics.remoteAccessStatus)
   193  	if err != nil {
   194  		result.Add(errors.Wrap(err, "failed to revert RemoteAccess service startup type"))
   195  	}
   196  
   197  	ifaceName, err := ics.getPublicInterfaceName()
   198  	if err != nil {
   199  		result.Add(errors.Wrap(err, "failed to get public interface name"))
   200  	}
   201  
   202  	_, err = ics.powerShell(getDisableSharingScript(ifaceName))
   203  	if err != nil {
   204  		result.Add(errors.Wrap(err, "failed to disable internet connection sharing"))
   205  	}
   206  
   207  	err = ics.disableICSAllInterfaces()
   208  	if err != nil {
   209  		result.Add(errors.Wrap(err, "failed to cleanup ICS before Enabling"))
   210  	}
   211  
   212  	return result.Error()
   213  }
   214  
   215  func (ics *serviceICS) getPublicInterfaceName() (string, error) {
   216  	out, err := ics.powerShell(`Get-WmiObject -Class Win32_IP4RouteTable | where { $_.destination -eq '0.0.0.0' -and $_.mask -eq '0.0.0.0'} | foreach { $_.InterfaceIndex }`)
   217  	if err != nil {
   218  		return "", errors.Wrap(err, "failed to get interface from the default route")
   219  	}
   220  	ifaceID, err := strconv.Atoi(strings.TrimSpace(string(out)))
   221  	if err != nil {
   222  		return "", errors.Wrap(err, "failed to parse interface ID")
   223  	}
   224  
   225  	ifaces, err := net.Interfaces()
   226  	if err != nil {
   227  		return "", errors.Wrap(err, "failed to get a list of network interfaces")
   228  	}
   229  
   230  	for _, iface := range ifaces {
   231  		if iface.Index == ifaceID {
   232  			return iface.Name, nil
   233  		}
   234  	}
   235  
   236  	return "", errors.New("interface not found")
   237  }
   238  
   239  func (ics *serviceICS) getInternalInterfaceName() (string, error) {
   240  	out, err := ics.powerShell(`Get-WmiObject Win32_NetworkAdapter | Where-Object {$_.ServiceName -eq "tap0901"} | foreach { $_.NetConnectionID }`)
   241  	if err != nil {
   242  		return "", errors.Wrap(err, "failed to detect internal interface name")
   243  	}
   244  
   245  	ifaceName := strings.TrimSpace(string(out))
   246  	if len(ifaceName) == 0 {
   247  		return "", errors.New("interface not found")
   248  	}
   249  
   250  	return ifaceName, nil
   251  }
   252  
   253  func incrementIP(ip net.IP) net.IP {
   254  	dup := make(net.IP, len(ip))
   255  	copy(dup, ip)
   256  	for j := len(dup) - 1; j >= 0; j-- {
   257  		dup[j]++
   258  		if dup[j] > 0 {
   259  			break
   260  		}
   261  	}
   262  	return dup
   263  }