Skip to content

Commit

Permalink
On Podman, detect and fail if port is bound to the loopback interface
Browse files Browse the repository at this point in the history
See redhat-developer#6510 for the rationale
  • Loading branch information
rm3l committed Feb 23, 2023
1 parent 6b9c6a9 commit fc47c36
Show file tree
Hide file tree
Showing 2 changed files with 212 additions and 0 deletions.
189 changes: 189 additions & 0 deletions pkg/dev/podmandev/port.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package podmandev

import (
"fmt"
"net"
"regexp"
"strconv"
"strings"

"k8s.io/klog"

"github.com/redhat-developer/odo/pkg/api"
"github.com/redhat-developer/odo/pkg/exec"
"github.com/redhat-developer/odo/pkg/remotecmd"
)

const (
// Value in the TCP States enum.
// See https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/net/tcp_states.h#n12
listen = 10
)

var ipv4HexRegExp = regexp.MustCompile(".{2}")

type listeningConnection struct {
localAddress string
localPort int
}

// detectRemotePortsBoundOnLoopback filters the given ports
// by returning only those that are actually bound to the loopback interface in the specified container.
func detectRemotePortsBoundOnLoopback(execClient exec.Client, podName string, containerName string, ports []api.ForwardedPort) ([]api.ForwardedPort, error) {
listening, err := getListeningConnections(execClient, podName, containerName)
if err != nil {
return nil, err
}
var boundToLocalhost []api.ForwardedPort
for _, p := range ports {
for _, conn := range listening {
if p.ContainerPort != conn.localPort {
continue
}
ip := net.ParseIP(conn.localAddress)
if ip == nil {
klog.V(6).Infof("invalid IP address: %q", conn.localAddress)
continue
}
if ip.IsLoopback() {
boundToLocalhost = append(boundToLocalhost, p)
break
}
}
}
return boundToLocalhost, nil
}

// getListeningConnections retrieves information about ports being listened and on which local address in the specified container.
// It works by parsing information from the /proc/net/{tcp,tcp6,udp,udp6} files, and is able to parse both IPv4 and IPv6 addresses.
// See https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt for more information about the structure of these files.
func getListeningConnections(execClient exec.Client, podName string, containerName string) ([]listeningConnection, error) {
cmd := []string{
remotecmd.ShellExecutable, "-c",
"cat /proc/net/tcp /proc/net/udp /proc/net/tcp6 /proc/net/udp6",
}
stdout, _, err := execClient.ExecuteCommand(cmd, podName, containerName, false, nil, nil)
if err != nil {
return nil, err
}

hexToInt := func(hex string) (int, error) {
i, parseErr := strconv.ParseInt(hex, 16, 32)
if parseErr != nil {
return 0, parseErr
}
return int(i), nil
}

hexRevIpV4ToString := func(hex string) (string, error) {
parts := ipv4HexRegExp.FindAllString(hex, -1)
result := make([]string, 0, len(parts))
for i := len(parts) - 1; i >= 0; i-- {
toInt, parseErr := hexToInt(parts[i])
if parseErr != nil {
return "", parseErr
}
result = append(result, fmt.Sprintf("%d", toInt))
}
return strings.Join(result, "."), nil
}

hexRevIpV6ToString := func(hex string) string {
// In IPv6, each group of the address is 2 bytes long (4 hex characters).
// See https://www.rfc-editor.org/rfc/rfc4291#page-4
i := []string{
hex[30:32],
hex[28:30],
hex[26:28],
hex[24:26],
hex[22:24],
hex[20:22],
hex[18:20],
hex[16:18],
hex[14:16],
hex[12:14],
hex[10:12],
hex[8:10],
hex[6:8],
hex[4:6],
hex[2:4],
hex[0:2],
}
return fmt.Sprintf("%v%v:%v%v:%v%v:%v%v:%v%v:%v%v:%v%v:%v%v",
i[12], i[13], i[14], i[15],
i[8], i[9], i[10], i[11],
i[4], i[5], i[7], i[7],
i[0], i[1], i[2], i[3])
}

var listeningConnections []listeningConnection
for _, l := range stdout {
if strings.Contains(l, "local_address") {
// ignore header line
continue
}

/*
We are interested only in the first 4 values, which provide information about the local address, port and the connection state.
See https://www.kernel.org/doc/Documentation/networking/proc_net_tcp.txt
46: 010310AC:9C4C 030310AC:1770 01
| | | | | |--> connection state
| | | | |------> remote TCP port number
| | | |-------------> remote IPv4 address
| | |--------------------> local TCP port number
| |---------------------------> local IPv4 address
|----------------------------------> number of entry
*/
split := strings.SplitN(strings.TrimSpace(l), " ", 5)
if len(split) < 4 {
klog.V(5).Infof("ignored line %q because it has less than 4 space-separated elements", l)
continue
}
stateHex := split[3]
state, err := hexToInt(stateHex)
if err != nil {
klog.V(5).Infof("[warn] could not decode state info from line %q: %v", l, err)
continue
}
if state != listen {
klog.V(5).Infof("ignored line because it indicates a connection in non-LISTEN mode: %q", l)
continue
}
localAddressAndPort := split[1]
addrPortList := strings.Split(localAddressAndPort, ":")
if len(addrPortList) < 2 {
klog.V(5).Infof("ignored line because it is not possible to determine local addr and port: %q", l)
continue
}

addrHex := addrPortList[0]
var addr string
switch len(addrHex) {
case 8:
addr, err = hexRevIpV4ToString(addrHex)
case 32:
addr = hexRevIpV6ToString(addrHex)
default:
err = fmt.Errorf("length must be 8 (IPv4) or 32 (IPv6), but was %d", len(addrHex))
}
if err != nil {
klog.V(5).Infof("[warn] could not decode address info from line %q: %v", err)
continue
}

portHex := addrPortList[1]
port, err := hexToInt(portHex)
if err != nil {
klog.V(5).Infof("[warn] could not decode port info from line %q: %v", l, err)
continue
}

listeningConnections = append(listeningConnections, listeningConnection{
localAddress: addr,
localPort: port,
})
}

return listeningConnections, nil
}
23 changes: 23 additions & 0 deletions pkg/dev/podmandev/reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package podmandev

