惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Secure Thoughts
S
Securelist
T
The Exploit Database - CXSecurity.com
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Schneier on Security
Schneier on Security
P
Proofpoint News Feed
Latest news
Latest news
C
CXSECURITY Database RSS Feed - CXSecurity.com
Google DeepMind News
Google DeepMind News
Microsoft Security Blog
Microsoft Security Blog
美团技术团队
T
Threat Research - Cisco Blogs
Know Your Adversary
Know Your Adversary
AWS News Blog
AWS News Blog
V2EX - 技术
V2EX - 技术
云风的 BLOG
云风的 BLOG
博客园 - 聂微东
腾讯CDC
月光博客
月光博客
G
Google Developers Blog
F
Fortinet All Blogs
C
Cybersecurity and Infrastructure Security Agency CISA
Hacker News - Newest:
Hacker News - Newest: "LLM"
阮一峰的网络日志
阮一峰的网络日志
Last Week in AI
Last Week in AI
Apple Machine Learning Research
Apple Machine Learning Research
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
爱范儿
爱范儿
WordPress大学
WordPress大学
Spread Privacy
Spread Privacy
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
T
Threatpost
N
News and Events Feed by Topic
Y
Y Combinator Blog
J
Java Code Geeks
N
News and Events Feed by Topic
T
Troy Hunt's Blog
Project Zero
Project Zero
博客园 - 【当耐特】
Microsoft Azure Blog
Microsoft Azure Blog
C
CERT Recently Published Vulnerability Notes
小众软件
小众软件
有赞技术团队
有赞技术团队
罗磊的独立博客
Martin Fowler
Martin Fowler
L
LINUX DO - 最新话题
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Proofpoint News Feed

Ivan on Containers, Kubernetes, and Server-Side

A grounded take on agentic coding for production environments Server-Side Playgrounds Reimagined: Build, Boot, and Network Your Own Virtual Labs [not a] Kubernetes 101 - Pods, Deployments, and Services As an Attempt To Automate Age-Old Infra Patterns JavaScript or TypeScript? How To Benefit From the Dichotomy On Software Design... and Good Writing Building a Firecracker-Powered Course Platform To Learn Docker and Kubernetes How To Publish a Port of a Running Container What Actually Happens When You Publish a Container Port A Visual Guide to SSH Tunnels: Local and Remote Port Forwarding Debugging Containers Like a Pro Docker: How To Debug Distroless And Slim Containers How To Extract Container Image Filesystem Using Docker | iximiuz Labs In Pursuit of Better Container Images: Alpine, Distroless, Apko, Chisel, DockerSlim, oh my! How To Start Programming In Go: Advice For Fellow DevOps Engineers Kubernetes Ephemeral Containers and kubectl debug Command How To Develop Kubernetes CLIs Like a Pro Docker Container Commands Explained: Understand, Don't Memorize | iximiuz Labs Learning Docker with Docker - Toying With DinD For Fun And Profit How To Extend Kubernetes API - Kubernetes vs. Django The Influence of Plumbing on Programming How To Call Kubernetes API from Go - Types and Common Machinery How To Call Kubernetes API using Simple HTTP Client Kubernetes API Basics - Resources, Kinds, and Objects OpenFaaS - Run Containerized Functions On Your Own Terms Learning Containers From The Bottom Up Docker Containers vs. Kubernetes Pods - Taking a Deeper Look | iximiuz Labs Learn-by-Doing Platforms for Dev, DevOps, and SRE Folks How HTTP Keep-Alive can cause TCP race condition How to Work with Container Images Using ctr | iximiuz Labs Multiple Containers, Same Port, no Reverse Proxy... Exploring Go net/http Package - On How Not To Set Socket Options Disposable Local Development Environments with Vagrant, Docker, and Arkade DevOps, SRE, and Platform Engineering My Choice of Programming Languages Prometheus Is Not a TSDB How to learn PromQL with Prometheus Playground Prometheus Cheat Sheet - Basics (Metrics, Labels, Time Series, Scraping) Rust - Writing Parsers With nom Parser Combinator Framework pq - parse and query log files as time series Prometheus Cheat Sheet - Moving Average, Max, Min, etc (Aggregation Over Time) Prometheus Cheat Sheet - How to Join Multiple Metrics (Vector Matching) The Need For Slimmer Containers Understanding Rust Privacy and Visibility Model Bridge vs. Switch: Takeaways from a Real Data Center Tour | iximiuz Labs From LAN to VXLAN: Networking Basics for Non-Network Engineers | iximiuz Labs KiND - How I Wasted a Day Loading Local Docker Images Go, HTTP handlers, panic, and deadlocks Exploring Kubernetes Operator Pattern Making Sense Out Of Cloud Native Buzz Service Discovery in Kubernetes: Combining the Best of Two Worlds API Developers Never REST How Container Networking Works: Building a Bridge Network From Scratch | iximiuz Labs Traefik: canary deployments with weighted load balancing Service Proxy, Pod, Sidecar, oh my! You Need Containers To Build Images You Don't Need an Image To Run a Container Not Every Container Has an Operating System Inside Working with container images in Go Master Go While Learning Containers Implementing Container Runtime Shim: Interactive Containers How to use Flask with gevent (uWSGI and Gunicorn editions) My 10 Years of Programming Experience Implementing Container Runtime Shim: First Code Implementing Container Runtime Shim: runc Kubernetes Repository On Flame Dealing with process termination in Linux (with Rust examples) conman - [the] Container Manager: Inception Journey From Containerization To Orchestration And Beyond Illustrated introduction to Linux iptables From Docker Container to Bootable Linux Disk Image Пишем свой веб-сервер на Python: протокол HTTP 9001 способ создать веб-сервер на Python Explaining async/await in 200 lines of code Explaining event loop in 100 lines of code Save the day with gevent Пишем свой веб-сервер на Python: процессы, потоки и асинхронный I/O Truly optional scalar types in protobuf3 (with Go examples) Node.js Writable streams distilled Node.js Readable streams distilled How to on starting processes (mostly in Linux) Дайджест интересных ссылок – Июль 2016 Пишем свой веб-сервер на Python: сокеты Наследование в JavaScript Мастерить!
Linux PTY - How docker attach and docker exec Commands Work Inside
Ivan Velichko · 2019-08-11 · via Ivan on Containers, Kubernetes, and Server-Side

