tonic: unexpected behavior with tls encryption - protocol error with invalid compression flag

Bug Report

Version

tonic = { version = "0.7.2", features = [
    "prost",
    "tls",
    "compression",
    "transport",
] }

Platform

Linux 20.04 x86_64

Description

Hey, we’re having issues using the tonic grpc client with tls encryption to talk to the gRPC service on the spot robot from Boston Dynamics. We’re able to connect to the gRPC AuthService using both the grpcio lib in Rust and the grpcio library in Python, however we’re unable to make it work using the tonic framework.

PS! Boston Dynamics spot protobuf definitions can be found here

The following code snippet with tonic fails:

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    const ENDPOINT: &str = "https://192.168.80.3:443";

    let certs = tokio::fs::read("./integrations/spot-integration/src/resources/robot.pem").await?;

    let token = &base64::encode(b"user:spot_password").to_string();
    let basic_token = format!("Basic {}", token);
    let header_value: MetadataValue<_> = basic_token.parse()?;

    let tls_config = ClientTlsConfig::new()
        .ca_certificate(Certificate::from_pem(certs.as_slice()))
        .domain_name("auth.spot.robot");

    let channel = Channel::from_static(ENDPOINT).tls_config(tls_config)?.connect().await?;

    //let mut auth_client = AuthServiceClient::<tonic::transport::Channel>::new(channel);
    let system_time_epoch = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
    let timestamp = Timestamp {
        seconds: (system_time_epoch.as_secs() as i64),
        nanos: 0,
    };
    let auth_token_request = GetAuthTokenRequest {
        header: Some(RequestHeader {
            request_timestamp: Some(timestamp),
            client_name: "client_name".to_string(),
            disable_rpc_logging: false,
        }),
        username: "user".to_string(),
        password: "spot_password".to_string(),
        ..Default::default()
    };

    let mut client =
        AuthServiceClient::<tonic::transport::Channel>::with_interceptor(channel, |mut req: Request<()>| {
            req.metadata_mut().insert("authorization", header_value.clone());
            // req.metadata_mut().insert("grpc-encoding", "identity".parse().unwrap());
            // req.metadata_mut()
            //     .insert("grpc-accept-encoding", "gzip,".parse().unwrap());
            // req.metadata_mut().remove("te");
            req.metadata_mut()
                .insert("content-type", "application/grpc+proto".parse().unwrap());

            println!("metadata: {:?}", req.metadata());
            Ok(req)
        });

    println!("{:?}", &client);
    let response = client.get_auth_token(auth_token_request).await.unwrap();

    // auth_client.get_auth_token likely returns an tonic::Status Err, won't reach these statements
    println!("Status {}", response.get_ref().status);
    println!("Token {}", response.get_ref().token);
    Ok(())
}

with the following message:

thread 'main' panicked at 'called Result::unwrap() on an Err value: Status { code: Internal, message: "protocol error: received message with invalid compression flag: 31 (valid flags are 0 and 1) while receiving response with status: 404 Not Found", metadata: MetadataMap { headers: {"server": "nginx/1.20.2", "date": "Thu, 07 Jul 2022 19:56:14 GMT", "content-type": "text/html", "x-frame-options": "SAMEORIGIN", "strict-transport-security": "max-age=31536000; includeSubDomains", "content-encoding": "gzip"} }, source: None }'

Anyone has an idea why this does not work?

The following examples with grpcio in Python and Rust works:

Python:

import grpc
import pkg_resources
from bosdyn.api.auth_service_pb2_grpc import AuthServiceStub
from bosdyn.api import auth_pb2
from bosdyn.api.header_pb2 import RequestHeader
from base64 import b64encode
from google.protobuf.timestamp_pb2 import Timestamp
import time

class Auth(grpc.AuthMetadataPlugin):
    "Inject an authorization header with a bearer token in GRPC requests"

    def __init__(self, username, password):#: str, token_supplier: Callable[[], str]):

        self.username = username
        self.password=password

    def __call__(
        self,
        context: grpc.AuthMetadataContext,
        callback: grpc.AuthMetadataPluginCallback,
    ) -> None:
        userAndPass = b64encode(f"{self.username}:{self.password}".encode("ascii")).decode("ascii")
        callback(
            (
                ("authorization", "Basic " + userAndPass),
            ),
            None,
        )
target = "192.168.80.3:443"
cert = pkg_resources.resource_stream('bosdyn.client.resources', 'robot.pem').read()
#print(cert)
auth_plugin=Auth(username="user", password = "spot_password")

