2019-08-23 13:26:58 +00:00
|
|
|
import itertools
|
|
|
|
import ipaddress
|
|
|
|
import logging
|
|
|
|
import atexit
|
2019-08-23 21:17:17 +00:00
|
|
|
import socket
|
2019-08-23 13:26:58 +00:00
|
|
|
|
|
|
|
import pyroute2
|
2019-08-23 21:17:17 +00:00
|
|
|
from pyroute2.netlink.rtnl import rtypes
|
2019-08-23 13:26:58 +00:00
|
|
|
import docker
|
|
|
|
from flask import request, jsonify
|
2019-08-22 14:34:10 +00:00
|
|
|
|
2019-08-23 15:47:37 +00:00
|
|
|
from . import NetDhcpError, app
|
2019-08-22 14:34:10 +00:00
|
|
|
|
2019-08-24 12:54:06 +00:00
|
|
|
OPTS_KEY = 'com.docker.network.generic'
|
|
|
|
OPT_PREFIX = 'devplayer0.net-dhcp'
|
|
|
|
OPT_BRIDGE = f'{OPT_PREFIX}.bridge'
|
2019-08-23 13:26:58 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger('gunicorn.error')
|
|
|
|
|
2019-08-23 21:17:17 +00:00
|
|
|
ndb = pyroute2.NDB()
|
2019-08-23 13:26:58 +00:00
|
|
|
@atexit.register
|
2019-08-23 21:17:17 +00:00
|
|
|
def close_ndb():
|
|
|
|
ndb.close()
|
2019-08-23 13:26:58 +00:00
|
|
|
|
|
|
|
client = docker.from_env()
|
|
|
|
@atexit.register
|
|
|
|
def close_docker():
|
|
|
|
client.close()
|
|
|
|
|
2019-08-23 15:47:37 +00:00
|
|
|
def veth_pair(e):
|
|
|
|
return f'dh-{e[:12]}', f'{e[:12]}-dh'
|
|
|
|
|
2019-08-23 13:26:58 +00:00
|
|
|
def iface_addrs(iface):
|
2019-08-23 21:17:17 +00:00
|
|
|
return list(map(lambda a: ipaddress.ip_interface((a['address'], a['prefixlen'])), iface.ipaddr))
|
2019-08-23 13:26:58 +00:00
|
|
|
def iface_nets(iface):
|
2019-08-23 21:17:17 +00:00
|
|
|
return list(map(lambda n: n.network, iface_addrs(iface)))
|
2019-08-23 13:26:58 +00:00
|
|
|
|
|
|
|
def get_bridges():
|
|
|
|
reserved_nets = set(map(ipaddress.ip_network, map(lambda c: c['Subnet'], \
|
|
|
|
itertools.chain.from_iterable(map(lambda i: i['Config'], filter(lambda i: i['Driver'] != 'net-dhcp', \
|
|
|
|
map(lambda n: n.attrs['IPAM'], client.networks.list())))))))
|
|
|
|
|
2019-08-23 21:17:17 +00:00
|
|
|
return dict(map(lambda i: (i['ifname'], i), filter(lambda i: i['kind'] == 'bridge' and not \
|
|
|
|
set(iface_nets(i)).intersection(reserved_nets), map(lambda i: ndb.interfaces[i.ifname], ndb.interfaces))))
|
2019-08-23 13:26:58 +00:00
|
|
|
|
2019-08-23 15:47:37 +00:00
|
|
|
def net_bridge(n):
|
2019-08-24 12:54:06 +00:00
|
|
|
return ndb.interfaces[client.networks.get(n).attrs['Options'][OPT_BRIDGE]]
|
2019-08-23 15:47:37 +00:00
|
|
|
|
2019-08-23 13:26:58 +00:00
|
|
|
@app.route('/NetworkDriver.GetCapabilities', methods=['POST'])
|
2019-08-22 14:34:10 +00:00
|
|
|
def net_get_capabilities():
|
|
|
|
return jsonify({
|
|
|
|
'Scope': 'local',
|
|
|
|
'ConnectivityScope': 'global'
|
2019-08-23 12:02:24 +00:00
|
|
|
})
|
2019-08-23 13:26:58 +00:00
|
|
|
|
|
|
|
@app.route('/NetworkDriver.CreateNetwork', methods=['POST'])
|
|
|
|
def create_net():
|
2019-08-23 13:48:49 +00:00
|
|
|
req = request.get_json(force=True)
|
2019-08-24 12:54:06 +00:00
|
|
|
if OPT_BRIDGE not in req['Options'][OPTS_KEY]:
|
2019-08-23 13:26:58 +00:00
|
|
|
return jsonify({'Err': 'No bridge provided'}), 400
|
|
|
|
|
2019-08-24 12:54:06 +00:00
|
|
|
desired = req['Options'][OPTS_KEY][OPT_BRIDGE]
|
2019-08-23 13:26:58 +00:00
|
|
|
bridges = get_bridges()
|
|
|
|
if desired not in bridges:
|
|
|
|
return jsonify({'Err': f'Bridge "{desired}" not found (or the specified bridge is already used by Docker)'}), 400
|
|
|
|
|
2019-08-23 21:21:43 +00:00
|
|
|
logger.info('Creating network "%s" (using bridge "%s")', req['NetworkID'], desired)
|
2019-08-23 13:26:58 +00:00
|
|
|
return jsonify({})
|
|
|
|
|
|
|
|
@app.route('/NetworkDriver.DeleteNetwork', methods=['POST'])
|
|
|
|
def delete_net():
|
2019-08-23 13:48:49 +00:00
|
|
|
return jsonify({})
|
2019-08-23 15:47:37 +00:00
|
|
|
|
|
|
|
@app.route('/NetworkDriver.CreateEndpoint', methods=['POST'])
|
|
|
|
def create_endpoint():
|
|
|
|
req = request.get_json(force=True)
|
|
|
|
req_iface = req['Interface']
|
|
|
|
|
|
|
|
bridge = net_bridge(req['NetworkID'])
|
2019-08-23 15:57:27 +00:00
|
|
|
bridge_addrs = iface_addrs(bridge)
|
2019-08-23 15:47:37 +00:00
|
|
|
|
|
|
|
if_host, if_container = veth_pair(req['EndpointID'])
|
2019-08-23 21:21:43 +00:00
|
|
|
logger.info('creating veth pair %s <=> %s', if_host, if_container)
|
2019-08-23 21:17:17 +00:00
|
|
|
if_host = (ndb.interfaces.create(ifname=if_host, kind='veth', peer=if_container)
|
|
|
|
.set('state', 'up')
|
2019-08-23 15:47:37 +00:00
|
|
|
.commit())
|
|
|
|
|
2019-08-23 21:17:17 +00:00
|
|
|
if_container = (ndb.interfaces[if_container]
|
|
|
|
.set('state', 'up')
|
2019-08-23 15:47:37 +00:00
|
|
|
.commit())
|
|
|
|
res_iface = {
|
|
|
|
'MacAddress': '',
|
|
|
|
'Address': '',
|
|
|
|
'AddressIPv6': ''
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
2019-08-23 21:38:09 +00:00
|
|
|
if 'MacAddress' not in req_iface or not req_iface['MacAddress']:
|
2019-08-23 21:17:17 +00:00
|
|
|
res_iface['MacAddress'] = if_container['address']
|
2019-08-23 15:47:37 +00:00
|
|
|
|
|
|
|
def try_addr(type_):
|
2019-08-24 12:54:06 +00:00
|
|
|
addr = None
|
2019-08-23 15:47:37 +00:00
|
|
|
k = 'AddressIPv6' if type_ == 'v6' else 'Address'
|
|
|
|
if k in req_iface and req_iface[k]:
|
2019-08-23 21:38:09 +00:00
|
|
|
# Just validate the address, Docker will add it to the interface for us
|
2019-08-24 12:54:06 +00:00
|
|
|
addr = ipaddress.ip_interface(req_iface[k])
|
|
|
|
for bridge_addr in bridge_addrs:
|
|
|
|
if addr.ip == bridge_addr.ip:
|
|
|
|
raise NetDhcpError(400, f'Address {addr} is already in use on bridge {bridge["ifname"]}')
|
|
|
|
|
|
|
|
logger.info('Adding address %s to %s', addr, if_container['ifname'])
|
2019-08-23 21:17:17 +00:00
|
|
|
elif type_ == 'v4':
|
2019-08-23 15:47:37 +00:00
|
|
|
raise NetDhcpError(400, f'DHCP{type_} is currently unsupported')
|
|
|
|
try_addr('v4')
|
|
|
|
try_addr('v6')
|
|
|
|
|
|
|
|
(bridge
|
|
|
|
.add_port(if_host)
|
|
|
|
.commit())
|
|
|
|
|
|
|
|
res = jsonify({
|
|
|
|
'Interface': res_iface
|
|
|
|
})
|
|
|
|
except NetDhcpError as e:
|
|
|
|
(if_host
|
|
|
|
.remove()
|
|
|
|
.commit())
|
|
|
|
logger.error(e)
|
|
|
|
res = jsonify({'Err': str(e)}), e.status
|
|
|
|
except Exception as e:
|
|
|
|
(if_host
|
|
|
|
.remove()
|
|
|
|
.commit())
|
|
|
|
res = jsonify({'Err': str(e)}), 500
|
|
|
|
finally:
|
|
|
|
return res
|
|
|
|
|
|
|
|
@app.route('/NetworkDriver.EndpointOperInfo', methods=['POST'])
|
|
|
|
def endpoint_info():
|
|
|
|
req = request.get_json(force=True)
|
|
|
|
|
|
|
|
bridge = net_bridge(req['NetworkID'])
|
|
|
|
if_host, _if_container = veth_pair(req['EndpointID'])
|
2019-08-23 21:17:17 +00:00
|
|
|
if_host = ndb.interfaces[if_host]
|
2019-08-23 15:47:37 +00:00
|
|
|
|
|
|
|
return jsonify({
|
2019-08-23 21:17:17 +00:00
|
|
|
'bridge': bridge['ifname'],
|
2019-08-23 15:47:37 +00:00
|
|
|
'if_host': {
|
2019-08-23 21:17:17 +00:00
|
|
|
'name': if_host['ifname'],
|
|
|
|
'mac': if_host['address']
|
2019-08-23 15:47:37 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
@app.route('/NetworkDriver.DeleteEndpoint', methods=['POST'])
|
|
|
|
def delete_endpoint():
|
|
|
|
req = request.get_json(force=True)
|
|
|
|
|
|
|
|
bridge = net_bridge(req['NetworkID'])
|
|
|
|
if_host, _if_container = veth_pair(req['EndpointID'])
|
2019-08-23 21:17:17 +00:00
|
|
|
if_host = ndb.interfaces[if_host]
|
2019-08-23 15:47:37 +00:00
|
|
|
|
2019-08-23 21:17:17 +00:00
|
|
|
bridge.del_port(if_host['ifname'])
|
2019-08-23 15:47:37 +00:00
|
|
|
(if_host
|
|
|
|
.remove()
|
|
|
|
.commit())
|
|
|
|
|
|
|
|
return jsonify({})
|
2019-08-23 16:07:54 +00:00
|
|
|
|
|
|
|
@app.route('/NetworkDriver.Join', methods=['POST'])
|
|
|
|
def join():
|
|
|
|
req = request.get_json(force=True)
|
|
|
|
|
|
|
|
bridge = net_bridge(req['NetworkID'])
|
|
|
|
_if_host, if_container = veth_pair(req['EndpointID'])
|
|
|
|
|
2019-08-23 16:25:55 +00:00
|
|
|
res = {
|
2019-08-23 16:07:54 +00:00
|
|
|
'InterfaceName': {
|
|
|
|
'SrcName': if_container,
|
2019-08-23 21:17:17 +00:00
|
|
|
'DstPrefix': bridge['ifname']
|
2019-08-23 16:25:55 +00:00
|
|
|
},
|
|
|
|
'StaticRoutes': []
|
|
|
|
}
|
2019-08-23 21:17:17 +00:00
|
|
|
for route in bridge.routes:
|
2019-08-24 13:14:01 +00:00
|
|
|
if route['type'] != rtypes['RTN_UNICAST']:
|
2019-08-23 21:17:17 +00:00
|
|
|
continue
|
|
|
|
|
2019-08-24 13:14:01 +00:00
|
|
|
if route['dst'] == '' or route['dst'] == '/0':
|
|
|
|
if route['family'] == socket.AF_INET and 'Gateway' not in res:
|
|
|
|
logger.info('Adding IPv4 gateway %s', route['gateway'])
|
|
|
|
res['Gateway'] = route['gateway']
|
|
|
|
elif route['family'] == socket.AF_INET6 and 'GatewayIPv6' not in res:
|
|
|
|
logger.info('Adding IPv6 gateway %s', route['gateway'])
|
|
|
|
res['GatewayIPv6'] = route['gateway']
|
2019-08-23 21:38:09 +00:00
|
|
|
elif route['gateway']:
|
2019-08-24 13:14:01 +00:00
|
|
|
dst = f'{route["dst"]}/{route["dst_len"]}'
|
|
|
|
logger.info('Adding route to %s via %s', dst, route['gateway'])
|
2019-08-23 21:38:09 +00:00
|
|
|
res['StaticRoutes'].append({
|
2019-08-24 13:14:01 +00:00
|
|
|
'Destination': dst,
|
2019-08-23 16:25:55 +00:00
|
|
|
'RouteType': 0,
|
2019-08-23 21:17:17 +00:00
|
|
|
'NextHop': route['gateway']
|
2019-08-23 16:25:55 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
return jsonify(res)
|
2019-08-23 16:07:54 +00:00
|
|
|
|
|
|
|
@app.route('/NetworkDriver.Leave', methods=['POST'])
|
|
|
|
def leave():
|
2019-08-23 21:17:17 +00:00
|
|
|
return jsonify({})
|