Skip to content

Commit

Permalink
ndb: Merge pull request #1180 from svinota/ndb-e1178-link-rename
Browse files Browse the repository at this point in the history
ndb link rename vs. altnames

Bug-Url: #1178
Bug-Url: #1180
  • Loading branch information
svinota authored Feb 27, 2024
2 parents 34d0768 + 684b598 commit 7a66831
Show file tree
Hide file tree
Showing 4 changed files with 158 additions and 43 deletions.
18 changes: 13 additions & 5 deletions pyroute2/iproute/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
NeighbourFieldFilter,
NeighbourIPRouteFilter,
)
from pyroute2.requests.probe import ProbeFieldFilter
from pyroute2.requests.route import RouteFieldFilter, RouteIPRouteFilter
from pyroute2.requests.rule import RuleFieldFilter, RuleIPRouteFilter

Expand Down Expand Up @@ -425,15 +426,22 @@ def probe(self, command, **kwarg):
'''
msg = probe_msg()
#
request = (
RequestProcessor(context=kwarg, prime=kwarg)
.apply_filter(ProbeFieldFilter())
.finalize()
)
#
msg['family'] = AF_INET
msg['proto'] = 1
msg['port'] = 0
msg['port'] = kwarg.get('port', 0)
msg['dst_len'] = 32
#
msg['attrs'].append(['PROBE_KIND', kwarg.get('kind', 'ping')])
msg['attrs'].append(['PROBE_DST', kwarg.get('dst', '0.0.0.0')])
msg['attrs'].append(['PROBE_NUM', kwarg.get('num', 1)])
msg['attrs'].append(['PROBE_TIMEOUT', kwarg.get('timeout', 1)])
for key, value in request.items():
nla = probe_msg.name2nla(key)
if msg.valid_nla(nla) and value is not None:
msg['attrs'].append([nla, value])

# iterate the results immediately, don't defer the probe run
return tuple(self.nlm_request(msg, msg_type=RTM_NEWPROBE, msg_flags=1))

Expand Down
45 changes: 39 additions & 6 deletions pyroute2/ndb/objects/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ class Interface(RTNL_Object):
'alt_ifname_list': lambda x: list(json.loads(x or '[]'))
}
field_filter = LinkFieldFilter
old_ifname = None

@classmethod
def _count(cls, view):
Expand Down Expand Up @@ -847,17 +848,25 @@ def add_altname(self, ifname):
new_list = set(self['alt_ifname_list'])
new_list.add(ifname)
self['alt_ifname_list'] = list(new_list)
return self

@check_auth('obj:modify')
def del_altname(self, ifname):
new_list = set(self['alt_ifname_list'])
new_list.remove(ifname)
self['alt_ifname_list'] = list(new_list)
return self

@check_auth('obj:modify')
def __setitem__(self, key, value):
if key == 'peer':
dict.__setitem__(self, key, value)
elif key == 'ifname':
if value in self['alt_ifname_list']:
self.del_altname(value)
if key in self and self.old_ifname is None:
self.old_ifname = self[key]
super(Interface, self).__setitem__(key, value)
elif key == 'target' and self.state == 'invalid':
dict.__setitem__(self, key, value)
elif key == 'net_ns_fd' and self.state == 'invalid':
Expand Down Expand Up @@ -977,9 +986,19 @@ def make_req(self, prime):
return req

@check_auth('obj:modify')
def apply_altnames(self, alt_ifname_setup):
alt_ifname_remove = set(self['alt_ifname_list']) - alt_ifname_setup
alt_ifname_add = alt_ifname_setup - set(self['alt_ifname_list'])
def apply_altnames(
self, alt_ifname_setup, alt_ifname_current, old_ifname=None
):
if 'alt_ifname_list' in self.changed:
self.changed.remove('alt_ifname_list')
if alt_ifname_current is None:
# load the current state
self.load_from_system()
self.load_sql(set_state=False)
alt_ifname_current = set(self['alt_ifname_list'])

alt_ifname_remove = alt_ifname_current - alt_ifname_setup
alt_ifname_add = alt_ifname_setup - alt_ifname_current
for ifname in alt_ifname_remove:
self.sources[self['target']].api(
'link', 'property_del', index=self['index'], altname=ifname
Expand All @@ -988,8 +1007,11 @@ def apply_altnames(self, alt_ifname_setup):
self.sources[self['target']].api(
'link', 'property_add', index=self['index'], altname=ifname
)
# reload alt ifnames from the system to check the state
self.load_from_system()
self.load_sql(set_state=False)
if old_ifname is not None and old_ifname in self['alt_ifname_list']:
alt_ifname_setup.add(old_ifname)
if set(self['alt_ifname_list']) != alt_ifname_setup:
raise Exception('could not setup alt ifnames')

Expand All @@ -1002,9 +1024,14 @@ def apply(self, rollback=False, req_filter=None, mode='apply'):
setns = self.state.get() == 'setns'
remove = self.state.get() == 'remove'
alt_ifname_setup = set(self['alt_ifname_list'])
if 'alt_ifname_list' in self.changed:
self.changed.remove('alt_ifname_list')
old_ifname = self.old_ifname if 'ifname' in self.changed else None
try:
if 'index' in self and (
self.old_ifname or 'alt_ifname_list' in self.changed
):
self.apply_altnames(alt_ifname_setup, None)
if 'alt_ifname_list' in self.changed:
self.changed.remove('alt_ifname_list')
super(Interface, self).apply(rollback, req_filter, mode)
if setns:
self.load_value('target', self['net_ns_fd'])
Expand All @@ -1013,7 +1040,9 @@ def apply(self, rollback=False, req_filter=None, mode='apply'):
if spec:
self.state.set('system')
if not remove:
self.apply_altnames(alt_ifname_setup)
self.apply_altnames(
alt_ifname_setup, set(self['alt_ifname_list']), old_ifname
)

except NetlinkError as e:
if (
Expand Down Expand Up @@ -1059,6 +1088,8 @@ def req_filter(req):
self.apply(rollback, req_filter, mode)
else:
raise
finally:
self.old_ifname = None
if ('net_ns_fd' in self.get('peer', {})) and (
self['peer']['net_ns_fd'] in self.view.ndb.sources
):
Expand Down Expand Up @@ -1101,13 +1132,15 @@ def hook_apply(self, method, **spec):
self.ndb._event_queue.put(update)

def load_from_system(self):
self.load_event.clear()
(
self.ndb._event_queue.put(
self.sources[self['target']].api(
self.api, 'get', index=self['index']
)
)
)
self.load_event.wait(1)

def load_sql(self, *argv, **kwarg):
spec = super(Interface, self).load_sql(*argv, **kwarg)
Expand Down
125 changes: 93 additions & 32 deletions pyroute2/netlink/rtnl/probe_msg.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import errno
import json
import shutil
import socket
import ssl
import subprocess

from pyroute2.netlink import nlmsg
Expand All @@ -24,51 +27,109 @@ class probe_msg(nlmsg):
('PROBE_KIND', 'asciiz'),
('PROBE_STDOUT', 'asciiz'),
('PROBE_STDERR', 'asciiz'),
('PROBE_SRC', 'target'),
('PROBE_DST', 'target'),
('PROBE_SRC', 'asciiz'),
('PROBE_DST', 'asciiz'),
('PROBE_NUM', 'uint8'),
('PROBE_TIMEOUT', 'uint8'),
('PROBE_HOSTNAME', 'asciiz'),
('PROBE_SSL_VERIFY', 'uint8'),
('PROBE_SSL_VERSION', 'asciiz'),
('PROBE_SSL_CERT_JSON', 'asciiz'),
('PROBE_SSL_CERT_DER', 'cdata'),
)


def proxy_newprobe(msg, nl):
def probe_ping(msg, nl):
num = msg.get('num')
timeout = msg.get('timeout')
dst = msg.get('dst')
kind = msg.get('kind')
args = [shutil.which(kind), '-c', f'{num}', '-W', f'{timeout}', f'{dst}']
if args[0] is None:
raise NetlinkError(errno.ENOENT, 'probe not found')

if kind.endswith('ping'):
args = [
shutil.which(kind),
'-c',
f'{num}',
'-W',
f'{timeout}',
f'{dst}',
]
if args[0] is None:
raise NetlinkError(errno.ENOENT, 'probe not found')

process = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
process = subprocess.Popen(
args, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
try:
out, err = process.communicate(timeout=timeout)
if out:
msg['attrs'].append(['PROBE_STDOUT', out])
if err:
msg['attrs'].append(['PROBE_STDERR', err])
except subprocess.TimeoutExpired:
process.terminate()
raise NetlinkError(errno.ETIMEDOUT, 'timeout expired')
finally:
process.stdout.close()
process.stderr.close()
return_code = process.wait()
if return_code != 0:
raise NetlinkError(errno.EHOSTUNREACH, 'probe failed')


def probe_tcp(msg, nl, close=True):
timeout = msg.get('timeout')
dst = msg.get('dst')
port = msg.get('port')
connection = None
try:
connection = socket.create_connection((dst, port), timeout=timeout)
except ConnectionRefusedError:
raise NetlinkError(errno.ECONNREFUSED, 'connection refused')
except TimeoutError:
raise NetlinkError(errno.ETIMEDOUT, 'timeout expired')
except Exception:
raise NetlinkError(errno.ECOMM, 'probe failed')
finally:
if close and connection is not None:
connection.close()
return connection


def probe_ssl(msg, nl):
hostname = msg.get('hostname') or msg.get('dst')
context = ssl.create_default_context()
context.verify_mode = msg.get('ssl_verify', ssl.CERT_REQUIRED)
with probe_tcp(msg, nl, close=False) as connection:
try:
out, err = process.communicate(timeout=timeout)
if out:
msg['attrs'].append(['PROBE_STDOUT', out])
if err:
msg['attrs'].append(['PROBE_STDERR', err])
except subprocess.TimeoutExpired:
process.terminate()
raise NetlinkError(errno.ETIMEDOUT, 'timeout expired')
finally:
process.stdout.close()
process.stderr.close()
return_code = process.wait()
if return_code != 0:
raise NetlinkError(errno.EHOSTUNREACH, 'probe failed')
with context.wrap_socket(
connection, server_hostname=hostname
) as ssl_wrap:
version = ssl_wrap.version()
peer_cert_json = ssl_wrap.getpeercert(binary_form=False)
peer_cert_der = ssl_wrap.getpeercert(binary_form=True)
if peer_cert_json is not None:
msg['attrs'].append(
['PROBE_SSL_CERT_JSON', json.dumps(peer_cert_json)]
)
if peer_cert_der is not None:
msg['attrs'].append(['PROBE_SSL_CERT_DER', peer_cert_der])
if version is not None:
msg['attrs'].append(['PROBE_SSL_VERSION', version])
except ssl.SSLError as e:
code = errno.EPROTO
if e.reason == 'UNSUPPORTED_PROTOCOL':
code = errno.EPROTONOSUPPORT
elif e.reason == 'CERTIFICATE_VERIFY_FAILED':
code = errno.EACCES
raise NetlinkError(code, e.strerror)
except Exception:
raise NetlinkError(errno.ECOMM, 'probe failed')


def proxy_newprobe(msg, nl):
kind = msg.get('kind')

if kind.endswith('ping'):
probe_ping(msg, nl)
elif kind == 'tcp':
probe_tcp(msg, nl)
elif kind == 'ssl':
probe_ssl(msg, nl)
else:
raise NetlinkError(errno.ENOTSUP, 'probe type not supported')

msg.reset()
msg.encode()
return {'verdict': 'return', 'data': msg.data}
13 changes: 13 additions & 0 deletions pyroute2/requests/probe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from .common import NLAKeyTransform


class ProbeFieldFilter(NLAKeyTransform):
_nla_prefix = 'PROBE_'

def finalize(self, context):
if 'kind' not in context:
context['kind'] = 'ping'
if 'num' not in context:
context['num'] = 1
if 'timeout' not in context:
context['timeout'] = 1

0 comments on commit 7a66831

Please sign in to comment.