@@ -21,6 +21,7 @@ import (
21
21
"time"
22
22
23
23
"github.com/armon/circbuf"
24
+ "github.com/cakturk/go-netstat/netstat"
24
25
"github.com/gliderlabs/ssh"
25
26
"github.com/google/uuid"
26
27
"github.com/pkg/sftp"
@@ -37,13 +38,15 @@ import (
37
38
)
38
39
39
40
const (
41
+ ProtocolNetstat = "netstat"
40
42
ProtocolReconnectingPTY = "reconnecting-pty"
41
43
ProtocolSSH = "ssh"
42
44
ProtocolDial = "dial"
43
45
)
44
46
45
47
type Options struct {
46
48
ReconnectingPTYTimeout time.Duration
49
+ NetstatInterval time.Duration
47
50
EnvironmentVariables map [string ]string
48
51
Logger slog.Logger
49
52
}
@@ -65,10 +68,14 @@ func New(dialer Dialer, options *Options) io.Closer {
65
68
if options .ReconnectingPTYTimeout == 0 {
66
69
options .ReconnectingPTYTimeout = 5 * time .Minute
67
70
}
71
+ if options .NetstatInterval == 0 {
72
+ options .NetstatInterval = 5 * time .Second
73
+ }
68
74
ctx , cancelFunc := context .WithCancel (context .Background ())
69
75
server := & agent {
70
76
dialer : dialer ,
71
77
reconnectingPTYTimeout : options .ReconnectingPTYTimeout ,
78
+ netstatInterval : options .NetstatInterval ,
72
79
logger : options .Logger ,
73
80
closeCancel : cancelFunc ,
74
81
closed : make (chan struct {}),
@@ -85,6 +92,8 @@ type agent struct {
85
92
reconnectingPTYs sync.Map
86
93
reconnectingPTYTimeout time.Duration
87
94
95
+ netstatInterval time.Duration
96
+
88
97
connCloseWait sync.WaitGroup
89
98
closeCancel context.CancelFunc
90
99
closeMutex sync.Mutex
@@ -225,6 +234,8 @@ func (a *agent) handlePeerConn(ctx context.Context, conn *peer.Conn) {
225
234
go a .handleReconnectingPTY (ctx , channel .Label (), channel .NetConn ())
226
235
case ProtocolDial :
227
236
go a .handleDial (ctx , channel .Label (), channel .NetConn ())
237
+ case ProtocolNetstat :
238
+ go a .handleNetstat (ctx , channel .Label (), channel .NetConn ())
228
239
default :
229
240
a .logger .Warn (ctx , "unhandled protocol from channel" ,
230
241
slog .F ("protocol" , channel .Protocol ()),
@@ -359,12 +370,10 @@ func (a *agent) createCommand(ctx context.Context, rawCommand string, env []stri
359
370
if err != nil {
360
371
return nil , xerrors .Errorf ("getting os executable: %w" , err )
361
372
}
362
- cmd .Env = append (cmd .Env , fmt .Sprintf ("USER=%s" , username ))
363
- cmd .Env = append (cmd .Env , fmt .Sprintf (`PATH=%s%c%s` , os .Getenv ("PATH" ), filepath .ListSeparator , filepath .Dir (executablePath )))
364
373
// Git on Windows resolves with UNIX-style paths.
365
374
// If using backslashes, it's unable to find the executable.
366
- unixExecutablePath : = strings .ReplaceAll (executablePath , "\\ " , "/" )
367
- cmd .Env = append (cmd .Env , fmt .Sprintf (`GIT_SSH_COMMAND=%s gitssh --` , unixExecutablePath ))
375
+ executablePath = strings .ReplaceAll (executablePath , "\\ " , "/" )
376
+ cmd .Env = append (cmd .Env , fmt .Sprintf (`GIT_SSH_COMMAND=%s gitssh --` , executablePath ))
368
377
// These prevent the user from having to specify _anything_ to successfully commit.
369
378
// Both author and committer must be set!
370
379
cmd .Env = append (cmd .Env , fmt .Sprintf (`GIT_AUTHOR_EMAIL=%s` , metadata .OwnerEmail ))
@@ -707,6 +716,87 @@ func (a *agent) handleDial(ctx context.Context, label string, conn net.Conn) {
707
716
Bicopy (ctx , conn , nconn )
708
717
}
709
718
719
+ type NetstatPort struct {
720
+ Name string `json:"name"`
721
+ Port uint16 `json:"port"`
722
+ }
723
+
724
+ type NetstatResponse struct {
725
+ Ports []NetstatPort `json:"ports"`
726
+ Error string `json:"error,omitempty"`
727
+ Took time.Duration `json:"took"`
728
+ }
729
+
730
+ func (a * agent ) handleNetstat (ctx context.Context , label string , conn net.Conn ) {
731
+ write := func (resp NetstatResponse ) error {
732
+ b , err := json .Marshal (resp )
733
+ if err != nil {
734
+ a .logger .Warn (ctx , "write netstat response" , slog .F ("label" , label ), slog .Error (err ))
735
+ return xerrors .Errorf ("marshal agent netstat response: %w" , err )
736
+ }
737
+ _ , err = conn .Write (b )
738
+ if err != nil {
739
+ a .logger .Warn (ctx , "write netstat response" , slog .F ("label" , label ), slog .Error (err ))
740
+ }
741
+ return err
742
+ }
743
+
744
+ scan := func () ([]NetstatPort , error ) {
745
+ if runtime .GOOS != "linux" && runtime .GOOS != "windows" {
746
+ return nil , xerrors .New (fmt .Sprintf ("Port scanning is not supported on %s" , runtime .GOOS ))
747
+ }
748
+
749
+ tabs , err := netstat .TCPSocks (func (s * netstat.SockTabEntry ) bool {
750
+ return s .State == netstat .Listen
751
+ })
752
+ if err != nil {
753
+ return nil , err
754
+ }
755
+
756
+ ports := []NetstatPort {}
757
+ for _ , tab := range tabs {
758
+ ports = append (ports , NetstatPort {
759
+ Name : tab .Process .Name ,
760
+ Port : tab .LocalAddr .Port ,
761
+ })
762
+ }
763
+ return ports , nil
764
+ }
765
+
766
+ scanAndWrite := func () {
767
+ start := time .Now ()
768
+ ports , err := scan ()
769
+ response := NetstatResponse {
770
+ Ports : ports ,
771
+ Took : time .Since (start ),
772
+ }
773
+ if err != nil {
774
+ response .Error = err .Error ()
775
+ }
776
+ _ = write (response )
777
+ }
778
+
779
+ scanAndWrite ()
780
+
781
+ // Using a timer instead of a ticker to ensure delay between calls otherwise
782
+ // if nestat took longer than the interval we would constantly run it.
783
+ timer := time .NewTimer (a .netstatInterval )
784
+ go func () {
785
+ defer conn .Close ()
786
+ defer timer .Stop ()
787
+
788
+ for {
789
+ select {
790
+ case <- ctx .Done ():
791
+ return
792
+ case <- timer .C :
793
+ scanAndWrite ()
794
+ timer .Reset (a .netstatInterval )
795
+ }
796
+ }
797
+ }()
798
+ }
799
+
710
800
// isClosed returns whether the API is closed or not.
711
801
func (a * agent ) isClosed () bool {
712
802
select {
0 commit comments