-
Notifications
You must be signed in to change notification settings - Fork 103
Add pesto support for dynamic port forwarding via pasta control socket
#755
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| // Pesto client for dynamic port forwarding on a running pasta instance. | ||
| // | ||
| // Pesto updates pasta's forwarding table via a UNIX domain socket (-c). | ||
| // Used by rootless bridge networking: pesto incrementally adds or deletes | ||
| // port forwarding rules for individual containers. | ||
| // | ||
| // Limitations: | ||
| // - IPv4 only (netavark DNAT is IPv4; IPv6 bindings cause RST) | ||
| // - TCP and UDP only (SCTP is silently skipped) | ||
|
|
||
| package pasta | ||
|
|
||
| import ( | ||
| "errors" | ||
| "fmt" | ||
| "os/exec" | ||
| "strings" | ||
|
|
||
| "github.com/sirupsen/logrus" | ||
| "go.podman.io/common/libnetwork/types" | ||
| "go.podman.io/common/pkg/config" | ||
| ) | ||
|
|
||
| const PestoBinaryName = "pesto" | ||
|
|
||
| // PestoAddPorts adds port forwarding rules to the running pasta instance | ||
| // via -A/--add. Idempotent: adding already-active ports is a no-op. | ||
| func PestoAddPorts(conf *config.Config, socketPath string, ports []types.PortMapping) error { | ||
| if socketPath == "" { | ||
| return errors.New("pesto control socket not available") | ||
| } | ||
| logrus.Debugf("pesto: adding %d port mappings", len(ports)) | ||
| return pestoModifyPorts(conf, socketPath, ports, "--add") | ||
| } | ||
|
|
||
| // PestoDeletePorts removes port forwarding rules from the running pasta | ||
| // instance via -D/--delete. | ||
| func PestoDeletePorts(conf *config.Config, socketPath string, ports []types.PortMapping) error { | ||
| if socketPath == "" { | ||
| return nil | ||
| } | ||
| logrus.Debugf("pesto: deleting %d port mappings", len(ports)) | ||
| return pestoModifyPorts(conf, socketPath, ports, "--delete") | ||
| } | ||
|
|
||
| func pestoModifyPorts(conf *config.Config, socketPath string, ports []types.PortMapping, mode string) error { | ||
| pestoPath, err := conf.FindHelperBinary(PestoBinaryName, true) | ||
| if err != nil { | ||
| return fmt.Errorf("could not find pesto binary: %w", err) | ||
| } | ||
|
|
||
| pestoArgs := portMappingsToPestoArgs(ports) | ||
| args := make([]string, 0, len(pestoArgs)+2) // +2 for mode and socket path | ||
| args = append(args, mode) | ||
| args = append(args, pestoArgs...) | ||
| args = append(args, socketPath) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can socketPath == ""
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it cannot be. It should be checked by the caller. Should I add a check to be sure? |
||
|
|
||
| logrus.Debugf("pesto arguments: %s", strings.Join(args, " ")) | ||
|
|
||
| out, err := exec.Command(pestoPath, args...).CombinedOutput() | ||
| if err != nil { | ||
| return fmt.Errorf("pesto failed: %w\noutput: %s", err, string(out)) | ||
| } | ||
| if len(out) > 0 { | ||
| logrus.Debugf("pesto output: %s", strings.TrimSpace(string(out))) | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // portMappingsToPestoArgs converts PortMappings into pesto CLI arguments. | ||
| // | ||
| // Pesto only forwards traffic from the host into the rootless netns. This | ||
| // does NOT perform DNAT to the container. Netavark handles that inside the | ||
| // netns. Therefore each mapping uses HostPort as both source and destination | ||
| // (e.g. "-t 0.0.0.0/8080") so traffic arrives at the port netavark expects. | ||
| func portMappingsToPestoArgs(ports []types.PortMapping) []string { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does this func make sense if ports is empty? i think you will get something in args that is not expected?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this makes sense. If no ports are specified, it will set |
||
| var args []string | ||
|
|
||
| for _, p := range ports { | ||
| // Netavark's DNAT rules use "dnat ip to" which only matches IPv4. | ||
| // Restrict pesto to the correct address family so pasta doesn't | ||
| // accept IPv6 connections that can't be DNAT'd (which causes RST). | ||
| addr := "0.0.0.0/" | ||
| if p.HostIP != "" { | ||
| if strings.Contains(p.HostIP, ":") { | ||
| addr = "[" + p.HostIP + "]/" | ||
| } else { | ||
| addr = p.HostIP + "/" | ||
| } | ||
| } | ||
|
|
||
| for protocol := range strings.SplitSeq(p.Protocol, ",") { | ||
| var flag string | ||
| switch protocol { | ||
| case "tcp": | ||
| flag = "-t" | ||
| case "udp": | ||
| flag = "-u" | ||
| default: | ||
| logrus.Warnf("pesto: unsupported protocol %q, skipping", protocol) | ||
| continue | ||
| } | ||
|
|
||
| portRange := p.Range | ||
| if portRange == 0 { | ||
| portRange = 1 | ||
| } | ||
|
|
||
| var arg string | ||
| if portRange == 1 { | ||
| arg = fmt.Sprintf("%s%d", addr, p.HostPort) | ||
| } else { | ||
| arg = fmt.Sprintf("%s%d-%d", addr, p.HostPort, p.HostPort+portRange-1) | ||
| } | ||
| args = append(args, flag, arg) | ||
| } | ||
| } | ||
|
|
||
| return args | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,156 @@ | ||
| package pasta | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "go.podman.io/common/libnetwork/types" | ||
| ) | ||
|
|
||
| func Test_portMappingsToPestoArgs(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| ports []types.PortMapping | ||
| want []string | ||
| }{ | ||
| { | ||
| name: "no ports returns nil", | ||
| ports: nil, | ||
| want: nil, | ||
| }, | ||
| { | ||
| name: "empty slice same as nil", | ||
| ports: []types.PortMapping{}, | ||
| want: nil, | ||
| }, | ||
| { | ||
| name: "single tcp port defaults to 0.0.0.0", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/8080"}, | ||
| }, | ||
| { | ||
| name: "single udp port", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1}, | ||
| }, | ||
| want: []string{"-u", "0.0.0.0/53"}, | ||
| }, | ||
| { | ||
| name: "tcp and udp port", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1}, | ||
| {HostPort: 53, ContainerPort: 53, Protocol: "udp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/80", "-u", "0.0.0.0/53"}, | ||
| }, | ||
| { | ||
| name: "dual protocol on single mapping", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 80, ContainerPort: 80, Protocol: "tcp,udp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/80", "-u", "0.0.0.0/80"}, | ||
| }, | ||
| { | ||
| name: "port range expands to host port range", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 8000, ContainerPort: 80, Protocol: "tcp", Range: 5}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/8000-8004"}, | ||
| }, | ||
| { | ||
| name: "range of zero treated as single port", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 0}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/80"}, | ||
| }, | ||
| { | ||
| name: "range of two", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 3000, ContainerPort: 3000, Protocol: "tcp", Range: 2}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/3000-3001"}, | ||
| }, | ||
| { | ||
| name: "explicit IPv4 host IP", | ||
| ports: []types.PortMapping{ | ||
| {HostIP: "127.0.0.1", HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "127.0.0.1/443"}, | ||
| }, | ||
| { | ||
| name: "IPv6 host IP gets brackets", | ||
| ports: []types.PortMapping{ | ||
| {HostIP: "::1", HostPort: 8080, ContainerPort: 80, Protocol: "tcp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "[::1]/8080"}, | ||
| }, | ||
| { | ||
| name: "full-form IPv6 host IP", | ||
| ports: []types.PortMapping{ | ||
| {HostIP: "fd00::1", HostPort: 80, ContainerPort: 80, Protocol: "udp", Range: 1}, | ||
| }, | ||
| want: []string{"-u", "[fd00::1]/80"}, | ||
| }, | ||
| { | ||
| name: "multiple tcp ports", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1}, | ||
| {HostPort: 443, ContainerPort: 443, Protocol: "tcp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/80", "-t", "0.0.0.0/443"}, | ||
| }, | ||
| { | ||
| name: "unsupported protocol is skipped", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 80, ContainerPort: 80, Protocol: "sctp", Range: 1}, | ||
| }, | ||
| want: nil, | ||
| }, | ||
| { | ||
| name: "unsupported protocol mixed with valid", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 80, ContainerPort: 80, Protocol: "tcp", Range: 1}, | ||
| {HostPort: 90, ContainerPort: 90, Protocol: "sctp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/80"}, | ||
| }, | ||
| { | ||
| name: "explicit host IP on udp", | ||
| ports: []types.PortMapping{ | ||
| {HostIP: "10.0.0.1", HostPort: 3000, ContainerPort: 3000, Protocol: "udp", Range: 1}, | ||
| }, | ||
| want: []string{"-u", "10.0.0.1/3000"}, | ||
| }, | ||
| { | ||
| name: "container port does not appear in args", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 9090, ContainerPort: 3000, Protocol: "tcp", Range: 1}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/9090"}, | ||
| }, | ||
| { | ||
| name: "host IP with range", | ||
| ports: []types.PortMapping{ | ||
| {HostIP: "10.0.0.1", HostPort: 3000, ContainerPort: 3000, Protocol: "udp", Range: 3}, | ||
| }, | ||
| want: []string{"-u", "10.0.0.1/3000-3002"}, | ||
| }, | ||
| { | ||
| name: "range with dual protocol", | ||
| ports: []types.PortMapping{ | ||
| {HostPort: 5000, ContainerPort: 5000, Protocol: "tcp,udp", Range: 3}, | ||
| }, | ||
| want: []string{"-t", "0.0.0.0/5000-5002", "-u", "0.0.0.0/5000-5002"}, | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| got := portMappingsToPestoArgs(tt.ports) | ||
| assert.Equal(t, tt.want, got) | ||
| }) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.