執行人: Andrushika
比照〈Linux containers in 500 lines of code〉,善用 Linux 核心提供的 namespaces, capabilities, seccomp 等機制,開發輕量級的 Linux container,使其具備部分 Docker 和 nerdctl 功能,並學習 tini 和 tiniktls,開發兼具輕量和安全的 init(1) 實作。
研讀 Linux containers in 500 lines of code 並著手練習,最終專案應指定為 MIT License 或 BSD License 一類較寬鬆的條款,避免直接自 GPL 授權的程式碼複製 (應適度改寫)。預期要能運作 Alpine Linux 在內的 Linux 系統。
限定用 C 語言或 Rust 語言開發,可改寫現有專案,但應向授課教師確認細節。
嘗試降低對超級使用者 (即root
) 權限的依賴
延伸閱讀: Rootless containers
參考專案:
改進以下草稿並強化實作細節
確認 tini 及 tiniktls 得以運作於自行開發的 Linux container
tini 是個極簡化的 init 系統,專門用來啟動單一子行程並等待其結束;即使該子行程已成為僵屍行程 (zombie process),也能正確偵測,並將收到的訊號 (signal) 一併轉發給它。若你在 Docker 容器中執行,只需在 docker run
時加上 --init
參數,就會自動注入 /sbin/docker-init
(即 tini
)到容器內,並取代原有的 ENTRYPOINT
,讓你的程式直接在 tini
底下執行。
在步調快速的容器化微服務世界中,效率、安全性與簡潔性至關重要。應用程式需要安全地通訊,通常會使用 TLS,但在每個小型服務中嵌入並管理完整的 TLS 堆疊,可能導致映像檔臃腫、資源消耗增加,以及擴大受攻擊面 (attack surface)。tiniktls 是 tini 程式的巧妙進化,它透過將 TLS 操作 offload 到 Linux 核心 (藉由 KTLS) 和 tini
本身,讓子應用程式能夠保持輕巧並專注於核心功能。
Docker 與其他容器技術徹底改變應用程式的部署方式。然而,在容器內正確執行應用程式,特別是作為行程 ID 1 (PID 1) 執行時,伴隨著特定的責任。Linux 系統中的 PID 1 很特別:它是 init 行程,負責接管孤兒行程 (orphaned processes,防止殭屍行程 [zombie processes] 產生) 並正確處理訊號,以確保容器得以優雅關閉。
許多應用程式並非設計來作為 PID 1 執行。於是 tini 應運而生:專為容器設計的最小化 init 程式,其主要考量如下:
tini
會將訊號 (如 SIGTERM
或 SIGINT
) 轉發給它所啟動的子行程,讓應用程式得以優雅關閉tini
(作為 PID 1) 會回收這些殭屍行程,防止資源洩漏tini
相當小巧,對容器映像檔增加的負擔很低基於這些特性,tini
是 Dockerfile 中常見的 ENTRYPOINT
,為主容器化應用程式確保穩健的行程管理。
雖然 TLS 對於安全性至關重要,但傳統的使用者空間 (userspace) TLS 函式庫 (如 OpenSSL、GnuTLS 或 rustls
) 會在使用者空間執行交握 (handshake) 和應用程式資料的對稱加解密。這涉及到系統呼叫、核心空間與使用者空間之間的資料複製,以及密碼學運算的 CPU 週期消耗。
KTLS 提供顯著的效能最佳化。KTLS 允許 TLS 會話 (session) 的對稱密碼學部分直接在核心中處理。其過程通常如下:
setsockopt()
函式,使用 SOL_TLS
層級以及 TLS_TX
(用於傳輸) 和 TLS_RX
(用於接收) 等選項傳遞給核心。特定的密碼學資訊是透過像 struct tls_crypto_info
這樣的結構或其特定加密套件 (cipher) 的變體 (例如 struct tls12_crypto_info_aes_gcm_128
) 來提供。KTLS 的優勢:
延伸閱讀:
tiniktls 將 tini
的概念又向前推進一步。有鑑於 tini
已是 PID 1 並且管理著主應用程式,tiniktls
將 TLS 交握邏輯和 KTLS offload 功能直接整合到這個 init 行程中。
核心架構與工作流程:
tini
基礎: tiniktls
保留 tini
的核心職責:
TINI_SUBREAPER
設定,還可以充當「子回收器」(subreaper)tiniktls
啟動時,會建立 Unix domain socket 對 (socketpair()
)。此 socket 對的一端 (main
函式中的 child_sock
) 會傳遞給 fork 出來的子應用程式,通常是透過像 TINIKTLS_FD
這樣的環境變數。這成為了控制通道。TINIKTLS_FD
使用簡單的純文字協定與 tiniktls
通訊CONNECT tls://hostname:port[?query_args]
:請求一個對外的 TLS 連線。ACCEPT tls://listen_addr:port[?query_args]
:請求 tiniktls
監聽傳入的 TLS 連線。CONNECT tcp://...
和 ACCEPT tcp://...
:用於純 TCP (無 TLS)CLOSE tls://listen_addr:port
:停止監聽cert=
、key=
)、期望的 TLS 版本 (tls=1.2
)、特定的加密套件 (ciphers=...
) 或連線參數 (backlog=
) 等。tiniktls
作為 TLS 交握引擎。當 tiniktls
收到 TLS 的 CONNECT
或 ACCEPT
命令時:
SSL_CTX_new
、SSL_new
、SSL_set_fd
等) 來執行 TLS 交握 (SSL_connect
用於客戶端,SSL_accept
用於伺服器端)tiniktls.c
中的 ktls12_cipher_list
、ktls13_cipher_suites
) 並啟用 KTLS 意圖 (SSL_OP_ENABLE_KTLS
設定於 SSL_CTX
)tiniktls
(在正確的實作中) 會提取協商好的會話金鑰、初始向量、序號和加密套件詳細資訊setsockopt()
函式,配合 SOL_TLS
、TLS_TX
和 TLS_RX
選項,將適當的 struct tls_crypto_info
(或其變體) 提供給核心。這會告知核心接管該 socket 的加解密工作tiniktls
會透過控制 socket (childfd
) 將回應傳回給子行程。OK
回應,tiniktls
會使用 sendmsg()
函式搭配 SCM_RIGHTS
選項,將網路 socket 的檔案描述子 (若使用 TLS,現在已啟用 KTLS) 傳遞給子行程read()
, write()
, send()
, recv()
等函式呼叫,傳送和接收未加密的應用程式資料epoll
的事件驅動架構:
tiniktls
使用 epoll
以非阻塞 (non-blocking) 方式管理多個檔案描述子:childfd
(控制 socket)、監聽 socket,以及正在進行連線或 TLS 交握的 socketktls_serve()
函式包含主事件迴圈,處理事件並推進每個受管理 socket 的狀態機 (state machine) (例如 STATE_TCP_CONNECTING
、STATE_TLS_HANDSHAKING_CLIENT
、STATE_KTLS_SETUP
等)考量因素:
tiniktls
管理。TLS 堆疊的安全性更新只需套用於 tiniktls
,而非每個獨立的應用程式tiniktls
接收到 FD 後,只需處理標準 socket 和純文字資料。tiniktls
為容器中的 TLS offload 提供輕巧高效的方案。對於那些重視映像檔大小的應用場景而言,尤具有吸引力,甚至可在特定情境提供 sidecar proxy 的的替代方案。
tiniktls
的效益分析:
應用情境:
tiniktls
(透過修改或擴展其現有命令集) 來監聽 TLS 連線、執行交握,然後 fork 舊版應用程式,並將已啟用 KTLS 的 socket 重新導向到子行程的 stdin/stdout根據 man page 上的 namespace(7) 定義:
A namespace wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. Changes to the global resource are visible to other processes that are members of the namespace, but are invisible to other processes. One use of namespaces is to implement containers.
namespace 有多種分類,有 cgroup, IPC, network, mount, PID, time, user, UTS 等等,負責隔離不同的系統資源,參閱官方文件。所以 user namespace 只是其中的一種,負責隔離 UID 和 GID,特別的是 user namespace 建立後可以讓行程擁有看起來像 root 的權限,可以再用來建立其他 namespace。(namespace 需要有 root 才可以建立)
雖然會紀錄 owner,但建立出來的 namespace 和其建立者 (user namespace) 並非從屬關係,層級上都是同一層。權限管理上,只要有權限控制一個 namespace 的 owner user namespace,就可以控制該 namespace。
每個行程都會對應到一套 namespace,namespace 是全系統存在並可見的,例如:
(print $1, $9, $10, $11
是資料對應的 column)
把 root 的權限拆成多個小項目,可以分配給不同行程;但只靠 capability 沒辦法管理所有權限,例如,有些執行的程式會直接檢查 uid = 0
,就可以直接執行,其不在 capability 的管轄範圍內,所以還需要依靠其他手段一起管理權限。
root 的權限拆成非常多細項,包含 CAP_SYS_NICE
(可以竄改行程的 nice 值), CAP_DAC_OVERRIDE
(在檔案讀、寫、執行時跳過權限檢查)等等,對於 container 來說,capability 沒有管理好對於 host 會是大災難,例如可能可以在容器內修改行程的 nice,而 scheduler 是沒有 namespace 隔離的,這時候整個系統的排程就會受到影響。
A full implementation of capabilities requires that:
每個 thread 都具有以下幾種 capability sets,用來處理不同情況下的權限管理機制:
thread 也可以目前不要這個權限(Permitted 裡面沒有),但當 exec() 某些有能力的 binary 時,可以選擇性地獲得它。
比較少,僅有三種:
特別把這兩個小規則拿出來講:
我一開始的疑問是,既然這兩個規則的作用那麼像,為何不能縮減成一條規則就好?
舉例來說,如果我想要讓某 binary 永遠能獲得權限,那透過 P(inheritable) & F(inheritable)
單一一條規則也能做到,為何需要另一條規則?
兩者的差異在責任歸屬的問題:
F(permitted) & P(bounding)
是單向授權、是 binary 本身說明「我要有這個能力」、「我的最低執行要求」;thread 無需為 binary 準備,也無須知道 capability 的存在。所以要另外依賴系統上限 P(bounding)
來控制才安全。
P(bounding)
有點像是權限控制的防火牆、絕對不能碰的底線
P(inheritable) & F(inheritable)
則是雙向奔赴。binary 說「我想要」、process 說「我願意給你」,這時候授予權限就很安全。
回到最初自己的疑問:如果想要讓某 binary 永遠能獲得權限,且只透過 P(inheritable) & F(inheritable)
這條規則,那實作的方式就需要讓 binary 在 F(inheritable)
聲明「需要的權限」,並讓 process 無條件具備攜帶很多 P(inheritable)
才會成立。這會使得提權極其容易發生。
以網路控制權限為例,這種情況下,就可以將 Thread 設定成:
被 exec 的 binary 設定為:
最終結果:
比較特別的是,因為 file 的 effective 被設定為 1,所以繼承到的 permitted 會立即生效。
Thread 設定成:
被 exec 的 binary 設定為:
最終結果:
Thread 設定成:
被 exec 的 binary 設定為:
最終結果:
是當 binary 具有 file capability (+ep) 時,若其自身「不會自行檢查權限才執行對應操作」、或針對權限不足時有額外失敗處理手段,那就容易造成漏洞發生。
為什麼是 +ep?當 file capability 的 Effective 被設為 1,那任何人來執行這個 binary,都會直接獲得對應的 Permitted 權限。如下,try_regain_cap
會認為當其被執行時,無論如何都會有 cap_mknod 權限。
但若當前的行程帶有 bounding,則 permitted 會被自動捨棄掉。try_regain_cap
本身不知道,可能會直接執行需要 cap_mknod
權限的行為,此時就會發生錯誤。
為了防止這種狀況,kernel 對於 capability-dumb binary 會有一層檢查:若行程在 execve 之後未能獲得全部的 F(permitted)
,那就會直接回傳 EPERM
。要嘛拿到全部的權限、要嘛不要執行。
當建立 container 的時候,也會希望 container 內部所看到的檔案系統與 host 是隔離的。
其實 mount 也運用到前面所提到的 namespace 技術,但 mount namespace 的概念有一些不一樣。先前討論 user namespace 時,新建的 namespace 中所使用的 uid, gid,和實際映射到 host 上面的不同;而 mount namespace 會完全相同。它比較像是建立了原先 mount tree 中所有 directory 名稱的「副本」,新 namespace 中同一個路徑仍然會指向同一個 fd。類似於 shallow copy shared pointer。而在新 namespace 中,若進行 mount()
和 umount()
,這些變動「預設」不會影響到 host 的掛載狀態。(但仍可能透過 mount propagation 傳播回 host 或其他 namespace)
如果單純使用 mount namespace 進行 touch, mkdir 等操作,還是會修改到 host 的檔案!
太酷了吧!但我不信邪,用實驗來驗證。
首先檢查目前 host 所處的 namespace:
檢查一下目前的 mount 狀態,會得到一串又臭又長的掛載點列表:
之後使用 unshare 進入新的 mount namespace。可以看見 namespace id 已經改變:
之後在新的 namespace 中,把能取消掛載的全部 unmount,再把所有掛載點列出來看一次,會發現大多數的都不見了:
之後再開一個新的 terminal,再把掛載點列出一次。此時會發現,原先的掛載點都平安無事(此處不再把結果印出)。
事實上,以上實驗的結果還受到 mount propagation 的影響,將在 mount() 中解釋
接下來去印證看看,在新的 namespace 中 mkdir
,這樣 host 看得到嗎?答案是看得到!
這個系統呼叫的功能如其名,就是將檔案系統掛載到另一個路徑下。其中有一個比較需要注意的機制:mount propagation,參照官方文件關於 MS_PRIVATE
, MS_SHARED
的說明。
先前的實驗中提及,在新的 mount namespace 中進行 mount()
, umount()
不會影響到 host。事實上,這和掛載點本身的 mountflags 有關係。namespace 可以自行決定自己的該掛載點要不要和大家同步 mount 狀態,或是斷開連結。在 container 的實作中,通常會設定為 private,防止被其他 namespace 影響,也防止自己影響別人。
有四種 propagation type 可以使用,詳細可參照官方文件搜尋 "Changing the propagation type of an existing mount"。
而先前的實驗能成功的原因,在 unshare(1) 有提及:
unshare since util-linux version 2.27 automatically sets propagation to private in a new mount namespace to make sure that the new namespace is really unshared.
因為 unshare 之後 propagation 就被自動設定為 private 了,所以沒問題。
這個系統呼叫可以讓當前的行程改變看待檔案目錄時的視角,比如:
access 的翻譯是「存取」,而非「訪問」(visit)
已修正!
這樣就會把 /tmp
當作存取檔案系統時的根目錄,後續若存取 /bin
,實際上會存取到 /tmp/bin
這個路徑。
但這個系統呼叫有一些危險,因為他只有改變路徑解析的視角,事實上使用者還是有機會「逃出」這個 root,比如在設定 chroot 之後仍保留其他開著的 fd,就可以透過 fchdir()
逃走:
這個系統呼叫可以真正將 /
的掛載樹替換掉,而非僅僅改變「視角」。由於 glibc 沒有提供 pivot_root()
的包裝,所以使用時需要以下列方式呼叫:
它做了兩件事情,拆開來說明比較好懂:
/
換成 new_root
所指定的路徑put_old
之下需要注意的是,pivot_root()
只能夠在新建立的 mount namespace 中使用,與 host 相同 mount namespace 中不允許呼叫。理由也很顯而易見,如果 host 中 /
被換掉,那就天下大亂了。且呼叫時 put_old
必須是 new_root
下的子目錄,如此就算換了新 root,也保留了對上層目錄的存取方式。
接下來舉例說明其使用方式,假如在新的 mount namespace 中,路徑如下:
接著執行 pivot_root("./newroot", "./newroot/oldroot")
進行重新指定 root,結果如下:
在 container 的實作中,實際上不希望改到 host 檔案系統上的東西,所以會在 pivot_root()
之後,把 oldroot
整個 umount()
掉。
根據 seccomp(2) 的說明,seccomp 的作用是:
operate on Secure Computing state of the process
其中 Secure Computing state 指的是 kernel 如何限制行程進行系統呼叫。若只依賴 capability 做權限管理,會漏掉一些「不需要 capability 就能執行的系統呼叫」,例如 clone(CLONE_NEWUSER)
、bpf()
等,而這些系統呼叫若在 container 中被成功執行,往往可以造成提權;所以需要加上 seccomp
機制,針對每個系統呼叫做更細緻的使用限制。
實作時,會使用 libseccomp 所提供的 API 來使用這個機制。
使用 seccomp 之前須初始化,其中 def_action
指的是「當遇到沒有被明確允許的系統呼叫」,對應的預設行為。
其中提供了多種預設行為,包含 SCMP_ACT_ALLOW
(預設允許所有系統呼叫)、SCMP_ACT_KILL_PROCESS
(直接中止整個行程)等等,可參閱 man page 說明。
用來細項指定對於每個系統呼叫的限制。使用上,我認為它比較像過濾的概念,而非「鎖死」哪些系統呼叫完全使用。先看該系統呼叫的定義就能明白:
ctx
是先前透過初始化得到的指標;action
用來指定「當和過濾規則相符的系統呼叫」觸發時對應的動作,可選擇 SCMP_ACT_KILL
直接中止執行緒、SCMP_ACT_ERRNO
對執行緒發送一個 errno
等等,可參閱 seccomp_rule_add(3)。
重點是剩下的幾個參數:syscall
用來指定要限制的系統呼叫,arg_cnt
指的是將對幾個 syscall
的參數設定過濾規則,最後 ...
處則需要註明對於 syscall
每個參數的精細限制。因為系統呼叫最多只會有六個參數,可以使用SCMP_A{0-5}()
,指定要對第幾個參數設定規則。
舉例來說,我想限制 clone
的使用,則可以寫:
以下實驗均以 cgroup v2 為例!
為了防止部分管理不佳的行程過度佔用系統資源,導致 DoS (Denial of Service) 的發生,Linux 提供了此機制來協助我們管理行程所使用的資源上限:如記憶體、CPU time、I/O 等。cgroups 的全名是 control groups,指的是將不同的行程分組,並規範他們總共能使用多少資源。
要使用這項管理機制,需透過核心掛載的 cgroup2 檔案系統寫入檔案、對不同資源進行限制。利用 mount
命令查看掛載點,可以看到掛載點在 /sys/fs/cgroup
底下:
可以把該目錄下的檔案列出看看:
也太多檔案了吧!先看 cgroup.procs
,先前提及 cgroups
會將行程分組進行管理,而 cgroup.procs
就需要宣告有哪些行程 (PID) 屬於這個組別。
而組別的管理是「階層式」的,也就是組別之中可以包含其他組別。在 cgroups(7) 裡頭有提到:
The cgroups for a controller are arranged in a hierarchy. This hierarchy is defined by creating, removing, and renaming subdirectories within the cgroup filesystem.
如上所述,要在「群組裡面建立群組」的話,只需要在掛載的檔案系統中,直接建立新目錄,檔案系統會自己為管理需要建立所須的檔案。可以做實驗來觀察一下,若在現存的 /sys/fs/cgroup/
直接建立新目錄:
由上可見,子代群組的相關檔案都被自動建立好了。
而階層關係中,子代群組能使用的資源也會受到親代群組的影響,參考 man page 所述:
At each level of the hierarchy, attributes (e.g., limits) can be defined. The limits, control, and accounting provided by cgroups generally have effect throughout the subhierarchy underneath the cgroup where the attributes are defined. Thus, for example, the limits placed on a cgroup at a higher level in the hierarchy cannot be exceeded by descendant cgroups.
舉例來說:
雖然 child
寫入 cpu.max
時能成功,但 child
實際能使用的只有 1 CPU。
此外,並非所有的 controller 都會被預設啟用,像是 cpu
選項就會預設關閉;如果你擁有一雙火眼金睛,應該會發現先前建立 test-my-cgroup
時,cpu.max
檔案沒有被自動建立。此時需要手動在親代組別的 cgroup.subtree_control
中指定使用 +cpu
,就能正常運作。
cgroup 其實還有比較舊的 v1 版本。先前提過的 v2 版本中,/sys/fs/cgroup/
下所對應的每個目錄都對應到一個群組,而目錄內的檔案可以控制群組的所有資源(memory, cpu, I/O)。結構看起來會像這樣子:
如果在使用 v2 時去檢視 /proc/self/cgroup
,會得到類似以下結果:
可以發現第二的 column 是空的(兩個冒號中間),這是因為 cgroup v1 原先的設計所影響。v1 把不同 controller 放在不同的 hierarchy 中,因此,同一個 cgroup 的資源管理會四散在不同的目錄下,結構長得像這樣子:
再檢視 /proc/self/cgroup
看看,印出的內容會和使用 v2 時有所不同(隨意舉例):
第一欄為 hierarchy ID,第二欄為該 hierarchy 所管理的資源(controllers),最後一欄是當前行程在該 hierarchy 中的路徑;這個路徑是相對於該 hierarchy 的 root。
v1 這種結構的設計原先是為了提供更好的靈活性而生。但實際使用上,發現這樣的靈活性沒什麼用,反而增加了資源管理的複雜度,所以在 v2 中簡化了管理結構。
In cgroups v1, the ability to mount different controllers against different hierarchies was intended to allow great flexibility for application design. In practice, though, the flexibility turned out to be less useful than expected, and in many cases added complexity. Therefore, in cgroups v2, all available controllers are mounted against a single hierarchy. —— cgroups(7) man page
其作用為改變行程看待 cgroups 結構的視角,概念上有點像是 chroot
,不過只針對 cgroup 作用。
Cgroup namespaces virtualize the view of a process's cgroups as seen via
/proc/pid/cgroup
and/proc/pid/mountinfo
. —— cgroup_namespaces(7) man page
舉例來說,如果有一個行程受到 cgroup 管理,沒有啟用 cgroup namespace 的狀況下,他看到的 /proc/self/cgroup
會是這樣的:
如果啟用了 cgroup namespace,行程將看不到 host 的 cgroup 分組結構:
這樣就算出現 container in container 的應用情境,每個 container 都仍能乾淨的管理自己那層的 cgroup 路徑。
-m
: 當作 rootfs 的目錄
-u
: user namespace 中的 UID
-c
: 要執行的指令及參數
注意:-c
後面的全部會被當成要執行的指令和參數
https://blog.csdn.net/Mculover666/article/details/106646339
這邊在 clone 時除了 time 和 user namespace,其他都用了新的,為 container 隔離環境打下基礎。user namespace 會在 child 建立後嘗試建立。
為甚麼漏了 time?
child 被 clone 後,會檢查自身是否具有建立 user namespace 的能力,並用 socket 將結果回傳 parent;parent process 在 clone child 後就持續監聽,若 child process 建立 user namespace 成功,則需要做 UID 和 GID mapping。
將 host 的 UID 對應到 child 的 UID,USERNS_OFFSET = 10000 表示 child 的 UID 0 對應到 host 的 UID 10000,USERNS_COUNT 則是可用的 PID 總數。
這一部相當重要,對於一個新建立的 user namespace,若沒有進行 mapping,他基本上沒辦法做任何事情,因為權限檢查全都會失敗。要經過 mapping 後,才可以 setgid, setuid。
丟掉比較危險的 capability,透過清空 bounding sets 和 inheritable set,而存在於 ambient 的元素必須同時存在於 inheritable 和 permitted set 中,所以清除 inheritable 的動作會直接清掉 ambient。這樣可以有效清除所有不想要傳遞下去的權限。
本函式會在 child process 中執行,此時已經在使用新的 mount namespace。為了在新的 namespace 中換 root 到 config->mount_dir
,原作者將對應目錄掛載到一個 tmp 下,並使用隨機產生 tmp 目錄名,確保每個啟動的 container 會得到不同的臨時掛載路徑,順便準備一個給 old root 放置的地方(給 pivot_root()
使用),最後丟掉 old root 的掛載,以防存取到 new root 以上的檔案。
memory/$hostname/memory.kmem.limit_in_bytes
, so that the contained process and its child processes can't total more than 1GB memory in nsdelegate
,cgroups2 檔案系統會直接將相關管理檔案的 owner 下放給 nonprivileged user。這樣就不需要 root 也可以寫入 cgroup。CAP_SETUID
的狀況也可以寫入 uid/gid map,但需要在容器裡禁用 setgroups,這是否會影響到 alpine linux 或 tini 在容器內的運作?
更新:用 deny setgroups 去規避 CAP_SETUID (CAP_SETGID) 的權限需求這條路有另一個限制,根據上方官方文件的 (b) 第一條,如果沒有 CAP_SETUID (CAP_SETGID),那就只能建立一個長度為 1 的 ID mapping。
目前使用 newuidmap 的 binary 來使親代行程在沒有 root 權限的狀況下短暫提權,來寫入對應的 id map。
https://github.com/Andrushika/light-container
AF_INET
(IPv4)、AF_INET6
(IPv6)、AF_UNIX
(local IPC, 沒有網路傳輸,直接在 memory 溝通)、AF_PACKET
(原始封包,直接處理 ethernet)、AF_NETLINK
(kernel 和 user space 溝通用,linux 特有)real user ID 和 effective user ID 為何要分開?