This article is about the internals of the attach and exec commands. For the higher-level explanation of the commands, see - Containers 101: attach vs. exec - what's the difference?

Have you ever been wondering how docker (or kubectl) attach command is implemented under the hood? If so, you're in the right place! This article covers the basics of Linux pseudoterminal capabilities and continuously shows how attach-like feature can be implemented in a ridiculously small amount of code.

Level up your server-side game — join 20,000 engineers getting insightful learning materials straight to their inbox.

Why PTY is needed

When a user starts a normal program in the console usually its stdin, stdout, and stderr streams will be connected to the controlling terminal of the session. It means that everything the user types in on the console goes to the program's standard input and everything the program prints goes back to the console:

me@host ~ $ cat
hi there
hi there

Apart from that, the controlling terminal is also responsible for signal handling. When a user presses Ctrl-C on his keyboard, no keystroke is usually delivered to the foreground program. The terminal operating in a conventional mode just omits the input byte sequence and instead sends a SIGINT signal to the foreground program (or more precisely the foreground process group):

me@host ~ $ cat
press Ctrl-C to kill me
press Ctrl-C to kill me
^C
me@host ~ $

A much better explanation of the terminal subsystem can be found in this article.

On the other hand, when a user wants to start a daemon the good old double fork technique is needed (or more precisely fork -> setsid -> fork). And the main purpose of it is to detach the daemon from the controlling terminal of the session.

But what if we need a daemonized process controlled by a terminal? Look at this perfectly legit docker use case:

$ docker run -it -d debian:latest bash
b1f7dab2d0629b1094e72fb6aff9f95cffab4643e8c7765185a4101694b148ff

$ docker attach b1f7dab2d062
root@b1f7dab2d062:/# ping ya.ru
PING ya.ru (87.250.250.242) 56(84) bytes of data.
64 bytes from ya.ru (87.250.250.242): icmp_seq=1 ttl=61 time=39.0 ms
64 bytes from ya.ru (87.250.250.242): icmp_seq=2 ttl=61 time=50.8 ms
64 bytes from ya.ru (87.250.250.242): icmp_seq=3 ttl=61 time=39.5 ms
^C
--- ya.ru ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 15ms
rtt min/avg/max/mdev = 39.030/43.097/50.781/5.441 ms

