Skip to content

Commit 794aecb

Browse files
committed
qr, https, etc
1 parent f4332cd commit 794aecb

File tree

12 files changed

+274
-30
lines changed

12 files changed

+274
-30
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/server.json
22
/sing-box-config.json
33
/data
4-
/dist
4+
/dist
5+
/lantern-server-manager

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
FROM alpine:edge
22

33
# Set the timezone and install CA certificates
4-
RUN apk --no-cache add ca-certificates tzdata
4+
RUN apk --no-cache add ca-certificates tzdata qrencode
55

66
COPY lantern-server-manager /app/server
77
COPY --from=ghcr.io/sagernet/sing-box /usr/local/bin/sing-box /usr/local/bin/sing-box

Makefile

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
lantern-server-manager:
2+
CGO_ENABLED=0 go build -ldflags="-extldflags=-static" -o lantern-server-manager ./cmd/...
3+
4+
packer:
5+
@cd cloud/packer
6+
@if [ -z "$(PKR_VAR_aws_secret_key)" ]; then \
7+
echo "Error: PKR_VAR_aws_secret_key is not set"; \
8+
exit 1; \
9+
fi
10+
@if [ -z "$(PKR_VAR_aws_access_key)" ]; then \
11+
echo "Error: PKR_VAR_aws_access_key is not set"; \
12+
exit 1; \
13+
fi
14+
# Make sure you have packer installed
15+
@if ! command -v packer &> /dev/null; then \
16+
echo "Error: packer is not installed"; \
17+
exit 1; \
18+
fi
19+
20+
@packer build .

auth/selfsigned.go

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// support HTTP/2.
2+
package auth
3+
4+
import (
5+
"bytes"
6+
"crypto/rand"
7+
"crypto/rsa"
8+
"crypto/tls"
9+
"crypto/x509"
10+
"crypto/x509/pkix"
11+
"encoding/pem"
12+
"fmt"
13+
"github.com/charmbracelet/log"
14+
"math/big"
15+
"net/http"
16+
"os"
17+
"path"
18+
"time"
19+
)
20+
21+
// Certificate returns a self-signed x509 certificate and private key.
22+
func Certificate(dataDir, ip string) (tls.Certificate, error) {
23+
certPEM, _ := os.ReadFile(path.Join(dataDir, "cert.pem"))
24+
keyPEM, _ := os.ReadFile(path.Join(dataDir, "key.pem"))
25+
if keyPEM == nil || certPEM == nil {
26+
log.Debug("Generating self-signed certificate")
27+
key, err := rsa.GenerateKey(rand.Reader, 2048)
28+
if err != nil {
29+
return tls.Certificate{}, err
30+
}
31+
32+
template := x509.Certificate{
33+
SerialNumber: big.NewInt(1234),
34+
Issuer: pkix.Name{
35+
CommonName: ip,
36+
},
37+
Subject: pkix.Name{
38+
CommonName: ip,
39+
},
40+
// We pre-expire the certs to make them explicitly invalid; they're only
41+
// useful in contexts where they are not verified or validated.
42+
NotBefore: time.Now(),
43+
NotAfter: time.Now(),
44+
DNSNames: []string{ip},
45+
KeyUsage: x509.KeyUsageDigitalSignature,
46+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
47+
}
48+
49+
certificateBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
50+
if err != nil {
51+
return tls.Certificate{}, err
52+
}
53+
54+
certPEMBuf := new(bytes.Buffer)
55+
err = pem.Encode(certPEMBuf, &pem.Block{
56+
Type: "CERTIFICATE",
57+
Bytes: certificateBytes,
58+
})
59+
if err != nil {
60+
return tls.Certificate{}, err
61+
}
62+
63+
keyPEMBuf := new(bytes.Buffer)
64+
err = pem.Encode(keyPEMBuf, &pem.Block{
65+
Type: "RSA PRIVATE KEY",
66+
Bytes: x509.MarshalPKCS1PrivateKey(key),
67+
})
68+
if err != nil {
69+
return tls.Certificate{}, err
70+
}
71+
certPEM = certPEMBuf.Bytes()
72+
keyPEM = keyPEMBuf.Bytes()
73+
} else {
74+
log.Debug("Using existing self-signed certificate")
75+
}
76+
return tls.X509KeyPair(certPEM, keyPEM)
77+
}
78+
79+
// SelfSignedListenAndServeTLS listens on the TCP network address addr and then calls
80+
// Serve with handler and a self-signed certificate to handle requests on
81+
// incoming TLS connections.
82+
func SelfSignedListenAndServeTLS(dataDir, publicIP, addr string, handler http.Handler) error {
83+
cert, err := Certificate(dataDir, publicIP)
84+
if err != nil {
85+
return fmt.Errorf("failed to generate certificate: %w", err)
86+
}
87+
88+
conf := &tls.Config{
89+
Certificates: []tls.Certificate{cert},
90+
}
91+
server := &http.Server{Addr: addr, Handler: handler, TLSConfig: conf}
92+
return server.ListenAndServeTLS("", "")
93+
}

