diff --git a/client.go b/client.go new file mode 100644 index 0000000..46c889b --- /dev/null +++ b/client.go @@ -0,0 +1,27 @@ +package main + +import "github.com/containers/podman/v5/libpod/define" + +type clientInfo struct { + OSArch string `json:"OS"` + Provider string `json:"provider"` + Version string `json:"version"` + BuildOrigin string `json:"buildOrigin,omitempty" yaml:",omitempty"` +} + +func getClientInfo() (*clientInfo, error) { + p, err := getProvider() + if err != nil { + return nil, err + } + vinfo, err := define.GetVersion() + if err != nil { + return nil, err + } + return &clientInfo{ + OSArch: vinfo.OsArch, + Provider: p, + Version: vinfo.Version, + BuildOrigin: vinfo.BuildOrigin, + }, nil +} diff --git a/client_supported.go b/client_supported.go new file mode 100644 index 0000000..995154f --- /dev/null +++ b/client_supported.go @@ -0,0 +1,15 @@ +//go:build amd64 || arm64 + +package main + +import ( + "github.com/containers/podman/v5/pkg/machine/provider" +) + +func getProvider() (string, error) { + p, err := provider.Get() + if err != nil { + return "", err + } + return p.VMType().String(), nil +} diff --git a/early_init_linux.go b/early_init_linux.go new file mode 100644 index 0000000..83f93d6 --- /dev/null +++ b/early_init_linux.go @@ -0,0 +1,33 @@ +package main + +import ( + "fmt" + "syscall" + + "github.com/sirupsen/logrus" +) + +func setRLimits() error { + rlimits := new(syscall.Rlimit) + if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + return fmt.Errorf("getting rlimits: %w", err) + } + rlimits.Cur = rlimits.Max + if err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, rlimits); err != nil { + return fmt.Errorf("setting new rlimits: %w", err) + } + return nil +} + +func setUMask() { + // Be sure we can create directories with 0755 mode. + syscall.Umask(0022) +} + +func earlyInitHook() { + if err := setRLimits(); err != nil { + logrus.Errorf("Failed to set rlimits: %v", err) + } + + setUMask() +} diff --git a/lib/ebpf/c/common.h b/lib/ebpf/c/common.h new file mode 100644 index 0000000..5f06d38 --- /dev/null +++ b/lib/ebpf/c/common.h @@ -0,0 +1,6 @@ +#include +#include +#include + +#include "sockmap.h" +#include "target_ip_map.h" diff --git a/lib/ebpf/c/contrack.bpf.c b/lib/ebpf/c/contrack.bpf.c new file mode 100644 index 0000000..9e81137 --- /dev/null +++ b/lib/ebpf/c/contrack.bpf.c @@ -0,0 +1,76 @@ +#include "common.h" + +enum filter_ret_code { + // 不处理当前连接 + PASS = 0, + // 处理当前连接 + STORE = 1, +}; + + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +static inline +void store_sock_ops(struct bpf_sock_ops *skops) +{ + struct sock_key key = { + .dip = skops->remote_ip4, + .sip = skops->local_ip4, + .sport = bpf_htonl(skops->local_port), /* convert to network byte order */ + .dport = skops->remote_port, + .family = skops->family, + }; + + int ret = bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST); + if (ret != 0) { + bpf_printk("sock_hash_update() failed, ret: %d\n", ret); + } + +} + +// 判断传入连接是否需要处理 +static inline +__u8 sock_filter(struct bpf_sock_ops *skops) +{ + if (skops->family != 2){ // AF_INET + return PASS; + } + /* + local traffic only, so check both local and remote IPs + */ + __u32 remote_ip = skops->remote_ip4; + __u32 local_ip = skops->local_ip4; + + // 检查 remote_ip 是否在目标 map 中 + if (!bpf_map_lookup_elem(&target_ip_map, &remote_ip)) { + return PASS; + } + + // 如果是本地流量,大概率remote ip 和 local ip 是相同的,此时可以省去一次 map 查询 + if (remote_ip == local_ip) { + return STORE; + } + + // 检查 local_ip 是否在目标 map 中 + if (!bpf_map_lookup_elem(&target_ip_map, &local_ip)) { + return PASS; + } + + return STORE; +} + +SEC("sockops") +int bpf_sockops_handler(struct bpf_sock_ops *skops) +{ + // only interested in established events + if (skops->op != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB && + skops->op != BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) { + return BPF_OK; + } + if (sock_filter(skops)){ // AF_INET + store_sock_ops(skops); // 将 socket 信息记录到到 sockmap + } + + return BPF_OK; +} + diff --git a/lib/ebpf/c/redirect.bpf.c b/lib/ebpf/c/redirect.bpf.c new file mode 100644 index 0000000..ff2e384 --- /dev/null +++ b/lib/ebpf/c/redirect.bpf.c @@ -0,0 +1,29 @@ +#include "common.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +SEC("sk_msg") +int bpf_redir(struct sk_msg_md *msg) +{ + struct sock_key key = { + .sip = msg->remote_ip4, + .dip = msg->local_ip4, + .dport = bpf_htonl(msg->local_port), /* convert to network byte order */ + .sport = msg->remote_port, + .family = msg->family, + }; + + int ret = bpf_msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS); + if (ret == SK_DROP) { + bpf_printk("redirect from %d.%d.%d.%d:%d to %d.%d.%d.%d:%d failed\n", + key.sip & 0xff, (key.sip >> 8) & 0xff, + (key.sip >> 16) & 0xff, (key.sip >> 24) & 0xff, + bpf_ntohl(key.sport), + key.dip & 0xff, (key.dip >> 8) & 0xff, + (key.dip >> 16) & 0xff, (key.dip >> 24) & 0xff, + bpf_ntohl(key.dport)); + } + + // 无论是否重定向成功,都返回 SK_PASS。这样失败的重定向还可以使用其他方法处理 + return SK_PASS; +} \ No newline at end of file diff --git a/lib/ebpf/c/sockmap.h b/lib/ebpf/c/sockmap.h new file mode 100644 index 0000000..451b441 --- /dev/null +++ b/lib/ebpf/c/sockmap.h @@ -0,0 +1,17 @@ +#include +#include + +struct sock_key { + __u32 sip; + __u32 dip; + __u32 sport; + __u32 dport; + __u32 family; +}; + +struct { + __uint(type, BPF_MAP_TYPE_SOCKHASH); + __uint(max_entries, 65535); + __type(key, struct sock_key); + __type(value, int); +} sock_ops_map SEC(".maps"); diff --git a/lib/ebpf/c/target_ip_map.h b/lib/ebpf/c/target_ip_map.h new file mode 100644 index 0000000..3776834 --- /dev/null +++ b/lib/ebpf/c/target_ip_map.h @@ -0,0 +1,10 @@ +#include +#include + +// 存储需要contrack的IP地址 +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u32); // key 类型(目标 IP 地址) + __type(value, __u8); // value 类型(标志位,无实际用途,设置为 1 表示存在) + __uint(max_entries, 1024); // 最多存储 1024 个目标 IP +} target_ip_map SEC(".maps"); \ No newline at end of file diff --git a/lib/ebpf/ebpf.tgz b/lib/ebpf/ebpf.tgz new file mode 100644 index 0000000..e73f6a9 Binary files /dev/null and b/lib/ebpf/ebpf.tgz differ diff --git a/lib/ebpf/ebpf/app.go b/lib/ebpf/ebpf/app.go new file mode 100644 index 0000000..f34b374 --- /dev/null +++ b/lib/ebpf/ebpf/app.go @@ -0,0 +1,53 @@ +package ebpf + +const DefaultInterfaceName string = "neto" + +// Ebpf should be initialized only once. when agent.reloads, +// agent.try to start ebpf again. so we need to keep track of +// how many instances of ebpf are running. only when InstanceRefCounter +// is 1, we should start ebpf. when InstanceRefCounter is 0, we should stop ebpf. +var InstanceRefCounter uint32 = 0 +var ebpf *Ebpf + +type App struct { + InterfaceName []string `json:"interface_name,omitempty"` +} + +func NewApp() *App { + return &App{} +} +func (a *App) Provision() error { + if len(a.InterfaceName) == 0 { + a.InterfaceName = []string{DefaultInterfaceName} + } + + InstanceRefCounter++ + if InstanceRefCounter == 1 { + + var err error + ebpf, err = ProvisionEBPF() + if err != nil { + return err + } + ebpf.SetIfName(a.InterfaceName) + + ebpf.Start() + } + + ebpf.SetIfName(a.InterfaceName) + + return nil +} + +func (a *App) Start() error { + return nil +} + +func (a *App) Stop() error { + InstanceRefCounter-- + if InstanceRefCounter == 0 { + ebpf.Stop() + } + + return nil +} diff --git a/lib/ebpf/ebpf/bpf/common.h b/lib/ebpf/ebpf/bpf/common.h new file mode 100644 index 0000000..5f06d38 --- /dev/null +++ b/lib/ebpf/ebpf/bpf/common.h @@ -0,0 +1,6 @@ +#include +#include +#include + +#include "sockmap.h" +#include "target_ip_map.h" diff --git a/lib/ebpf/ebpf/bpf/contrack.bpf.c b/lib/ebpf/ebpf/bpf/contrack.bpf.c new file mode 100644 index 0000000..9e81137 --- /dev/null +++ b/lib/ebpf/ebpf/bpf/contrack.bpf.c @@ -0,0 +1,76 @@ +#include "common.h" + +enum filter_ret_code { + // 不处理当前连接 + PASS = 0, + // 处理当前连接 + STORE = 1, +}; + + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +static inline +void store_sock_ops(struct bpf_sock_ops *skops) +{ + struct sock_key key = { + .dip = skops->remote_ip4, + .sip = skops->local_ip4, + .sport = bpf_htonl(skops->local_port), /* convert to network byte order */ + .dport = skops->remote_port, + .family = skops->family, + }; + + int ret = bpf_sock_hash_update(skops, &sock_ops_map, &key, BPF_NOEXIST); + if (ret != 0) { + bpf_printk("sock_hash_update() failed, ret: %d\n", ret); + } + +} + +// 判断传入连接是否需要处理 +static inline +__u8 sock_filter(struct bpf_sock_ops *skops) +{ + if (skops->family != 2){ // AF_INET + return PASS; + } + /* + local traffic only, so check both local and remote IPs + */ + __u32 remote_ip = skops->remote_ip4; + __u32 local_ip = skops->local_ip4; + + // 检查 remote_ip 是否在目标 map 中 + if (!bpf_map_lookup_elem(&target_ip_map, &remote_ip)) { + return PASS; + } + + // 如果是本地流量,大概率remote ip 和 local ip 是相同的,此时可以省去一次 map 查询 + if (remote_ip == local_ip) { + return STORE; + } + + // 检查 local_ip 是否在目标 map 中 + if (!bpf_map_lookup_elem(&target_ip_map, &local_ip)) { + return PASS; + } + + return STORE; +} + +SEC("sockops") +int bpf_sockops_handler(struct bpf_sock_ops *skops) +{ + // only interested in established events + if (skops->op != BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB && + skops->op != BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB) { + return BPF_OK; + } + if (sock_filter(skops)){ // AF_INET + store_sock_ops(skops); // 将 socket 信息记录到到 sockmap + } + + return BPF_OK; +} + diff --git a/lib/ebpf/ebpf/bpf/redirect.bpf.c b/lib/ebpf/ebpf/bpf/redirect.bpf.c new file mode 100644 index 0000000..ff2e384 --- /dev/null +++ b/lib/ebpf/ebpf/bpf/redirect.bpf.c @@ -0,0 +1,29 @@ +#include "common.h" + +char LICENSE[] SEC("license") = "Dual BSD/GPL"; + +SEC("sk_msg") +int bpf_redir(struct sk_msg_md *msg) +{ + struct sock_key key = { + .sip = msg->remote_ip4, + .dip = msg->local_ip4, + .dport = bpf_htonl(msg->local_port), /* convert to network byte order */ + .sport = msg->remote_port, + .family = msg->family, + }; + + int ret = bpf_msg_redirect_hash(msg, &sock_ops_map, &key, BPF_F_INGRESS); + if (ret == SK_DROP) { + bpf_printk("redirect from %d.%d.%d.%d:%d to %d.%d.%d.%d:%d failed\n", + key.sip & 0xff, (key.sip >> 8) & 0xff, + (key.sip >> 16) & 0xff, (key.sip >> 24) & 0xff, + bpf_ntohl(key.sport), + key.dip & 0xff, (key.dip >> 8) & 0xff, + (key.dip >> 16) & 0xff, (key.dip >> 24) & 0xff, + bpf_ntohl(key.dport)); + } + + // 无论是否重定向成功,都返回 SK_PASS。这样失败的重定向还可以使用其他方法处理 + return SK_PASS; +} \ No newline at end of file diff --git a/lib/ebpf/ebpf/bpf/sockmap.h b/lib/ebpf/ebpf/bpf/sockmap.h new file mode 100644 index 0000000..451b441 --- /dev/null +++ b/lib/ebpf/ebpf/bpf/sockmap.h @@ -0,0 +1,17 @@ +#include +#include + +struct sock_key { + __u32 sip; + __u32 dip; + __u32 sport; + __u32 dport; + __u32 family; +}; + +struct { + __uint(type, BPF_MAP_TYPE_SOCKHASH); + __uint(max_entries, 65535); + __type(key, struct sock_key); + __type(value, int); +} sock_ops_map SEC(".maps"); diff --git a/lib/ebpf/ebpf/bpf/target_ip_map.h b/lib/ebpf/ebpf/bpf/target_ip_map.h new file mode 100644 index 0000000..3776834 --- /dev/null +++ b/lib/ebpf/ebpf/bpf/target_ip_map.h @@ -0,0 +1,10 @@ +#include +#include + +// 存储需要contrack的IP地址 +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __type(key, __u32); // key 类型(目标 IP 地址) + __type(value, __u8); // value 类型(标志位,无实际用途,设置为 1 表示存在) + __uint(max_entries, 1024); // 最多存储 1024 个目标 IP +} target_ip_map SEC(".maps"); \ No newline at end of file diff --git a/lib/ebpf/ebpf/contrack/contrack.go b/lib/ebpf/ebpf/contrack/contrack.go new file mode 100644 index 0000000..9a22fa3 --- /dev/null +++ b/lib/ebpf/ebpf/contrack/contrack.go @@ -0,0 +1,82 @@ +package contrack + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" +) + +type Contrack struct { + obj contrackObjects + cg link.Link + targetIPs map[uint32]struct{} +} + +func New() *Contrack { + return &Contrack{ + obj: contrackObjects{}, + targetIPs: make(map[uint32]struct{}), + } +} + +func (c *Contrack) Start() error { + if err := loadContrackObjects(&c.obj, nil); err != nil { + return fmt.Errorf("loading contrack objects failed: %w", err) + } + + cgroupPath, err := detectCgroupPath() + if err != nil { + return fmt.Errorf("attaching contrack cgroup failed for detecting cgroup path: %w", err) + } + + c.cg, err = link.AttachCgroup(link.CgroupOptions{ + Path: cgroupPath, + Attach: ebpf.AttachCGroupSockOps, + Program: c.obj.BpfSockopsHandler, + }) + if err != nil { + return fmt.Errorf("attaching contrack cgroup failed: %w", err) + } + + return nil +} + +func (c *Contrack) Close() { + if c.cg != nil { + c.cg.Close() + } + c.obj.Close() +} + +func (c *Contrack) SockOpsMap() *ebpf.Map { + return c.obj.SockOpsMap +} + +// AddTargetIp adds an IP to the target_ip_map +func (c *Contrack) AddTargetIP(ip *IP) error { + value := uint8(1) // Value is not used, just needs to exist + if err := c.obj.TargetIpMap.Put(ip.Key(), value); err != nil { + return fmt.Errorf("failed to update target_ip_map with IP %s: %v", ip, err) + } + + c.targetIPs[ip.Key()] = struct{}{} + return nil +} + +// RemoveTargetIPWithPreCheck removes an IP from the target_ip_map after checking if it exists +func (c *Contrack) RemoveTargetIPWithPreCheck(ip *IP) error { + if _, ok := c.targetIPs[ip.Key()]; !ok { + return fmt.Errorf("IP %s is not in target_ip_map", ip) + } + return c.RemoveTargetIP(ip) +} + +// RemoveTargetIP removes an IP from the target_ip_map +func (c *Contrack) RemoveTargetIP(ip *IP) error { + if err := c.obj.TargetIpMap.Delete(ip.Key()); err != nil { + return fmt.Errorf("failed to remove IP %s from target_ip_map: %v", ip, err) + } + delete(c.targetIPs, ip.Key()) + return nil +} diff --git a/lib/ebpf/ebpf/contrack/contrack_bpfeb.go b/lib/ebpf/ebpf/contrack/contrack_bpfeb.go new file mode 100644 index 0000000..baeab6e --- /dev/null +++ b/lib/ebpf/ebpf/contrack/contrack_bpfeb.go @@ -0,0 +1,144 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build mips || mips64 || ppc64 || s390x + +package contrack + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type contrackSockKey struct { + Sip uint32 + Dip uint32 + Sport uint32 + Dport uint32 + Family uint32 +} + +// loadContrack returns the embedded CollectionSpec for contrack. +func loadContrack() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ContrackBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load contrack: %w", err) + } + + return spec, err +} + +// loadContrackObjects loads contrack and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *contrackObjects +// *contrackPrograms +// *contrackMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadContrackObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadContrack() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// contrackSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackSpecs struct { + contrackProgramSpecs + contrackMapSpecs + contrackVariableSpecs +} + +// contrackProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackProgramSpecs struct { + BpfSockopsHandler *ebpf.ProgramSpec `ebpf:"bpf_sockops_handler"` +} + +// contrackMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackMapSpecs struct { + SockOpsMap *ebpf.MapSpec `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.MapSpec `ebpf:"target_ip_map"` +} + +// contrackVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackVariableSpecs struct { +} + +// contrackObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackObjects struct { + contrackPrograms + contrackMaps + contrackVariables +} + +func (o *contrackObjects) Close() error { + return _ContrackClose( + &o.contrackPrograms, + &o.contrackMaps, + ) +} + +// contrackMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackMaps struct { + SockOpsMap *ebpf.Map `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.Map `ebpf:"target_ip_map"` +} + +func (m *contrackMaps) Close() error { + return _ContrackClose( + m.SockOpsMap, + m.TargetIpMap, + ) +} + +// contrackVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackVariables struct { +} + +// contrackPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackPrograms struct { + BpfSockopsHandler *ebpf.Program `ebpf:"bpf_sockops_handler"` +} + +func (p *contrackPrograms) Close() error { + return _ContrackClose( + p.BpfSockopsHandler, + ) +} + +func _ContrackClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed contrack_bpfeb.o +var _ContrackBytes []byte diff --git a/lib/ebpf/ebpf/contrack/contrack_bpfeb.o b/lib/ebpf/ebpf/contrack/contrack_bpfeb.o new file mode 100644 index 0000000..e69de29 diff --git a/lib/ebpf/ebpf/contrack/contrack_bpfel.go b/lib/ebpf/ebpf/contrack/contrack_bpfel.go new file mode 100644 index 0000000..35eefca --- /dev/null +++ b/lib/ebpf/ebpf/contrack/contrack_bpfel.go @@ -0,0 +1,144 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 + +package contrack + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type contrackSockKey struct { + Sip uint32 + Dip uint32 + Sport uint32 + Dport uint32 + Family uint32 +} + +// loadContrack returns the embedded CollectionSpec for contrack. +func loadContrack() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_ContrackBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load contrack: %w", err) + } + + return spec, err +} + +// loadContrackObjects loads contrack and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *contrackObjects +// *contrackPrograms +// *contrackMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadContrackObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadContrack() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// contrackSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackSpecs struct { + contrackProgramSpecs + contrackMapSpecs + contrackVariableSpecs +} + +// contrackProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackProgramSpecs struct { + BpfSockopsHandler *ebpf.ProgramSpec `ebpf:"bpf_sockops_handler"` +} + +// contrackMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackMapSpecs struct { + SockOpsMap *ebpf.MapSpec `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.MapSpec `ebpf:"target_ip_map"` +} + +// contrackVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type contrackVariableSpecs struct { +} + +// contrackObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackObjects struct { + contrackPrograms + contrackMaps + contrackVariables +} + +func (o *contrackObjects) Close() error { + return _ContrackClose( + &o.contrackPrograms, + &o.contrackMaps, + ) +} + +// contrackMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackMaps struct { + SockOpsMap *ebpf.Map `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.Map `ebpf:"target_ip_map"` +} + +func (m *contrackMaps) Close() error { + return _ContrackClose( + m.SockOpsMap, + m.TargetIpMap, + ) +} + +// contrackVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackVariables struct { +} + +// contrackPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadContrackObjects or ebpf.CollectionSpec.LoadAndAssign. +type contrackPrograms struct { + BpfSockopsHandler *ebpf.Program `ebpf:"bpf_sockops_handler"` +} + +func (p *contrackPrograms) Close() error { + return _ContrackClose( + p.BpfSockopsHandler, + ) +} + +func _ContrackClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed contrack_bpfel.o +var _ContrackBytes []byte diff --git a/lib/ebpf/ebpf/contrack/contrack_bpfel.o b/lib/ebpf/ebpf/contrack/contrack_bpfel.o new file mode 100644 index 0000000..e69de29 diff --git a/lib/ebpf/ebpf/contrack/gen.go b/lib/ebpf/ebpf/contrack/gen.go new file mode 100644 index 0000000..896d15f --- /dev/null +++ b/lib/ebpf/ebpf/contrack/gen.go @@ -0,0 +1,3 @@ +package contrack + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go contrack ../bpf/contrack.bpf.c diff --git a/lib/ebpf/ebpf/contrack/ip.go b/lib/ebpf/ebpf/contrack/ip.go new file mode 100644 index 0000000..6c62549 --- /dev/null +++ b/lib/ebpf/ebpf/contrack/ip.go @@ -0,0 +1,41 @@ +package contrack + +import ( + "bytes" + "encoding/binary" + "net" +) + +type IP struct { + net.IP + key uint32 +} + +func (ip IP) Key() uint32 { + return ip.key +} + +func ParseIPStr(ipStr string) *IP { + ip := net.ParseIP(ipStr) + if ip == nil { + return nil + } + + return ParseIP(ip) +} + +func ParseIP(ip net.IP) *IP { + parsedIP := ip.To4() + if parsedIP == nil { + return nil + } + + var key uint32 + buf := bytes.NewReader(parsedIP) + binary.Read(buf, binary.LittleEndian, &key) + + return &IP{ + IP: parsedIP, + key: key, + } +} diff --git a/lib/ebpf/ebpf/contrack/util.go b/lib/ebpf/ebpf/contrack/util.go new file mode 100644 index 0000000..d5434ce --- /dev/null +++ b/lib/ebpf/ebpf/contrack/util.go @@ -0,0 +1,29 @@ +package contrack + +import ( + "bufio" + "errors" + "os" + "strings" +) + +// detectCgroupPath returns the first-found mount point of type cgroup2 +// and stores it in the cgroupPath global variable. +func detectCgroupPath() (string, error) { + f, err := os.Open("/proc/mounts") + if err != nil { + return "", err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + // example fields: cgroup2 /sys/fs/cgroup/unified cgroup2 rw,nosuid,nodev,noexec,relatime 0 0 + fields := strings.Split(scanner.Text(), " ") + if len(fields) >= 3 && fields[2] == "cgroup2" { + return fields[1], nil + } + } + + return "", errors.New("cgroup2 not mounted") +} diff --git a/lib/ebpf/ebpf/ebpf.go b/lib/ebpf/ebpf/ebpf.go new file mode 100644 index 0000000..3c51050 --- /dev/null +++ b/lib/ebpf/ebpf/ebpf.go @@ -0,0 +1,118 @@ +package ebpf + +import ( + "context" + "errors" + "floares/lib/ebpf/ebpf/contrack" + "floares/lib/ebpf/ebpf/redirect" + "fmt" + "slices" + + "github.com/cilium/ebpf/rlimit" + "github.com/vishvananda/netlink" + "go.uber.org/zap" +) + +type Ebpf struct { + contrack *contrack.Contrack + redirect *redirect.Redirect + + cancel context.CancelFunc + ifName []string + logger *zap.Logger +} + +func (e *Ebpf) Start() { + var ctx context.Context + ctx, e.cancel = context.WithCancel(context.Background()) + + addrUpdate := make(chan netlink.AddrUpdate) + netlink.AddrSubscribe(addrUpdate, ctx.Done()) + + go func() { + for { + select { + case addr := <-addrUpdate: + if filterByIfName(addr, e.ifName) { + fmt.Println(addr.LinkIndex, addr.LinkAddress) + parsedIP := contrack.ParseIP(addr.LinkAddress.IP) + if parsedIP == nil { + e.logger.Debug("support only ipv4 now, not: ", zap.String("ip", addr.LinkAddress.IP.String())) + continue + } + if addr.NewAddr { + if err := e.contrack.AddTargetIP(parsedIP); err != nil { + e.logger.Error("failed to add target ip", zap.String("ip", parsedIP.String()), zap.Error(err)) + } else { + e.logger.Debug("added target ip", zap.String("ip", parsedIP.String())) + } + } else { + if err := e.contrack.RemoveTargetIPWithPreCheck(parsedIP); err != nil { + e.logger.Error("failed to remove target ip", zap.String("ip", parsedIP.String()), zap.Error(err)) + } else { + e.logger.Debug("removed target ip", zap.String("ip", parsedIP.String())) + } + } + } + case <-ctx.Done(): + return + } + } + }() +} + +func (e *Ebpf) Stop() { + e.cancel() +} + +func (e *Ebpf) SetIfName(ifName []string) { + e.ifName = ifName +} + +func (e *Ebpf) SetLogger(logger *zap.Logger) { + e.logger = logger +} + +// ProvisionEBPF caller must call this function only once. +func ProvisionEBPF() (*Ebpf, error) { + // Remove resource limits for kernels <5.11. + if err := rlimit.RemoveMemlock(); err != nil { + return nil, fmt.Errorf("removing memlock: %w", err) + } + + con := contrack.New() + + if err := con.Start(); err != nil { + return nil, fmt.Errorf("starting contrack failed: %w", err) + } + + red := redirect.New(con.SockOpsMap()) + if err := red.Start(); err != nil { + return nil, fmt.Errorf("starting redirect failed: %w", err) + } + + return &Ebpf{ + contrack: con, + redirect: red, + }, nil +} + +// filterByIfName filters the AddrUpdate by interface name. +// When AddrUpdate.LinkIndex is not found there is two cases, we handle it in different ways: +// 1. The addr is added. returns false. +// 2. The addr is removed, return true. Because the link is deleted first and then the addr is removed. +func filterByIfName(au netlink.AddrUpdate, ifName []string) bool { + link, err := netlink.LinkByIndex(au.LinkIndex) + if err != nil { + if errors.As(err, &netlink.LinkNotFoundError{}) && !au.NewAddr { + return true + } + return false + } + + if slices.Contains(ifName, link.Attrs().Name) { + return true + } + + return false +} diff --git a/lib/ebpf/ebpf/redirect/gen.go b/lib/ebpf/ebpf/redirect/gen.go new file mode 100644 index 0000000..fd57745 --- /dev/null +++ b/lib/ebpf/ebpf/redirect/gen.go @@ -0,0 +1,3 @@ +package redirect + +//go:generate go run github.com/cilium/ebpf/cmd/bpf2go redirect ../bpf/redirect.bpf.c diff --git a/lib/ebpf/ebpf/redirect/redirect.go b/lib/ebpf/ebpf/redirect/redirect.go new file mode 100644 index 0000000..ca5caa9 --- /dev/null +++ b/lib/ebpf/ebpf/redirect/redirect.go @@ -0,0 +1,47 @@ +package redirect + +import ( + "fmt" + + "github.com/cilium/ebpf" +) + +type Redirect struct { + sockMap *ebpf.Map + obj redirectObjects +} + +func New(sockMap *ebpf.Map) *Redirect { + return &Redirect{ + sockMap: sockMap, + obj: redirectObjects{}, + } +} + +func (s *Redirect) Start() (err error) { + if err = s.loadProgram(); err != nil { + return fmt.Errorf("loading redirect objects failed: %w", err) + } + + err = AttachSkMsg(SkMsgOptions{ + Program: s.obj.BpfRedir, + Map: s.sockMap.FD(), + }) + if err != nil { + return fmt.Errorf("attaching redirect failed: %w", err) + } + + return nil +} + +func (s *Redirect) Close() { + s.obj.Close() +} + +func (s *Redirect) loadProgram() error { + return loadRedirectObjects(&s.obj, &ebpf.CollectionOptions{ + MapReplacements: map[string]*ebpf.Map{ + "sock_ops_map": s.sockMap, + }, + }) +} diff --git a/lib/ebpf/ebpf/redirect/redirect_bpfeb.go b/lib/ebpf/ebpf/redirect/redirect_bpfeb.go new file mode 100644 index 0000000..7882f32 --- /dev/null +++ b/lib/ebpf/ebpf/redirect/redirect_bpfeb.go @@ -0,0 +1,144 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build mips || mips64 || ppc64 || s390x + +package redirect + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type redirectSockKey struct { + Sip uint32 + Dip uint32 + Sport uint32 + Dport uint32 + Family uint32 +} + +// loadRedirect returns the embedded CollectionSpec for redirect. +func loadRedirect() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_RedirectBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load redirect: %w", err) + } + + return spec, err +} + +// loadRedirectObjects loads redirect and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *redirectObjects +// *redirectPrograms +// *redirectMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadRedirectObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadRedirect() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// redirectSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectSpecs struct { + redirectProgramSpecs + redirectMapSpecs + redirectVariableSpecs +} + +// redirectProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectProgramSpecs struct { + BpfRedir *ebpf.ProgramSpec `ebpf:"bpf_redir"` +} + +// redirectMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectMapSpecs struct { + SockOpsMap *ebpf.MapSpec `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.MapSpec `ebpf:"target_ip_map"` +} + +// redirectVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectVariableSpecs struct { +} + +// redirectObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectObjects struct { + redirectPrograms + redirectMaps + redirectVariables +} + +func (o *redirectObjects) Close() error { + return _RedirectClose( + &o.redirectPrograms, + &o.redirectMaps, + ) +} + +// redirectMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectMaps struct { + SockOpsMap *ebpf.Map `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.Map `ebpf:"target_ip_map"` +} + +func (m *redirectMaps) Close() error { + return _RedirectClose( + m.SockOpsMap, + m.TargetIpMap, + ) +} + +// redirectVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectVariables struct { +} + +// redirectPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectPrograms struct { + BpfRedir *ebpf.Program `ebpf:"bpf_redir"` +} + +func (p *redirectPrograms) Close() error { + return _RedirectClose( + p.BpfRedir, + ) +} + +func _RedirectClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed redirect_bpfeb.o +var _RedirectBytes []byte diff --git a/lib/ebpf/ebpf/redirect/redirect_bpfeb.o b/lib/ebpf/ebpf/redirect/redirect_bpfeb.o new file mode 100644 index 0000000..e69de29 diff --git a/lib/ebpf/ebpf/redirect/redirect_bpfel.go b/lib/ebpf/ebpf/redirect/redirect_bpfel.go new file mode 100644 index 0000000..23e3f1e --- /dev/null +++ b/lib/ebpf/ebpf/redirect/redirect_bpfel.go @@ -0,0 +1,144 @@ +// Code generated by bpf2go; DO NOT EDIT. +//go:build 386 || amd64 || arm || arm64 || loong64 || mips64le || mipsle || ppc64le || riscv64 + +package redirect + +import ( + "bytes" + _ "embed" + "fmt" + "io" + + "github.com/cilium/ebpf" +) + +type redirectSockKey struct { + Sip uint32 + Dip uint32 + Sport uint32 + Dport uint32 + Family uint32 +} + +// loadRedirect returns the embedded CollectionSpec for redirect. +func loadRedirect() (*ebpf.CollectionSpec, error) { + reader := bytes.NewReader(_RedirectBytes) + spec, err := ebpf.LoadCollectionSpecFromReader(reader) + if err != nil { + return nil, fmt.Errorf("can't load redirect: %w", err) + } + + return spec, err +} + +// loadRedirectObjects loads redirect and converts it into a struct. +// +// The following types are suitable as obj argument: +// +// *redirectObjects +// *redirectPrograms +// *redirectMaps +// +// See ebpf.CollectionSpec.LoadAndAssign documentation for details. +func loadRedirectObjects(obj interface{}, opts *ebpf.CollectionOptions) error { + spec, err := loadRedirect() + if err != nil { + return err + } + + return spec.LoadAndAssign(obj, opts) +} + +// redirectSpecs contains maps and programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectSpecs struct { + redirectProgramSpecs + redirectMapSpecs + redirectVariableSpecs +} + +// redirectProgramSpecs contains programs before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectProgramSpecs struct { + BpfRedir *ebpf.ProgramSpec `ebpf:"bpf_redir"` +} + +// redirectMapSpecs contains maps before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectMapSpecs struct { + SockOpsMap *ebpf.MapSpec `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.MapSpec `ebpf:"target_ip_map"` +} + +// redirectVariableSpecs contains global variables before they are loaded into the kernel. +// +// It can be passed ebpf.CollectionSpec.Assign. +type redirectVariableSpecs struct { +} + +// redirectObjects contains all objects after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectObjects struct { + redirectPrograms + redirectMaps + redirectVariables +} + +func (o *redirectObjects) Close() error { + return _RedirectClose( + &o.redirectPrograms, + &o.redirectMaps, + ) +} + +// redirectMaps contains all maps after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectMaps struct { + SockOpsMap *ebpf.Map `ebpf:"sock_ops_map"` + TargetIpMap *ebpf.Map `ebpf:"target_ip_map"` +} + +func (m *redirectMaps) Close() error { + return _RedirectClose( + m.SockOpsMap, + m.TargetIpMap, + ) +} + +// redirectVariables contains all global variables after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectVariables struct { +} + +// redirectPrograms contains all programs after they have been loaded into the kernel. +// +// It can be passed to loadRedirectObjects or ebpf.CollectionSpec.LoadAndAssign. +type redirectPrograms struct { + BpfRedir *ebpf.Program `ebpf:"bpf_redir"` +} + +func (p *redirectPrograms) Close() error { + return _RedirectClose( + p.BpfRedir, + ) +} + +func _RedirectClose(closers ...io.Closer) error { + for _, closer := range closers { + if err := closer.Close(); err != nil { + return err + } + } + return nil +} + +// Do not access this directly. +// +//go:embed redirect_bpfel.o +var _RedirectBytes []byte diff --git a/lib/ebpf/ebpf/redirect/redirect_bpfel.o b/lib/ebpf/ebpf/redirect/redirect_bpfel.o new file mode 100644 index 0000000..e69de29 diff --git a/lib/ebpf/ebpf/redirect/skmsg.go b/lib/ebpf/ebpf/redirect/skmsg.go new file mode 100644 index 0000000..a4419b3 --- /dev/null +++ b/lib/ebpf/ebpf/redirect/skmsg.go @@ -0,0 +1,33 @@ +package redirect + +import ( + "fmt" + + "github.com/cilium/ebpf" + "github.com/cilium/ebpf/link" +) + +type SkMsgOptions struct { + // Program must be an SkMsg BPF program. + Program *ebpf.Program + + // Map is the BPF map the program is attached to. + Map int +} + +func AttachSkMsg(opts SkMsgOptions) error { + if t := opts.Program.Type(); t != ebpf.SkMsg { + return fmt.Errorf("invalid program type %s, expected SkMsg", t) + } + + err := link.RawAttachProgram(link.RawAttachProgramOptions{ + Target: opts.Map, + Program: opts.Program, + Attach: ebpf.AttachSkMsgVerdict, + }) + if err != nil { + return fmt.Errorf("failed to attach link: %w", err) + } + + return nil +} diff --git a/main b/main new file mode 100755 index 0000000..c9f73f5 Binary files /dev/null and b/main differ diff --git a/root.go b/root.go new file mode 100644 index 0000000..91db223 --- /dev/null +++ b/root.go @@ -0,0 +1,581 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "runtime" + "strings" + + "github.com/containers/common/pkg/ssh" + "github.com/containers/podman/v5/cmd/podman/common" + "github.com/containers/podman/v5/cmd/podman/registry" + "github.com/containers/podman/v5/libpod/define" + "github.com/containers/podman/v5/pkg/bindings" + "github.com/containers/podman/v5/pkg/domain/entities" + "github.com/containers/storage" + "github.com/sirupsen/logrus" + "github.com/spf13/cobra" + "sigs.k8s.io/yaml" +) + +// HelpTemplate is the help template for podman commands +// This uses the short and long options. +// command should not use this. +const helpTemplate = `{{.Short}} + +Description: + {{.Long}} + +{{if or .Runnable .HasSubCommands}}{{.UsageString}}{{end}}` + +// UsageTemplate is the usage template for podman commands +// This blocks the displaying of the global options. The main podman +// command should not use this. +const usageTemplate = `Usage:{{if (and .Runnable (not .HasAvailableSubCommands))}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.UseLine}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: + {{.Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +Options: +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} +{{end}} +` + +var ( + //rootCmd = &cobra.Command{ + // // In shell completion, there is `.exe` suffix on Windows. + // // This does not provide the same experience across platforms + // // and was mentioned in [#16499](https://github.com/containers/podman/issues/16499). + // Use: strings.TrimSuffix(filepath.Base(os.Args[0]), ".exe") + " [options]", + // Long: "Manage pods, containers and images", + // SilenceUsage: true, + // SilenceErrors: true, + // TraverseChildren: true, + // PersistentPreRunE: persistentPreRunE, + // RunE: validate.SubCommandExists, + // //PersistentPostRunE: persistentPostRunE, + // Version: version.Version.String(), + // DisableFlagsInUseLine: true, + //} + + defaultLogLevel = "warn" + logLevel = defaultLogLevel + dockerConfig = "" + debug bool + + requireCleanup = true + + // Defaults for capturing/redirecting the command output since (the) cobra is + // global-hungry and doesn't allow you to attach anything that allows us to + // transform the noStdout BoolVar to a string that we can assign to useStdout. + noStdout = false + useStdout = "" +) + +func Init() { + // Hooks are called before PersistentPreRunE(). These hooks affect global + // state and are executed after processing the command-line, but before + // actually running the command. + cobra.OnInitialize( + stdOutHook, // Caution, this hook redirects stdout and output from any following hooks may be affected. + loggingHook, + syslogHook, + earlyInitHook, + configHook, + ) + + // backwards compat still allow --cni-config-dir + //rootCmd.Flags().SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName { + // if name == "cni-config-dir" { + // name = "network-config-dir" + // } + // return pflag.NormalizedName(name) + //}) + //rootCmd.SetUsageTemplate(usageTemplate) +} + +func Execute() { + //if err := rootCmd.ExecuteContext(registry.Context()); err != nil { + // if registry.GetExitCode() == 0 { + // registry.SetExitCode(define.ExecErrorCodeGeneric) + // } + // if registry.IsRemote() { + // if errors.As(err, &bindings.ConnectError{}) { + // fmt.Fprintln(os.Stderr, "Cannot connect to Podman. Please verify your connection to the Linux system using `podman system connection list`, or try `podman machine init` and `podman machine start` to manage a new Linux VM") + // } + // } + // fmt.Fprintln(os.Stderr, formatError(err)) + //} + // + //_ = shutdown.Stop() + // + //if requireCleanup { + // // The cobra post-run is not being executed in case of + // // a previous error, so make sure that the engine(s) + // // are correctly shutdown. + // // + // // See https://github.com/spf13/cobra/issues/914 + // logrus.Debugf("Shutting down engines") + // if engine := registry.ImageEngine(); engine != nil { + // engine.Shutdown(registry.Context()) + // } + // if engine := registry.ContainerEngine(); engine != nil { + // engine.Shutdown(registry.Context()) + // } + //} + // + //os.Exit(registry.GetExitCode()) +} + +// readRemoteCliFlags reads cli flags related to operating podman remotely +func readRemoteCliFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) error { + + conf := podmanConfig.ContainersConfDefaultsRO + contextConn, host := cmd.Root().LocalFlags().Lookup("context"), cmd.Root().LocalFlags().Lookup("host") + conn, url := cmd.Root().LocalFlags().Lookup("connection"), cmd.Root().LocalFlags().Lookup("url") + + switch { + case conn != nil && conn.Changed: + if contextConn != nil && contextConn.Changed { + return fmt.Errorf("use of --connection and --context at the same time is not allowed") + } + con, err := conf.GetConnection(conn.Value.String(), false) + if err != nil { + return err + } + podmanConfig.URI = con.URI + podmanConfig.Identity = con.Identity + podmanConfig.MachineMode = con.IsMachine + case url.Changed: + podmanConfig.URI = url.Value.String() + case contextConn != nil && contextConn.Changed: + service := contextConn.Value.String() + if service != "default" { + con, err := conf.GetConnection(service, false) + if err != nil { + return err + } + podmanConfig.URI = con.URI + podmanConfig.Identity = con.Identity + podmanConfig.MachineMode = con.IsMachine + } + case host.Changed: + podmanConfig.URI = host.Value.String() + default: + // No cli options set, in case CONTAINER_CONNECTION was set to something + // invalid this contains the error, see setupRemoteConnection(). + // Important so that we can show a proper useful error message but still + // allow the cli overwrites (https://github.com/containers/podman/pull/22997). + return podmanConfig.ConnectionError + } + return nil +} + +// setupRemoteConnection returns information about the active service destination +// The order of priority is: +// 1. cli flags (--connection ,--url ,--context ,--host); +// 2. Env variables (CONTAINER_HOST and CONTAINER_CONNECTION); +// 3. ActiveService from containers.conf; +// 4. RemoteURI; +// Returns the name of the default connection if any. +func setupRemoteConnection(podmanConfig *entities.PodmanConfig) string { + conf := podmanConfig.ContainersConfDefaultsRO + connEnv, hostEnv, sshkeyEnv := os.Getenv("CONTAINER_CONNECTION"), os.Getenv("CONTAINER_HOST"), os.Getenv("CONTAINER_SSHKEY") + + switch { + case connEnv != "": + con, err := conf.GetConnection(connEnv, false) + if err != nil { + podmanConfig.ConnectionError = err + return connEnv + } + podmanConfig.URI = con.URI + podmanConfig.Identity = con.Identity + podmanConfig.MachineMode = con.IsMachine + return con.Name + case hostEnv != "": + if sshkeyEnv != "" { + podmanConfig.Identity = sshkeyEnv + } + podmanConfig.URI = hostEnv + default: + con, err := conf.GetConnection("", true) + if err == nil { + podmanConfig.URI = con.URI + podmanConfig.Identity = con.Identity + podmanConfig.MachineMode = con.IsMachine + return con.Name + } + podmanConfig.URI = registry.DefaultAPIAddress() + } + return "" +} + +func persistentPreRunE(cmd *cobra.Command, args []string) error { + logrus.Debugf("Called %s.PersistentPreRunE(%s)", cmd.Name(), strings.Join(os.Args, " ")) + + // Help, completion and commands with subcommands are special cases, no need for more setup + // Completion cmd is used to generate the shell scripts + + podmanConfig := registry.PodmanConfig() + log.Println(*podmanConfig) + if !registry.IsRemote() { + if cmd.Flag("hooks-dir").Changed { + podmanConfig.ContainersConf.Engine.HooksDir.Set(podmanConfig.HooksDir) + } + + // Currently it is only possible to restore a container with the same runtime + // as used for checkpointing. It should be possible to make crun and runc + // compatible to restore a container with another runtime then checkpointed. + // Currently that does not work. + // To make it easier for users we will look into the checkpoint archive and + // set the runtime to the one used during checkpointing. + + } + + if err := readRemoteCliFlags(cmd, podmanConfig); err != nil { + return fmt.Errorf("read cli flags: %w", err) + } + + // Special case if command is hidden completion command ("__complete","__completeNoDesc") + // Since __completeNoDesc is an alias the cm.Name is always __complete + //if cmd.Name() == cobra.ShellCompRequestCmd { + // // Parse the cli arguments after the completion cmd (always called as second argument) + // // This ensures that the --url, --identity and --connection flags are properly set + // compCmd, _, err := cmd.Root().Traverse(os.Args[2:]) + // if err != nil { + // return err + // } + // // If we don't complete the root cmd hide all root flags + // // so they won't show up in the completions on subcommands. + // if compCmd != compCmd.Root() { + // compCmd.Root().Flags().VisitAll(func(flag *pflag.Flag) { + // flag.Hidden = true + // }) + // } + // // No need for further setup the completion logic setups the engines as needed. + // requireCleanup = false + // return nil + //} + + c := &cobra.Command{} + // Prep the engines + if _, err := registry.NewImageEngine(c, args); err != nil { + // Note: this is gross, but it is the hand we are dealt + if registry.IsRemote() && errors.As(err, &bindings.ConnectError{}) && cmd.Name() == "info" && c.Parent() == c.Root() { + clientDesc, err := getClientInfo() + // we eat the error here. if this fails, they just don't any client info + if err == nil { + b, _ := yaml.Marshal(clientDesc) + fmt.Println(string(b)) + } + } + return err + } + if _, err := registry.NewContainerEngine(c, args); err != nil { + return err + } + + // Hard code TMPDIR functions to use /var/tmp, if user did not override + if _, ok := os.LookupEnv("TMPDIR"); !ok { + if tmpdir, err := podmanConfig.ContainersConfDefaultsRO.ImageCopyTmpDir(); err != nil { + logrus.Warnf("Failed to retrieve default tmp dir: %s", err.Error()) + } else { + os.Setenv("TMPDIR", tmpdir) + } + } + + //if !registry.IsRemote() { + // if c.Flag("cpu-profile").Changed { + // f, err := os.Create(podmanConfig.CPUProfile) + // if err != nil { + // return err + // } + // if err := pprof.StartCPUProfile(f); err != nil { + // return err + // } + // } + // if c.Flag("memory-profile").Changed { + // // Same value as the default in github.com/pkg/profile. + // runtime.MemProfileRate = 4096 + // if rate := os.Getenv("MemProfileRate"); rate != "" { + // r, err := strconv.Atoi(rate) + // if err != nil { + // return err + // } + // runtime.MemProfileRate = r + // } + // } + // + // if podmanConfig.MaxWorks <= 0 { + // return fmt.Errorf("maximum workers must be set to a positive number (got %d)", podmanConfig.MaxWorks) + // } + // if err := parallel.SetMaxThreads(uint(podmanConfig.MaxWorks)); err != nil { + // return err + // } + //} + // Setup Rootless environment, IFF: + // 1) in ABI mode + // 2) running as non-root + // 3) command doesn't require Parent Namespace + _, found := c.Annotations[registry.ParentNSRequired] + if !registry.IsRemote() && !found { + cgroupMode := "" + _, noMoveProcess := c.Annotations[registry.NoMoveProcess] + if flag := c.LocalFlags().Lookup("cgroups"); flag != nil { + cgroupMode = flag.Value.String() + } + err := registry.ContainerEngine().SetupRootless(registry.Context(), noMoveProcess, cgroupMode) + if err != nil { + return err + } + } + return nil +} + +func configHook() { + if dockerConfig != "" { + if err := os.Setenv("DOCKER_CONFIG", dockerConfig); err != nil { + fmt.Fprintf(os.Stderr, "cannot set DOCKER_CONFIG=%s: %s", dockerConfig, err.Error()) + os.Exit(1) + } + } +} + +func loggingHook() { + var found bool + if debug { + if logLevel != defaultLogLevel { + fmt.Fprintf(os.Stderr, "Setting --log-level and --debug is not allowed\n") + os.Exit(1) + } + logLevel = "debug" + } + for _, l := range common.LogLevels { + if l == strings.ToLower(logLevel) { + found = true + break + } + } + if !found { + fmt.Fprintf(os.Stderr, "Log Level %q is not supported, choose from: %s\n", logLevel, strings.Join(common.LogLevels, ", ")) + os.Exit(1) + } + + level, err := logrus.ParseLevel(logLevel) + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(1) + } + logrus.SetLevel(level) + + if logrus.IsLevelEnabled(logrus.InfoLevel) { + logrus.Infof("%s filtering at log level %s", os.Args[0], logrus.GetLevel()) + } +} + +// used for capturing podman's formatted output to some file as per the -out and -noout flags. +func stdOutHook() { + // if noStdOut was specified, then assign /dev/null as the standard file for output. + if noStdout { + useStdout = os.DevNull + } + // if we were given a filename for output, then open that and use it. we end up leaking + // the file since it's intended to be in scope as long as our process is running. + if useStdout != "" { + if fd, err := os.OpenFile(useStdout, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm); err == nil { + os.Stdout = fd + + // if we couldn't open the file for write, then just bail with an error. + } else { + fmt.Fprintf(os.Stderr, "unable to open file for standard output: %s\n", err.Error()) + os.Exit(1) + } + } +} + +func rootFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) { + connectionName := setupRemoteConnection(podmanConfig) + + lFlags := cmd.Flags() + + sshFlagName := "ssh" + lFlags.StringVar(&podmanConfig.SSHMode, sshFlagName, string(ssh.GolangMode), "define the ssh mode") + + connectionFlagName := "connection" + lFlags.StringP(connectionFlagName, "c", connectionName, "Connection to use for remote Podman service (CONTAINER_CONNECTION)") + + urlFlagName := "url" + lFlags.StringVar(&podmanConfig.URI, urlFlagName, podmanConfig.URI, "URL to access Podman service (CONTAINER_HOST)") + lFlags.StringVarP(&podmanConfig.URI, "host", "H", podmanConfig.URI, "Used for Docker compatibility") + _ = lFlags.MarkHidden("host") + + lFlags.StringVar(&dockerConfig, "config", "", "Location of authentication config file") + + // Context option added just for compatibility with DockerCLI. + lFlags.String("context", "default", "Name of the context to use to connect to the daemon (This flag is a NOOP and provided solely for scripting compatibility.)") + _ = lFlags.MarkHidden("context") + + identityFlagName := "identity" + lFlags.StringVar(&podmanConfig.Identity, identityFlagName, podmanConfig.Identity, "path to SSH identity file, (CONTAINER_SSHKEY)") + + // Flags that control or influence any kind of output. + outFlagName := "out" + lFlags.StringVar(&useStdout, outFlagName, "", "Send output (stdout) from podman to a file") + + lFlags.BoolVar(&noStdout, "noout", false, "do not output to stdout") + _ = lFlags.MarkHidden("noout") // Superseded by --out + + lFlags.BoolVarP(&podmanConfig.Remote, "remote", "r", registry.IsRemote(), "Access remote Podman service") + pFlags := cmd.PersistentFlags() + if registry.IsRemote() { + if err := lFlags.MarkHidden("remote"); err != nil { + logrus.Warnf("Unable to mark --remote flag as hidden: %s", err.Error()) + } + podmanConfig.Remote = true + } else { + // The --module's are actually used and parsed in + // `registry.PodmanConfig()`. But we also need to expose them + // as a flag here to a) make sure that rootflags are aware of + // this flag and b) to have shell completions. + moduleFlagName := "module" + lFlags.StringArray(moduleFlagName, nil, "Load the containers.conf(5) module") + + // A *hidden* flag to change the database backend. + pFlags.StringVar(&podmanConfig.ContainersConf.Engine.DBBackend, "db-backend", podmanConfig.ContainersConfDefaultsRO.Engine.DBBackend, "Database backend to use") + + cgroupManagerFlagName := "cgroup-manager" + pFlags.StringVar(&podmanConfig.ContainersConf.Engine.CgroupManager, cgroupManagerFlagName, podmanConfig.ContainersConfDefaultsRO.Engine.CgroupManager, "Cgroup manager to use (\"cgroupfs\"|\"systemd\")") + + pFlags.StringVar(&podmanConfig.CPUProfile, "cpu-profile", "", "Path for the cpu-profiling results") + pFlags.StringVar(&podmanConfig.MemoryProfile, "memory-profile", "", "Path for the memory-profiling results") + + conmonFlagName := "conmon" + pFlags.StringVar(&podmanConfig.ConmonPath, conmonFlagName, "", "Path of the conmon binary") + + // TODO (6.0): --network-cmd-path is deprecated, remove this option with the next major release + // We need to find all the places that use r.config.Engine.NetworkCmdPath and remove it + networkCmdPathFlagName := "network-cmd-path" + pFlags.StringVar(&podmanConfig.ContainersConf.Engine.NetworkCmdPath, networkCmdPathFlagName, podmanConfig.ContainersConfDefaultsRO.Engine.NetworkCmdPath, "Path to the command for configuring the network") + + networkConfigDirFlagName := "network-config-dir" + pFlags.StringVar(&podmanConfig.ContainersConf.Network.NetworkConfigDir, networkConfigDirFlagName, podmanConfig.ContainersConfDefaultsRO.Network.NetworkConfigDir, "Path of the configuration directory for networks") + + pFlags.StringVar(&podmanConfig.ContainersConf.Containers.DefaultMountsFile, "default-mounts-file", podmanConfig.ContainersConfDefaultsRO.Containers.DefaultMountsFile, "Path to default mounts file") + + eventsBackendFlagName := "events-backend" + pFlags.StringVar(&podmanConfig.ContainersConf.Engine.EventsLogger, eventsBackendFlagName, podmanConfig.ContainersConfDefaultsRO.Engine.EventsLogger, `Events backend to use ("file"|"journald"|"none")`) + + hooksDirFlagName := "hooks-dir" + pFlags.StringArrayVar(&podmanConfig.HooksDir, hooksDirFlagName, podmanConfig.ContainersConfDefaultsRO.Engine.HooksDir.Get(), "Set the OCI hooks directory path (may be set multiple times)") + + pFlags.IntVar(&podmanConfig.MaxWorks, "max-workers", (runtime.NumCPU()*3)+1, "The maximum number of workers for parallel operations") + + namespaceFlagName := "namespace" + pFlags.StringVar(&podmanConfig.ContainersConf.Engine.Namespace, namespaceFlagName, podmanConfig.ContainersConfDefaultsRO.Engine.Namespace, "Set the libpod namespace, used to create separate views of the containers and pods on the system") + _ = pFlags.MarkHidden(namespaceFlagName) + + networkBackendFlagName := "network-backend" + pFlags.StringVar(&podmanConfig.ContainersConf.Network.NetworkBackend, networkBackendFlagName, podmanConfig.ContainersConfDefaultsRO.Network.NetworkBackend, `Network backend to use ("cni"|"netavark")`) + _ = pFlags.MarkHidden(networkBackendFlagName) + + rootFlagName := "root" + pFlags.StringVar(&podmanConfig.GraphRoot, rootFlagName, "", "Path to the graph root directory where images, containers, etc. are stored") + + pFlags.StringVar(&podmanConfig.RegistriesConf, "registries-conf", "", "Path to a registries.conf to use for image processing") + + runrootFlagName := "runroot" + pFlags.StringVar(&podmanConfig.Runroot, runrootFlagName, "", "Path to the 'run directory' where all state information is stored") + + imageStoreFlagName := "imagestore" + pFlags.StringVar(&podmanConfig.ImageStore, imageStoreFlagName, "", "Path to the 'image store', different from 'graph root', use this to split storing the image into a separate 'image store', see 'man containers-storage.conf' for details") + + pFlags.BoolVar(&podmanConfig.TransientStore, "transient-store", false, "Enable transient container storage") + + pFlags.StringArrayVar(&podmanConfig.PullOptions, "pull-option", nil, "Specify an option to change how the image is pulled") + + runtimeFlagName := "runtime" + pFlags.StringVar(&podmanConfig.RuntimePath, runtimeFlagName, "youki", "Path to the OCI-compatible binary used to run containers.") + + // -s is deprecated due to conflict with -s on subcommands + storageDriverFlagName := "storage-driver" + pFlags.StringVar(&podmanConfig.StorageDriver, storageDriverFlagName, "", "Select which storage driver is used to manage storage of images and containers") + + tmpdirFlagName := "tmpdir" + pFlags.StringVar(&podmanConfig.ContainersConf.Engine.TmpDir, tmpdirFlagName, podmanConfig.ContainersConfDefaultsRO.Engine.TmpDir, "Path to the tmp directory for libpod state content.\n\nNote: use the environment variable 'TMPDIR' to change the temporary storage location for container images, '/var/tmp'.\n") + + pFlags.BoolVar(&podmanConfig.Trace, "trace", false, "Enable opentracing output (default false)") + + volumePathFlagName := "volumepath" + pFlags.StringVar(&podmanConfig.ContainersConf.Engine.VolumePath, volumePathFlagName, podmanConfig.ContainersConfDefaultsRO.Engine.VolumePath, "Path to the volume directory in which volume data is stored") + + // Hide these flags for both ABI and Tunneling + for _, f := range []string{ + "cpu-profile", + "db-backend", + "default-mounts-file", + "max-workers", + "memory-profile", + "pull-option", + "registries-conf", + "trace", + } { + if err := pFlags.MarkHidden(f); err != nil { + logrus.Warnf("Unable to mark %s flag as hidden: %s", f, err.Error()) + } + } + } + storageOptFlagName := "storage-opt" + pFlags.StringArrayVar(&podmanConfig.StorageOpts, storageOptFlagName, []string{}, "Used to pass an option to the storage driver") + + // Override default --help information of `--help` global flag + var dummyHelp bool + pFlags.BoolVar(&dummyHelp, "help", false, "Help for podman") + + logLevelFlagName := "log-level" + pFlags.StringVar(&logLevel, logLevelFlagName, logLevel, fmt.Sprintf("Log messages above specified level (%s)", strings.Join(common.LogLevels, ", "))) + + lFlags.BoolVarP(&debug, "debug", "D", false, "Docker compatibility, force setting of log-level") + _ = lFlags.MarkHidden("debug") + + // Only create these flags for ABI connections + if !registry.IsRemote() { + runtimeflagFlagName := "runtime-flag" + pFlags.StringArrayVar(&podmanConfig.RuntimeFlags, runtimeflagFlagName, []string{}, "add global flags for the container runtime") + + pFlags.BoolVar(&podmanConfig.Syslog, "syslog", false, "Output logging information to syslog as well as the console (default false)") + } +} + +func formatError(err error) string { + var message string + switch { + case errors.Is(err, define.ErrOCIRuntime): + // OCIRuntimeErrors include the reason for the failure in the + // second to last message in the error chain. + message = fmt.Sprintf( + "Error: %s: %s", + define.ErrOCIRuntime.Error(), + strings.TrimSuffix(err.Error(), ": "+define.ErrOCIRuntime.Error()), + ) + case errors.Is(err, storage.ErrDuplicateName): + message = fmt.Sprintf("Error: %s, or use --replace to instruct Podman to do so.", err.Error()) + default: + if logrus.IsLevelEnabled(logrus.TraceLevel) { + message = fmt.Sprintf("Error: %+v", err) + } else { + message = fmt.Sprintf("Error: %v", err) + } + } + return message +} diff --git a/syslog_common.go b/syslog_common.go new file mode 100644 index 0000000..a5782be --- /dev/null +++ b/syslog_common.go @@ -0,0 +1,24 @@ +//go:build linux || freebsd + +package main + +import ( + "log/syslog" + + "github.com/containers/podman/v5/cmd/podman/registry" + "github.com/sirupsen/logrus" + logrusSyslog "github.com/sirupsen/logrus/hooks/syslog" +) + +func syslogHook() { + if !registry.PodmanConfig().Syslog { + return + } + + hook, err := logrusSyslog.NewSyslogHook("", "", syslog.LOG_INFO, "") + if err != nil { + logrus.Debug("Failed to initialize syslog hook: " + err.Error()) + } else { + logrus.AddHook(hook) + } +}