import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
Expand Down Expand Up @@ -113,6 +114,28 @@ func (o *DevClient) reconcile(
componentStatus.RunExecuted = true
}

// By default, Podman will not forward to container applications listening on the loopback interface.
// So we are trying to detect such cases accordingly.
// See https://github.com/redhat-developer/odo/issues/6510#issuecomment-1439986558
loopbackPorts, err := detectRemotePortsBoundOnLoopback(o.execClient, pod.Name, pod.Spec.Containers[0].Name, fwPorts)
if err != nil {
return fmt.Errorf("unable to detect container ports bound on the loopback interface: %w", err)
}
klog.V(4).Infof("detected %d ports bound on the loopback interface in the pod: %v", len(loopbackPorts), loopbackPorts)
if len(loopbackPorts) != 0 {
list := make([]string, 0, len(loopbackPorts))
for _, p := range loopbackPorts {
list = append(list, fmt.Sprintf("%s (%d)", p.PortName, p.ContainerPort))
}
log.Errorf(`Detected that the following port(s) are accessible only on the container loopback interface: %s.
Port forwarding on Podman currently does not work with applications listening on the loopback interface.
Either change the application to make those port(s) available on all interfaces (0.0.0.0), or rerun 'odo dev' with any of the following flags:
- --ignore-localhost: no error will be returned by odo, but port-forwarding on those ports might not work on Podman.
- --forward-localhost: odo will inject a dedicated side container to redirect traffic to the application.
`, strings.Join(list, ", "))
return errors.New("cannot make port forwarding work with ports bound to the loopback interface only")
}

for _, fwPort := range fwPorts {
s := fmt.Sprintf("Forwarding from %s:%d -> %d", fwPort.LocalAddress, fwPort.LocalPort, fwPort.ContainerPort)
fmt.Fprintf(out, " - %s", log.SboldColor(color.FgGreen, s))
Expand Down

0 comments on commit fc47c36

Please sign in to comment.