Minimal Docker for Networking Rust Binary

Viewed 342

I have a Rust binary I want to put in a docker image. To minimize the image size, I used http://github.com/larsks/dockerize/ which adds libc and a few nss library files. However it's failing to resolve DNS and I want to know what's the hidden system file dependency of reqwest or hyper.


Here is an example:

Cargo.toml

[package]                                                                                                                                                                                                                                  
name = "example"    
version = "0.1.0"    
edition = "2018"    
    
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html    
    
[dependencies]    
reqwest = { version = "0.11.4", default-features = false, features = ["json", "rustls-tls"] }    
tokio = { version = "1.8.1", features = [ "full" ] }    

src/main.rs

#[tokio::main]                                                                                                                                                                                                                            
async fn main() {    
    println!("{:?}", reqwest::get("https://bing.com").await);
}    

Building the image with certs and strace:

❯ dockerize --tag test --add-file /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt --add-file /usr/bin/strace /usr/bin/strace --add-file $(pwd)/target/debug/example /app/example

Successfully tagged test:latest

The resulting docker image:

│ Current Layer Contents ├───────────────────────────────────────────────────────────────────────────────────────────
Permission     UID:GID       Size  Filetree
-rw-r--r--         0:0       25 B  ├── Dockerfile
drwxr-xr-x         0:0      60 MB  ├── app
-rwxr-xr-x         0:0      60 MB  │   └── example
drwxr-xr-x         0:0     203 kB  ├── etc
-rw-r--r--         0:0      126 B  │   ├── group
-rw-r--r--         0:0       76 B  │   ├── nsswitch.conf
-rw-r--r--         0:0      513 B  │   ├── passwd
drwxr-xr-x         0:0     202 kB  │   └── ssl
drwxr-xr-x         0:0     202 kB  │       └── certs
-r--r--r--         0:0     202 kB  │           └── ca-certificates.crt
drwxr-xr-x         0:0     343 kB  ├── lib64
-rwxr-xr-x         0:0     222 kB  │   ├── ld-linux-x86-64.so.2
-rwxr-xr-x         0:0      40 kB  │   ├── libnss_compat.so.2
-rwxr-xr-x         0:0      31 kB  │   ├── libnss_dns.so.2
-rwxr-xr-x         0:0      51 kB  │   └── libnss_files.so.2
drwxr-xr-x         0:0     4.2 MB  └── usr
drwxr-xr-x         0:0     4.2 MB      └── lib
-rwxr-xr-x         0:0     2.2 MB          ├── libc.so.6
-rwxr-xr-x         0:0      23 kB          ├── libdl.so.2
-rw-r--r--         0:0     476 kB          ├── libgcc_s.so.1
-rwxr-xr-x         0:0     1.3 MB          ├── libm.so.6
-rwxr-xr-x         0:0      40 kB          ├── libnss_compat.so.2
-rwxr-xr-x         0:0      31 kB          ├── libnss_dns.so.2
-rwxr-xr-x         0:0      51 kB          ├── libnss_files.so.2
-rwxr-xr-x         0:0     154 kB          └── libpthread.so.0

Running the binary fails with "Device or resource busy" msg:

❯ docker run test /app/example
Err(reqwest::Error { kind: Request, url: Url { scheme: "https", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("bing.com")), port: None, path: "/", query: None, fragment: None }, source: hyper::Error(Connect, ConnectError("dns error", Os { code: 16, kind: ResourceBusy, message: "Device or resource busy" })) })

What's confusing is that ping binary in the same environment just works. This means reqwest or hyper needs additional setting or file. I ran strace to find out what they require but couldn't find it. I also tried the trust-dns feature of reqwest and it works for only some domains.

Here is strace of the binary:

❯ docker run test strace /app/example 2>&1 | rg open
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v3/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/glibc-hwcaps/x86-64-v2/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/tls/x86_64/x86_64/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/tls/x86_64/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/tls/x86_64/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/tls/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/x86_64/x86_64/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/x86_64/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/x86_64/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/libgcc_s.so.1", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libm.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/self/maps", O_RDONLY|O_CLOEXEC) = 3
openat(AT_FDCWD, "/proc/self/cgroup", O_RDONLY|O_CLOEXEC) = 3

What should I add to the image so the reqwest+hyper with tls works?

2 Answers

Hyper by default resolves DNS by getaddrinfo().
There are users experiencing similar issues using getaddrinfo() within docker.

#include "netdb.h"                                                                                                                                                                                                                                                                                                                                       
#include "stdio.h"    
    
int main( int argc, const char* argv[] )    
{    
   const struct addrinfo* hints = 0;    
   struct addrinfo* aihead = 0;    
    
   int error = getaddrinfo( "www.google.com", "80", hints, &aihead);    
   if ( error )    
   {    
      printf( "Got error: %s\n", gai_strerror( error ) );    
      return error;    
   } else {    
      printf( "got dns ok." );    
   }    
}    
   

The solution is to copy /usr/lib/libnss* and /usr/lib/libresolv* as well in addition to the current listing in the question.

An alternative solution is to configure Hyper not to use getaddrinfo and keep the address resolution entirely in rust.

You can do this in Reqwest by setting the trust_dns feature on the crate, which makes it use trust_dns_resolver instead of getaddrinfo.

Related