credentials = grpc.composite_channel_credentials(
        grpc.ssl_channel_credentials(root_certificates=cert),
        grpc.metadata_call_credentials(auth_plugin),
    )
options = [('grpc.ssl_target_name_override', "auth.spot.robot")]
channel = grpc.secure_channel(target, credentials, options=options)



stub = AuthServiceStub(channel)
print(stub)
header = RequestHeader(request_timestamp = Timestamp(seconds=int(time.time())), client_name="client_name")
request = auth_pb2.GetAuthTokenRequest(header=header, username="user", password= "spot_password")

print(request)
print(channel)

print("-----------Response-----------")
print(stub.GetAuthToken(request))
print("------------------------------")

Rust

use grpcio::{ChannelBuilder, ChannelCredentialsBuilder, EnvBuilder};
use protobuf::well_known_types::Timestamp;
use protos::auth::GetAuthTokenRequest;
use protos::auth_service_grpc::AuthServiceClient;
use protos::header::RequestHeader;
use std::fs;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

fn main() {
    let env = Arc::new(EnvBuilder::new().build());

    let cert = fs::read("./resources/robot.pem").unwrap();
    let cred = ChannelCredentialsBuilder::new().root_cert(cert).build();

    let channel = ChannelBuilder::new(env)
        .override_ssl_target("auth.spot.robot")
        .secure_connect("192.168.80.3:443", cred);

    let auth_client = AuthServiceClient::new(channel);

    let system_time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();

    let auth_token_request = GetAuthTokenRequest {
        header: ::protobuf::SingularPtrField::some(RequestHeader {
            request_timestamp: ::protobuf::SingularPtrField::some(Timestamp {
                seconds: system_time.as_secs() as i64,
                nanos: system_time.subsec_nanos() as i32,
                ..Default::default()
            }),
            client_name: "some_client_name".to_string(),
            ..Default::default()
        }),
        username: "user".to_string(),
        password: "spot_password".to_string(),
        ..Default::default()
    };

    println!("request {:?}", auth_token_request);

    let response = auth_client.get_auth_token(&auth_token_request).unwrap();

    println!("Status {:?}", response.status);
    println!("Token {}", response.token);
}

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 26 (10 by maintainers)

Most upvoted comments

Finally figured this one out. Solution: use the origin property in Channel, and add the origin the server expects. In my case, for an id request:

    let channel  = Channel::from_static(robot_address) // robot address is something like https://192.168.8.8:443
        .origin("https://id.spot.robot".parse().unwrap()) // <- this is the critical call
        .tls_config(tls)?
        .connect().await?;

Documentation for origin is:

 /// Override the `origin`, mainly useful when you are reaching a Server/LoadBalancer
    /// which serves multiple services at the same time.
    /// It will play the role of SNI (Server Name Indication).

I saw similar 404 errors from this server when using other gRPC libraries (like Go). When the origin wasn’t properly set in the TLS layer. That sent me digging.

In the case of Tonic, I wasn’t manually overriding the server name correctly. Even though the IP address was correct, and the TLS domain was correct, the TLS domain wasn’t transferring to the gRPC call.

This server needs something like “https://id.spot.robot:443” + “/bosdyn.api.RobotIdService/GetRobotId” (the latter is added by the framework, not the user).

For those coming from or familiar with other languages that leverage gRPC:

In gRPC go, it’s handled when you load the root TLS cert from a file, through the serverNameOverride parameter. This handles cert verification and seemingly passes the same origin in the http request.

func NewClientTLSFromFile(certFile, serverNameOverride string) (TransportCredentials, error) {...} // in tls.go

In gRPC python, you set the grpc.ssl_target_name_override variable, and pass it to the grpc.secure_channel method. It’s handled something like this:

options = [('grpc.ssl_target_name_override', "auth.spot.robot")]
channel = grpc.secure_channel(target, credentials, options=options)

In gRPC C++, it’s similar to python:

    grpc::ChannelArguments args;
    args.SetSslTargetNameOverride("id.spot.robot");
    auto channel_creds = grpc::SslCredentials(options);
    return grpc::CreateCustomChannel(address, channel_creds, args);

When using Tonic, you set the domain for the TLS only (id.spot.robot, in my case), afterwards you set the origin in the channel (https://id.spot.robot, in my case).

       let tls = ClientTlsConfig::new()
        .domain_name(domain) // id.spot.robot
        .ca_certificate(server_cert);

 let channel  = Channel::from_static(robot_address)
        .origin("https://id.spot.robot".parse().unwrap())
        .tls_config(tls)?
        .connect().await?;

Issue closed, with satisfaction.