2015-01-15 15:59:01 +00:00
# -*- coding: utf-8 -*-
#
# Copyright (C) 2015 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2015-01-18 22:41:53 +00:00
import socket
2015-01-20 19:09:20 +00:00
from aiohttp . web import HTTPConflict
2015-01-23 20:01:23 +00:00
from gns3server . config import Config
2015-01-15 23:50:36 +00:00
2015-01-23 20:01:23 +00:00
import logging
log = logging . getLogger ( __name__ )
2015-01-19 21:43:35 +00:00
2015-01-20 12:24:00 +00:00
2016-05-14 00:48:10 +00:00
# This ports are disallowed by Chrome and Firefox to avoid issues, we skip them as well
BANNED_PORTS = set ( ( 1 , 7 , 9 , 11 , 13 , 15 , 17 , 19 , 20 , 21 , 22 , 23 , 25 , 37 , 42 , 43 , 53 , 77 , 79 , 87 , 95 , 101 , 102 , 103 ,
104 , 109 , 110 , 111 , 113 , 115 , 117 , 119 , 123 , 135 , 139 , 143 , 179 , 389 , 465 , 512 , 513 , 514 , 515 , 526 ,
530 , 531 , 532 , 540 , 556 , 563 , 587 , 601 , 636 , 993 , 995 , 2049 , 3659 , 4045 , 6000 , 6665 , 6666 , 6667 ,
6668 , 6669 ) )
2016-05-03 19:07:01 +00:00
2015-01-23 20:01:23 +00:00
class PortManager :
2015-01-31 21:34:49 +00:00
2015-01-15 15:59:01 +00:00
"""
2015-01-18 22:41:53 +00:00
: param host : IP address to bind for console connections
2015-01-15 15:59:01 +00:00
"""
2016-10-26 12:43:47 +00:00
def __init__ ( self ) :
self . _console_host = None
2015-07-21 01:22:20 +00:00
# UDP host must be 0.0.0.0, reason: https://github.com/GNS3/gns3-server/issues/265
self . _udp_host = " 0.0.0.0 "
2015-01-18 22:41:53 +00:00
self . _used_tcp_ports = set ( )
self . _used_udp_ports = set ( )
2015-01-15 15:59:01 +00:00
2015-01-23 20:01:23 +00:00
server_config = Config . instance ( ) . get_section_config ( " Server " )
remote_console_connections = server_config . getboolean ( " allow_remote_console " )
2016-03-25 16:32:04 +00:00
console_start_port_range = server_config . getint ( " console_start_port_range " , 5000 )
console_end_port_range = server_config . getint ( " console_end_port_range " , 10000 )
2015-02-02 03:43:55 +00:00
self . _console_port_range = ( console_start_port_range , console_end_port_range )
2015-02-02 21:52:58 +00:00
log . debug ( " Console port range is {} - {} " . format ( console_start_port_range , console_end_port_range ) )
2015-02-02 03:43:55 +00:00
udp_start_port_range = server_config . getint ( " udp_start_port_range " , 10000 )
udp_end_port_range = server_config . getint ( " udp_end_port_range " , 20000 )
self . _udp_port_range = ( udp_start_port_range , udp_end_port_range )
2015-02-02 21:52:58 +00:00
log . debug ( " UDP port range is {} - {} " . format ( udp_start_port_range , udp_end_port_range ) )
2015-02-02 03:43:55 +00:00
2015-01-24 22:32:58 +00:00
@classmethod
def instance ( cls ) :
"""
Singleton to return only one instance of PortManager .
: returns : instance of PortManager
"""
if not hasattr ( cls , " _instance " ) or cls . _instance is None :
cls . _instance = cls ( )
return cls . _instance
2015-01-18 22:41:53 +00:00
@property
def console_host ( self ) :
2016-10-26 12:43:47 +00:00
assert self . _console_host is not None
2015-01-18 22:41:53 +00:00
return self . _console_host
@console_host.setter
2015-03-03 13:37:34 +00:00
def console_host ( self , new_host ) :
2016-10-26 12:43:47 +00:00
"""
If allow remote connection we need to bind console host to 0.0 .0 .0
"""
server_config = Config . instance ( ) . get_section_config ( " Server " )
remote_console_connections = server_config . getboolean ( " allow_remote_console " )
if remote_console_connections :
log . warning ( " Remote console connections are allowed " )
self . _console_host = " 0.0.0.0 "
else :
self . _console_host = new_host
2015-01-18 22:41:53 +00:00
@property
def console_port_range ( self ) :
return self . _console_port_range
2015-11-03 11:34:22 +00:00
@console_port_range.setter
2015-01-18 22:41:53 +00:00
def console_port_range ( self , new_range ) :
assert isinstance ( new_range , tuple )
self . _console_port_range = new_range
@property
def udp_host ( self ) :
return self . _udp_host
@udp_host.setter
2016-05-14 00:48:10 +00:00
def udp_host ( self , new_host ) :
2015-01-18 22:41:53 +00:00
self . _udp_host = new_host
@property
def udp_port_range ( self ) :
return self . _udp_port_range
2015-11-03 11:34:22 +00:00
@udp_port_range.setter
2015-01-18 22:41:53 +00:00
def udp_port_range ( self , new_range ) :
assert isinstance ( new_range , tuple )
self . _udp_port_range = new_range
2015-02-24 00:42:55 +00:00
@property
def tcp_ports ( self ) :
return self . _used_tcp_ports
@property
def udp_ports ( self ) :
return self . _used_udp_ports
2015-01-18 22:41:53 +00:00
@staticmethod
2016-06-01 23:50:31 +00:00
def find_unused_port ( start_port , end_port , host = " 127.0.0.1 " , socket_type = " TCP " , ignore_ports = None ) :
2015-01-18 22:41:53 +00:00
"""
Finds an unused port in a range .
: param start_port : first port in the range
: param end_port : last port in the range
: param host : host / address for bind ( )
: param socket_type : TCP ( default ) or UDP
: param ignore_ports : list of port to ignore within the range
"""
if end_port < start_port :
2015-05-06 08:40:51 +00:00
raise HTTPConflict ( text = " Invalid port range {} - {} " . format ( start_port , end_port ) )
2015-01-18 22:41:53 +00:00
last_exception = None
for port in range ( start_port , end_port + 1 ) :
2016-06-01 23:50:31 +00:00
if ignore_ports and ( port in ignore_ports or port in BANNED_PORTS ) :
2015-01-18 22:41:53 +00:00
continue
2015-12-07 11:26:46 +00:00
2015-01-18 22:41:53 +00:00
try :
2016-02-05 09:06:34 +00:00
PortManager . _check_port ( host , port , socket_type )
2016-10-18 07:57:32 +00:00
if host != " 0.0.0.0 " :
PortManager . _check_port ( " 0.0.0.0 " , port , socket_type )
2016-02-05 09:06:34 +00:00
return port
2015-01-18 22:41:53 +00:00
except OSError as e :
last_exception = e
if port + 1 == end_port :
break
else :
continue
2015-01-21 02:02:22 +00:00
raise HTTPConflict ( text = " Could not find a free port between {} and {} on host {} , last exception: {} " . format ( start_port ,
2015-01-21 10:33:24 +00:00
end_port ,
host ,
last_exception ) )
2016-02-05 09:06:34 +00:00
2015-12-07 11:26:46 +00:00
@staticmethod
def _check_port ( host , port , socket_type ) :
"""
Check if an a port is available and raise an OSError if port is not available
: returns : boolean
"""
if socket_type == " UDP " :
socket_type = socket . SOCK_DGRAM
else :
socket_type = socket . SOCK_STREAM
for res in socket . getaddrinfo ( host , port , socket . AF_UNSPEC , socket_type , 0 , socket . AI_PASSIVE ) :
af , socktype , proto , _ , sa = res
with socket . socket ( af , socktype , proto ) as s :
s . setsockopt ( socket . SOL_SOCKET , socket . SO_REUSEADDR , 1 )
s . bind ( sa ) # the port is available if bind is a success
return True
2015-07-03 22:06:25 +00:00
def get_free_tcp_port ( self , project , port_range_start = None , port_range_end = None ) :
2015-01-18 22:41:53 +00:00
"""
2015-02-24 00:42:55 +00:00
Get an available TCP port and reserve it
2015-03-21 23:19:12 +00:00
: param project : Project instance
2015-01-18 22:41:53 +00:00
"""
2015-07-03 22:06:25 +00:00
# use the default range is not specific one is given
if port_range_start is None and port_range_end is None :
port_range_start = self . _console_port_range [ 0 ]
port_range_end = self . _console_port_range [ 1 ]
port = self . find_unused_port ( port_range_start ,
port_range_end ,
2015-01-18 22:41:53 +00:00
host = self . _console_host ,
socket_type = " TCP " ,
ignore_ports = self . _used_tcp_ports )
self . _used_tcp_ports . add ( port )
2015-03-21 23:19:12 +00:00
project . record_tcp_port ( port )
2015-02-24 00:42:55 +00:00
log . debug ( " TCP port {} has been allocated " . format ( port ) )
2015-01-15 15:59:01 +00:00
return port
2015-12-07 11:26:46 +00:00
def reserve_tcp_port ( self , port , project , port_range_start = None , port_range_end = None ) :
2015-01-18 22:41:53 +00:00
"""
2015-12-07 11:26:46 +00:00
Reserve a specific TCP port number . If not available replace it
by another .
2015-01-18 22:41:53 +00:00
: param port : TCP port number
2015-03-21 23:19:12 +00:00
: param project : Project instance
2015-12-07 11:26:46 +00:00
: param port_range_start : Port range to use
: param port_range_end : Port range to use
: returns : The TCP port
2015-01-18 22:41:53 +00:00
"""
2015-12-07 11:26:46 +00:00
# use the default range is not specific one is given
if port_range_start is None and port_range_end is None :
port_range_start = self . _console_port_range [ 0 ]
port_range_end = self . _console_port_range [ 1 ]
2015-01-18 22:41:53 +00:00
if port in self . _used_tcp_ports :
2015-12-07 11:26:46 +00:00
old_port = port
port = self . get_free_tcp_port ( project , port_range_start = port_range_start , port_range_end = port_range_end )
msg = " TCP port {} already in use on host {} . Port has been replaced by {} " . format ( old_port , self . _console_host , port )
2016-04-07 20:42:52 +00:00
log . debug ( msg )
#project.emit("log.warning", {"message": msg})
2015-12-07 11:26:46 +00:00
return port
2016-04-06 13:58:29 +00:00
if port < port_range_start or port > port_range_end :
2015-12-07 11:26:46 +00:00
old_port = port
port = self . get_free_tcp_port ( project , port_range_start = port_range_start , port_range_end = port_range_end )
msg = " TCP port {} is outside the range {} - {} on host {} . Port has been replaced by {} " . format ( old_port , port_range_start , port_range_end , self . _console_host , port )
2016-04-07 20:42:52 +00:00
log . debug ( msg )
#project.emit("log.warning", {"message": msg})
2015-12-07 11:26:46 +00:00
return port
try :
PortManager . _check_port ( self . _console_host , port , " TCP " )
except OSError :
old_port = port
port = self . get_free_tcp_port ( project , port_range_start = port_range_start , port_range_end = port_range_end )
msg = " TCP port {} already in use on host {} . Port has been replaced by {} " . format ( old_port , self . _console_host , port )
2016-04-07 20:42:52 +00:00
log . debug ( msg )
#project.emit("log.warning", {"message": msg})
2015-12-07 11:26:46 +00:00
return port
2015-01-18 22:41:53 +00:00
self . _used_tcp_ports . add ( port )
2015-03-21 23:19:12 +00:00
project . record_tcp_port ( port )
2015-02-24 00:42:55 +00:00
log . debug ( " TCP port {} has been reserved " . format ( port ) )
2015-01-20 19:54:46 +00:00
return port
2015-01-18 22:41:53 +00:00
2015-03-21 23:19:12 +00:00
def release_tcp_port ( self , port , project ) :
2015-01-18 22:41:53 +00:00
"""
2015-02-24 00:42:55 +00:00
Release a specific TCP port number
2015-01-18 22:41:53 +00:00
: param port : TCP port number
2015-03-21 23:19:12 +00:00
: param project : Project instance
2015-01-18 22:41:53 +00:00
"""
2015-01-23 23:38:46 +00:00
if port in self . _used_tcp_ports :
self . _used_tcp_ports . remove ( port )
2015-03-21 23:19:12 +00:00
project . remove_tcp_port ( port )
2015-02-24 00:42:55 +00:00
log . debug ( " TCP port {} has been released " . format ( port ) )
2015-01-18 22:41:53 +00:00
2015-03-21 23:19:12 +00:00
def get_free_udp_port ( self , project ) :
2015-01-15 15:59:01 +00:00
"""
2015-01-18 22:41:53 +00:00
Get an available UDP port and reserve it
2015-03-21 23:19:12 +00:00
: param project : Project instance
2015-01-18 22:41:53 +00:00
"""
port = self . find_unused_port ( self . _udp_port_range [ 0 ] ,
self . _udp_port_range [ 1 ] ,
host = self . _udp_host ,
socket_type = " UDP " ,
ignore_ports = self . _used_udp_ports )
self . _used_udp_ports . add ( port )
2015-03-21 23:19:12 +00:00
project . record_udp_port ( port )
2015-02-24 00:42:55 +00:00
log . debug ( " UDP port {} has been allocated " . format ( port ) )
2015-01-18 22:41:53 +00:00
return port
2015-01-15 15:59:01 +00:00
2015-03-21 23:19:12 +00:00
def reserve_udp_port ( self , port , project ) :
2015-01-15 15:59:01 +00:00
"""
2015-01-18 22:41:53 +00:00
Reserve a specific UDP port number
2015-01-15 15:59:01 +00:00
2015-01-18 22:41:53 +00:00
: param port : UDP port number
2015-03-21 23:19:12 +00:00
: param project : Project instance
2015-01-15 15:59:01 +00:00
"""
2015-01-18 22:41:53 +00:00
if port in self . _used_udp_ports :
2016-02-05 09:06:34 +00:00
raise HTTPConflict ( text = " UDP port {} already in use on host {} " . format ( port , self . _console_host ) )
2015-10-30 14:15:28 +00:00
if port < self . _udp_port_range [ 0 ] or port > self . _udp_port_range [ 1 ] :
raise HTTPConflict ( text = " UDP port {} is outside the range {} - {} " . format ( port , self . _udp_port_range [ 0 ] , self . _udp_port_range [ 1 ] ) )
2015-01-18 22:41:53 +00:00
self . _used_udp_ports . add ( port )
2015-03-21 23:19:12 +00:00
project . record_udp_port ( port )
2015-02-24 00:42:55 +00:00
log . debug ( " UDP port {} has been reserved " . format ( port ) )
2015-01-18 22:41:53 +00:00
2015-04-15 13:58:31 +00:00
def release_udp_port ( self , port , project ) :
2015-01-15 15:59:01 +00:00
"""
2015-01-18 22:41:53 +00:00
Release a specific UDP port number
2015-01-15 23:50:36 +00:00
2015-01-18 22:41:53 +00:00
: param port : UDP port number
2015-03-21 23:19:12 +00:00
: param project : Project instance
2015-01-18 22:41:53 +00:00
"""
2015-01-15 15:59:01 +00:00
2015-01-23 23:38:46 +00:00
if port in self . _used_udp_ports :
self . _used_udp_ports . remove ( port )
2015-03-21 23:19:12 +00:00
project . remove_udp_port ( port )
2015-02-24 00:42:55 +00:00
log . debug ( " UDP port {} has been released " . format ( port ) )