@@ -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 {