Skip to content

Commit d2778ca

Browse files
committed
fix: validation rules resolved
1 parent b4a6ba1 commit d2778ca

6 files changed

Lines changed: 202 additions & 37 deletions

File tree

README.md

Lines changed: 82 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ telepath -f /etc/telepath/telepath.json
9797
```
9898

9999
## Define Config file
100-
Config file is a JSON file which contains list of config. Here I have attached a sample config file-
100+
The configuration file is a JSON array of objects. Each object defines a tunnel.
101101

102102
```json
103103
[
@@ -117,41 +117,94 @@ Config file is a JSON file which contains list of config. Here I have attached a
117117
"key": "/etc/autossh/id_rsa",
118118
"passphrase": "passphrase",
119119
"jump": {
120-
"host": "jump-host-ip",
120+
"host": "jump-1-ip",
121121
"port": 22,
122122
"username": "user",
123123
"authType": "KEY",
124124
"password": "",
125125
"key": "/etc/autossh/id_rsa",
126-
"passphrase": "passphrase"
127-
}
128-
}
129-
},
130-
{
131-
"name": "mysql",
132-
"type": "R",
133-
"localPort": 3306,
134-
"localHost": "0.0.0.0",
135-
"remotePort": 3306,
136-
"remoteHost": "0.0.0.0",
137-
"server": {
138-
"host": "final-host-ip",
139-
"port": 22,
140-
"username": "user",
141-
"authType": "KEY",
142-
"password": "",
143-
"key": "/etc/autossh/id_rsa",
144-
"passphrase": "passphrase",
145-
"jump": {
146-
"host": "jump-host-ip",
147-
"port": 22,
148-
"username": "user",
149-
"authType": "KEY",
150-
"password": "",
151-
"key": "/etc/autossh/id_rsa",
152-
"passphrase": "passphrase"
126+
"passphrase": "passphrase",
127+
"jump": {
128+
"host": "jump-2-ip",
129+
"port": 22,
130+
"username": "user",
131+
"authType": "KEY",
132+
"password": "",
133+
"key": "/etc/autossh/id_rsa",
134+
"passphrase": "passphrase"
135+
}
153136
}
154137
}
155138
}
156139
]
157140
```
141+
142+
### Fields Description
143+
144+
| Field | Type | Required | Description |
145+
|-----------------|----------------|----------|-----------------------------------------------------------------------------|
146+
| `name` | string || Identifier for the tunnel. |
147+
| `type` | string || Tunnel type: `L` for remote → local, `R` for local → remote. |
148+
| `localPort` | number || Port on the local machine. |
149+
| `localHost` | string || Local host IP or `0.0.0.0` to bind all interfaces. |
150+
| `remotePort` | number || Port on the remote machine. |
151+
| `remoteHost` | string || Remote host IP or `0.0.0.0`. |
152+
| `server` | object || Final destination SSH server configuration. |
153+
| `server.host` | string || SSH server IP or hostname. |
154+
| `server.port` | number || SSH server port, usually 22. |
155+
| `server.username` | string || SSH username. |
156+
| `server.authType` | string || Authentication type: `KEY` or `PASS`. |
157+
| `server.key` | string | 🔹 | Path to SSH key file if `authType` is `KEY`. |
158+
| `server.password` | string | 🔹 | Password if `authType` is `PASS`. |
159+
| `server.passphrase` | string | 🔹 | Passphrase for the SSH key if required. |
160+
| `server.jump` | object/null || Optional jump host configuration (recursive structure). |
161+
162+
> **Note:** Jump hosts are optional and can be nested multiple times.
163+
164+
### Tunnel Type
165+
- **L (Local)**: Forwards traffic from **remote → local**
166+
- **R (Remote)**: Forwards traffic from **local → remote**
167+
168+
### Example Topology Diagram
169+
```mermaid
170+
flowchart LR
171+
A[Local Machine] -->|SSH Tunnel| J1[Jump Host 2]
172+
J1 --> J2[Jump Host 1]
173+
J2 --> S[Final SSH Server]
174+
S --> M[MongoDB:27017]
175+
```
176+
177+
- **A:** Your local machine
178+
- **J1, J2:** Intermediate jump hosts
179+
- **S:** Final SSH server
180+
- **M:** MongoDB service running on the remote host
181+
182+
### Simple Tunnel Diagram (No Jump Hosts)
183+
```mermaid
184+
flowchart LR
185+
L[Local Machine] -->|SSH Tunnel| F[Final Server]
186+
F --> D[Service:27017]
187+
```
188+
189+
- **L:** Local machine
190+
- **F:** Final SSH server
191+
- **D:** Remote service (MongoDB, PostgreSQL, etc.)
192+
193+
### Authentication Flow
194+
1. **KEY authentication**
195+
- Uses a private key (`key`) and optional `passphrase`.
196+
2. **Password authentication**
197+
- Uses `password` field directly.
198+
199+
```mermaid
200+
flowchart TB
201+
LocalMachine --> SSHAuth[SSH Authentication]
202+
SSHAuth -->|KEY| PrivateKey["Key + Passphrase"]
203+
SSHAuth -->|PASS| Password["Password"]
204+
```
205+
206+
## Usage Notes
207+
- You can have multiple tunnels defined in the JSON array.
208+
- Jump hosts can be nested arbitrarily.
209+
- Each tunnel should have a unique `name`.
210+
- All ports and hosts are configurable to support complex network setups.

main.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ var (
2121

2222
func main() {
2323
var configPath string
24+
var dryRun bool = false
2425

2526
cli.VersionPrinter = func(c *cli.Context) {
2627
fmt.Printf("Version: %s\n", AppVersion)
@@ -40,6 +41,12 @@ func main() {
4041
Required: true,
4142
Destination: &configPath,
4243
},
44+
&cli.BoolFlag{
45+
Name: "dry-run",
46+
Usage: "use to tests your configuration.",
47+
Required: false,
48+
Destination: &dryRun,
49+
},
4350
},
4451
Action: func(ctx *cli.Context) error {
4552
cfgs, err := services.ParseConfig(configPath)
@@ -51,6 +58,11 @@ func main() {
5158
return err
5259
}
5360

61+
if dryRun {
62+
fmt.Println("Your configuration format is valid.")
63+
return nil
64+
}
65+
5466
// Root context — cancelled on Ctrl+C / SIGTERM
5567
rootCtx, cancel := context.WithCancel(context.Background())
5668
defer cancel()

man/telepath.1

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@ telepath [global options] [file]
2020
.SH COMMANDS
2121
.PP
2222
\fB--config-file, -f\fP [path of config file.]
23+
.PP
24+
\fB--dry-run [use this flag to tests your configuration. (default: false)]
2325

2426
.SH EXAMPLES
2527
.TP
2628
telepath --config-file=/etc/telepath/config.json
2729
.TP
2830
telepath -f /etc/telepath/config.json
31+
.TP
32+
telepath -f /etc/telepath/config.json --dry-run
2933

3034
.SH SEE ALSO
3135
.BR gozen (1),

models/config.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package models
22

3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"github.com/tech-thinker/telepath/constants"
8+
)
9+
310
type Server struct {
411
Host string `json:"host"`
512
Port int `json:"port"`
@@ -11,6 +18,45 @@ type Server struct {
1118
Jump *Server `json:"jump"`
1219
}
1320

21+
func (s *Server) String() string {
22+
return fmt.Sprintf("%+v", *s)
23+
}
24+
25+
func (s *Server) Validate(fieldName string) error {
26+
if len(s.Host) == 0 {
27+
return fmt.Errorf("`%s.host` must be present.", fieldName)
28+
}
29+
30+
if s.Port == 0 {
31+
return fmt.Errorf("`%s.port` is invalid.", fieldName)
32+
}
33+
34+
if len(s.Username) == 0 {
35+
return fmt.Errorf("`%s.username` must be present.", fieldName)
36+
}
37+
38+
if !slices.Contains([]string{constants.CREDIENTIAL_KEY, constants.CREDIENTIAL_PASS}, s.AuthType) {
39+
return fmt.Errorf("`%s.authType` must be present.", fieldName)
40+
}
41+
42+
if s.AuthType == constants.CREDIENTIAL_KEY && len(s.Key) == 0 {
43+
return fmt.Errorf("`%s.key` must be present for authType KEY.", fieldName)
44+
}
45+
46+
if s.AuthType == constants.CREDIENTIAL_PASS && len(s.Password) == 0 {
47+
return fmt.Errorf("`%s.password` must be present for authType PASS.", fieldName)
48+
}
49+
50+
if s.Jump != nil {
51+
err := s.Jump.Validate(fmt.Sprintf("%s.jump", fieldName))
52+
if err != nil {
53+
return err
54+
}
55+
}
56+
57+
return nil
58+
}
59+
1460
type Config struct {
1561
Name string `json:"name"`
1662
Type string `json:"type"`
@@ -20,3 +66,43 @@ type Config struct {
2066
RemoteHost string `json:"remoteHost"`
2167
Server *Server `json:"server"`
2268
}
69+
70+
func (c *Config) String() string {
71+
return fmt.Sprintf("%+v", *c)
72+
}
73+
74+
func (c *Config) Validate() error {
75+
if len(c.Name) == 0 {
76+
return fmt.Errorf("`name` must be present.")
77+
}
78+
79+
if !slices.Contains([]string{"L", "R"}, c.Type) {
80+
return fmt.Errorf("`type` must be L or R.")
81+
}
82+
83+
if c.LocalPort <= 0 {
84+
return fmt.Errorf("`localPort` is invalid.")
85+
}
86+
87+
if len(c.LocalHost) == 0 {
88+
c.LocalHost = "0.0.0.0"
89+
}
90+
91+
if c.RemotePort <= 0 {
92+
return fmt.Errorf("`remotePort` is invalid.")
93+
}
94+
95+
if len(c.RemoteHost) == 0 {
96+
c.RemoteHost = "0.0.0.0"
97+
}
98+
99+
if c.Server == nil {
100+
return fmt.Errorf("`server` must be present.")
101+
}
102+
103+
err := c.Server.Validate("server")
104+
if err != nil {
105+
return err
106+
}
107+
return nil
108+
}

services/configParser.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,28 @@ package services
33
import (
44
"encoding/json"
55
"os"
6+
"regexp"
67

78
"github.com/tech-thinker/telepath/models"
89
)
910

1011
func ParseConfig(configPath string) ([]models.Config, error) {
11-
configFile, err := os.Open(configPath)
12+
data, err := os.ReadFile(configPath)
1213
if err != nil {
1314
return nil, err
1415
}
15-
defer configFile.Close()
1616

17+
// Remove single-line comments: // comment
18+
reSingle := regexp.MustCompile(`(?m)//.*$`)
19+
data = reSingle.ReplaceAll(data, []byte(""))
20+
21+
// Remove multi-line comments: /* comment */
22+
reMulti := regexp.MustCompile(`(?s)/\*.*?\*/`)
23+
data = reMulti.ReplaceAll(data, []byte(""))
24+
25+
// Parse cleaned JSON
1726
var config []models.Config
18-
if err := json.NewDecoder(configFile).Decode(&config); err != nil {
27+
if err := json.Unmarshal(data, &config); err != nil {
1928
return nil, err
2029
}
2130

services/validator.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
package services
22

33
import (
4-
"fmt"
5-
64
"github.com/tech-thinker/telepath/models"
75
)
86

97
func ValidateConfig(configs []models.Config) ([]models.Config, error) {
8+
var validCfgs []models.Config
109
for _, cfg := range configs {
11-
if cfg.Type != "L" && cfg.Type != "R" {
12-
return nil, fmt.Errorf("invalid tunnel type %s for %s", cfg.Type, cfg.Name)
10+
err := cfg.Validate()
11+
if err != nil {
12+
return nil, err
1313
}
14+
validCfgs = append(validCfgs, cfg)
1415
}
15-
return configs, nil
16+
return validCfgs, nil
1617
}

0 commit comments

Comments
 (0)