Skip to content

Commit 1b2918e

Browse files
committed
pesto: integrate port forwarding into netavark Setup/Teardown with state tracking
Fixes: podman-container-tools/podman#28769 Signed-off-by: Jan Rodák <hony.com@seznam.cz>
1 parent f1a708b commit 1b2918e

7 files changed

Lines changed: 391 additions & 47 deletions

File tree

common/libnetwork/internal/rootlessnetns/netns_freebsd.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ func (n *Netns) Info() *types.RootlessNetnsInfo {
3535
func (n *Netns) PestoSocketPath() string {
3636
return ""
3737
}
38+
39+
func (n *Netns) PestoAddrStatePath(containerID string) string {
40+
return ""
41+
}
42+
43+
func (n *Netns) WipePestoAddrState() {}

common/libnetwork/internal/rootlessnetns/netns_linux.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ func (n *Netns) getOrCreateNetns() (netns.NetNS, bool, error) {
132132
logrus.Warnf("failed to read rootless netns program pid: %v", err)
133133
}
134134
// In case of errors continue and setup the network cmd again.
135+
// Wipe per-container pesto address state so that Setup() will
136+
// re-publish port mappings after pasta is restarted.
137+
n.WipePestoAddrState()
135138
} else {
136139
// Special case, the file might exist already but is not a valid netns.
137140
// One reason could be that a previous setup was killed between creating
@@ -634,6 +637,28 @@ func (n *Netns) PestoSocketPath() string {
634637
return n.getPath(pestoSocketFile)
635638
}
636639

640+
// PestoAddrStatePath returns the path to the per-container pesto address
641+
// state file, used to track which container IPs are active for port forwarding.
642+
func (n *Netns) PestoAddrStatePath(containerID string) string {
643+
return n.getPath("pesto-addrs-" + containerID + ".json")
644+
}
645+
646+
// WipePestoAddrState removes all pesto address state files from the rootless
647+
// netns directory. Called after pasta is restarted so that stale state from
648+
// the previous pasta process doesn't prevent Setup from re-publishing ports.
649+
func (n *Netns) WipePestoAddrState() {
650+
matches, err := filepath.Glob(n.getPath("pesto-addrs-*.json"))
651+
if err != nil {
652+
logrus.Warnf("pesto: failed to glob addr state files: %v", err)
653+
return
654+
}
655+
for _, f := range matches {
656+
if err := os.Remove(f); err != nil && !errors.Is(err, fs.ErrNotExist) {
657+
logrus.Warnf("pesto: failed to remove stale addr state %s: %v", f, err)
658+
}
659+
}
660+
}
661+
637662
func refCount(dir string, inc int) (int, error) {
638663
file := filepath.Join(dir, refCountFile)
639664
content, err := os.ReadFile(file)

common/libnetwork/netavark/network.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,13 @@ type netavarkNetwork struct {
7373
rootlessNetns *rootlessnetns.Netns
7474

7575
// rootlessPortForwarder is the value of config.RootlessPortForwarder from
76-
// containers.conf. When set to config.RootlessPortForwarderPasta, HostIP
77-
// is stripped from port mappings before passing to netavark because pasta's
78-
// splice changes the destination IP.
76+
// containers.conf. When set to config.RootlessPortForwarderPasta, pesto
77+
// handles port forwarding directly and netavark DNAT rules are skipped.
7978
rootlessPortForwarder string
79+
80+
// config is the containers.conf config, needed for FindHelperBinary
81+
// when invoking pesto for dynamic port forwarding.
82+
config *config.Config
8083
}
8184

8285
type InitConfig struct {
@@ -167,6 +170,7 @@ func NewNetworkInterface(conf *InitConfig) (types.ContainerNetwork, error) {
167170
syslog: conf.Syslog,
168171
rootlessNetns: netns,
169172
rootlessPortForwarder: conf.Config.Network.RootlessPortForwarder,
173+
config: conf.Config,
170174
}
171175

172176
return n, nil
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
//go:build linux || freebsd
2+
3+
package netavark
4+
5+
import (
6+
"encoding/json"
7+
"errors"
8+
"io/fs"
9+
"os"
10+
"sort"
11+
12+
"go.podman.io/common/libnetwork/types"
13+
)
14+
15+
// pestoAddrState tracks which container IPs are currently used for pesto
16+
// port forwarding. Stored as a per-container JSON file in the rootless
17+
// netns directory so connect/disconnect operations can detect whether
18+
// ports are already published and remap when necessary.
19+
type pestoAddrState struct {
20+
ActiveIPv4 string `json:"active_ipv4,omitempty"`
21+
ActiveIPv4Network string `json:"active_ipv4_network,omitempty"`
22+
ActiveIPv6 string `json:"active_ipv6,omitempty"`
23+
ActiveIPv6Network string `json:"active_ipv6_network,omitempty"`
24+
NetworkIPs map[string]pestoNetworkIPs `json:"network_ips"`
25+
}
26+
27+
// pestoNetworkIPs holds the first IPv4 and IPv6 address assigned to a
28+
// container on a specific network.
29+
type pestoNetworkIPs struct {
30+
IPv4 string `json:"ipv4,omitempty"`
31+
IPv6 string `json:"ipv6,omitempty"`
32+
}
33+
34+
func readPestoAddrState(path string) (*pestoAddrState, error) {
35+
data, err := os.ReadFile(path)
36+
if err != nil {
37+
if errors.Is(err, fs.ErrNotExist) {
38+
return nil, nil
39+
}
40+
return nil, err
41+
}
42+
var state pestoAddrState
43+
if err := json.Unmarshal(data, &state); err != nil {
44+
return nil, err
45+
}
46+
return &state, nil
47+
}
48+
49+
func writePestoAddrState(path string, state *pestoAddrState) error {
50+
data, err := json.Marshal(state)
51+
if err != nil {
52+
return err
53+
}
54+
tmp := path + ".tmp"
55+
if err := os.WriteFile(tmp, data, 0o600); err != nil {
56+
return err
57+
}
58+
return os.Rename(tmp, path)
59+
}
60+
61+
func removePestoAddrState(path string) {
62+
_ = os.Remove(path)
63+
}
64+
65+
// containerIPsPerNetwork extracts the first IPv4 and IPv6 from each network
66+
// in the StatusBlock result. The map is keyed by network name.
67+
func containerIPsPerNetwork(result map[string]types.StatusBlock) map[string]pestoNetworkIPs {
68+
out := make(map[string]pestoNetworkIPs, len(result))
69+
for netName, status := range result {
70+
var ips pestoNetworkIPs
71+
for _, netInt := range status.Interfaces {
72+
for _, netAddr := range netInt.Subnets {
73+
if netAddr.IPNet.IP.To4() != nil {
74+
if ips.IPv4 == "" {
75+
ips.IPv4 = netAddr.IPNet.IP.String()
76+
}
77+
} else if ips.IPv6 == "" {
78+
ips.IPv6 = netAddr.IPNet.IP.String()
79+
}
80+
}
81+
}
82+
out[netName] = ips
83+
}
84+
return out
85+
}
86+
87+
// sortedNetworkNames returns the keys of a pestoNetworkIPs map in sorted
88+
// order so that IP selection is deterministic across runs.
89+
func sortedNetworkNames(m map[string]pestoNetworkIPs) []string {
90+
names := make([]string, 0, len(m))
91+
for name := range m {
92+
names = append(names, name)
93+
}
94+
sort.Strings(names)
95+
return names
96+
}
97+
98+
// firstIPsWithNetwork picks the first IPv4 and IPv6 from the network IPs
99+
// map (sorted by network name), returning the IP and the owning network.
100+
func firstIPsWithNetwork(netIPs map[string]pestoNetworkIPs) (ipv4, ipv4Net, ipv6, ipv6Net string) {
101+
for _, name := range sortedNetworkNames(netIPs) {
102+
ips := netIPs[name]
103+
if ipv4 == "" && ips.IPv4 != "" {
104+
ipv4 = ips.IPv4
105+
ipv4Net = name
106+
}
107+
if ipv6 == "" && ips.IPv6 != "" {
108+
ipv6 = ips.IPv6
109+
ipv6Net = name
110+
}
111+
if ipv4 != "" && ipv6 != "" {
112+
break
113+
}
114+
}
115+
return
116+
}
117+
118+
// firstIPv4FromState picks the first available IPv4 from the remaining
119+
// NetworkIPs in the state (sorted by network name for determinism).
120+
func firstIPv4FromState(state *pestoAddrState) (ip, network string) {
121+
for _, name := range sortedNetworkNames(state.NetworkIPs) {
122+
if v4 := state.NetworkIPs[name].IPv4; v4 != "" {
123+
return v4, name
124+
}
125+
}
126+
return "", ""
127+
}
128+
129+
// firstIPv6FromState picks the first available IPv6 from the remaining
130+
// NetworkIPs in the state (sorted by network name for determinism).
131+
func firstIPv6FromState(state *pestoAddrState) (ip, network string) {
132+
for _, name := range sortedNetworkNames(state.NetworkIPs) {
133+
if v6 := state.NetworkIPs[name].IPv6; v6 != "" {
134+
return v6, name
135+
}
136+
}
137+
return "", ""
138+
}

0 commit comments

Comments
 (0)