cloud/packer/main.pkr.hcl

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
packer {
2+
required_plugins {
3+
amazon = {
4+
source = "github.com/hashicorp/amazon"
5+
version = "~> 1"
6+
}
7+
}
8+
}
9+
10+
source "amazon-ebs" "amazon-linux" {
11+
ami_name = "lantern-server-manager-${var.version}-{{timestamp}}"
12+
instance_type = "t2.micro"
13+
region = var.aws_region
14+
source_ami_filter {
15+
filters = {
16+
name = "*amzn3-ami-hvm-*"
17+
root-device-type = "ebs"
18+
virtualization-type = "hvm"
19+
}
20+
most_recent = true
21+
owners = ["amazon"]
22+
}
23+
ssh_username = "ec2-user"
24+
}
25+
26+
build {
27+
sources = ["source.amazon-ebs.amazon-linux"]
28+
29+
provisioner "file" {
30+
source = "../lantern-server-manager.service"
31+
destination = "/tmp/lantern-server-manager.service"
32+
}
33+
34+
provisioner "shell" {
35+
inline = [
36+
"yum install -y qrencode",
37+
"curl -L https://github.com/SagerNet/sing-box/releases/download/v${var.sing_box_version}/sing-box-${var.sing_box_version}-linux-amd64.tar.gz -o /tmp/sing-box.tar.gz",
38+
"curl -L https://github.com/getlantern/lantern-server-manager/releases/download/v${var.version}/lantern-server-manager_${var.version}_linux_amd64.tar.gz -o /tmp/lantern-server-manager.tar.gz",
39+
"tar -xzf /tmp/lantern-server-manager.tar.gz -C /tmp",
40+
"tar -xzf /tmp/sing-box.tar.gz -C /tmp",
41+
"sudo mkdir -p /opt/lantern",
42+
"sudo mv /tmp/sing-box-${var.sing_box_version}-linux-amd64/sing-box /usr/local/bin/sing-box",
43+
"sudo mv /tmp/lantern-server-manager.service /opt/lantern/lantern-server-manager.service",
44+
"sudo mv /tmp/lantern-server-manager /opt/lantern/lantern-server-manager",
45+
"sudo systemctl enable /opt/lantern/lantern-server-manager.service",
46+
"rm /home/ec2-user/.ssh/authorized_keys",
47+
"sudo rm /root/.ssh/authorized_keys"
48+
]
49+
}
50+
}

cloud/packer/vars.pkr.hcl

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
variable "aws_region" {
2+
default = "us-east-1"
3+
}
4+
5+
variable "aws_secret_key" {
6+
type = string
7+
sensitive = true
8+
}
9+
10+
variable "aws_access_key" {
11+
type = string
12+
sensitive = true
13+
}
14+
15+
variable "version" {
16+
type = string
17+
default = "0.0.2"
18+
}
19+
20+
variable "sing_box_version" {
21+
type = string
22+
default = "1.11.7"
23+
}

