@@ -17,10 +17,11 @@ limitations under the License.
1717package server
1818
1919import (
20+ "bytes"
2021 "fmt"
2122 "io"
22- "net "
23- "sync "
23+ "os/exec "
24+ "strings "
2425
2526 "github.com/containernetworking/plugins/pkg/ns"
2627 "github.com/pkg/errors"
@@ -44,8 +45,9 @@ func (c *criService) PortForward(ctx context.Context, r *runtime.PortForwardRequ
4445 return c .streamServer .GetPortForward (r )
4546}
4647
47- // portForward requires it uses netns to enter the sandbox namespace,
48- // and forward stream for a specific port.
48+ // portForward requires `socat` on the node. It uses netns to enter the sandbox namespace,
49+ // and run `socat` insidethe namespace to forward stream for a specific port. The `socat`
50+ // command keeps running until it exits or client disconnect.
4951func (c * criService ) portForward (id string , port int32 , stream io.ReadWriteCloser ) error {
5052 s , err := c .sandboxStore .Get (id )
5153 if err != nil {
@@ -55,32 +57,46 @@ func (c *criService) portForward(id string, port int32, stream io.ReadWriteClose
5557 return errors .Errorf ("network namespace for sandbox %q is closed" , id )
5658 }
5759
60+ socat , err := exec .LookPath ("socat" )
61+ if err != nil {
62+ return errors .Wrap (err , "failed to find socat" )
63+ }
64+
65+ // Check https://linux.die.net/man/1/socat for meaning of the options.
66+ args := []string {socat , "-" , fmt .Sprintf ("TCP4:localhost:%d" , port )}
67+
68+ logrus .Infof ("Executing port forwarding command %q in network namespace %q" , strings .Join (args , " " ), s .NetNS .GetPath ())
5869 err = s .NetNS .GetNs ().Do (func (_ ns.NetNS ) error {
59- var wg sync.WaitGroup
60- client , err := net .Dial ("tcp4" , fmt .Sprintf ("localhost:%d" , port ))
70+ cmd := exec .Command (args [0 ], args [1 :]... )
71+ cmd .Stdout = stream
72+
73+ stderr := new (bytes.Buffer )
74+ cmd .Stderr = stderr
75+
76+ // If we use Stdin, command.Run() won't return until the goroutine that's copying
77+ // from stream finishes. Unfortunately, if you have a client like telnet connected
78+ // via port forwarding, as long as the user's telnet client is connected to the user's
79+ // local listener that port forwarding sets up, the telnet session never exits. This
80+ // means that even if socat has finished running, command.Run() won't ever return
81+ // (because the client still has the connection and stream open).
82+ //
83+ // The work around is to use StdinPipe(), as Wait() (called by Run()) closes the pipe
84+ // when the command (socat) exits.
85+ in , err := cmd .StdinPipe ()
6186 if err != nil {
62- return errors .Wrapf (err , "failed to dial %q" , port )
87+ return errors .Wrap (err , "failed to create stdin pipe" )
6388 }
64- wg .Add (1 )
6589 go func () {
66- defer client .Close ()
67- if _ , err := io .Copy (client , stream ); err != nil {
90+ if _ , err := io .Copy (in , stream ); err != nil {
6891 logrus .WithError (err ).Errorf ("Failed to copy port forward input for %q port %d" , id , port )
6992 }
70- logrus . Infof ( "Finish copy port forward input for %q port %d" , id , port )
71- wg . Done ( )
93+ in . Close ( )
94+ logrus . Debugf ( "Finish copying port forward input for %q port %d" , id , port )
7295 }()
73- wg .Add (1 )
74- go func () {
75- defer stream .Close ()
76- if _ , err := io .Copy (stream , client ); err != nil {
77- logrus .WithError (err ).Errorf ("Failed to copy port forward output for %q port %d" , id , port )
78- }
79- logrus .Infof ("Finish copy port forward output for %q port %d" , id , port )
80- wg .Done ()
81- }()
82- wg .Wait ()
8396
97+ if err := cmd .Run (); err != nil {
98+ return errors .Errorf ("nsenter command returns error: %v, stderr: %q" , err , stderr .String ())
99+ }
84100 return nil
85101 })
86102 if err != nil {
0 commit comments