Skip to content

Commit ab2f56d

Browse files
authored
Merge pull request #156 from alpacax/145-tunneling
feat: Support for Websh based TCP-tunneling
2 parents e750b93 + dbcb7b0 commit ab2f56d

File tree

13 files changed

+661
-3
lines changed

13 files changed

+661
-3
lines changed

cmd/alpamon/command/root.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/alpacax/alpamon/cmd/alpamon/command/ftp"
1111
"github.com/alpacax/alpamon/cmd/alpamon/command/setup"
12+
"github.com/alpacax/alpamon/cmd/alpamon/command/tunnel"
1213
"github.com/alpacax/alpamon/pkg/collector"
1314
"github.com/alpacax/alpamon/pkg/config"
1415
"github.com/alpacax/alpamon/pkg/db"
@@ -37,7 +38,7 @@ var RootCmd = &cobra.Command{
3738

3839
func init() {
3940
setup.SetConfigPaths(name)
40-
RootCmd.AddCommand(setup.SetupCmd, ftp.FtpCmd)
41+
RootCmd.AddCommand(setup.SetupCmd, ftp.FtpCmd, tunnel.TunnelWorkerCmd)
4142
}
4243

4344
func runAgent() {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package tunnel
2+
3+
import (
4+
"github.com/alpacax/alpamon/pkg/runner"
5+
"github.com/spf13/cobra"
6+
)
7+
8+
// TunnelWorkerCmd is the subcommand for running the tunnel worker subprocess.
9+
// It is invoked by the main alpamon process with demoted user credentials.
10+
var TunnelWorkerCmd = &cobra.Command{
11+
Use: "tunnel-worker <targetAddr>",
12+
Short: "Tunnel worker subprocess for TCP relay",
13+
Args: cobra.ExactArgs(1),
14+
Run: func(cmd *cobra.Command, args []string) {
15+
targetAddr := args[0] // e.g., "127.0.0.1:3306"
16+
runner.RunTunnelWorker(targetAddr)
17+
},
18+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ require (
1919
github.com/shirou/gopsutil/v4 v4.24.8
2020
github.com/spf13/cobra v1.8.1
2121
github.com/stretchr/testify v1.9.0
22+
github.com/xtaci/smux v1.5.44
2223
golang.org/x/term v0.30.0
2324
gopkg.in/go-playground/validator.v9 v9.31.0
2425
gopkg.in/ini.v1 v1.67.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
114114
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
115115
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
116116
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
117+
github.com/xtaci/smux v1.5.44 h1:7T61zLfFX1jokXj6d+lPaxHnVwgYiJ7EN94DAudKqpg=
118+
github.com/xtaci/smux v1.5.44/go.mod h1:IGQ9QYrBphmb/4aTnLEcJby0TNr3NV+OslIOMrX825Q=
117119
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
118120
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
119121
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=

pkg/config/config.go

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/rs/zerolog"
1212
"github.com/rs/zerolog/log"
13+
"github.com/xtaci/smux"
1314
"gopkg.in/ini.v1"
1415
)
1516

@@ -18,10 +19,28 @@ var (
1819
)
1920

2021
const (
21-
MinConnectInterval = 5 * time.Second
22-
MaxConnectInterval = 300 * time.Second
22+
MinConnectInterval = 5 * time.Second
23+
MaxConnectInterval = 300 * time.Second
24+
SmuxKeepAliveInterval = 10 * time.Second
25+
SmuxKeepAliveTimeout = 30 * time.Second
26+
SmuxMaxFrameSize = 32768 // 32KB
27+
SmuxMaxReceiveBuffer = 4194304 // 4MB
28+
SmuxMaxStreamBuffer = 65536 // 64KB per stream
2329
)
2430

31+
// GetSmuxConfig returns optimized smux configuration for tunnel connections.
32+
func GetSmuxConfig() *smux.Config {
33+
cfg := smux.DefaultConfig()
34+
35+
cfg.KeepAliveInterval = SmuxKeepAliveInterval
36+
cfg.KeepAliveTimeout = SmuxKeepAliveTimeout
37+
cfg.MaxFrameSize = SmuxMaxFrameSize
38+
cfg.MaxReceiveBuffer = SmuxMaxReceiveBuffer
39+
cfg.MaxStreamBuffer = SmuxMaxStreamBuffer
40+
41+
return cfg
42+
}
43+
2544
func InitSettings(settings Settings) {
2645
GlobalSettings = settings
2746
}
@@ -133,6 +152,7 @@ func validateConfig(config Config, wsPath string) (bool, Settings) {
133152
}
134153
}
135154
}
155+
136156
return valid, settings
137157
}
138158