cmd/cmd_serve.go

+24-13
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package main
22

33
import (
44
"fmt"
5-
"github.com/sagernet/sing-box/option"
65
"net/http"
76
"time"
87

98
"github.com/charmbracelet/log"
9+
"github.com/sagernet/sing-box/option"
1010

1111
"github.com/getlantern/lantern-server-manager/auth"
1212
"github.com/getlantern/lantern-server-manager/common"
@@ -17,10 +17,7 @@ type ServeCmd struct {
1717
singboxConfig *option.Options
1818
}
1919

20-
func (c ServeCmd) Run() error {
21-
if !common.CheckSingBoxInstalled() {
22-
return fmt.Errorf("sing-box not found in PATH")
23-
}
20+
func (c *ServeCmd) readConfigs() error {
2421
var err error
2522
c.serverConfig, err = common.ReadServerConfig(args.DataDir)
2623
if err != nil {
@@ -35,13 +32,27 @@ func (c ServeCmd) Run() error {
3532
return fmt.Errorf("failed to read sing-box config: %w", err)
3633
}
3734
}
35+
if err = common.ValidateSingBoxConfig(args.DataDir); err != nil {
36+
return fmt.Errorf("failed to validate sing-box config: %w", err)
37+
}
3838

39-
if err := common.RestartSingBox(); err != nil {
39+
if err = common.RestartSingBox(args.DataDir); err != nil {
4040
return fmt.Errorf("failed to start sing-box: %w", err)
4141
}
4242

43+
return nil
44+
}
45+
46+
func (c *ServeCmd) Run() error {
47+
if !common.CheckSingBoxInstalled() {
48+
return fmt.Errorf("sing-box not found in PATH")
49+
}
50+
if err := c.readConfigs(); err != nil {
51+
return err
52+
}
53+
4354
printRootToken(c.serverConfig, c.singboxConfig)
44-
go common.CheckConnectivity(c.serverConfig.IP, c.serverConfig.Port)
55+
go common.CheckConnectivity(c.serverConfig.ExternalIP, c.serverConfig.Port)
4556
srv := http.NewServeMux()
4657
srv.Handle("GET /api/v1/health", http.HandlerFunc(c.healthCheckHandler))
4758
srv.Handle("GET /api/v1/connect-config", auth.Middleware(c.serverConfig.HMACSecret, http.HandlerFunc(c.getConnectConfigHandler)))
@@ -57,11 +68,11 @@ func (c ServeCmd) Run() error {
5768
fmt.Fprintf(w, "Welcome to Lantern Server Manager. In future, there will be UI here!")
5869
})
5970

60-
return http.ListenAndServe(fmt.Sprintf(":%d", c.serverConfig.Port), srv)
71+
return auth.SelfSignedListenAndServeTLS(args.DataDir, c.serverConfig.ExternalIP, fmt.Sprintf(":%d", c.serverConfig.Port), srv)
6172
}
6273

63-
func (c ServeCmd) getConnectConfigHandler(writer http.ResponseWriter, r *http.Request) {
64-
cfg, err := common.GenerateSingBoxConnectConfig(args.DataDir, c.serverConfig.IP, auth.GetRequestUsername(r))
74+
func (c *ServeCmd) getConnectConfigHandler(writer http.ResponseWriter, r *http.Request) {
75+
cfg, err := common.GenerateSingBoxConnectConfig(args.DataDir, c.serverConfig.ExternalIP, auth.GetRequestUsername(r))
6576
if err != nil {
6677
log.Errorf("failed to generate connect config: %v", err)
6778
http.Error(writer, "failed to generate connect config", http.StatusInternalServerError)
@@ -73,7 +84,7 @@ func (c ServeCmd) getConnectConfigHandler(writer http.ResponseWriter, r *http.Re
7384

7485
const ShareLinkExpiration = 24 * time.Hour
7586

76-
func (c ServeCmd) getShareLinkHandler(w http.ResponseWriter, r *http.Request) {
87+
func (c *ServeCmd) getShareLinkHandler(w http.ResponseWriter, r *http.Request) {
7788
username := r.PathValue("name")
7889
accessToken, err := auth.GenerateAccessToken(c.serverConfig.HMACSecret, username, time.Now().Add(ShareLinkExpiration))
7990
if err != nil {
@@ -85,7 +96,7 @@ func (c ServeCmd) getShareLinkHandler(w http.ResponseWriter, r *http.Request) {
8596
_, _ = w.Write([]byte(fmt.Sprintf(`{"token": "%s"}`, accessToken)))
8697
}
8798

88-
func (c ServeCmd) revokeAccess(w http.ResponseWriter, r *http.Request) {
99+
func (c *ServeCmd) revokeAccess(w http.ResponseWriter, r *http.Request) {
89100
username := r.PathValue("name")
90101
if err := common.RevokeUser(args.DataDir, username); err != nil {
91102
log.Errorf("failed to revoke user: %v", err)
@@ -96,7 +107,7 @@ func (c ServeCmd) revokeAccess(w http.ResponseWriter, r *http.Request) {
96107
_, _ = w.Write([]byte(fmt.Sprintf(`{"status": "ok"}`)))
97108
}
98109

99-
func (c ServeCmd) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
110+
func (c *ServeCmd) healthCheckHandler(w http.ResponseWriter, r *http.Request) {
100111
w.Header().Set("Content-Type", "application/json")
101112
_, _ = w.Write([]byte(`{"status": "ok"}`))
102113
}

common/public.go

+22-4
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,41 @@
11
package common
22

33
import (
4+
"crypto/tls"
45
"errors"
56
"fmt"
67
"github.com/charmbracelet/log"
8+
"github.com/mroth/jitter"
79
"io"
810
"net/http"
911
"strings"
1012
"time"
1113
)
1214

15+
// CheckConnectivity checks the connectivity to the server via it's public ExternalIP and port.
1316
func CheckConnectivity(ip string, port int) {
1417
time.Sleep(1 * time.Second)
15-
_, err := http.Get(fmt.Sprintf("http://%s:%d/api/v1/health", ip, port))
16-
if err != nil {
17-
log.Errorf("Connectivity check failed. Please check the configuration: %v", err)
18+
ticker := jitter.NewTicker(time.Minute, 0.2)
19+
defer ticker.Stop()
20+
client := http.Client{
21+
Transport: &http.Transport{
22+
TLSClientConfig: &tls.Config{
23+
InsecureSkipVerify: true,
24+
},
25+
},
26+
}
27+
for {
28+
req, _ := http.NewRequest("GET", fmt.Sprintf("https://%s:%d/api/v1/health", ip, port), nil)
29+
30+
_, err := client.Do(req)
31+
if err != nil {
32+
log.Errorf("Connectivity check failed. Please check the configuration, make sure that port %d is open. Error: %v", port, err)
33+
}
34+
<-ticker.C
1835
}
1936
}
2037

38+
// GetPublicIP fetches the public ExternalIP address of the server by trying a list of known services.
2139
func GetPublicIP() (string, error) {
2240
hostsToTry := []string{
2341
"https://icanhazip.com/",
@@ -39,5 +57,5 @@ func GetPublicIP() (string, error) {
3957
_ = resp.Body.Close()
4058
return strings.TrimSpace(string(body)), nil
4159
}
42-
return "", errors.New("no public IP found")
60+
return "", errors.New("no public ExternalIP found")
4361
}

0 commit comments

Comments
 (0)