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)
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:
Documentation for origin is:
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.
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:
In gRPC C++, it’s similar to python:
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).
Issue closed, with satisfaction.