pkg/runner/command.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,55 @@ func (cr *CommandRunner) handleInternalCmd() (int, string) {
182182
}
183183

184184
return 0, "Spawned a ftp terminal."
185+
case "opentunnel":
186+
log.Debug().
187+
Str("sessionID", cr.data.SessionID).
188+
Int("targetPort", cr.data.TargetPort).
189+
Str("url", cr.data.URL).
190+
Msg("Received opentunnel command")
191+
192+
// Validate port range (1-65535, 0 is reserved)
193+
if cr.data.TargetPort < 1 || cr.data.TargetPort > 65535 {
194+
return 1, fmt.Sprintf("opentunnel: Invalid target port %d. Must be between 1 and 65535.", cr.data.TargetPort)
195+
}
196+
197+
data := openTunnelData{
198+
SessionID: cr.data.SessionID,
199+
TargetPort: cr.data.TargetPort,
200+
URL: cr.data.URL,
201+
}
202+
err := cr.validateData(data)
203+
if err != nil {
204+
return 1, fmt.Sprintf("opentunnel: Not enough information. %s", err.Error())
205+
}
206+
207+
// Check if tunnel already exists
208+
if _, exists := GetActiveTunnel(cr.data.SessionID); exists {
209+
return 1, fmt.Sprintf("opentunnel: Tunnel session %s already exists.", cr.data.SessionID)
210+
}
211+
212+
tunnelClient := NewTunnelClient(
213+
cr.data.SessionID,
214+
cr.data.TargetPort,
215+
cr.data.URL,
216+
)
217+
go tunnelClient.RunTunnelBackground()
218+
219+
return 0, fmt.Sprintf("Spawned a tunnel for session %s, target port %d.", cr.data.SessionID, cr.data.TargetPort)
220+
case "closetunnel":
221+
data := closeTunnelData{
222+
SessionID: cr.data.SessionID,
223+
}
224+
err := cr.validateData(data)
225+
if err != nil {
226+
return 1, fmt.Sprintf("closetunnel: Not enough information. %s", err.Error())
227+
}
228+
229+
if err := CloseTunnel(cr.data.SessionID); err != nil {
230+
return 1, fmt.Sprintf("closetunnel: %s", err.Error())
231+
}
232+
233+
return 0, fmt.Sprintf("Closed tunnel session %s.", cr.data.SessionID)
185234
case "resizepty":
186235
if terminals[cr.data.SessionID] != nil {
187236
err := terminals[cr.data.SessionID].resize(cr.data.Rows, cr.data.Cols)

pkg/runner/command_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ type CommandData struct {
7878
AssignmentID string `json:"assignment_id"`
7979
ServerID string `json:"server_id"`
8080
ChainNames []string `json:"chain_names"` // for firewall-reorder-chains
81+
TargetPort int `json:"target_port"` // for tunneling
8182
}
8283

8384
type firewallData struct {
@@ -158,6 +159,16 @@ type openFtpData struct {
158159
HomeDirectory string `validate:"required"`
159160
}
160161

162+
type openTunnelData struct {
163+
SessionID string `validate:"required"`
164+
TargetPort int `validate:"required"`
165+
URL string `validate:"required"`
166+
}
167+
168+
type closeTunnelData struct {
169+
SessionID string `validate:"required"`
170+
}
171+
161172
type commandFin struct {
162173
Success bool `json:"success"`
163174
Result string `json:"result"`

0 commit comments

Comments
 (0)