Skip to content

Commit 1e461c0

Browse files
committed
Initial commit
0 parents  commit 1e461c0

12 files changed

+1071
-0
lines changed

.gitignore

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
### Go template
2+
# If you prefer the allow list template instead of the deny list, see community template:
3+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
4+
#
5+
# Binaries for programs and plugins
6+
*.exe
7+
*.exe~
8+
*.dll
9+
*.so
10+
*.dylib
11+
12+
# Test binary, built with `go test -c`
13+
*.test
14+
15+
# Output of the go coverage tool, specifically when used with LiteIDE
16+
*.out
17+
18+
# Dependency directories (remove the comment below to include it)
19+
# vendor/
20+
21+
# Go workspace file
22+
go.work
23+
24+
*.db
25+
*.yaml
26+
*.json
27+
*.toml

README.md

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Jerusalem Tunnel
2+
3+
Jerusalem Tunnel is a cross-platform `ngrok` alternative written in Go. It allows clients to reserve a port and complete
4+
a handshake using a secret key shared between the server and the client, identified uniquely by a clientID.
5+
6+
## Features
7+
8+
- **Cross-Platform**: Works on Windows, macOS, and Linux.
9+
- **Port Reservation**: Reserve a port for the client.
10+
- **Secure Handshake**: Client and server complete a handshake using a secret key and unique clientID.
11+
12+
## Installation
13+
14+
1. Ensure you have [Go 1.22](https://golang.org/dl/) or later installed.
15+
2. Clone the repository:
16+
17+
```bash
18+
git clone https://github.com/yourusername/jerusalem-tunnel.git
19+
cd jerusalem-tunnel
20+
```
21+
22+
3. Build the project:
23+
24+
```bash
25+
go build -o jerusalem-tunnel main.go
26+
```
27+
28+
## Usage
29+
30+
Start the server:
31+
32+
./jerusalem-tunnel server --config config.yaml
33+
34+
35+
## Configuration
36+
37+
The server requires a configuration file in YAML format to run. Example `server.yaml`:
38+
39+
```yaml
40+
PORT_RANGE: "2000...8999"
41+
SECRET_KEY: "2y6sUp8cBSfNDk7Jq5uLm0xHAIOb9ZGqE4hR1WVXtCwKjP3dYzvTn2QiFXe8rMb6"
42+
SERVER_PORT: 8901
43+
```
44+
45+
## Contributing
46+
47+
Contributions are welcome! Please fork the repository and submit a pull request.
48+
49+
## License
50+
51+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.

app.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package main
2+
3+
import (
4+
"github.com/common-nighthawk/go-figure"
5+
"github.com/spf13/viper"
6+
"log"
7+
"os"
8+
"strconv"
9+
"strings"
10+
)
11+
12+
func main() {
13+
art := figure.NewColorFigure("Jerusalem", "slant", "green", true)
14+
art.Print()
15+
16+
print("\n\n")
17+
18+
viper.AutomaticEnv()
19+
viper.SetConfigName("server")
20+
viper.AddConfigPath(".")
21+
viper.AddConfigPath("$HOME/.config/jerusalem")
22+
viper.AddConfigPath("/etc/jerusalem/")
23+
24+
if len(os.Args) > 1 && strings.HasPrefix(os.Args[1], "--config") {
25+
configPath := strings.Split(os.Args[1], "=")
26+
if len(configPath) > 1 {
27+
viper.SetConfigFile(configPath[1])
28+
}
29+
}
30+
31+
if err := viper.ReadInConfig(); err != nil {
32+
log.Printf("Config file not found: %v", err)
33+
}
34+
35+
// Read variables
36+
pStr := strings.TrimSpace(viper.GetString("server_port"))
37+
s := strings.TrimSpace(viper.GetString("secret_key"))
38+
pr := strings.TrimSpace(viper.GetString("port_range"))
39+
40+
if pr == "" || !strings.Contains(pr, "...") {
41+
log.Fatal("missing or invalid PORT_RANGE")
42+
}
43+
if pStr == "" {
44+
log.Fatal("missing SERVER_PORT")
45+
}
46+
if s == "" {
47+
s = DefaultSecret
48+
}
49+
if len(s) != 64 {
50+
log.Fatal("SECRET_KEY must be 64 characters long")
51+
}
52+
53+
prParts := strings.Split(pr, "...")
54+
minP, err := strconv.ParseUint(prParts[0], 10, 16)
55+
if err != nil || minP < 1024 || minP > 65535 {
56+
log.Fatalf("invalid PORT_RANGE: %v", err)
57+
}
58+
maxP, err := strconv.ParseUint(prParts[1], 10, 16)
59+
if err != nil || maxP < 1024 || maxP > 65535 || maxP < minP {
60+
log.Fatalf("invalid PORT_RANGE: %v", err)
61+
}
62+
p, err := strconv.ParseUint(pStr, 10, 16)
63+
if err != nil || p < 1024 || p > 65535 {
64+
log.Fatalf("invalid SERVER_PORT: %s", pStr)
65+
}
66+
db, err := NewTCPClientRepository()
67+
if err != nil {
68+
log.Fatalf("error creating database: %v", err)
69+
}
70+
71+
svr := NewServer(uint16(p), RangeInclusive{min: uint16(minP), max: uint16(maxP)}, db, &s)
72+
if err := svr.StartServer(); err != nil {
73+
log.Fatalf("Server error: %v", err)
74+
}
75+
}

auth.go

+147
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"crypto/hmac"
6+
"crypto/sha256"
7+
"encoding/hex"
8+
"fmt"
9+
"github.com/google/uuid"
10+
"net"
11+
"slices"
12+
"strconv"
13+
"strings"
14+
)
15+
16+
// Authenticator represents an object responsible for handling client authentication and generating and validating answers.
17+
//
18+
// Fields:
19+
// - k []byte: the secret key used for generating answers and validating answers.
20+
// - pr *RangeInclusive: the range of ports used for finding free ports during authentication.
21+
// - db *TcpClientRepository: the repository for accessing TCP client data.
22+
//
23+
// Methods:
24+
// - GenerateAnswer(ch uuid.UUID) string: generates an answer for a challenge.
25+
// - ValidateAnswer(ch uuid.UUID, ans string) bool: validates an answer for a challenge.
26+
// - PerformServerHandshake(stream *Codec) error: performs the server handshake with the client.
27+
// - handleClientAuth(stream *Codec, id string) error: handles the client authentication process.
28+
// - findFreePort() (uint16, error): finds a free port within the specified range.
29+
type Authenticator struct {
30+
k []byte
31+
pr *RangeInclusive
32+
db *TcpClientRepository
33+
}
34+
35+
// NewAuthenticator creates a new instance of the Authenticator struct and initializes it with the provided parameters.
36+
// The secret parameter is used to generate the authentication key, which is a SHA-256 hash of the secret.
37+
// The db parameter is a reference to a TcpClientRepository, which is used to interact with the client database.
38+
// The pr parameter is a reference to a RangeInclusive struct, which represents the range of ports that can be used.
39+
// The function returns a pointer to the newly created Authenticator instance.
40+
func NewAuthenticator(secret string, db *TcpClientRepository, pr *RangeInclusive) *Authenticator {
41+
h := sha256.Sum256([]byte(secret))
42+
return &Authenticator{k: h[:], pr: pr, db: db}
43+
}
44+
45+
// GenerateAnswer generates an answer using the HMAC-SHA256 algorithm.
46+
// It takes a uuid.UUID as a challenge, appends it to the key provided during
47+
// Authenticator initialization, and computes the HMAC-SHA256 hash. The result
48+
// is then encoded to a hexadecimal string and returned.
49+
func (a *Authenticator) GenerateAnswer(ch uuid.UUID) string {
50+
m := hmac.New(sha256.New, a.k)
51+
m.Write(ch[:])
52+
return hex.EncodeToString(m.Sum(nil))
53+
}
54+
55+
// ValidateAnswer validates the answer provided by the client for a challenge.
56+
// It decodes the answer from a hex-string to bytes and computes the HMAC of
57+
// the challenge using the provided key. Then it checks if the computed HMAC
58+
// is equal to the decoded answer. Returns true if the answer is valid, false
59+
// otherwise.
60+
func (a *Authenticator) ValidateAnswer(ch uuid.UUID, ans string) bool {
61+
b, err := hex.DecodeString(ans)
62+
if err != nil {
63+
return false
64+
}
65+
m := hmac.New(sha256.New, a.k)
66+
m.Write(ch[:])
67+
em := m.Sum(nil)
68+
return hmac.Equal(em, b)
69+
}
70+
71+
// PerformServerHandshake performs the server-side handshake process with a client.
72+
// It sends a challenge message to the client, receives the client's response,
73+
// validates it, and handles the authentication process if the response is valid.
74+
// If the handshake fails at any step, an error is returned.
75+
func (a *Authenticator) PerformServerHandshake(stream *Codec) error {
76+
ch := uuid.New()
77+
if err := stream.Send(ServerMessage{Type: MtChallenge, Challenge: ch}); err != nil {
78+
return err
79+
}
80+
81+
var msg ClientMessage
82+
ctx, cancel := context.WithTimeout(context.Background(), NetworkTimeout)
83+
defer cancel()
84+
85+
if err := stream.Recv(ctx, &msg); err != nil {
86+
return err
87+
}
88+
89+
if msg.Type == MtAuthenticate && a.ValidateAnswer(ch, msg.Authenticate) {
90+
if err := a.handleClientAuth(stream, msg.ClientId); err != nil {
91+
return err
92+
}
93+
} else {
94+
return ErrInvalidSecret
95+
}
96+
97+
return nil
98+
}
99+
100+
// `handleClientAuth` handles the authentication of a client. It validates the client ID, retrieves the client information from the database, generates a free port if necessary, stores the client information in the database, and sends the free port to the client through the stream.
101+
func (a *Authenticator) handleClientAuth(stream *Codec, id string) error {
102+
if strings.TrimSpace(id) == "" {
103+
return ErrInvalidClientId
104+
}
105+
106+
c, ex, err := a.db.GetByClientID(id)
107+
if err != nil {
108+
return err
109+
}
110+
111+
var p uint16
112+
if !ex {
113+
bl, err := a.db.GetAllPorts()
114+
if err != nil {
115+
return err
116+
}
117+
p, err = a.findFreePort(bl)
118+
if err != nil {
119+
return err
120+
}
121+
err = a.db.Create(&TCPClient{ID: p, ClientID: id, Port: p})
122+
if err != nil {
123+
return err
124+
}
125+
} else {
126+
p = c.Port
127+
}
128+
129+
return stream.Send(ServerMessage{Type: MtFreePort, Port: p})
130+
}
131+
132+
// findFreePort finds a free port within the range of min and max ports specified in the Authenticator's RangeInclusive property.
133+
// It tries to listen on each port in the range and returns the first port that is available.
134+
// If no port is available, it returns an error indicating that no port was found in the specified range.
135+
func (a *Authenticator) findFreePort(skip []uint16) (uint16, error) {
136+
for p := a.pr.min; p <= a.pr.max; p++ {
137+
if slices.Contains(skip, p) {
138+
continue
139+
}
140+
l, err := net.Listen("tcp", ":"+strconv.Itoa(int(p)))
141+
if err == nil {
142+
l.Close()
143+
return p, nil
144+
}
145+
}
146+
return 0, fmt.Errorf("no available port found in the range %d-%d", a.pr.min, a.pr.max)
147+
}