Even though we started a container completely detached from the console, we managed to connect to it later and even delivered a SIGINT by pressing Ctrl-C. Under the hood of this trick lies Linux pseudoterminal interface (PTY):

A pseudoterminal (sometimes abbreviated "pty") is a pair of virtual
character devices that provide a bidirectional communication channel.
One end of the channel is called the master; the other end is called
the slave.  The slave end of the pseudoterminal provides an interface
that behaves exactly like a classical terminal.  A process that
expects to be connected to a terminal, can open the slave end of a
pseudoterminal and then be driven by a program that has opened the
master end.  Anything that is written on the master end is provided
to the process on the slave end as though it was input typed on a
terminal.  For example, writing the interrupt character (usually
control-C) to the master device would cause an interrupt signal
(SIGINT) to be generated for the foreground process group that is
connected to the slave.  Conversely, anything that is written to the
slave end of the pseudoterminal can be read by the process that is
connected to the master end.

The master end of the PTY pair is just a file descriptor. When docker launches bash process in the example above, it allocates a PTY pair and sets the slave end as a controlling terminal for bash. When a user attach-es to the running container, docker just binds his stdin & stdout to the master file descriptor. Nothing restricts us actually from implementing a client-server system here. The PTY pair may reside on the server-side when the user's console be on a local machine and the data transfer can easily go over the network. Let's try to implement it!

Remote:
    +----------+                                      +----------------+
    |   shim   | <-- [pty] -- read/write -- [pts] --> |   ping ya.ru   |
    +----------+                                      +----------------+
          |
          |
      [network]
          |
Local:    |
    +----------+
    |  client  | <-- [terminal in RAW mode] --> user via xterm (iterm2, etc).
    +----------+

Server PTY program example

We are going to start with the server-side. Let's create a PTY pair (as usually error handling is omitted for brevity and the full version of the code can be found on GitHub). To be as lightweight as possible or server shim is written in C:

#define PTSNAME_SIZE 1024

struct pt_info {
  int master_fd;
  char slave_name[PTSNAME_SIZE];
};

void create_pt(struct pt_info *p) {
  p->master_fd = posix_openpt(O_RDWR);
  grantpt(p->master_fd);
  unlockpt(p->master_fd);
  ptsname_r(p->master_fd, p->slave_name, PTSNAME_SIZE);
}

And start a process controlled by the pseudoterminal:

void run_slave(struct pt_info *pti, char *const command[]) {
  close(pti->master_fd);  // we don't need it on the slave side
  setsid();               // new terminal => new session

  int fds = open(pti->slave_name, O_RDWR);
  if (fds >= 0) {
      dup2(fds, 0);  // bind stdin to termianl
      dup2(fds, 1);  // bind stdout to termianl
      dup2(fds, 2);  // bind stderr to termianl
      close(fds);
      execv(command[0], command);
  } else {
      perror("open(pts_name) failed");
  }
  _exit(127);
}

int main(int argc, char *argv[]) {
    struct pt_info pti;
    create_pt(&pti);

    pid = fork();
    if (pid == 0) {
      run_slave(&pti, argv + 2);
    } else {
      // run master code...
    }
    return 0;
}

The only missing part at the moment is binding of the master fd with the incoming socket connections. Let's start a server socket, and for each accepted connection we will be forwarding bytes from it to the master fd and bytes read from the master fd back to the connection:

int socket_listen(const char *port) {
  // sock = socket() && bind() && listen()
  return sock;
}

int write_all(int fd, const void *buf, size_t count) {
  // while remain > 0: write(fd, buf, remain)
  return 0;
}

int epoll_add_fd(int epoll, int fd) {
  struct epoll_event ev;
  ev.data.fd = fd;
  ev.events = EPOLLIN;
  return epoll_ctl(epoll, EPOLL_CTL_ADD, fd, &ev);
}

struct atsock {
  int fd;
  struct atsock *next;
};

struct atsock *atsock_new(int conn_fd) {
  /* allocate new bucket */
}

struct atsock *atsock_save(struct atsock *head, struct atsock *new) {
  /* add new bucket to linked list */
}

struct atsock *atsock_erase(struct atsock *head, int conn_fd) {
  /* remove bucket from linked list */
}

