Skip to content

Commit 41b58b2

Browse files
authored
feat: feature to create dPanel machine behind tunnel (#4)
* feat: init feature tunnel creation for home lab * feat: feature to create dPanel machine behind tunnel
1 parent 37997e1 commit 41b58b2

22 files changed

Lines changed: 1155 additions & 45 deletions

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
1+
install:
2+
@export GOPRIVATE="github.com/devetek/*" && go mod tidy
3+
14
build:
25
./scripts/build.sh
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
services:
2+
dpanel-cli-local-docker-1:
3+
image: prakasa1904/ansible:ubuntu-22.04
4+
hostname: dpanel-cli-local-docker-1
5+
container_name: dpanel-cli-local-docker-1
6+
privileged: true
7+
ports:
8+
- 10000:22
9+
volumes:
10+
# ssh config
11+
- ${PWD}/ssh-key/sshd_config:/etc/ssh/sshd_config
12+
- ${PWD}/ssh-key/authorized_keys:/root/.ssh/authorized_keys
13+
# mount app to docker
14+
- ${PWD}/cmd:/root/apps/d-panel-cli/cmd
15+
- ${PWD}/internal:/root/apps/d-panel-cli/internal
16+
- ${PWD}/scripts:/root/apps/d-panel-cli/scripts
17+
- ${PWD}/go.mod:/root/apps/d-panel-cli/go.mod
18+
- ${PWD}/go.sum:/root/apps/d-panel-cli/go.sum
19+
restart: unless-stopped

cmd/cli/machineCmd.go

Lines changed: 86 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@ package main
33
import (
44
"fmt"
55
"os/user"
6+
"strings"
67

78
"github.com/devetek/d-panel-cli/internal/api"
89
"github.com/devetek/d-panel-cli/internal/helper"
910
"github.com/devetek/d-panel-cli/internal/logger"
11+
"github.com/devetek/d-panel-cli/internal/tunnel"
1012
"github.com/devetek/d-panel/pkg/dmachine"
13+
"github.com/devetek/d-panel/pkg/drouter"
1114
"github.com/devetek/d-panel/pkg/dsecret"
1215
"github.com/spf13/cobra"
1316
"go.uber.org/zap"
@@ -20,6 +23,13 @@ type MachineCmd struct {
2023
sshIP string
2124
sshPort string
2225
httpPort string
26+
// use behind tunnel if this machine is behind NAT without public IP
27+
behindTunnel bool
28+
// used when your machine want to expose dPanel agent behind proxy
29+
// this option used by dPanel to access your machine, example input:
30+
// - http://my-machine-01.devetek.app -> for insecure connection / HTTP
31+
// - https://my-machine-01.devetek.app -> for Secure connection / HTTPS
32+
domain string
2333
}
2434

2535
func NewMachineCmd(logger *zap.Logger) *MachineCmd {
@@ -48,7 +58,7 @@ func (m *MachineCmd) create() *cobra.Command {
4858
Run: func(cmd *cobra.Command, args []string) {
4959
// check if user has sudo access in golang
5060
if !helper.IsSudo() {
51-
logger.Error("You must run this command as sudo")
61+
logger.Error("You must run this command as sudo, currenty dpanel-agent required to running under root")
5262
return
5363
}
5464

@@ -58,7 +68,7 @@ func (m *MachineCmd) create() *cobra.Command {
5868
// check if session exist
5969
err := client.CheckSessionExist()
6070
if err != nil {
61-
logger.Error("Error check session exist: " + err.Error())
71+
logger.Error("Please login to your dPanel account, use command 'dnocs auth login --email=\"email@email.com\" --password=\"password\"'")
6272
return
6373
}
6474

@@ -104,25 +114,27 @@ func (m *MachineCmd) create() *cobra.Command {
104114
}
105115
}
106116

107-
// make sure sshIP is not empty
108-
if m.sshIP == "" {
109-
// get my public IP automatically
110-
m.sshIP, err = helper.GetMyIP()
111-
if err != nil {
112-
logger.Error("Error get my public IP " + err.Error())
113-
return
117+
if !m.behindTunnel {
118+
// make sure sshIP is not empty
119+
if m.sshIP == "" {
120+
// get my public IP automatically
121+
m.sshIP, err = helper.GetMyIP()
122+
if err != nil {
123+
logger.Error("Error get my public IP " + err.Error())
124+
return
125+
}
114126
}
115-
}
116127

117-
if m.httpPort == "" {
118-
// get available port
119-
availablePort, err := helper.FindAvailablePort()
120-
if err != nil {
121-
logger.Error("Error get available port + " + err.Error())
122-
return
123-
}
128+
if m.httpPort == "" {
129+
// get available port
130+
availablePort, err := helper.FindAvailablePort()
131+
if err != nil {
132+
logger.Error("Error get available port + " + err.Error())
133+
return
134+
}
124135

125-
m.httpPort = fmt.Sprintf("%d", availablePort)
136+
m.httpPort = fmt.Sprintf("%d", availablePort)
137+
}
126138
}
127139

128140
currentUser, err := user.Current()
@@ -131,18 +143,69 @@ func (m *MachineCmd) create() *cobra.Command {
131143
return
132144
}
133145

146+
// integrate with tunnel
147+
if m.behindTunnel {
148+
var currentTunnel = tunnel.NewTunnel()
149+
150+
// check tunnel configs
151+
var tunnelConfig = currentTunnel.GetConfig()
152+
if len(tunnelConfig) == 0 {
153+
logger.Error("This machine is not connected to dPanel tunnel")
154+
return
155+
}
156+
157+
var tunnelHTTPPort string
158+
var originHTTPPort string
159+
for _, tunnel := range tunnelConfig {
160+
if strings.Contains(tunnel.ID, "ssh-") {
161+
m.sshIP = tunnel.TunnelHost
162+
m.sshPort = tunnel.ListenerPort
163+
}
164+
165+
if strings.Contains(tunnel.ID, "http-") {
166+
tunnelHTTPPort = tunnel.ListenerPort
167+
originHTTPPort = tunnel.ServicePort
168+
}
169+
}
170+
171+
// set payload
172+
var payload = drouter.PayloadRouter{
173+
AdvanceMode: false,
174+
Type: "proxy_pass",
175+
Name: fmt.Sprintf("http-%s-to-%s", tunnelHTTPPort, originHTTPPort),
176+
Domain: fmt.Sprintf("http-%s-to-%s 1", tunnelHTTPPort, originHTTPPort),
177+
MachineID: 11,
178+
Upstream: fmt.Sprintf("localhost:%s", tunnelHTTPPort),
179+
}
180+
181+
router, err := client.CreateRouter(payload)
182+
if err != nil {
183+
logger.Error("Failed to create HTTP server for this machine, with error " + err.Error())
184+
logger.Error("Login to dPanel, open https://cloud-beta.terpusat.com/router, and delete existing domain")
185+
return
186+
}
187+
188+
// set domain for this machine
189+
m.httpPort = tunnelHTTPPort
190+
m.domain = router.Data.Domain
191+
}
192+
134193
// register new server
135194
newServer := dmachine.Payload{
136195
Provider: "other",
137196
SecretID: fmt.Sprintf("%d", mySSHKey.ID),
138197
Address: m.sshIP,
139198
SSHPort: m.sshPort,
140199
HTTPPort: m.httpPort,
141-
Domain: "",
200+
Domain: m.domain,
142201
SSHUser: currentUser.Username,
143202
}
144203

145-
// TODO: Create file ~/.devetek/machine.json to prevent multiple registration
204+
// check if server already registered
205+
if client.IsRegistered() {
206+
logger.Error("Server already registered with your account")
207+
return
208+
}
146209

147210
// register new server
148211
server, err := client.RegisterServer(newServer)
@@ -164,7 +227,9 @@ func (m *MachineCmd) create() *cobra.Command {
164227

165228
runCmd.PersistentFlags().StringVarP(&m.sshIP, "ssh-ip", "i", "", "SSH IP of your machine")
166229
runCmd.PersistentFlags().StringVarP(&m.sshPort, "ssh-port", "s", "22", "SSH port of your machine")
167-
runCmd.PersistentFlags().StringVarP(&m.httpPort, "http-port", "t", "9000", "HTTP port of your machine")
230+
runCmd.PersistentFlags().StringVarP(&m.httpPort, "http-port", "p", "9000", "HTTP port of your machine")
231+
runCmd.PersistentFlags().StringVarP(&m.domain, "http-domain", "d", "", "HTTP domain of agent (optional)")
232+
runCmd.PersistentFlags().BoolVarP(&m.behindTunnel, "behind-tunnel", "t", false, "Read tunnel config and auto create domain")
168233

169234
return runCmd
170235
}

cmd/cli/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ func init() {
2626

2727
rootCmd.AddCommand(
2828
NewAuthCmd(logger).Connect(),
29+
NewTunnelCmd(logger).Connect(),
2930
NewMachineCmd(logger).Connect(),
3031
versionCmd(),
32+
systemInfoCmd(),
3133
)
3234
}
3335

cmd/cli/systemInfoCmd.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package main
2+
3+
import (
4+
"runtime"
5+
6+
"github.com/devetek/d-panel-cli/internal/logger"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
func systemInfoCmd() *cobra.Command {
11+
return &cobra.Command{
12+
Use: "info",
13+
Short: "Prints the version",
14+
Run: func(cmd *cobra.Command, args []string) {
15+
logger.Normal("Your System Information:")
16+
logger.Success("OS: " + runtime.GOOS)
17+
logger.Success("Arch: " + runtime.GOARCH)
18+
},
19+
}
20+
}

cmd/cli/tunnelCmd.go

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/devetek/d-panel-cli/internal/api"
7+
"github.com/devetek/d-panel-cli/internal/helper"
8+
"github.com/devetek/d-panel-cli/internal/logger"
9+
"github.com/devetek/d-panel-cli/internal/tunnel"
10+
"github.com/devetek/tuman/pkg/marijan"
11+
"github.com/spf13/cobra"
12+
"go.uber.org/zap"
13+
)
14+
15+
type TunnelCmd struct {
16+
cmd *cobra.Command
17+
zapLogger *zap.Logger
18+
19+
tunnelHttpListener string
20+
tunnelHttpService string
21+
tunnelSshListener string
22+
tunnelSshService string
23+
}
24+
25+
func NewTunnelCmd(logger *zap.Logger) *TunnelCmd {
26+
return &TunnelCmd{
27+
zapLogger: logger,
28+
cmd: &cobra.Command{
29+
Use: "tunnel",
30+
Short: "Manage dPanel tunnel",
31+
},
32+
}
33+
}
34+
35+
func (m *TunnelCmd) Connect() *cobra.Command {
36+
m.cmd.AddCommand(
37+
m.create(),
38+
)
39+
40+
return m.cmd
41+
}
42+
43+
func (m *TunnelCmd) create() *cobra.Command {
44+
var runCmd = &cobra.Command{
45+
Use: "create",
46+
Short: "Open connection to tunnel",
47+
Long: `Create public access to this machine use tunnel, make it accessible from dPanel.`,
48+
Run: func(cmd *cobra.Command, args []string) {
49+
// init dPanel client
50+
client := api.NewClient()
51+
52+
// check if session exist
53+
err := client.CheckSessionExist()
54+
if err != nil {
55+
logger.Error("Please login to your dPanel account, use command 'dnocs auth login --email=\"email@email.com\" --password=\"password\"'")
56+
return
57+
}
58+
59+
// TODO: Remove sync communication after MQTT architecture completed!
60+
if m.tunnelHttpListener == "" {
61+
logger.Error("Please set HTTP public listener in the tunnel")
62+
return
63+
}
64+
65+
if m.tunnelSshListener == "" {
66+
logger.Error("Please set SSH public listener in the tunnel")
67+
return
68+
}
69+
70+
if !helper.IsSudo() {
71+
logger.Error("You must run this command as sudo, currenty tunnel required to running under root")
72+
return
73+
}
74+
75+
if helper.IsPortUsed(tunnel.TunnelHost, m.tunnelHttpListener) {
76+
logger.Error("Port already in used in the tunnel server, choose another HTTP port or contact prakasa@devetek.com")
77+
return
78+
}
79+
80+
if helper.IsPortUsed(tunnel.TunnelHost, m.tunnelSshListener) {
81+
logger.Error("Port already in used in the tunnel server, choose another SSH port or contact prakasa@devetek.com")
82+
return
83+
}
84+
85+
var tunnelCreation = tunnel.NewTunnel().SetConfig([]marijan.Config{
86+
{
87+
NoTCP: false,
88+
ID: fmt.Sprintf("ssh-%s-to-%s", m.tunnelSshListener, m.tunnelSshService),
89+
TunnelHost: tunnel.TunnelHost,
90+
TunnelPort: tunnel.TunnelPort,
91+
ListenerHost: "0.0.0.0",
92+
ListenerPort: m.tunnelSshListener,
93+
ServiceHost: "localhost",
94+
ServicePort: m.tunnelSshService,
95+
State: marijan.ConfigStateActive,
96+
},
97+
{
98+
NoTCP: false,
99+
ID: fmt.Sprintf("http-%s-to-%s", m.tunnelHttpListener, m.tunnelHttpService),
100+
TunnelHost: tunnel.TunnelHost,
101+
TunnelPort: tunnel.TunnelPort,
102+
ListenerHost: "0.0.0.0",
103+
ListenerPort: m.tunnelHttpListener,
104+
ServiceHost: "localhost",
105+
ServicePort: m.tunnelHttpService,
106+
State: marijan.ConfigStateActive,
107+
},
108+
})
109+
110+
err = tunnelCreation.Download()
111+
if err != nil {
112+
logger.Error(err.Error())
113+
}
114+
115+
err = tunnelCreation.Extract()
116+
if err != nil {
117+
logger.Error(err.Error())
118+
}
119+
120+
err = tunnelCreation.CreateService()
121+
if err != nil {
122+
logger.Error(err.Error())
123+
}
124+
125+
},
126+
}
127+
128+
runCmd.PersistentFlags().StringVarP(&m.tunnelHttpListener, "tunnel-http-listener", "", "", "Public SSH listener to your machine")
129+
runCmd.PersistentFlags().StringVarP(&m.tunnelHttpService, "tunnel-http-service", "", "9000", "HTTP port of your machine")
130+
runCmd.PersistentFlags().StringVarP(&m.tunnelSshListener, "tunnel-ssh-listener", "", "", "Public SSH listener to your machine")
131+
runCmd.PersistentFlags().StringVarP(&m.tunnelSshService, "tunnel-ssh-service", "", "22", "SSH port of your machine")
132+
133+
return runCmd
134+
}

cmd/cli/versionCmd.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
package main
22

33
import (
4-
"fmt"
5-
4+
"github.com/devetek/d-panel-cli/internal/logger"
65
"github.com/spf13/cobra"
76
)
87

9-
var currentVersion string = ""
8+
var currentVersion string = "dev"
109

1110
func versionCmd() *cobra.Command {
1211
return &cobra.Command{
1312
Use: "version",
1413
Short: "Prints the version",
1514
Run: func(cmd *cobra.Command, args []string) {
16-
fmt.Println(currentVersion)
15+
logger.Success("Version: " + currentVersion)
1716
},
1817
}
1918
}

0 commit comments

Comments
 (0)