codec.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net"
7+
)
8+
9+
// Codec is a type that represents a codec for encoding and decoding data to and from a network connection using JSON format.
10+
// It contains fields for the underlying JSON encoder and decoder, as well as the network connection itself.
11+
// The Codec type provides methods for receiving, sending, and closing the connection.
12+
//
13+
// Usage example:
14+
//
15+
// conn, _ := net.Dial("tcp", "localhost:8080")
16+
// codec := NewCodec(conn)
17+
// defer codec.Close()
18+
//
19+
// err := codec.Send(data)
20+
// if err != nil {
21+
// log.Fatal(err)
22+
// }
23+
//
24+
// err = codec.RecvTimeout(&result)
25+
// if err != nil {
26+
// log.Fatal(err)
27+
// }
28+
//
29+
// err = codec.Close()
30+
// if err != nil {
31+
// log.Fatal(err)
32+
// }
33+
type Codec struct {
34+
decoder *json.Decoder
35+
encoder *json.Encoder
36+
conn net.Conn
37+
}
38+
39+
// NewCodec creates a new instance of the Codec struct using the provided net.Conn connection.
40+
// The Codec is responsible for encoding and decoding messages between the client and server.
41+
// It sets the decoder and encoder to use the JSON format and assigns the connection to the codec.
42+
// The returned Codec pointer can be used to send and receive messages.
43+
func NewCodec(conn net.Conn) *Codec {
44+
return &Codec{
45+
decoder: json.NewDecoder(conn),
46+
encoder: json.NewEncoder(conn),
47+
conn: conn,
48+
}
49+
}
50+
51+
// Recv reads a message from the codec's decoder and assigns it to the provided variable.
52+
// It uses a separate goroutine to decode the message, so it can be cancelled using the provided context.
53+
// If the context is cancelled, Recv returns the context error.
54+
// If decoding the message fails, Recv returns the decoding error.
55+
// Usage example: st.Recv(ctx, &msg)
56+
func (d *Codec) Recv(ctx context.Context, v interface{}) error {
57+
errChan := make(chan error, 1)
58+
go func() {
59+
errChan <- d.decoder.Decode(v)
60+
}()
61+
select {
62+
case <-ctx.Done():
63+
return ctx.Err()
64+
case err := <-errChan:
65+
return err
66+
}
67+
}
68+
69+
func (d *Codec) RecvTimeout(v interface{}) error {
70+
ctx, cancel := context.WithTimeout(context.Background(), NetworkTimeout)
71+
defer cancel()
72+
return d.Recv(ctx, v)
73+
}
74+
75+
// Send sends the given value to the remote connection using the encoder of the Codec.
76+
// It returns an error if the encoding process fails.
77+
func (d *Codec) Send(v interface{}) error {
78+
return d.encoder.Encode(v)
79+
}
80+
81+
// Close closes the underlying network connection of the Codec and releases any resources associated with it.
82+
// It returns an error if there was a problem closing the connection.
83+
func (d *Codec) Close() error {
84+
return d.conn.Close()
85+
}

0 commit comments

Comments
 (0)