void run_master(struct pt_info *pti, const char *port) {
  int attach_sock = socket_listen(port);

  int epoll = epoll_create1(0);
  epoll_add_fd(epoll, pti->master_fd);
  epoll_add_fd(epoll, attach_sock);

  struct atsock *head = NULL;
  struct epoll_event evlist[64];
  while (1) {
    int nready = epoll_wait(epoll, evlist, 64, -1);

    for (int i = 0; i < nready; i++) {
      int fd = evlist[i].data.fd;
      if (evlist[i].events & EPOLLIN == 0) {
        continue;
      }

      if (fd == pti->master_fd) {
        // read from pty and forward data to each attached socket
        char buf[1024];
        int nread = read(fd, buf, 1024);
        struct atsock *cur = head;
        while (nread && cur) {
          write_all(cur->fd, buf, nread);
          cur = cur->next;
        }
        continue;
      }

      if (fd == attach_sock) {
        // accept incoming connection
        int conn = accept(fd, NULL, NULL);
        head = atsock_save(head, atsock_new(conn));
        epoll_add_fd(epoll, conn);
        printf("accepted new sock conn\n");
        continue;
      }

      // read from attached socket and forward to pty
      char buf[1024];
      int nread = read(fd, buf, 1024);
      if (nread == 0) {
        // hanndle disconnect
        head = atsock_erase(head, fd);
        epoll_ctl(epoll, EPOLL_CTL_DEL, fd, NULL);
        printf("disconnected sock\n");
        continue;
      }

      write_all(pti->master_fd, buf, nread);
    }
  }

  // cleanup code
}

Finally, we can run our code with a simple telnet session as a client:

Demo - server PTY program example.

Client PTY program example

Looks like our server-side works fine but the telnet client is not that handy. We can switch it to character mode, but we still need to disable echoing of the user input and maybe apply some more adjustments.

Let's hack a tiny yet much more usable client. The idea is simple - since the process we want to attach is already controlled by an instance of PTY on the server-side, we need to disable user input handling capabilities of the terminal on the client-side. I.e. switch it from the conventional (or baked) to the raw mode. More on the technique can be read here.

Since we don't care much about the footprint of the client-side the code is going to be in Go:

package main

import (
  "io"
  "net"
  "os"
  "sync"

  "golang.org/x/sys/unix"
)

func main() {
  saved, err := tcget(os.Stdin.Fd())
  if err != nil {
    panic(err)
  }
  defer func() {
    tcset(os.Stdin.Fd(), saved)
  }()

  raw := makeraw(*saved)
  tcset(os.Stdin.Fd(), &raw)

  conn, err := net.Dial("tcp", os.Args[1])
  if err != nil {
    panic(err)
  }

  var wg sync.WaitGroup
  wg.Add(2)

  go func() {
    io.Copy(conn, os.Stdin)
    wg.Done()
  }()

  go func() {
    io.Copy(os.Stdout, conn)
    wg.Done()
  }()

  wg.Wait()
}

func tcget(fd uintptr) (*unix.Termios, error) {
  termios, err := unix.IoctlGetTermios(int(fd), unix.TCGETS)
  if err != nil {
    return nil, err
  }
  return termios, nil
}

func tcset(fd uintptr, p *unix.Termios) error {
  return unix.IoctlSetTermios(int(fd), unix.TCSETS, p)
}

func makeraw(t unix.Termios) unix.Termios {
  t.Iflag &^= (unix.IGNBRK | unix.BRKINT | unix.PARMRK | unix.ISTRIP | unix.INLCR | unix.IGNCR | unix.ICRNL | unix.IXON)
  t.Oflag &^= unix.OPOST
  t.Lflag &^= (unix.ECHO | unix.ECHONL | unix.ICANON | unix.ISIG | unix.IEXTEN)
  t.Cflag &^= (unix.CSIZE | unix.PARENB)
  t.Cflag &^= unix.CS8
  t.Cc[unix.VMIN] = 1
  t.Cc[unix.VTIME] = 0
  return t
}

Basically, we switch local terminal to raw mode, connect to the server socket and bind stdin & stdout to the connection file descriptor.

Let's try to use it:

Demo - client PTY program example.

Instead of conclusion

Linux is awesome, and PTY is yet another example of its powerful capabilities driving the containerization revolution.

For the full version of the code check out the project on GitHub - ptyme.

Level up your server-side game — join 20,000 engineers getting insightful learning materials straight to their inbox: