1# Copyright 2020 gRPC authors.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Channelz debugging tool for xDS test client/server.
15
16This is intended as a debugging / local development helper and not executed
17as a part of interop test suites.
18
19Typical usage examples:
20
21    # Show channel and server socket pair
22    python -m bin.run_channelz --flagfile=config/local-dev.cfg
23
24    # Evaluate setup for different security configurations
25    python -m bin.run_channelz --flagfile=config/local-dev.cfg --security=tls
26    python -m bin.run_channelz --flagfile=config/local-dev.cfg --security=mtls_error
27
28    # More information and usage options
29    python -m bin.run_channelz --helpfull
30"""
31import hashlib
32
33from absl import app
34from absl import flags
35from absl import logging
36
37from bin.lib import common
38from framework import xds_flags
39from framework import xds_k8s_flags
40from framework.infrastructure import gcp
41from framework.infrastructure import k8s
42from framework.rpc import grpc_channelz
43from framework.test_app import client_app
44from framework.test_app import server_app
45
46# Flags
47_SECURITY = flags.DEFINE_enum('security',
48                              default=None,
49                              enum_values=[
50                                  'mtls', 'tls', 'plaintext', 'mtls_error',
51                                  'server_authz_error'
52                              ],
53                              help='Show info for a security setup')
54flags.adopt_module_key_flags(xds_flags)
55flags.adopt_module_key_flags(xds_k8s_flags)
56# Running outside of a test suite, so require explicit resource_suffix.
57flags.mark_flag_as_required(xds_flags.RESOURCE_SUFFIX.name)
58flags.register_validator(xds_flags.SERVER_XDS_PORT.name,
59                         lambda val: val > 0,
60                         message="Run outside of a test suite, must provide"
61                         " the exact port value (must be greater than 0).")
62
63logger = logging.get_absl_logger()
64
65# Type aliases
66_Channel = grpc_channelz.Channel
67_Socket = grpc_channelz.Socket
68_ChannelState = grpc_channelz.ChannelState
69_XdsTestServer = server_app.XdsTestServer
70_XdsTestClient = client_app.XdsTestClient
71
72
73def debug_cert(cert):
74    if not cert:
75        return '<missing>'
76    sha1 = hashlib.sha1(cert)
77    return f'sha1={sha1.hexdigest()}, len={len(cert)}'
78
79
80def debug_sock_tls(tls):
81    return (f'local:  {debug_cert(tls.local_certificate)}\n'
82            f'remote: {debug_cert(tls.remote_certificate)}')
83
84
85def get_deployment_pods(k8s_ns, deployment_name):
86    deployment = k8s_ns.get_deployment(deployment_name)
87    return k8s_ns.list_deployment_pods(deployment)
88
89
90def debug_security_setup_negative(test_client):
91    """Debug negative cases: mTLS Error, Server AuthZ error
92
93    1) mTLS Error: Server expects client mTLS cert,
94       but client configured only for TLS.
95    2) AuthZ error: Client does not authorize server because of mismatched
96       SAN name.
97    """
98    # Client side.
99    client_correct_setup = True
100    channel: _Channel = test_client.wait_for_server_channel_state(
101        state=_ChannelState.TRANSIENT_FAILURE)
102    try:
103        subchannel, *subchannels = list(
104            test_client.channelz.list_channel_subchannels(channel))
105    except ValueError:
106        print("Client setup fail: subchannel not found. "
107              "Common causes: test client didn't connect to TD; "
108              "test client exhausted retries, and closed all subchannels.")
109        return
110
111    # Client must have exactly one subchannel.
112    logger.debug('Found subchannel, %s', subchannel)
113    if subchannels:
114        client_correct_setup = False
115        print(f'Unexpected subchannels {subchannels}')
116    subchannel_state: _ChannelState = subchannel.data.state.state
117    if subchannel_state is not _ChannelState.TRANSIENT_FAILURE:
118        client_correct_setup = False
119        print('Subchannel expected to be in '
120              'TRANSIENT_FAILURE, same as its channel')
121
122    # Client subchannel must have no sockets.
123    sockets = list(test_client.channelz.list_subchannels_sockets(subchannel))
124    if sockets:
125        client_correct_setup = False
126        print(f'Unexpected subchannel sockets {sockets}')
127
128    # Results.
129    if client_correct_setup:
130        print('Client setup pass: the channel '
131              'to the server has exactly one subchannel '
132              'in TRANSIENT_FAILURE, and no sockets')
133
134
135def debug_security_setup_positive(test_client, test_server):
136    """Debug positive cases: mTLS, TLS, Plaintext."""
137    test_client.wait_for_active_server_channel()
138    client_sock: _Socket = test_client.get_active_server_channel_socket()
139    server_sock: _Socket = test_server.get_server_socket_matching_client(
140        client_sock)
141
142    server_tls = server_sock.security.tls
143    client_tls = client_sock.security.tls
144
145    print(f'\nServer certs:\n{debug_sock_tls(server_tls)}')
146    print(f'\nClient certs:\n{debug_sock_tls(client_tls)}')
147    print()
148
149    if server_tls.local_certificate:
150        eq = server_tls.local_certificate == client_tls.remote_certificate
151        print(f'(TLS)  Server local matches client remote: {eq}')
152    else:
153        print('(TLS)  Not detected')
154
155    if server_tls.remote_certificate:
156        eq = server_tls.remote_certificate == client_tls.local_certificate
157        print(f'(mTLS) Server remote matches client local: {eq}')
158    else:
159        print('(mTLS) Not detected')
160
161
162def debug_basic_setup(test_client, test_server):
163    """Show channel and server socket pair"""
164    test_client.wait_for_active_server_channel()
165    client_sock: _Socket = test_client.get_active_server_channel_socket()
166    server_sock: _Socket = test_server.get_server_socket_matching_client(
167        client_sock)
168
169    logger.debug('Client socket: %s\n', client_sock)
170    logger.debug('Matching server socket: %s\n', server_sock)
171
172
173def main(argv):
174    if len(argv) > 1:
175        raise app.UsageError('Too many command-line arguments.')
176
177    # Must be called before KubernetesApiManager or GcpApiManager init.
178    xds_flags.set_socket_default_timeout_from_flag()
179
180    # Flags.
181    should_port_forward: bool = xds_k8s_flags.DEBUG_USE_PORT_FORWARDING.value
182    is_secure: bool = bool(_SECURITY.value)
183
184    # Setup.
185    gcp_api_manager = gcp.api.GcpApiManager()
186    k8s_api_manager = k8s.KubernetesApiManager(xds_k8s_flags.KUBE_CONTEXT.value)
187
188    # Server.
189    server_namespace = common.make_server_namespace(k8s_api_manager)
190    server_runner = common.make_server_runner(
191        server_namespace,
192        gcp_api_manager,
193        port_forwarding=should_port_forward,
194        secure=is_secure)
195    # Find server pod.
196    server_pod: k8s.V1Pod = common.get_server_pod(server_runner,
197                                                  xds_flags.SERVER_NAME.value)
198
199    # Client
200    client_namespace = common.make_client_namespace(k8s_api_manager)
201    client_runner = common.make_client_runner(
202        client_namespace,
203        gcp_api_manager,
204        port_forwarding=should_port_forward,
205        secure=is_secure)
206    # Find client pod.
207    client_pod: k8s.V1Pod = common.get_client_pod(client_runner,
208                                                  xds_flags.CLIENT_NAME.value)
209
210    # Ensure port forwarding stopped.
211    common.register_graceful_exit(server_runner, client_runner)
212
213    # Create server app for the server pod.
214    test_server: _XdsTestServer = common.get_test_server_for_pod(
215        server_runner,
216        server_pod,
217        test_port=xds_flags.SERVER_PORT.value,
218        secure_mode=is_secure)
219    test_server.set_xds_address(xds_flags.SERVER_XDS_HOST.value,
220                                xds_flags.SERVER_XDS_PORT.value)
221
222    # Create client app for the client pod.
223    test_client: _XdsTestClient = common.get_test_client_for_pod(
224        client_runner, client_pod, server_target=test_server.xds_uri)
225
226    with test_client, test_server:
227        if _SECURITY.value in ('mtls', 'tls', 'plaintext'):
228            debug_security_setup_positive(test_client, test_server)
229        elif _SECURITY.value in ('mtls_error', 'server_authz_error'):
230            debug_security_setup_negative(test_client)
231        else:
232            debug_basic_setup(test_client, test_server)
233
234    logger.info('SUCCESS!')
235
236
237if __name__ == '__main__':
238    app.run(main)
239