staging #60
@@ -33,6 +33,10 @@ The following environment variables can be configured in the `.env` file:
|
|||||||
| `BUFFER_SIZE` | Buffer size for io.Copy operations in bytes (4096-1048576) | `32768` | No |
|
| `BUFFER_SIZE` | Buffer size for io.Copy operations in bytes (4096-1048576) | `32768` | No |
|
||||||
| `PPROF_ENABLED` | Enable pprof profiling server | `false` | No |
|
| `PPROF_ENABLED` | Enable pprof profiling server | `false` | No |
|
||||||
| `PPROF_PORT` | Port for pprof server | `6060` | No |
|
| `PPROF_PORT` | Port for pprof server | `6060` | No |
|
||||||
|
| `MODE` | Runtime mode: `standalone` (default, no gRPC/auth) or `node` (enable gRPC + auth) | `standalone` | No |
|
||||||
|
| `GRPC_ADDRESS` | gRPC server address/host used in `node` mode | `localhost` | No |
|
||||||
|
| `GRPC_PORT` | gRPC server port used in `node` mode | `8080` | No |
|
||||||
|
| `NODE_TOKEN` | Authentication token sent to controller in `node` mode | - (required in `node`) | Yes (node mode) |
|
||||||
|
|
||||||
**Note:** All environment variables now use UPPERCASE naming. The application includes sensible defaults for all variables, so you can run it without a `.env` file for basic functionality.
|
**Note:** All environment variables now use UPPERCASE naming. The application includes sensible defaults for all variables, so you can run it without a `.env` file for basic functionality.
|
||||||
|
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -8,12 +8,12 @@ require (
|
|||||||
github.com/charmbracelet/bubbles v0.21.0
|
github.com/charmbracelet/bubbles v0.21.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/golang/protobuf v1.5.4
|
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/libdns/cloudflare v0.2.2
|
github.com/libdns/cloudflare v0.2.2
|
||||||
github.com/muesli/termenv v0.16.0
|
github.com/muesli/termenv v0.16.0
|
||||||
golang.org/x/crypto v0.46.0
|
golang.org/x/crypto v0.46.0
|
||||||
google.golang.org/grpc v1.78.0
|
google.golang.org/grpc v1.78.0
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -49,7 +49,6 @@ require (
|
|||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/tools v0.39.0 // indirect
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace git.fossy.my.id/bagas/tunnel-please-grpc => ../tunnel-please-grpc
|
replace git.fossy.my.id/bagas/tunnel-please-grpc => ../tunnel-please-grpc
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
|
"tunnel_pls/internal/config"
|
||||||
|
|
||||||
"tunnel_pls/session"
|
"tunnel_pls/session"
|
||||||
|
|
||||||
proto "git.fossy.my.id/bagas/tunnel-please-grpc/gen"
|
proto "git.fossy.my.id/bagas/tunnel-please-grpc/gen"
|
||||||
@@ -125,34 +127,86 @@ func New(config *GrpcConfig, sessionRegistry session.Registry) (*Client, error)
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) SubscribeEvents(ctx context.Context, identity string) error {
|
func (c *Client) SubscribeEvents(ctx context.Context, identity, authToken string) error {
|
||||||
subscribe, err := c.eventService.Subscribe(ctx)
|
const (
|
||||||
if err != nil {
|
baseBackoff = time.Second
|
||||||
return err
|
maxBackoff = 30 * time.Second
|
||||||
}
|
)
|
||||||
err = subscribe.Send(&proto.Client{
|
|
||||||
Type: proto.EventType_AUTHENTICATION,
|
|
||||||
Payload: &proto.Client_AuthEvent{
|
|
||||||
AuthEvent: &proto.Authentication{
|
|
||||||
Identity: identity,
|
|
||||||
AuthToken: "test_auth_key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
backoff := baseBackoff
|
||||||
log.Println("Authentication failed to send to gRPC server:", err)
|
wait := func() error {
|
||||||
return err
|
if backoff <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-time.After(backoff):
|
||||||
|
return nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
log.Println("Authentication Successfully sent to gRPC server")
|
growBackoff := func() {
|
||||||
err = c.processEventStream(subscribe)
|
backoff *= 2
|
||||||
if err != nil {
|
if backoff > maxBackoff {
|
||||||
return err
|
backoff = maxBackoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
subscribe, err := c.eventService.Subscribe(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if !isConnectionError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status.Code(err) == codes.Unauthenticated {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
growBackoff()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = subscribe.Send(&proto.Node{
|
||||||
|
Type: proto.EventType_AUTHENTICATION,
|
||||||
|
Payload: &proto.Node_AuthEvent{
|
||||||
|
AuthEvent: &proto.Authentication{
|
||||||
|
Identity: identity,
|
||||||
|
AuthToken: authToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Authentication failed to send to gRPC server:", err)
|
||||||
|
if isConnectionError(err) {
|
||||||
|
if err := wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
growBackoff()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Println("Authentication Successfully sent to gRPC server")
|
||||||
|
backoff = baseBackoff
|
||||||
|
|
||||||
|
if err = c.processEventStream(subscribe); err != nil {
|
||||||
|
if isConnectionError(err) {
|
||||||
|
log.Printf("Reconnect to controller within %v sec", backoff.Seconds())
|
||||||
|
if err := wait(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
growBackoff()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Client, proto.Controller]) error {
|
func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Node, proto.Events]) error {
|
||||||
for {
|
for {
|
||||||
recv, err := subscribe.Recv()
|
recv, err := subscribe.Recv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -160,6 +214,10 @@ func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Cli
|
|||||||
log.Printf("connection error receiving from gRPC server: %v", err)
|
log.Printf("connection error receiving from gRPC server: %v", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if status.Code(err) == codes.Unauthenticated {
|
||||||
|
log.Printf("Authentication failed: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
log.Printf("non-connection receive error from gRPC server: %v", err)
|
log.Printf("non-connection receive error from gRPC server: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -167,11 +225,11 @@ func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Cli
|
|||||||
case proto.EventType_SLUG_CHANGE:
|
case proto.EventType_SLUG_CHANGE:
|
||||||
oldSlug := recv.GetSlugEvent().GetOld()
|
oldSlug := recv.GetSlugEvent().GetOld()
|
||||||
newSlug := recv.GetSlugEvent().GetNew()
|
newSlug := recv.GetSlugEvent().GetNew()
|
||||||
session, err := c.sessionRegistry.Get(oldSlug)
|
sess, err := c.sessionRegistry.Get(oldSlug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errSend := subscribe.Send(&proto.Client{
|
errSend := subscribe.Send(&proto.Node{
|
||||||
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||||
Payload: &proto.Client_SlugEventResponse{
|
Payload: &proto.Node_SlugEventResponse{
|
||||||
SlugEventResponse: &proto.SlugChangeEventResponse{
|
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
@@ -189,9 +247,9 @@ func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Cli
|
|||||||
}
|
}
|
||||||
err = c.sessionRegistry.Update(oldSlug, newSlug)
|
err = c.sessionRegistry.Update(oldSlug, newSlug)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errSend := subscribe.Send(&proto.Client{
|
errSend := subscribe.Send(&proto.Node{
|
||||||
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||||
Payload: &proto.Client_SlugEventResponse{
|
Payload: &proto.Node_SlugEventResponse{
|
||||||
SlugEventResponse: &proto.SlugChangeEventResponse{
|
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||||
Success: false,
|
Success: false,
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
@@ -207,10 +265,10 @@ func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Cli
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
session.GetInteraction().Redraw()
|
sess.GetInteraction().Redraw()
|
||||||
err = subscribe.Send(&proto.Client{
|
err = subscribe.Send(&proto.Node{
|
||||||
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
Type: proto.EventType_SLUG_CHANGE_RESPONSE,
|
||||||
Payload: &proto.Client_SlugEventResponse{
|
Payload: &proto.Node_SlugEventResponse{
|
||||||
SlugEventResponse: &proto.SlugChangeEventResponse{
|
SlugEventResponse: &proto.SlugChangeEventResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: "",
|
Message: "",
|
||||||
@@ -231,6 +289,7 @@ func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Cli
|
|||||||
for _, ses := range sessions {
|
for _, ses := range sessions {
|
||||||
detail := ses.Detail()
|
detail := ses.Detail()
|
||||||
details = append(details, &proto.Detail{
|
details = append(details, &proto.Detail{
|
||||||
|
Node: config.Getenv("domain", "localhost"),
|
||||||
ForwardingType: detail.ForwardingType,
|
ForwardingType: detail.ForwardingType,
|
||||||
Slug: detail.Slug,
|
Slug: detail.Slug,
|
||||||
UserId: detail.UserID,
|
UserId: detail.UserID,
|
||||||
@@ -238,9 +297,9 @@ func (c *Client) processEventStream(subscribe grpc.BidiStreamingClient[proto.Cli
|
|||||||
StartedAt: timestamppb.New(detail.StartedAt),
|
StartedAt: timestamppb.New(detail.StartedAt),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
err = subscribe.Send(&proto.Client{
|
err = subscribe.Send(&proto.Node{
|
||||||
Type: proto.EventType_GET_SESSIONS,
|
Type: proto.EventType_GET_SESSIONS,
|
||||||
Payload: &proto.Client_GetSessionsEvent{
|
Payload: &proto.Node_GetSessionsEvent{
|
||||||
GetSessionsEvent: &proto.GetSessionsResponse{
|
GetSessionsEvent: &proto.GetSessionsResponse{
|
||||||
Details: details,
|
Details: details,
|
||||||
},
|
},
|
||||||
@@ -264,16 +323,16 @@ func (c *Client) GetConnection() *grpc.ClientConn {
|
|||||||
return c.conn
|
return c.conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) AuthorizeConn(ctx context.Context, token string) (authorized bool, err error) {
|
func (c *Client) AuthorizeConn(ctx context.Context, token string) (authorized bool, user string, err error) {
|
||||||
check, err := c.authorizeConnectionService.Check(ctx, &proto.CheckRequest{AuthToken: token})
|
check, err := c.authorizeConnectionService.Check(ctx, &proto.CheckRequest{AuthToken: token})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, "UNAUTHORIZED", err
|
||||||
|
}
|
||||||
|
|
||||||
}
|
|
||||||
if check.GetResponse() == proto.AuthorizationResponse_MESSAGE_TYPE_UNAUTHORIZED {
|
if check.GetResponse() == proto.AuthorizationResponse_MESSAGE_TYPE_UNAUTHORIZED {
|
||||||
return false, nil
|
return false, "UNAUTHORIZED", nil
|
||||||
}
|
}
|
||||||
return true, nil
|
return true, check.GetUser(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) Close() error {
|
func (c *Client) Close() error {
|
||||||
@@ -289,15 +348,12 @@ func (c *Client) CheckServerHealth(ctx context.Context) error {
|
|||||||
resp, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{
|
resp, err := healthClient.Check(ctx, &grpc_health_v1.HealthCheckRequest{
|
||||||
Service: "",
|
Service: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("health check failed: %w", err)
|
return fmt.Errorf("health check failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.Status != grpc_health_v1.HealthCheckResponse_SERVING {
|
if resp.Status != grpc_health_v1.HealthCheckResponse_SERVING {
|
||||||
return fmt.Errorf("server not serving: %v", resp.Status)
|
return fmt.Errorf("server not serving: %v", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
75
main.go
75
main.go
@@ -7,6 +7,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
_ "net/http/pprof"
|
_ "net/http/pprof"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"tunnel_pls/internal/config"
|
"tunnel_pls/internal/config"
|
||||||
"tunnel_pls/internal/grpc/client"
|
"tunnel_pls/internal/grpc/client"
|
||||||
@@ -29,6 +30,9 @@ func main() {
|
|||||||
|
|
||||||
log.Printf("Starting %s", version.GetVersion())
|
log.Printf("Starting %s", version.GetVersion())
|
||||||
|
|
||||||
|
mode := strings.ToLower(config.Getenv("MODE", "standalone"))
|
||||||
|
isNodeMode := mode == "node"
|
||||||
|
|
||||||
pprofEnabled := config.Getenv("PPROF_ENABLED", "false")
|
pprofEnabled := config.Getenv("PPROF_ENABLED", "false")
|
||||||
if pprofEnabled == "true" {
|
if pprofEnabled == "true" {
|
||||||
pprofPort := config.Getenv("PPROF_PORT", "6060")
|
pprofPort := config.Getenv("PPROF_PORT", "6060")
|
||||||
@@ -64,40 +68,55 @@ func main() {
|
|||||||
sshConfig.AddHostKey(private)
|
sshConfig.AddHostKey(private)
|
||||||
sessionRegistry := session.NewRegistry()
|
sessionRegistry := session.NewRegistry()
|
||||||
|
|
||||||
grpcClient, err := client.New(&client.GrpcConfig{
|
var grpcClient *client.Client
|
||||||
Address: "localhost:8080",
|
var cancel context.CancelFunc = func() {}
|
||||||
UseTLS: false,
|
var ctx context.Context = context.Background()
|
||||||
InsecureSkipVerify: false,
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
KeepAlive: true,
|
|
||||||
MaxRetries: 3,
|
|
||||||
}, sessionRegistry)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer func(grpcClient *client.Client) {
|
|
||||||
err := grpcClient.Close()
|
|
||||||
if err != nil {
|
|
||||||
|
|
||||||
|
if isNodeMode {
|
||||||
|
grpcHost := config.Getenv("GRPC_ADDRESS", "localhost")
|
||||||
|
grpcPort := config.Getenv("GRPC_PORT", "8080")
|
||||||
|
grpcAddr := fmt.Sprintf("%s:%s", grpcHost, grpcPort)
|
||||||
|
nodeToken := config.Getenv("NODE_TOKEN", "")
|
||||||
|
if nodeToken == "" {
|
||||||
|
log.Fatalf("NODE_TOKEN is required in node mode")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}(grpcClient)
|
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
grpcClient, err = client.New(&client.GrpcConfig{
|
||||||
err = grpcClient.CheckServerHealth(ctx)
|
Address: grpcAddr,
|
||||||
if err != nil {
|
UseTLS: false,
|
||||||
log.Fatalf("gRPC health check failed: %s", err)
|
InsecureSkipVerify: false,
|
||||||
return
|
Timeout: 10 * time.Second,
|
||||||
}
|
KeepAlive: true,
|
||||||
cancel()
|
MaxRetries: 3,
|
||||||
|
}, sessionRegistry)
|
||||||
ctx, cancel = context.WithCancel(context.Background())
|
|
||||||
go func() {
|
|
||||||
identity := config.Getenv("DOMAIN", "localhost")
|
|
||||||
err = grpcClient.SubscribeEvents(ctx, identity)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}()
|
defer func(grpcClient *client.Client) {
|
||||||
|
err := grpcClient.Close()
|
||||||
|
if err != nil {
|
||||||
|
|
||||||
|
}
|
||||||
|
}(grpcClient)
|
||||||
|
|
||||||
|
ctx, cancel = context.WithTimeout(context.Background(), time.Second*5)
|
||||||
|
err = grpcClient.CheckServerHealth(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("gRPC health check failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
ctx, cancel = context.WithCancel(context.Background())
|
||||||
|
go func() {
|
||||||
|
identity := config.Getenv("DOMAIN", "localhost")
|
||||||
|
err = grpcClient.SubscribeEvents(ctx, identity, nodeToken)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
app, err := server.NewServer(sshConfig, sessionRegistry, grpcClient)
|
app, err := server.NewServer(sshConfig, sessionRegistry, grpcClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -83,17 +83,13 @@ func (s *Server) handleConnection(conn net.Conn) {
|
|||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
log.Println("SSH connection established:", sshConn.User())
|
log.Println("SSH connection established:", sshConn.User())
|
||||||
|
|
||||||
//Fallback: kalau auth gagal userID di set UNAUTHORIZED
|
user := "UNAUTHORIZED"
|
||||||
authorized, _ := s.grpcClient.AuthorizeConn(ctx, sshConn.User())
|
if s.grpcClient != nil {
|
||||||
|
_, u, _ := s.grpcClient.AuthorizeConn(ctx, sshConn.User())
|
||||||
var userID string
|
user = u
|
||||||
if authorized {
|
|
||||||
userID = sshConn.User()
|
|
||||||
} else {
|
|
||||||
userID = "UNAUTHORIZED"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sshSession := session.New(sshConn, forwardingReqs, chans, s.sessionRegistry, userID)
|
sshSession := session.New(sshConn, forwardingReqs, chans, s.sessionRegistry, user)
|
||||||
err = sshSession.Start()
|
err = sshSession.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("SSH session ended with error: %v", err)
|
log.Printf("SSH session ended with error: %v", err)
|
||||||
|
|||||||
Reference in New Issue
Block a user