feat(identity): implement identity management include secp256k1 and wireguards
Kim committed
May 13, 2026 at 17:04 UTC
820ab736ad6f264fd73a09b8a52d0fa939a642db
30 files changed
+919
-641
cmd/relay-server/admin.go
+7
-6
@@ -9,6 +9,7 @@ import (
9
"sync"
10
"time"
11
12
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
13
"github.com/gosuda/portal-tunnel/v2/portal/policy"
14
"github.com/gosuda/portal-tunnel/v2/types"
15
"github.com/gosuda/portal-tunnel/v2/utils"
@@ -251,7 +252,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
252
utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidAddress, "invalid address")
253
return
254
}
254
- identity, err := utils.NormalizeIdentity(types.Identity{
255
+ normalizedIdentity, err := identity.NormalizeIdentity(types.Identity{
256
Name: name,
257
Address: address,
258
})
@@ -259,7 +260,7 @@ func (f *Frontend) serveAdmin(w http.ResponseWriter, r *http.Request) {
260
utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, "invalid identity")
261
return
262
}
262
- identityKey := identity.Key()
263
+ identityKey := normalizedIdentity.Key()
264
approver := runtime.Approver()
265
266
type identityAction struct {
@@ -486,12 +487,12 @@ func (s persistedAdminState) apply(runtime *policy.Runtime) error {
487
}
488
}
489
runtime.Approver().SetDecisions(
489
- utils.NormalizeIdentityKeys(s.ApprovedIdentityKeys),
490
- utils.NormalizeIdentityKeys(s.DeniedIdentityKeys),
490
+ identity.NormalizeIdentityKeys(s.ApprovedIdentityKeys),
491
+ identity.NormalizeIdentityKeys(s.DeniedIdentityKeys),
492
)
492
- runtime.SetBannedIdentityKeys(utils.NormalizeIdentityKeys(s.BannedIdentityKeys))
493
+ runtime.SetBannedIdentityKeys(identity.NormalizeIdentityKeys(s.BannedIdentityKeys))
494
runtime.IPFilter().SetBannedIPs(s.BannedIPs)
494
- runtime.BPSManager().SetIdentityBPSLimits(utils.NormalizeIdentityKeyBPS(s.IdentityBPS))
495
+ runtime.BPSManager().SetIdentityBPSLimits(identity.NormalizeIdentityKeyBPS(s.IdentityBPS))
496
applyOptionalPolicy(s.UDPEnabled, s.UDPMaxLeases, runtime.IsUDPEnabled, runtime.UDPMaxLeases, runtime.SetUDPPolicy)
497
applyOptionalPolicy(s.TCPPortEnabled, s.TCPPortMaxLeases, runtime.IsTCPPortEnabled, runtime.TCPPortMaxLeases, runtime.SetTCPPortPolicy)
498
return nil
cmd/relay-server/frontend.go
+4
-3
@@ -19,6 +19,7 @@ import (
19
20
"github.com/gosuda/portal-tunnel/v2/cmd/portal-tunnel/installer"
21
"github.com/gosuda/portal-tunnel/v2/portal"
22
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
23
"github.com/gosuda/portal-tunnel/v2/types"
24
"github.com/gosuda/portal-tunnel/v2/utils"
25
)
@@ -51,7 +52,7 @@ func NewFrontend(server *portal.Server, identityPath string, defaultLandingPageE
52
if runtime == nil {
53
return nil, errors.New("frontend requires policy runtime")
54
}
54
- adminSettingsPath := utils.ResolveRelayAdminSettingsPath(identityPath)
55
+ adminSettingsPath := identity.ResolveRelayAdminSettingsPath(identityPath)
56
if adminSettingsPath == "" {
57
return nil, errors.New("frontend requires identity path")
58
}
@@ -59,8 +60,8 @@ func NewFrontend(server *portal.Server, identityPath string, defaultLandingPageE
60
if err != nil {
61
return nil, err
62
}
62
- identity := server.RelayIdentity()
63
- auth, err := newAdminAuth(identity.AdminSecretKey)
63
+ relayIdentity := server.RelayIdentity()
64
+ auth, err := newAdminAuth(relayIdentity.AdminSecretKey)
65
if err != nil {
66
return nil, err
67
}
cmd/relay-server/main.go
+2
-1
@@ -14,6 +14,7 @@ import (
14
15
"github.com/gosuda/portal-tunnel/v2/portal"
16
"github.com/gosuda/portal-tunnel/v2/portal/acme"
17
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
18
"github.com/gosuda/portal-tunnel/v2/portal/overlay"
19
"github.com/gosuda/portal-tunnel/v2/types"
20
"github.com/gosuda/portal-tunnel/v2/utils"
@@ -110,7 +111,7 @@ func runServeCommand(args []string) error {
111
printRootUsage(os.Stderr)
112
return err
113
}
113
- cfg.IdentityPath = utils.ResolveRelayStateDir(cfg.IdentityPath)
114
+ cfg.IdentityPath = identity.ResolveRelayStateDir(cfg.IdentityPath)
115
116
log.Info().
117
Str("release_version", types.ReleaseVersion).
portal/acme/acme.go
+3
-2
@@ -25,6 +25,7 @@ import (
25
"github.com/go-acme/lego/v4/registration"
26
"github.com/rs/zerolog/log"
27
28
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
29
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
30
"github.com/gosuda/portal-tunnel/v2/utils"
31
)
@@ -162,7 +163,7 @@ func NewManager(cfg Config) (*Manager, error) {
163
if cfg.ENSGaslessAddress == "" {
164
return nil, errors.New("ens gasless address is required when ens gasless import is enabled")
165
}
165
- address, err := utils.NormalizeEVMAddress(cfg.ENSGaslessAddress)
166
+ address, err := identity.NormalizeEVMAddress(cfg.ENSGaslessAddress)
167
if err != nil {
168
return nil, fmt.Errorf("normalize ens gasless address: %w", err)
169
}
@@ -624,7 +625,7 @@ func (m *Manager) SyncENSGaslessHostname(ctx context.Context, hostname, address
625
return fmt.Errorf("hostname %q is outside acme base domain %q", hostname, m.cfg.BaseDomain)
626
}
627
627
- address, err := utils.NormalizeEVMAddress(address)
628
+ address, err := identity.NormalizeEVMAddress(address)
629
if err != nil {
630
return fmt.Errorf("normalize ens gasless address: %w", err)
631
}
portal/api_server.go
+3
-2
@@ -15,6 +15,7 @@ import (
15
"github.com/rs/zerolog/log"
16
17
"github.com/gosuda/portal-tunnel/v2/portal/auth"
18
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
19
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
20
"github.com/gosuda/portal-tunnel/v2/types"
21
"github.com/gosuda/portal-tunnel/v2/utils"
@@ -191,7 +192,7 @@ func (s *Server) handleRelayDiscoveryAnnounce(w http.ResponseWriter, r *http.Req
192
return
193
}
194
194
- desc, err := utils.NormalizeDescriptor(req.Descriptor)
195
+ desc, err := identity.NormalizeRelayDescriptor(req.Descriptor)
196
if err != nil {
197
utils.WriteAPIError(w, http.StatusBadRequest, types.APIErrorCodeInvalidRequest, err.Error())
198
return
@@ -459,7 +460,7 @@ func (s *Server) handleHop(w http.ResponseWriter, r *http.Request) {
460
utils.InvalidRequestError(fmt.Errorf("forward relay: %w", err)).Write(w)
461
return
462
}
462
- if err := s.syncOverlayPeers(s.relaySet.OverlayPeerStates()); err != nil {
463
+ if err := s.syncOverlayPeers(s.relaySet.OverlayPeerDescriptor()); err != nil {
464
utils.WriteAPIError(w, http.StatusInternalServerError, types.APIErrorCodeInternal, err.Error())
465
return
466
}
portal/auth/hop_route.go
+19
-10
@@ -6,6 +6,7 @@ import (
6
"strings"
7
"time"
8
9
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
10
"github.com/gosuda/portal-tunnel/v2/types"
11
"github.com/gosuda/portal-tunnel/v2/utils"
12
)
@@ -15,7 +16,7 @@ var ErrHopRouteSignatureInvalid = errors.New("hop route signature is invalid")
16
func normalizeHopRoute(route *types.HopRoute, requireOwner bool) error {
17
ownerPublicKey := strings.ToLower(utils.TrimHexPrefix(strings.TrimSpace(route.OwnerPublicKey)))
18
if ownerPublicKey != "" {
18
- if _, err := utils.ParseSecp256k1PublicKeyHex(ownerPublicKey); err != nil {
19
+ if _, err := identity.ParseSecp256k1PublicKeyHex(ownerPublicKey); err != nil {
20
return fmt.Errorf("hop route owner public key: %w", err)
21
}
22
} else if requireOwner {
@@ -40,7 +41,10 @@ func normalizeHopRoute(route *types.HopRoute, requireOwner bool) error {
41
return nil
42
}
43
43
-func SignHopRoute(method string, route types.HopRoute, identity types.Identity, expiresAt time.Time) (types.HopRoute, error) {
44
+func SignHopRoute(method string, route types.HopRoute, authority identity.Authority, expiresAt time.Time) (types.HopRoute, error) {
45
+ if authority == nil {
46
+ return types.HopRoute{}, errors.New("hop route signing authority is required")
47
+ }
48
route.ExpiresAt = expiresAt.UTC()
49
route.Signature = ""
50
route.OwnerPublicKey = ""
@@ -48,20 +52,25 @@ func SignHopRoute(method string, route types.HopRoute, identity types.Identity,
52
if err := normalizeHopRoute(&route, false); err != nil {
53
return types.HopRoute{}, err
54
}
51
- identity, err := utils.NormalizeStoredIdentity(identity)
52
- if err != nil {
53
- return types.HopRoute{}, err
54
- }
55
- if strings.TrimSpace(identity.PrivateKey) == "" || strings.TrimSpace(identity.PublicKey) == "" {
55
+ ownerIdentity := authority.Identity()
56
+ ownerPublicKey := strings.ToLower(utils.TrimHexPrefix(strings.TrimSpace(ownerIdentity.PublicKey)))
57
+ if ownerPublicKey == "" {
58
return types.HopRoute{}, errors.New("hop route owner identity is required")
59
}
60
+ if _, err := identity.ParseSecp256k1PublicKeyHex(ownerPublicKey); err != nil {
61
+ return types.HopRoute{}, fmt.Errorf("hop route owner public key: %w", err)
62
+ }
63
59
- route.OwnerPublicKey = identity.PublicKey
64
+ route.OwnerPublicKey = ownerPublicKey
65
payload, err := types.HopRouteBytes(method, route)
66
if err != nil {
67
return types.HopRoute{}, err
68
}
64
- route.Signature, err = utils.SignSHA256Secp256k1DER(payload, identity.PrivateKey)
69
+ signature, err := authority.SignSHA256Secp256k1(payload)
70
+ if err != nil {
71
+ return types.HopRoute{}, err
72
+ }
73
+ route.Signature, err = signature.DERHex()
74
if err != nil {
75
return types.HopRoute{}, err
76
}
@@ -79,7 +88,7 @@ func VerifyHopRoute(method string, route types.HopRoute) (types.HopRoute, error)
88
if err != nil {
89
return types.HopRoute{}, err
90
}
82
- if err := utils.VerifySHA256Secp256k1DER(payload, route.OwnerPublicKey, signature); err != nil {
91
+ if err := identity.VerifySHA256Secp256k1DER(payload, route.OwnerPublicKey, signature); err != nil {
92
return types.HopRoute{}, ErrHopRouteSignatureInvalid
93
}
94
route.Signature = signature
portal/auth/lease_token.go
+18
-16
@@ -9,6 +9,7 @@ import (
9
jose "github.com/go-jose/go-jose/v4"
10
"github.com/go-jose/go-jose/v4/jwt"
11
12
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
13
"github.com/gosuda/portal-tunnel/v2/types"
14
"github.com/gosuda/portal-tunnel/v2/utils"
15
)
@@ -24,12 +25,11 @@ type LeaseAccessTokenClaims struct {
25
}
26
27
type es256kOpaqueSigner struct {
27
- keyID string
28
- privateKey *secp256k1.PrivateKey
28
+ authority identity.Authority
29
}
30
31
func (s *es256kOpaqueSigner) Public() *jose.JSONWebKey {
32
- return &jose.JSONWebKey{KeyID: s.keyID}
32
+ return &jose.JSONWebKey{}
33
}
34
35
func (s *es256kOpaqueSigner) Algs() []jose.SignatureAlgorithm {
@@ -40,10 +40,14 @@ func (s *es256kOpaqueSigner) SignPayload(payload []byte, alg jose.SignatureAlgor
40
if alg != leaseTokenAlgorithm {
41
return nil, jose.ErrUnsupportedAlgorithm
42
}
43
- if s == nil || s.privateKey == nil {
43
+ if s == nil || s.authority == nil {
44
return nil, errors.New("signing key is required")
45
}
46
- return utils.SignSHA256Secp256k1Raw64(payload, s.privateKey)
46
+ signature, err := s.authority.SignSHA256Secp256k1(payload)
47
+ if err != nil {
48
+ return nil, err
49
+ }
50
+ return signature.Raw64()
51
}
52
53
type es256kOpaqueVerifier struct {
@@ -57,8 +61,8 @@ func (v *es256kOpaqueVerifier) VerifyPayload(payload []byte, signature []byte, a
61
if v == nil || v.publicKey == nil {
62
return errors.New("verification key is required")
63
}
60
- if err := utils.VerifySHA256Secp256k1Raw64(payload, signature, v.publicKey); err != nil {
61
- if errors.Is(err, utils.ErrSecp256k1SignatureInvalid) {
64
+ if err := identity.VerifySHA256Secp256k1Raw64(payload, signature, v.publicKey); err != nil {
65
+ if errors.Is(err, identity.ErrSecp256k1SignatureInvalid) {
66
return errors.New("token signature is invalid")
67
}
68
return err
@@ -66,12 +70,11 @@ func (v *es256kOpaqueVerifier) VerifyPayload(payload []byte, signature []byte, a
70
return nil
71
}
72
69
-func IssueLeaseAccessToken(privateKeyHex, keyID, issuer string, identity types.Identity, ttl time.Duration) (string, LeaseAccessTokenClaims, error) {
70
- privateKey, _, err := utils.ParseSecp256k1PrivateKeyHex(privateKeyHex, false)
71
- if err != nil {
72
- return "", LeaseAccessTokenClaims{}, err
73
+func IssueLeaseAccessToken(authority identity.Authority, issuer string, leaseIdentity types.Identity, ttl time.Duration) (string, LeaseAccessTokenClaims, error) {
74
+ if authority == nil {
75
+ return "", LeaseAccessTokenClaims{}, errors.New("lease token signing authority is required")
76
}
74
- normalizedIdentity, err := utils.NormalizeIdentity(identity)
77
+ normalizedIdentity, err := identity.NormalizeIdentity(leaseIdentity)
78
if err != nil {
79
return "", LeaseAccessTokenClaims{}, err
80
}
@@ -79,8 +82,7 @@ func IssueLeaseAccessToken(privateKeyHex, keyID, issuer string, identity types.I
82
signer, err := jose.NewSigner(jose.SigningKey{
83
Algorithm: leaseTokenAlgorithm,
84
Key: &es256kOpaqueSigner{
82
- keyID: strings.TrimSpace(keyID),
83
- privateKey: privateKey,
85
+ authority: authority,
86
},
87
}, (&jose.SignerOptions{}).WithType("JWT"))
88
if err != nil {
@@ -110,7 +112,7 @@ func IssueLeaseAccessToken(privateKeyHex, keyID, issuer string, identity types.I
112
}
113
114
func VerifyLeaseAccessToken(token, publicKeyHex, issuer string, now time.Time) (LeaseAccessTokenClaims, error) {
113
- publicKey, err := utils.ParseSecp256k1PublicKeyHex(publicKeyHex)
115
+ publicKey, err := identity.ParseSecp256k1PublicKeyHex(publicKeyHex)
116
if err != nil {
117
return LeaseAccessTokenClaims{}, err
118
}
@@ -124,7 +126,7 @@ func VerifyLeaseAccessToken(token, publicKeyHex, issuer string, now time.Time) (
126
if err := parsed.Claims(&es256kOpaqueVerifier{publicKey: publicKey}, &claims); err != nil {
127
return LeaseAccessTokenClaims{}, err
128
}
127
- normalizedClaimsIdentity, err := utils.NormalizeIdentity(claims.Identity)
129
+ normalizedClaimsIdentity, err := identity.NormalizeIdentity(claims.Identity)
130
if err != nil {
131
return LeaseAccessTokenClaims{}, err
132
}
portal/auth/register_challenge.go
+2
-1
@@ -8,6 +8,7 @@ import (
8
9
"github.com/spruceid/siwe-go"
10
11
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
12
"github.com/gosuda/portal-tunnel/v2/types"
13
"github.com/gosuda/portal-tunnel/v2/utils"
14
)
@@ -29,7 +30,7 @@ type RegisterChallenge struct {
30
}
31
32
func NewRegisterChallenge(req types.RegisterChallengeRequest, domain, uri string, now time.Time, ttl time.Duration) (*RegisterChallenge, error) {
32
- normalizedIdentity, err := utils.NormalizeIdentity(req.Identity)
33
+ normalizedIdentity, err := identity.NormalizeIdentity(req.Identity)
34
if err != nil {
35
return nil, err
36
}
portal/auth/relay_descriptor.go
+25
-15
@@ -7,38 +7,48 @@ import (
7
"fmt"
8
"strings"
9
10
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
11
"github.com/gosuda/portal-tunnel/v2/types"
11
- "github.com/gosuda/portal-tunnel/v2/utils"
12
)
13
14
// SignRelayDescriptor returns a copy of desc with its Signature field
15
-// populated by signing the canonical bytes with the supplied secp256k1
16
-// private key (hex encoded). The signature is recoverable, so verifiers do
17
-// not need to know the public key out of band; they recover it from the
18
-// signature and check it derives the descriptor's Address field.
19
-func SignRelayDescriptor(desc types.RelayDescriptor, privateKeyHex string) (types.RelayDescriptor, error) {
20
- privateKey, _, err := utils.ParseSecp256k1PrivateKeyHex(privateKeyHex, true)
21
- if err != nil {
22
- return types.RelayDescriptor{}, fmt.Errorf("relay descriptor signing key: %w", err)
15
+// populated by signing the canonical bytes with authority. The signature is
16
+// recoverable, so verifiers do not need to know the public key out of band;
17
+// they recover it from the signature and check it derives the descriptor's
18
+// Address field.
19
+func SignRelayDescriptor(desc types.RelayDescriptor, authority identity.Authority) (types.RelayDescriptor, error) {
20
+ if authority == nil {
21
+ return types.RelayDescriptor{}, errors.New("relay descriptor signing authority is required")
22
+ }
23
+ signingIdentity := authority.Identity()
24
+ if desc.Address == "" {
25
+ desc.Address = signingIdentity.Address
26
}
27
28
desc.Signature = ""
26
- normalized, err := utils.NormalizeDescriptor(desc)
29
+ normalized, err := identity.NormalizeRelayDescriptor(desc)
30
if err != nil {
31
return types.RelayDescriptor{}, fmt.Errorf("normalize relay descriptor for signing: %w", err)
32
}
33
+ if signingIdentity.Address != "" && !strings.EqualFold(strings.TrimSpace(signingIdentity.Address), strings.TrimSpace(normalized.Address)) {
34
+ return types.RelayDescriptor{}, errors.New("relay descriptor address does not match signing authority")
35
+ }
36
desc = normalized
37
38
canonical, err := types.CanonicalBytes(desc)
39
if err != nil {
40
return types.RelayDescriptor{}, fmt.Errorf("canonicalize relay descriptor: %w", err)
41
}
36
- signature, err := utils.SignSHA256Secp256k1Compact(canonical, privateKey, true)
42
+ signature, err := authority.SignSHA256Secp256k1(canonical)
43
+ if err != nil {
44
+ return types.RelayDescriptor{}, err
45
+ }
46
+ compactSignature, err := signature.Compact()
47
if err != nil {
48
return types.RelayDescriptor{}, err
49
}
50
41
- desc.Signature = base64.StdEncoding.EncodeToString(signature)
51
+ desc.Signature = base64.StdEncoding.EncodeToString(compactSignature)
52
return desc, nil
53
}
54
@@ -59,7 +69,7 @@ func VerifyRelayDescriptor(desc types.RelayDescriptor) (types.RelayDescriptor, e
69
70
unsignedCopy := desc
71
unsignedCopy.Signature = ""
62
- normalized, err := utils.NormalizeDescriptor(unsignedCopy)
72
+ normalized, err := identity.NormalizeRelayDescriptor(unsignedCopy)
73
if err != nil {
74
return types.RelayDescriptor{}, fmt.Errorf("relay descriptor signature is invalid: normalize: %w", err)
75
}
@@ -68,13 +78,13 @@ func VerifyRelayDescriptor(desc types.RelayDescriptor) (types.RelayDescriptor, e
78
return types.RelayDescriptor{}, fmt.Errorf("canonicalize relay descriptor: %w", err)
79
}
80
71
- publicKey, err := utils.RecoverSHA256Secp256k1Compact(canonical, signature)
81
+ publicKey, err := identity.RecoverSHA256Secp256k1Compact(canonical, signature)
82
if err != nil {
83
return types.RelayDescriptor{}, fmt.Errorf("relay descriptor signature is invalid: %w", err)
84
}
85
86
publicKeyHex := hex.EncodeToString(publicKey.SerializeCompressed())
77
- derivedAddress, err := utils.AddressFromCompressedPublicKeyHex(publicKeyHex)
87
+ derivedAddress, err := identity.AddressFromCompressedPublicKeyHex(publicKeyHex)
88
if err != nil {
89
return types.RelayDescriptor{}, fmt.Errorf("derive address from recovered key: %w", err)
90
}
portal/discovery/announce_test.go
+9
-5
@@ -5,17 +5,17 @@ import (
5
"time"
6
7
"github.com/gosuda/portal-tunnel/v2/portal/auth"
8
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
9
"github.com/gosuda/portal-tunnel/v2/types"
9
- "github.com/gosuda/portal-tunnel/v2/utils"
10
)
11
12
func mustSigningIdentity(t *testing.T) types.Identity {
13
t.Helper()
14
- identity, err := utils.ResolveSecp256k1Identity("")
14
+ signingIdentity, err := identity.ResolveSecp256k1Identity("")
15
if err != nil {
16
- t.Fatalf("ResolveSecp256k1Identity() error = %v", err)
16
+ t.Fatalf("identity.ResolveSecp256k1Identity() error = %v", err)
17
}
18
- return identity
18
+ return signingIdentity
19
}
20
21
func mustUnsignedDescriptor(t *testing.T, signing types.Identity, relayURL string) types.RelayDescriptor {
@@ -32,13 +32,17 @@ func mustUnsignedDescriptor(t *testing.T, signing types.Identity, relayURL strin
32
33
func mustSignedDescriptor(t *testing.T, signing types.Identity, relayURL string, issuedAt time.Time) types.RelayDescriptor {
34
t.Helper()
35
+ authority, err := identity.NewLocalAuthority(signing)
36
+ if err != nil {
37
+ t.Fatalf("identity.NewLocalAuthority() error = %v", err)
38
+ }
39
signed, err := auth.SignRelayDescriptor(types.RelayDescriptor{
40
Address: signing.Address,
41
Version: types.DiscoveryVersion,
42
IssuedAt: issuedAt,
43
ExpiresAt: issuedAt.Add(DiscoveryDescriptorTTL),
44
APIHTTPSAddr: relayURL,
41
- }, signing.PrivateKey)
45
+ }, authority)
46
if err != nil {
47
t.Fatalf("SignRelayDescriptor() error = %v", err)
48
}
portal/discovery/policy_test.go
+8
-4
@@ -6,16 +6,20 @@ import (
6
"time"
7
8
"github.com/gosuda/portal-tunnel/v2/portal/auth"
9
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
10
"github.com/gosuda/portal-tunnel/v2/types"
10
- "github.com/gosuda/portal-tunnel/v2/utils"
11
)
12
13
func mustPolicyRelayDescriptor(t *testing.T, relayURL string) types.RelayDescriptor {
14
t.Helper()
15
16
- signing, err := utils.ResolveSecp256k1Identity("")
16
+ signing, err := identity.ResolveSecp256k1Identity("")
17
if err != nil {
18
- t.Fatalf("ResolveSecp256k1Identity() error = %v", err)
18
+ t.Fatalf("identity.ResolveSecp256k1Identity() error = %v", err)
19
+ }
20
+ authority, err := identity.NewLocalAuthority(signing)
21
+ if err != nil {
22
+ t.Fatalf("identity.NewLocalAuthority() error = %v", err)
23
}
24
now := time.Now().UTC()
25
signed, err := auth.SignRelayDescriptor(types.RelayDescriptor{
@@ -24,7 +28,7 @@ func mustPolicyRelayDescriptor(t *testing.T, relayURL string) types.RelayDescrip
28
IssuedAt: now,
29
ExpiresAt: now.Add(time.Hour),
30
APIHTTPSAddr: relayURL,
27
- }, signing.PrivateKey)
31
+ }, authority)
32
if err != nil {
33
t.Fatalf("SignRelayDescriptor() error = %v", err)
34
}
portal/discovery/refresher.go
+8
-4
@@ -21,7 +21,7 @@ const (
21
22
type OverlayRuntime interface {
23
DiscoverRelay(context.Context, types.RelayDescriptor) (types.DiscoveryResponse, error)
24
- Sync([]RelayState) error
24
+ Sync([]types.RelayDescriptor) error
25
}
26
27
type Refresher struct {
@@ -206,11 +206,15 @@ func (r *Refresher) refreshHTTPS(ctx context.Context) error {
206
}
207
208
func (r *Refresher) refreshOverlay(ctx context.Context) error {
209
- states := r.relaySet.OverlayPeerStates()
209
+ states := r.relaySet.overlayPeerRelayStates()
210
if len(states) == 0 {
211
return nil
212
}
213
- if err := r.overlay.Sync(states); err != nil {
213
+ descriptors := make([]types.RelayDescriptor, 0, len(states))
214
+ for _, state := range states {
215
+ descriptors = append(descriptors, state.Descriptor)
216
+ }
217
+ if err := r.overlay.Sync(descriptors); err != nil {
218
return err
219
}
220
relaySetChanged := false
@@ -251,7 +255,7 @@ func (r *Refresher) refreshOverlay(ctx context.Context) error {
255
if !relaySetChanged {
256
return nil
257
}
254
- if err := r.overlay.Sync(r.relaySet.OverlayPeerStates()); err != nil {
258
+ if err := r.overlay.Sync(r.relaySet.OverlayPeerDescriptor()); err != nil {
259
return err
260
}
261
return nil
portal/discovery/relayset.go
+15
-12
@@ -287,7 +287,7 @@ func (s *RelaySet) PriorityRelaysWithTrace(clientState ClientState) ([]string, t
287
Int("output_count", len(trace.OutputURLs)).
288
Str("mode", trace.Mode).
289
Bool("congested", trace.Congested).
290
- Strs("top3", first3(trace.OutputURLs)).
290
+ Strs("relay_urls", trace.OutputURLs).
291
Msg("relay selection")
292
return result, trace
293
}
@@ -322,7 +322,7 @@ func (s *RelaySet) PriorityMultiHopWithTrace(clientState ClientState) ([]string,
322
Int("output_count", len(trace.OutputURLs)).
323
Str("mode", trace.Mode).
324
Bool("congested", trace.Congested).
325
- Strs("top3", first3(trace.OutputURLs)).
325
+ Strs("relay_urls", trace.OutputURLs).
326
Msg("relay selection")
327
return result, trace
328
}
@@ -335,16 +335,7 @@ func (s *RelaySet) PriorityMultiHop(clientState ClientState) []string {
335
return out
336
}
337
338
-// first3 returns a slice containing the first three elements of s, or all
339
-// elements if s has fewer than three. It never modifies the input slice.
340
-func first3(s []string) []string {
341
- if len(s) <= 3 {
342
- return s
343
- }
344
- return s[:3]
345
-}
346
-
347
-func (s *RelaySet) OverlayPeerStates() []RelayState {
338
+func (s *RelaySet) overlayPeerRelayStates() []RelayState {
339
now := time.Now().UTC()
340
s.mu.RLock()
341
out := make([]RelayState, 0, len(s.relays))
@@ -361,6 +352,18 @@ func (s *RelaySet) OverlayPeerStates() []RelayState {
352
return out
353
}
354
355
+func (s *RelaySet) OverlayPeerDescriptor() []types.RelayDescriptor {
356
+ states := s.overlayPeerRelayStates()
357
+ if len(states) == 0 {
358
+ return nil
359
+ }
360
+ out := make([]types.RelayDescriptor, 0, len(states))
361
+ for _, state := range states {
362
+ out = append(out, state.Descriptor)
363
+ }
364
+ return out
365
+}
366
+
367
func (s *RelaySet) OverlayRelayDescriptor(relayURL string, now time.Time) (types.RelayDescriptor, bool) {
368
if now.IsZero() {
369
now = time.Now().UTC()
portal/identity/authority.go
new
+53
@@ -0,0 +1,53 @@
1
+package identity
2
+
3
+import (
4
+ "errors"
5
+
6
+ "github.com/gosuda/portal-tunnel/v2/types"
7
+)
8
+
9
+type Authority interface {
10
+ Identity() types.Identity
11
+ SignEthereumPersonalMessage(message string) (string, error)
12
+ SignSHA256Secp256k1(payload []byte) (Secp256k1Signature, error)
13
+}
14
+
15
+type LocalAuthority struct {
16
+ identity types.Identity
17
+}
18
+
19
+func NewLocalAuthority(raw types.Identity) (LocalAuthority, error) {
20
+ normalized, err := normalizeStoredIdentity(raw)
21
+ if err != nil {
22
+ return LocalAuthority{}, err
23
+ }
24
+ if normalized.PrivateKey == "" {
25
+ return LocalAuthority{}, errors.New("authority private key is required")
26
+ }
27
+ if normalized.PublicKey == "" {
28
+ return LocalAuthority{}, errors.New("authority public key is required")
29
+ }
30
+ if normalized.Address == "" {
31
+ return LocalAuthority{}, errors.New("authority address is required")
32
+ }
33
+ return LocalAuthority{identity: normalized}, nil
34
+}
35
+
36
+func (a LocalAuthority) Identity() types.Identity {
37
+ identity := a.identity.Copy()
38
+ identity.PrivateKey = ""
39
+ identity.TokenSecret = ""
40
+ return identity
41
+}
42
+
43
+func (a LocalAuthority) SignEthereumPersonalMessage(message string) (string, error) {
44
+ return signEthereumPersonalMessage(message, a.identity.PrivateKey)
45
+}
46
+
47
+func (a LocalAuthority) SignSHA256Secp256k1(payload []byte) (Secp256k1Signature, error) {
48
+ privateKey, _, err := parseSecp256k1PrivateKeyHex(a.identity.PrivateKey, true)
49
+ if err != nil {
50
+ return Secp256k1Signature{}, err
51
+ }
52
+ return signSHA256Secp256k1(payload, privateKey)
53
+}
portal/identity/secp256k1.go
renamed
+109
-156
@@ -1,42 +1,79 @@
1
-package utils
1
+package identity
2
3
import (
4
- "crypto/rand"
4
"crypto/sha256"
6
- "encoding/base64"
5
"encoding/hex"
6
"errors"
7
"fmt"
10
- "net/netip"
8
"strings"
9
10
"github.com/decred/dcrd/dcrec/secp256k1/v4"
11
"github.com/decred/dcrd/dcrec/secp256k1/v4/ecdsa"
15
- "golang.org/x/crypto/curve25519"
12
"golang.org/x/crypto/sha3"
13
14
"github.com/gosuda/portal-tunnel/v2/types"
15
)
16
17
const (
22
- // CompactSecp256k1SignatureSize is the byte length of a compact
18
+ // compactSecp256k1SignatureSize is the byte length of a compact
19
// recoverable secp256k1 ECDSA signature.
24
- CompactSecp256k1SignatureSize = 65
25
- // RawSecp256k1SignatureSize is the byte length of the JOSE ES256K
20
+ compactSecp256k1SignatureSize = 65
21
+ // rawSecp256k1SignatureSize is the byte length of the JOSE ES256K
22
// signature form, r || s with no recovery header.
27
- RawSecp256k1SignatureSize = 64
23
+ rawSecp256k1SignatureSize = 64
24
)
25
26
// ErrSecp256k1SignatureInvalid marks a well-formed signature that does not
27
// verify for the payload and public key.
28
var ErrSecp256k1SignatureInvalid = errors.New("signature is invalid")
29
30
+type Secp256k1Signature struct {
31
+ compact []byte
32
+}
33
+
34
+func newSecp256k1SignatureFromCompact(compact []byte) (Secp256k1Signature, error) {
35
+ normalized, err := copySecp256k1CompactSignature(compact)
36
+ if err != nil {
37
+ return Secp256k1Signature{}, err
38
+ }
39
+ return Secp256k1Signature{compact: normalized}, nil
40
+}
41
+
42
+func (s Secp256k1Signature) Compact() ([]byte, error) {
43
+ return copySecp256k1CompactSignature(s.compact)
44
+}
45
+
46
+func (s Secp256k1Signature) Raw64() ([]byte, error) {
47
+ compact, err := copySecp256k1CompactSignature(s.compact)
48
+ if err != nil {
49
+ return nil, err
50
+ }
51
+
52
+ signature := make([]byte, rawSecp256k1SignatureSize)
53
+ copy(signature[:32], compact[1:33])
54
+ copy(signature[32:], compact[33:65])
55
+ return signature, nil
56
+}
57
+
58
+func (s Secp256k1Signature) DERHex() (string, error) {
59
+ raw, err := s.Raw64()
60
+ if err != nil {
61
+ return "", err
62
+ }
63
+
64
+ signature, err := secp256k1SignatureFromRaw64(raw)
65
+ if err != nil {
66
+ return "", err
67
+ }
68
+ return hex.EncodeToString(signature.Serialize()), nil
69
+}
70
+
71
func NormalizeEVMAddress(raw string) (string, error) {
72
trimmed := strings.TrimSpace(raw)
73
if trimmed == "" {
74
return "", errors.New("address is required")
75
}
39
- hexPart := TrimHexPrefix(trimmed)
76
+ hexPart := trimHexPrefix(trimmed)
77
if hexPart == trimmed {
78
return "", errors.New("address must start with 0x")
79
}
@@ -98,8 +135,8 @@ func AddressFromCompressedPublicKeyHex(rawPublicKey string) (string, error) {
135
return NormalizeEVMAddress("0x" + hex.EncodeToString(hash[len(hash)-20:]))
136
}
137
101
-func SignEthereumPersonalMessage(message, privateKeyHex string) (string, error) {
102
- privateKey, _, err := ParseSecp256k1PrivateKeyHex(privateKeyHex, false)
138
+func signEthereumPersonalMessage(message, privateKeyHex string) (string, error) {
139
+ privateKey, _, err := parseSecp256k1PrivateKeyHex(privateKeyHex, false)
140
if err != nil {
141
return "", err
142
}
@@ -133,7 +170,7 @@ func ResolveSecp256k1Identity(rawPrivateKey string) (types.Identity, error) {
170
privateKeyHex = hex.EncodeToString(privateKey.Serialize())
171
}
172
136
- privateKey, normalizedKeyHex, err := ParseSecp256k1PrivateKeyHex(privateKeyHex, true)
173
+ privateKey, normalizedKeyHex, err := parseSecp256k1PrivateKeyHex(privateKeyHex, true)
174
if err != nil {
175
return types.Identity{}, err
176
}
@@ -151,49 +188,18 @@ func ResolveSecp256k1Identity(rawPrivateKey string) (types.Identity, error) {
188
}, nil
189
}
190
154
-func SignSHA256Secp256k1DER(payload []byte, privateKeyHex string) (string, error) {
155
- privateKey, _, err := ParseSecp256k1PrivateKeyHex(privateKeyHex, false)
156
- if err != nil {
157
- return "", err
158
- }
159
-
160
- hash := sha256.Sum256(payload)
161
- signature := ecdsa.Sign(privateKey, hash[:])
162
- return hex.EncodeToString(signature.Serialize()), nil
163
-}
164
-
165
-// SignSHA256Secp256k1Compact signs the SHA-256 digest of payload and returns
166
-// the compact recoverable secp256k1 ECDSA signature.
167
-func SignSHA256Secp256k1Compact(payload []byte, privateKey *secp256k1.PrivateKey, compressed bool) ([]byte, error) {
191
+func signSHA256Secp256k1(payload []byte, privateKey *secp256k1.PrivateKey) (Secp256k1Signature, error) {
192
if privateKey == nil {
169
- return nil, errors.New("signing key is required")
193
+ return Secp256k1Signature{}, errors.New("signing key is required")
194
}
195
hash := sha256.Sum256(payload)
172
- signature := ecdsa.SignCompact(privateKey, hash[:], compressed)
173
- if len(signature) != CompactSecp256k1SignatureSize {
174
- return nil, errors.New("invalid compact signature length")
175
- }
176
- return signature, nil
177
-}
178
-
179
-// SignSHA256Secp256k1Raw64 signs the SHA-256 digest of payload and returns
180
-// the raw r || s signature form used by ES256K.
181
-func SignSHA256Secp256k1Raw64(payload []byte, privateKey *secp256k1.PrivateKey) ([]byte, error) {
182
- compact, err := SignSHA256Secp256k1Compact(payload, privateKey, false)
183
- if err != nil {
184
- return nil, err
185
- }
186
-
187
- signature := make([]byte, RawSecp256k1SignatureSize)
188
- copy(signature[:32], compact[1:33])
189
- copy(signature[32:], compact[33:65])
190
- return signature, nil
196
+ return newSecp256k1SignatureFromCompact(ecdsa.SignCompact(privateKey, hash[:], true))
197
}
198
199
// RecoverSHA256Secp256k1Compact recovers the public key from a compact
200
// recoverable signature over the SHA-256 digest of payload.
201
func RecoverSHA256Secp256k1Compact(payload, signature []byte) (*secp256k1.PublicKey, error) {
196
- if len(signature) != CompactSecp256k1SignatureSize {
202
+ if len(signature) != compactSecp256k1SignatureSize {
203
return nil, errors.New("invalid compact signature length")
204
}
205
@@ -214,19 +220,58 @@ func VerifySHA256Secp256k1Raw64(payload, signature []byte, publicKey *secp256k1.
220
if publicKey == nil {
221
return errors.New("verification key is required")
222
}
217
- if len(signature) != RawSecp256k1SignatureSize {
223
+ if len(signature) != rawSecp256k1SignatureSize {
224
return errors.New("invalid es256k signature length")
225
}
226
227
+ parsed, err := secp256k1SignatureFromRaw64(signature)
228
+ if err != nil {
229
+ return err
230
+ }
231
+
232
+ return verifySHA256Secp256k1Signature(payload, parsed, publicKey)
233
+}
234
+
235
+func copySecp256k1CompactSignature(compact []byte) ([]byte, error) {
236
+ if len(compact) != compactSecp256k1SignatureSize {
237
+ return nil, errors.New("invalid compact signature length")
238
+ }
239
+ if _, err := secp256k1CompactRecoveryID(compact[0]); err != nil {
240
+ return nil, err
241
+ }
242
+ if _, err := secp256k1SignatureFromRaw64(compact[1:]); err != nil {
243
+ return nil, err
244
+ }
245
+
246
+ normalized := make([]byte, compactSecp256k1SignatureSize)
247
+ copy(normalized, compact)
248
+ return normalized, nil
249
+}
250
+
251
+func secp256k1CompactRecoveryID(header byte) (byte, error) {
252
+ switch {
253
+ case header >= 27 && header <= 30:
254
+ return header - 27, nil
255
+ case header >= 31 && header <= 34:
256
+ return header - 31, nil
257
+ default:
258
+ return 0, errors.New("invalid compact signature header")
259
+ }
260
+}
261
+
262
+func secp256k1SignatureFromRaw64(signature []byte) (*ecdsa.Signature, error) {
263
+ if len(signature) != rawSecp256k1SignatureSize {
264
+ return nil, errors.New("invalid es256k signature length")
265
+ }
266
+
267
var r, s secp256k1.ModNScalar
268
if overflow := r.SetByteSlice(signature[:32]); overflow || r.IsZero() {
223
- return errors.New("invalid es256k signature r")
269
+ return nil, errors.New("invalid es256k signature r")
270
}
271
if overflow := s.SetByteSlice(signature[32:]); overflow || s.IsZero() {
226
- return errors.New("invalid es256k signature s")
272
+ return nil, errors.New("invalid es256k signature s")
273
}
228
-
229
- return verifySHA256Secp256k1Signature(payload, ecdsa.NewSignature(&r, &s), publicKey)
274
+ return ecdsa.NewSignature(&r, &s), nil
275
}
276
277
func VerifySHA256Secp256k1DER(payload []byte, publicKeyHex, signatureHex string) error {
@@ -239,7 +284,7 @@ func VerifySHA256Secp256k1DER(payload []byte, publicKeyHex, signatureHex string)
284
if sigText == "" {
285
return errors.New("signature is required")
286
}
242
- sigText = TrimHexPrefix(sigText)
287
+ sigText = trimHexPrefix(sigText)
288
289
sigBytes, err := hex.DecodeString(sigText)
290
if err != nil {
@@ -267,111 +312,12 @@ func verifySHA256Secp256k1Signature(payload []byte, signature *ecdsa.Signature,
312
return nil
313
}
314
270
-func NormalizeWireGuardPrivateKey(raw string) (string, error) {
271
- key, err := decodeWireGuardKey(raw)
272
- if err != nil {
273
- return "", err
274
- }
275
- clampWireGuardPrivateKey(&key)
276
- return base64.StdEncoding.EncodeToString(key[:]), nil
277
-}
278
-
279
-func GenerateWireGuardPrivateKey() (string, error) {
280
- var key [32]byte
281
- if _, err := rand.Read(key[:]); err != nil {
282
- return "", err
283
- }
284
- clampWireGuardPrivateKey(&key)
285
- return base64.StdEncoding.EncodeToString(key[:]), nil
286
-}
287
-
288
-func WireGuardPublicKeyFromPrivate(raw string) (string, error) {
289
- privateKey, err := decodeWireGuardKey(raw)
290
- if err != nil {
291
- return "", err
292
- }
293
- clampWireGuardPrivateKey(&privateKey)
294
- var publicKey [32]byte
295
- curve25519.ScalarBaseMult(&publicKey, &privateKey)
296
- return base64.StdEncoding.EncodeToString(publicKey[:]), nil
297
-}
298
-
299
-func DeriveWireGuardOverlayIPv4(publicKey string) (string, error) {
300
- decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(publicKey))
301
- if err != nil {
302
- return "", errors.New("wireguard public key must be base64 encoded")
303
- }
304
- if len(decoded) != 32 {
305
- return "", errors.New("wireguard public key must be 32 bytes")
306
- }
307
-
308
- sum := sha256.Sum256(decoded)
309
- return netip.AddrFrom4([4]byte{
310
- 100,
311
- 64 + (sum[0] & 0x3f),
312
- sum[1],
313
- 1 + (sum[2] % 254),
314
- }).String(), nil
315
-}
316
-
317
-func WireGuardKeyHex(raw string) (string, error) {
318
- key, err := decodeWireGuardKey(raw)
319
- if err != nil {
320
- return "", err
321
- }
322
- return hex.EncodeToString(key[:]), nil
323
-}
324
-
325
-func decodeWireGuardKey(raw string) ([32]byte, error) {
326
- var key [32]byte
327
- value := strings.TrimSpace(raw)
328
- if value == "" {
329
- return key, errors.New("wireguard key is required")
330
- }
331
-
332
- var decoded []byte
333
- var err error
334
- if len(value) == 64 && !strings.Contains(value, "=") {
335
- decoded, err = hex.DecodeString(value)
336
- } else {
337
- decoded, err = base64.StdEncoding.DecodeString(value)
338
- }
339
- if err != nil {
340
- return key, errors.New("wireguard key must be base64 or hex encoded")
341
- }
342
- if len(decoded) != len(key) {
343
- return key, errors.New("wireguard key must be 32 bytes")
344
- }
345
- copy(key[:], decoded)
346
- return key, nil
347
-}
348
-
349
-func clampWireGuardPrivateKey(key *[32]byte) {
350
- key[0] &= 248
351
- key[31] = (key[31] & 127) | 64
352
-}
353
-
354
-func ValidateWireGuardPublicKey(raw string) error {
355
- key := strings.TrimSpace(raw)
356
- if key == "" {
357
- return errors.New("wireguard_public_key is required")
358
- }
359
- decoded, err := base64.StdEncoding.DecodeString(key)
360
- if err != nil {
361
- return errors.New("wireguard_public_key must be base64 encoded")
362
- }
363
- if len(decoded) != 32 {
364
- return errors.New("wireguard_public_key must be 32 bytes")
365
- }
366
- return nil
367
-}
368
-
315
func ParseSecp256k1PublicKeyHex(raw string) (*secp256k1.PublicKey, error) {
316
publicKeyHex := strings.TrimSpace(raw)
317
if publicKeyHex == "" {
318
return nil, errors.New("public key is required")
319
}
374
- publicKeyHex = TrimHexPrefix(publicKeyHex)
320
+ publicKeyHex = trimHexPrefix(publicKeyHex)
321
322
decoded, err := hex.DecodeString(publicKeyHex)
323
if err != nil {
@@ -385,12 +331,12 @@ func ParseSecp256k1PublicKeyHex(raw string) (*secp256k1.PublicKey, error) {
331
return publicKey, nil
332
}
333
388
-func ParseSecp256k1PrivateKeyHex(raw string, requireNonZero bool) (*secp256k1.PrivateKey, string, error) {
334
+func parseSecp256k1PrivateKeyHex(raw string, requireNonZero bool) (*secp256k1.PrivateKey, string, error) {
335
privateKeyHex := strings.TrimSpace(raw)
336
if privateKeyHex == "" {
337
return nil, "", errors.New("private key is required")
338
}
393
- privateKeyHex = TrimHexPrefix(privateKeyHex)
339
+ privateKeyHex = trimHexPrefix(privateKeyHex)
340
341
decoded, err := hex.DecodeString(privateKeyHex)
342
if err != nil {
@@ -423,3 +369,10 @@ func ParseSecp256k1PrivateKeyHex(raw string, requireNonZero bool) (*secp256k1.Pr
369
}
370
return key, privateKeyHex, nil
371
}
372
+
373
+func trimHexPrefix(raw string) string {
374
+ if len(raw) >= 2 && raw[0] == '0' && (raw[1] == 'x' || raw[1] == 'X') {
375
+ return raw[2:]
376
+ }
377
+ return raw
378
+}
portal/identity/store.go
renamed
+194
-257
@@ -1,4 +1,4 @@
1
-package utils
1
+package identity
2
3
import (
4
"encoding/json"
@@ -6,18 +6,18 @@ import (
6
"fmt"
7
"math"
8
"net"
9
- "net/url"
9
"os"
10
"path/filepath"
11
"strings"
12
13
"github.com/gosuda/portal-tunnel/v2/types"
14
+ "github.com/gosuda/portal-tunnel/v2/utils"
15
)
16
17
func NormalizeIdentity(identity types.Identity) (types.Identity, error) {
18
normalized := identity.Copy()
19
20
- name, err := NormalizeDNSLabel(identity.Name)
20
+ name, err := utils.NormalizeDNSLabel(identity.Name)
21
if err != nil {
22
return types.Identity{}, err
23
}
@@ -31,7 +31,7 @@ func NormalizeIdentity(identity types.Identity) (types.Identity, error) {
31
return normalized, nil
32
}
33
34
-func NormalizeDescriptor(desc types.RelayDescriptor) (types.RelayDescriptor, error) {
34
+func NormalizeRelayDescriptor(desc types.RelayDescriptor) (types.RelayDescriptor, error) {
35
desc.Address = strings.TrimSpace(desc.Address)
36
desc.Version = strings.TrimSpace(desc.Version)
37
desc.APIHTTPSAddr = strings.TrimSpace(desc.APIHTTPSAddr)
@@ -47,7 +47,7 @@ func NormalizeDescriptor(desc types.RelayDescriptor) (types.RelayDescriptor, err
47
}
48
49
if desc.APIHTTPSAddr != "" {
50
- normalized, err := NormalizeRelayURL(desc.APIHTTPSAddr)
50
+ normalized, err := utils.NormalizeRelayURL(desc.APIHTTPSAddr)
51
if err != nil {
52
return types.RelayDescriptor{}, fmt.Errorf("normalize api https addr: %w", err)
53
}
@@ -98,7 +98,7 @@ func NormalizeDescriptor(desc types.RelayDescriptor) (types.RelayDescriptor, err
98
}
99
100
func RelayWireGuardEndpoint(desc types.RelayDescriptor) (string, error) {
101
- host := PortalRootHost(desc.APIHTTPSAddr)
101
+ host := utils.PortalRootHost(desc.APIHTTPSAddr)
102
if host == "" {
103
return "", errors.New("api_https_addr host is required")
104
}
@@ -121,7 +121,7 @@ func ResolveRelayStateDir(path string) string {
121
}
122
}
123
124
-func ResolveRelayIdentityPath(path string) string {
124
+func resolveRelayIdentityPath(path string) string {
125
stateDir := ResolveRelayStateDir(path)
126
if stateDir == "" {
127
return ""
@@ -137,12 +137,13 @@ func ResolveRelayAdminSettingsPath(path string) string {
137
return filepath.Join(stateDir, types.RelayAdminSettingsFilename)
138
}
139
140
-func NormalizeStoredIdentity(identity types.Identity) (types.Identity, error) {
140
+func normalizeStoredIdentity(identity types.Identity) (types.Identity, error) {
141
normalized := identity.Copy()
142
normalized.Name = strings.TrimSpace(normalized.Name)
143
normalized.Address = strings.TrimSpace(normalized.Address)
144
normalized.PublicKey = strings.TrimSpace(normalized.PublicKey)
145
normalized.PrivateKey = strings.TrimSpace(normalized.PrivateKey)
146
+ normalized.TokenSecret = strings.TrimSpace(normalized.TokenSecret)
147
148
switch {
149
case normalized.PrivateKey != "":
@@ -150,7 +151,7 @@ func NormalizeStoredIdentity(identity types.Identity) (types.Identity, error) {
151
if err != nil {
152
return types.Identity{}, err
153
}
153
- if normalized.PublicKey != "" && !strings.EqualFold(TrimHexPrefix(normalized.PublicKey), resolved.PublicKey) {
154
+ if normalized.PublicKey != "" && !strings.EqualFold(utils.TrimHexPrefix(normalized.PublicKey), resolved.PublicKey) {
155
return types.Identity{}, errors.New("identity public key does not match private key")
156
}
157
if normalized.Address != "" && !strings.EqualFold(normalized.Address, resolved.Address) {
@@ -164,7 +165,7 @@ func NormalizeStoredIdentity(identity types.Identity) (types.Identity, error) {
165
if err != nil {
166
return types.Identity{}, err
167
}
167
- normalized.PublicKey = strings.ToLower(TrimHexPrefix(normalized.PublicKey))
168
+ normalized.PublicKey = strings.ToLower(utils.TrimHexPrefix(normalized.PublicKey))
169
if normalized.Address == "" {
170
normalized.Address = address
171
break
@@ -183,9 +184,9 @@ func NormalizeStoredIdentity(identity types.Identity) (types.Identity, error) {
184
return normalized, nil
185
}
186
186
-func NormalizeStoredRelayIdentity(identity types.RelayIdentity) (types.RelayIdentity, error) {
187
+func normalizeStoredRelayIdentity(identity types.RelayIdentity) (types.RelayIdentity, error) {
188
normalized := identity.Copy()
188
- baseIdentity, err := NormalizeStoredIdentity(normalized.Identity)
189
+ baseIdentity, err := normalizeStoredIdentity(normalized.Identity)
190
if err != nil {
191
return types.RelayIdentity{}, err
192
}
@@ -225,10 +226,11 @@ func NormalizeStoredRelayIdentity(identity types.RelayIdentity) (types.RelayIden
226
}
227
228
type storedIdentity struct {
228
- Name string `json:"name,omitempty"`
229
- Address string `json:"address,omitempty"`
230
- PublicKey string `json:"public_key,omitempty"`
231
- PrivateKey string `json:"private_key,omitempty"`
229
+ Name string `json:"name,omitempty"`
230
+ Address string `json:"address,omitempty"`
231
+ PublicKey string `json:"public_key,omitempty"`
232
+ PrivateKey string `json:"private_key,omitempty"`
233
+ TokenSecret string `json:"token_secret,omitempty"`
234
}
235
236
type storedRelayIdentity struct {
@@ -239,41 +241,52 @@ type storedRelayIdentity struct {
241
EncryptedClientHelloSeed string `json:"encrypted_client_hello_seed,omitempty"`
242
}
243
242
-func SaveIdentity(path string, identity types.Identity) error {
244
+func saveIdentity(path string, identity types.Identity) error {
245
path = strings.TrimSpace(path)
246
if path == "" {
247
return errors.New("identity path is required")
248
}
247
- normalized, err := NormalizeStoredIdentity(identity)
249
+ normalized, err := normalizeStoredIdentity(identity)
250
if err != nil {
251
return err
252
}
251
- if err := WriteJSONFile(path, storedIdentity{
252
- Name: normalized.Name,
253
- Address: normalized.Address,
254
- PublicKey: normalized.PublicKey,
255
- PrivateKey: normalized.PrivateKey,
253
+ normalized, err = ensureTokenSecret(normalized)
254
+ if err != nil {
255
+ return err
256
+ }
257
+ if err := utils.WriteJSONFile(path, storedIdentity{
258
+ Name: normalized.Name,
259
+ Address: normalized.Address,
260
+ PublicKey: normalized.PublicKey,
261
+ PrivateKey: normalized.PrivateKey,
262
+ TokenSecret: normalized.TokenSecret,
263
}, 0o600); err != nil {
264
return fmt.Errorf("write identity file: %w", err)
265
}
266
return nil
267
}
268
262
-func SaveRelayIdentity(path string, identity types.RelayIdentity) error {
263
- path = ResolveRelayIdentityPath(path)
269
+func saveRelayIdentity(path string, identity types.RelayIdentity) error {
270
+ path = resolveRelayIdentityPath(path)
271
if path == "" {
272
return errors.New("identity path is required")
273
}
267
- normalized, err := NormalizeStoredRelayIdentity(identity)
274
+ normalized, err := normalizeStoredRelayIdentity(identity)
275
+ if err != nil {
276
+ return err
277
+ }
278
+ baseIdentity, err := ensureTokenSecret(normalized.Identity)
279
if err != nil {
280
return err
281
}
271
- if err := WriteJSONFile(path, storedRelayIdentity{
282
+ normalized.Identity = baseIdentity
283
+ if err := utils.WriteJSONFile(path, storedRelayIdentity{
284
storedIdentity: storedIdentity{
273
- Name: normalized.Name,
274
- Address: normalized.Address,
275
- PublicKey: normalized.PublicKey,
276
- PrivateKey: normalized.PrivateKey,
285
+ Name: normalized.Name,
286
+ Address: normalized.Address,
287
+ PublicKey: normalized.PublicKey,
288
+ PrivateKey: normalized.PrivateKey,
289
+ TokenSecret: normalized.TokenSecret,
290
},
291
AdminSecretKey: normalized.AdminSecretKey,
292
WireGuardPublicKey: normalized.WireGuardPublicKey,
@@ -285,38 +298,40 @@ func SaveRelayIdentity(path string, identity types.RelayIdentity) error {
298
return nil
299
}
300
288
-func LoadIdentity(path string) (types.Identity, error) {
301
+func loadIdentity(path string) (types.Identity, error) {
302
path = strings.TrimSpace(path)
303
if path == "" {
304
return types.Identity{}, errors.New("identity path is required")
305
}
306
var payload storedIdentity
294
- if err := ReadJSONFile(path, &payload); err != nil {
307
+ if err := utils.ReadJSONFile(path, &payload); err != nil {
308
return types.Identity{}, fmt.Errorf("read identity file: %w", err)
309
}
297
- return NormalizeStoredIdentity(types.Identity{
298
- Name: payload.Name,
299
- Address: payload.Address,
300
- PublicKey: payload.PublicKey,
301
- PrivateKey: payload.PrivateKey,
310
+ return normalizeStoredIdentity(types.Identity{
311
+ Name: payload.Name,
312
+ Address: payload.Address,
313
+ PublicKey: payload.PublicKey,
314
+ PrivateKey: payload.PrivateKey,
315
+ TokenSecret: payload.TokenSecret,
316
})
317
}
318
305
-func LoadRelayIdentity(path string) (types.RelayIdentity, error) {
306
- path = ResolveRelayIdentityPath(path)
319
+func loadRelayIdentity(path string) (types.RelayIdentity, error) {
320
+ path = resolveRelayIdentityPath(path)
321
if path == "" {
322
return types.RelayIdentity{}, errors.New("identity path is required")
323
}
324
var payload storedRelayIdentity
311
- if err := ReadJSONFile(path, &payload); err != nil {
325
+ if err := utils.ReadJSONFile(path, &payload); err != nil {
326
return types.RelayIdentity{}, fmt.Errorf("read identity file: %w", err)
327
}
314
- return NormalizeStoredRelayIdentity(types.RelayIdentity{
328
+ return normalizeStoredRelayIdentity(types.RelayIdentity{
329
Identity: types.Identity{
316
- Name: payload.Name,
317
- Address: payload.Address,
318
- PublicKey: payload.PublicKey,
319
- PrivateKey: payload.PrivateKey,
330
+ Name: payload.Name,
331
+ Address: payload.Address,
332
+ PublicKey: payload.PublicKey,
333
+ PrivateKey: payload.PrivateKey,
334
+ TokenSecret: payload.TokenSecret,
335
},
336
AdminSecretKey: payload.AdminSecretKey,
337
WireGuardPublicKey: payload.WireGuardPublicKey,
@@ -325,7 +340,7 @@ func LoadRelayIdentity(path string) (types.RelayIdentity, error) {
340
})
341
}
342
328
-func ParseIdentityJSON(raw string) (types.Identity, error) {
343
+func parseIdentityJSON(raw string) (types.Identity, error) {
344
raw = strings.TrimSpace(raw)
345
if raw == "" {
346
return types.Identity{}, errors.New("identity json is required")
@@ -335,21 +350,22 @@ func ParseIdentityJSON(raw string) (types.Identity, error) {
350
if err := json.Unmarshal([]byte(raw), &payload); err != nil {
351
return types.Identity{}, fmt.Errorf("decode identity json: %w", err)
352
}
338
- return NormalizeStoredIdentity(types.Identity{
339
- Name: payload.Name,
340
- Address: payload.Address,
341
- PublicKey: payload.PublicKey,
342
- PrivateKey: payload.PrivateKey,
353
+ return normalizeStoredIdentity(types.Identity{
354
+ Name: payload.Name,
355
+ Address: payload.Address,
356
+ PublicKey: payload.PublicKey,
357
+ PrivateKey: payload.PrivateKey,
358
+ TokenSecret: payload.TokenSecret,
359
})
360
}
361
346
-func LoadOrCreateIdentity(path string, identity types.Identity) (types.Identity, bool, error) {
362
+func loadOrCreateIdentity(path string, identity types.Identity) (types.Identity, bool, error) {
363
path = strings.TrimSpace(path)
364
if path == "" {
365
return types.Identity{}, false, errors.New("identity path is required")
366
}
367
352
- stored, err := LoadIdentity(path)
368
+ stored, err := loadIdentity(path)
369
switch {
370
case err == nil:
371
if name := strings.TrimSpace(identity.Name); name != "" {
@@ -364,13 +380,16 @@ func LoadOrCreateIdentity(path string, identity types.Identity) (types.Identity,
380
if privateKey := strings.TrimSpace(identity.PrivateKey); privateKey != "" {
381
stored.PrivateKey = privateKey
382
}
383
+ if tokenSecret := strings.TrimSpace(identity.TokenSecret); tokenSecret != "" {
384
+ stored.TokenSecret = tokenSecret
385
+ }
386
if strings.TrimSpace(stored.PrivateKey) == "" {
387
return types.Identity{}, false, errors.New("stored identity private key is required")
388
}
370
- if err := SaveIdentity(path, stored); err != nil {
389
+ if err := saveIdentity(path, stored); err != nil {
390
return types.Identity{}, false, fmt.Errorf("persist identity: %w", err)
391
}
373
- loaded, err := LoadIdentity(path)
392
+ loaded, err := loadIdentity(path)
393
if err != nil {
394
return types.Identity{}, false, fmt.Errorf("load identity: %w", err)
395
}
@@ -391,29 +410,77 @@ func LoadOrCreateIdentity(path string, identity types.Identity) (types.Identity,
410
created.PublicKey = generated.PublicKey
411
}
412
created.PrivateKey = generated.PrivateKey
394
- if err := SaveIdentity(path, created); err != nil {
413
+ if strings.TrimSpace(created.TokenSecret) == "" {
414
+ created, err = ensureTokenSecret(created)
415
+ if err != nil {
416
+ return types.Identity{}, false, err
417
+ }
418
+ }
419
+ if err := saveIdentity(path, created); err != nil {
420
return types.Identity{}, false, fmt.Errorf("persist identity: %w", err)
421
}
397
- loaded, err := LoadIdentity(path)
422
+ loaded, err := loadIdentity(path)
423
if err != nil {
424
return types.Identity{}, false, fmt.Errorf("load identity: %w", err)
425
}
426
return loaded, true, nil
427
}
428
429
+func ResolveListenerIdentity(baseIdentity types.Identity, target, identityPath, identityJSON string) (types.Identity, bool, error) {
430
+ identityPath = strings.TrimSpace(identityPath)
431
+ identityJSON = strings.TrimSpace(identityJSON)
432
+ resolvedName, err := resolveExposeName(baseIdentity.Name, target, identityPath, identityJSON)
433
+ if err != nil {
434
+ return types.Identity{}, false, err
435
+ }
436
+ baseIdentity.Name = resolvedName
437
+ if identityJSON != "" {
438
+ provided, err := parseIdentityJSON(identityJSON)
439
+ if err != nil {
440
+ return types.Identity{}, false, err
441
+ }
442
+ provided.Name = baseIdentity.Name
443
+ if identityPath != "" {
444
+ if err := saveIdentity(identityPath, provided); err != nil {
445
+ return types.Identity{}, false, fmt.Errorf("persist identity: %w", err)
446
+ }
447
+ provided, err = loadIdentity(identityPath)
448
+ if err != nil {
449
+ return types.Identity{}, false, fmt.Errorf("load identity: %w", err)
450
+ }
451
+ }
452
+ resolved, err := resolveLeaseIdentity(provided)
453
+ return resolved, false, err
454
+ }
455
+ if identityPath == "" {
456
+ resolved, err := resolveLeaseIdentity(baseIdentity)
457
+ return resolved, false, err
458
+ }
459
+
460
+ loaded, created, err := loadOrCreateIdentity(identityPath, baseIdentity)
461
+ if err != nil {
462
+ return types.Identity{}, false, err
463
+ }
464
+ resolved, err := resolveLeaseIdentity(loaded)
465
+ if err != nil {
466
+ return types.Identity{}, false, err
467
+ }
468
+ return resolved, created, nil
469
+}
470
+
471
func LoadOrCreateRelayIdentity(path, rootHost string, discoveryEnabled bool) (types.RelayIdentity, error) {
405
- path = ResolveRelayIdentityPath(path)
472
+ path = resolveRelayIdentityPath(path)
473
if path == "" {
474
return types.RelayIdentity{}, errors.New("identity path is required")
475
}
476
rootHost = strings.TrimSpace(rootHost)
410
- if normalizedRootHost := PortalRootHost(rootHost); normalizedRootHost != "" {
477
+ if normalizedRootHost := utils.PortalRootHost(rootHost); normalizedRootHost != "" {
478
rootHost = normalizedRootHost
479
} else {
413
- rootHost = NormalizeHostname(rootHost)
480
+ rootHost = utils.NormalizeHostname(rootHost)
481
}
482
416
- stored, err := LoadRelayIdentity(path)
483
+ stored, err := loadRelayIdentity(path)
484
switch {
485
case err == nil:
486
if rootHost != "" {
@@ -423,10 +490,10 @@ func LoadOrCreateRelayIdentity(path, rootHost string, discoveryEnabled bool) (ty
490
if err := populateRelayIdentity(&stored, discoveryEnabled); err != nil {
491
return types.RelayIdentity{}, err
492
}
426
- if err := SaveRelayIdentity(path, stored); err != nil {
493
+ if err := saveRelayIdentity(path, stored); err != nil {
494
return types.RelayIdentity{}, fmt.Errorf("persist identity: %w", err)
495
}
429
- loaded, err := LoadRelayIdentity(path)
496
+ loaded, err := loadRelayIdentity(path)
497
if err != nil {
498
return types.RelayIdentity{}, fmt.Errorf("load identity: %w", err)
499
}
@@ -449,14 +516,18 @@ func LoadOrCreateRelayIdentity(path, rootHost string, discoveryEnabled bool) (ty
516
created.PublicKey = generated.PublicKey
517
}
518
created.PrivateKey = generated.PrivateKey
519
+ created.Identity, err = ensureTokenSecret(created.Identity)
520
+ if err != nil {
521
+ return types.RelayIdentity{}, err
522
+ }
523
524
if err := populateRelayIdentity(&created, discoveryEnabled); err != nil {
525
return types.RelayIdentity{}, err
526
}
456
- if err := SaveRelayIdentity(path, created); err != nil {
527
+ if err := saveRelayIdentity(path, created); err != nil {
528
return types.RelayIdentity{}, fmt.Errorf("persist identity: %w", err)
529
}
459
- loaded, err := LoadRelayIdentity(path)
530
+ loaded, err := loadRelayIdentity(path)
531
if err != nil {
532
return types.RelayIdentity{}, fmt.Errorf("load identity: %w", err)
533
}
@@ -467,9 +538,14 @@ func populateRelayIdentity(identity *types.RelayIdentity, discoveryEnabled bool)
538
if identity == nil {
539
return errors.New("relay identity is required")
540
}
541
+ baseIdentity, err := ensureTokenSecret(identity.Identity)
542
+ if err != nil {
543
+ return err
544
+ }
545
+ identity.Identity = baseIdentity
546
547
if strings.TrimSpace(identity.AdminSecretKey) == "" {
472
- adminSecretKey, err := identity.DeriveToken("admin-secret")
548
+ adminSecretKey, err := DeriveToken(identity.Identity, "admin-secret")
549
if err != nil {
550
return fmt.Errorf("derive relay admin secret key: %w", err)
551
}
@@ -486,55 +562,13 @@ func populateRelayIdentity(identity *types.RelayIdentity, discoveryEnabled bool)
562
}
563
564
if strings.TrimSpace(identity.EncryptedClientHelloSeed) == "" {
489
- identity.EncryptedClientHelloSeed = RandomID("")
565
+ identity.EncryptedClientHelloSeed = utils.RandomID("")
566
}
567
568
return nil
569
}
570
495
-func ResolveListenerIdentity(identity types.Identity, target, identityPath, identityJSON string) (types.Identity, bool, error) {
496
- identityPath = strings.TrimSpace(identityPath)
497
- identityJSON = strings.TrimSpace(identityJSON)
498
- resolvedName, err := resolveExposeName(identity.Name, target, identityPath, identityJSON)
499
- if err != nil {
500
- return types.Identity{}, false, err
501
- }
502
- identity.Name = resolvedName
503
- if identityJSON != "" {
504
- provided, err := ParseIdentityJSON(identityJSON)
505
- if err != nil {
506
- return types.Identity{}, false, err
507
- }
508
- provided.Name = identity.Name
509
- if identityPath != "" {
510
- if err := SaveIdentity(identityPath, provided); err != nil {
511
- return types.Identity{}, false, fmt.Errorf("persist identity: %w", err)
512
- }
513
- provided, err = LoadIdentity(identityPath)
514
- if err != nil {
515
- return types.Identity{}, false, fmt.Errorf("load identity: %w", err)
516
- }
517
- }
518
- resolved, err := ResolveLeaseIdentity(provided)
519
- return resolved, false, err
520
- }
521
- if identityPath == "" {
522
- resolved, err := ResolveLeaseIdentity(identity)
523
- return resolved, false, err
524
- }
525
-
526
- loaded, created, err := LoadOrCreateIdentity(identityPath, identity)
527
- if err != nil {
528
- return types.Identity{}, false, err
529
- }
530
- resolved, err := ResolveLeaseIdentity(loaded)
531
- if err != nil {
532
- return types.Identity{}, false, err
533
- }
534
- return resolved, created, nil
535
-}
536
-
537
-func NormalizeIdentityKey(raw string) string {
571
+func normalizeIdentityKey(raw string) string {
572
key := strings.ToLower(strings.TrimSpace(raw))
573
if key == "" {
574
return ""
@@ -547,7 +581,7 @@ func NormalizeIdentityKey(raw string) string {
581
}
582
583
func NormalizeIdentityKeys(inputs []string) []string {
550
- return normalizeUniqueStrings(inputs, NormalizeIdentityKey)
584
+ return normalizeUniqueStrings(inputs, normalizeIdentityKey)
585
}
586
587
func NormalizeIdentityKeyBPS(inputs map[string]int64) map[string]int64 {
@@ -557,7 +591,7 @@ func NormalizeIdentityKeyBPS(inputs map[string]int64) map[string]int64 {
591
592
out := make(map[string]int64, len(inputs))
593
for input, bps := range inputs {
560
- key := NormalizeIdentityKey(input)
594
+ key := normalizeIdentityKey(input)
595
if key == "" || bps <= 0 {
596
continue
597
}
@@ -569,10 +603,38 @@ func NormalizeIdentityKeyBPS(inputs map[string]int64) map[string]int64 {
603
return out
604
}
605
572
-func ResolveLeaseIdentity(identity types.Identity) (types.Identity, error) {
606
+func resolveExposeName(name, target, identityPath, identityJSON string) (string, error) {
607
+ if name = strings.TrimSpace(name); name != "" {
608
+ return name, nil
609
+ }
610
+ if identityJSON = strings.TrimSpace(identityJSON); identityJSON != "" {
611
+ providedIdentity, err := parseIdentityJSON(identityJSON)
612
+ if err != nil {
613
+ return "", err
614
+ }
615
+ if name := strings.TrimSpace(providedIdentity.Name); name != "" {
616
+ return name, nil
617
+ }
618
+ }
619
+ if identityPath = strings.TrimSpace(identityPath); identityPath != "" {
620
+ storedIdentity, err := loadIdentity(identityPath)
621
+ switch {
622
+ case err == nil:
623
+ if name := strings.TrimSpace(storedIdentity.Name); name != "" {
624
+ return name, nil
625
+ }
626
+ case !errors.Is(err, os.ErrNotExist):
627
+ return "", err
628
+ }
629
+ }
630
+
631
+ return utils.DefaultExposeName(target, utils.RandomID("cli_"))
632
+}
633
+
634
+func resolveLeaseIdentity(identity types.Identity) (types.Identity, error) {
635
resolved := identity.Copy()
636
575
- name, err := NormalizeDNSLabel(resolved.Name)
637
+ name, err := utils.NormalizeDNSLabel(resolved.Name)
638
if err != nil {
639
return types.Identity{}, err
640
}
@@ -597,154 +659,29 @@ func ResolveLeaseIdentity(identity types.Identity) (types.Identity, error) {
659
660
resolved.PublicKey = signingIdentity.PublicKey
661
resolved.PrivateKey = signingIdentity.PrivateKey
600
- return resolved, nil
601
-}
602
-
603
-var exposeNameOpeners = []string{
604
- "arcade", "bouncy", "bravo", "bubble", "candy", "cosmic", "dapper", "electric",
605
- "fancy", "fizzy", "flashy", "fuzzy", "gentle", "glitter", "golden", "happy",
606
- "hyper", "jazzy", "jolly", "lively", "lucky", "magic", "mellow", "minty",
607
- "misty", "moonlit", "mystic", "neon", "nova", "peppy", "pixel", "playful",
608
- "poppy", "rapid", "rocket", "rowdy", "snappy", "snazzy", "sparkly", "spicy",
609
- "sprightly", "starry", "sunny", "swift", "tangy", "tidy", "toasty", "turbo",
610
- "velvet", "vivid", "wavy", "whimsy", "wild", "wonky", "zany", "zesty",
611
-}
612
-
613
-var exposeNameCenters = []string{
614
- "alpaca", "badger", "banjo", "beacon", "biscuit", "capybara", "comet", "cricket",
615
- "dragon", "falcon", "feather", "fjord", "fox", "gadget", "gecko", "gizmo",
616
- "harbor", "heron", "iguana", "jelly", "koala", "lemur", "mango", "narwhal",
617
- "nebula", "noodle", "octopus", "otter", "panda", "pepper", "phoenix", "pickle",
618
- "puffin", "quokka", "radar", "ranger", "rocket", "scooter", "seahorse", "skylark",
619
- "sprocket", "starling", "sunbeam", "taco", "thimble", "tiger", "toucan", "triton",
620
- "walrus", "widget", "willow", "wombat", "yeti", "zeppelin", "zigzag", "zinnia",
621
-}
622
-
623
-var exposeNameClosers = []string{
624
- "arcade", "beacon", "boogie", "bounce", "burst", "cascade", "chorus", "dash",
625
- "disco", "drift", "echo", "fiesta", "flare", "flash", "flight", "flip",
626
- "glow", "groove", "jam", "jive", "launch", "loop", "march", "orbit",
627
- "parade", "party", "pulse", "quest", "rally", "riot", "ripple", "rodeo",
628
- "roll", "rush", "serenade", "shuffle", "signal", "sketch", "spark", "sprint",
629
- "starlight", "stride", "sway", "swoop", "twirl", "uplift", "vibe", "voyage",
630
- "whirl", "wink", "zap", "zenith", "zip", "zoom", "zest", "zone",
631
-}
632
-
633
-const (
634
- defaultExposeTargetPort = "3000"
635
- defaultExposeTargetHost = "127.0.0.1"
636
-)
637
-
638
-// DefaultExposeName generates a deterministic 3-word DNS label from a target
639
-// address and seed using FNV-1a hashing. The algorithm matches the frontend
640
-// implementation in frontend/src/lib/exposeName.ts:buildDefaultExposeName.
641
-func DefaultExposeName(target, rawSeed string) (string, error) {
642
- seed := strings.TrimSpace(rawSeed)
643
- if cut, ok := strings.CutPrefix(seed, "cli_"); ok {
644
- seed = cut
645
- }
646
- if seed == "" {
647
- seed = "portal"
648
- }
649
-
650
- input := []byte(seed + "|" + normalizeExposeTarget(target))
651
- first := fnv1a32(input, 0x811c9dc5)
652
- second := fnv1a32(input, 0x9e3779b9)
653
- third := fnv1a32(input, 0x85ebca6b)
654
-
655
- label := strings.Join([]string{
656
- exposeNameOpeners[int(first&0xff)%len(exposeNameOpeners)],
657
- exposeNameCenters[int(second&0xff)%len(exposeNameCenters)],
658
- exposeNameClosers[int(third&0xff)%len(exposeNameClosers)],
659
- }, "-")
660
-
661
- return NormalizeDNSLabel(label)
662
+ return ensureTokenSecret(resolved)
663
}
664
664
-// normalizeExposeTarget normalizes a target address for deterministic name
665
-// generation. Must match frontend/src/lib/exposeName.ts:normalizeExposeTarget.
666
-func normalizeExposeTarget(raw string) string {
667
- trimmed := strings.TrimSpace(raw)
668
- candidate := trimmed
669
- if candidate == "" {
670
- candidate = defaultExposeTargetPort
671
- }
672
-
673
- if isAllDigits(candidate) {
674
- return defaultExposeTargetHost + ":" + candidate
675
- }
676
-
677
- if strings.Contains(candidate, "://") {
678
- u, err := url.Parse(candidate)
679
- if err != nil {
680
- return candidate
681
- }
682
- if (u.Scheme == "http" || u.Scheme == "https") &&
683
- u.Host != "" &&
684
- (u.Path == "" || u.Path == "/") &&
685
- u.RawQuery == "" &&
686
- u.Fragment == "" {
687
- return u.Host
688
- }
689
- return candidate
690
- }
691
-
692
- u, err := url.Parse("tcp://" + candidate)
693
- if err != nil || u.Hostname() == "" {
694
- return candidate
695
- }
696
- port := u.Port()
697
- if port == "" {
698
- port = "80"
699
- }
700
- return net.JoinHostPort(u.Hostname(), port)
701
-}
702
-
703
-func isAllDigits(s string) bool {
704
- if s == "" {
705
- return false
706
- }
707
- for _, c := range s {
708
- if c < '0' || c > '9' {
709
- return false
710
- }
665
+func normalizeUniqueStrings(inputs []string, normalize func(string) string) []string {
666
+ if len(inputs) == 0 {
667
+ return nil
668
}
712
- return true
713
-}
669
715
-func resolveExposeName(name, target, identityPath, identityJSON string) (string, error) {
716
- if name = strings.TrimSpace(name); name != "" {
717
- return name, nil
718
- }
719
- if identityJSON = strings.TrimSpace(identityJSON); identityJSON != "" {
720
- identity, err := ParseIdentityJSON(identityJSON)
721
- if err != nil {
722
- return "", err
723
- }
724
- if name := strings.TrimSpace(identity.Name); name != "" {
725
- return name, nil
670
+ out := make([]string, 0, len(inputs))
671
+ seen := make(map[string]struct{}, len(inputs))
672
+ for _, input := range inputs {
673
+ normalized := normalize(input)
674
+ if normalized == "" {
675
+ continue
676
}
727
- }
728
- if identityPath = strings.TrimSpace(identityPath); identityPath != "" {
729
- identity, err := LoadIdentity(identityPath)
730
- switch {
731
- case err == nil:
732
- if name := strings.TrimSpace(identity.Name); name != "" {
733
- return name, nil
734
- }
735
- case !errors.Is(err, os.ErrNotExist):
736
- return "", err
677
+ if _, ok := seen[normalized]; ok {
678
+ continue
679
}
680
+ seen[normalized] = struct{}{}
681
+ out = append(out, normalized)
682
}
739
-
740
- return DefaultExposeName(target, RandomID("cli_"))
741
-}
742
-
743
-func fnv1a32(data []byte, seed uint32) uint32 {
744
- h := seed
745
- for _, b := range data {
746
- h ^= uint32(b)
747
- h *= 0x01000193
683
+ if len(out) == 0 {
684
+ return nil
685
}
749
- return h
686
+ return out
687
}
portal/identity/token.go
new
+50
@@ -0,0 +1,50 @@
1
+package identity
2
+
3
+import (
4
+ "crypto/hmac"
5
+ "crypto/rand"
6
+ "crypto/sha256"
7
+ "encoding/base64"
8
+ "errors"
9
+ "fmt"
10
+ "strconv"
11
+ "strings"
12
+
13
+ "github.com/gosuda/portal-tunnel/v2/types"
14
+)
15
+
16
+func ensureTokenSecret(identity types.Identity) (types.Identity, error) {
17
+ identity = identity.Copy()
18
+ identity.TokenSecret = strings.TrimSpace(identity.TokenSecret)
19
+ if identity.TokenSecret != "" {
20
+ return identity, nil
21
+ }
22
+
23
+ var secret [32]byte
24
+ if _, err := rand.Read(secret[:]); err != nil {
25
+ return types.Identity{}, fmt.Errorf("generate token secret: %w", err)
26
+ }
27
+ identity.TokenSecret = base64.RawURLEncoding.EncodeToString(secret[:])
28
+ return identity, nil
29
+}
30
+
31
+// DeriveToken derives a deterministic identity-scoped token from ordered
32
+// length-prefixed token parts. The first part should identify the token family.
33
+func DeriveToken(identity types.Identity, parts ...string) (string, error) {
34
+ tokenSecret := strings.TrimSpace(identity.TokenSecret)
35
+ if tokenSecret == "" {
36
+ return "", errors.New("identity token secret is required")
37
+ }
38
+
39
+ mac := hmac.New(sha256.New, []byte(tokenSecret))
40
+ _, _ = mac.Write([]byte("Portal identity token v1\n"))
41
+ _, _ = mac.Write([]byte(identity.Key()))
42
+ for _, part := range parts {
43
+ part = strings.TrimSpace(part)
44
+ _, _ = mac.Write([]byte("\n"))
45
+ _, _ = mac.Write([]byte(strconv.Itoa(len(part))))
46
+ _, _ = mac.Write([]byte(":"))
47
+ _, _ = mac.Write([]byte(part))
48
+ }
49
+ return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)), nil
50
+}
portal/identity/wireguard.go
new
+112
@@ -0,0 +1,112 @@
1
+package identity
2
+
3
+import (
4
+ "crypto/rand"
5
+ "crypto/sha256"
6
+ "encoding/base64"
7
+ "encoding/hex"
8
+ "errors"
9
+ "net/netip"
10
+ "strings"
11
+
12
+ "golang.org/x/crypto/curve25519"
13
+)
14
+
15
+func NormalizeWireGuardPrivateKey(raw string) (string, error) {
16
+ key, err := decodeKey(raw)
17
+ if err != nil {
18
+ return "", err
19
+ }
20
+ clampPrivateKey(&key)
21
+ return base64.StdEncoding.EncodeToString(key[:]), nil
22
+}
23
+
24
+func GenerateWireGuardPrivateKey() (string, error) {
25
+ var key [32]byte
26
+ if _, err := rand.Read(key[:]); err != nil {
27
+ return "", err
28
+ }
29
+ clampPrivateKey(&key)
30
+ return base64.StdEncoding.EncodeToString(key[:]), nil
31
+}
32
+
33
+func WireGuardPublicKeyFromPrivate(raw string) (string, error) {
34
+ privateKey, err := decodeKey(raw)
35
+ if err != nil {
36
+ return "", err
37
+ }
38
+ clampPrivateKey(&privateKey)
39
+ var publicKey [32]byte
40
+ curve25519.ScalarBaseMult(&publicKey, &privateKey)
41
+ return base64.StdEncoding.EncodeToString(publicKey[:]), nil
42
+}
43
+
44
+func DeriveWireGuardOverlayIPv4(publicKey string) (string, error) {
45
+ decoded, err := base64.StdEncoding.DecodeString(strings.TrimSpace(publicKey))
46
+ if err != nil {
47
+ return "", errors.New("wireguard public key must be base64 encoded")
48
+ }
49
+ if len(decoded) != 32 {
50
+ return "", errors.New("wireguard public key must be 32 bytes")
51
+ }
52
+
53
+ sum := sha256.Sum256(decoded)
54
+ return netip.AddrFrom4([4]byte{
55
+ 100,
56
+ 64 + (sum[0] & 0x3f),
57
+ sum[1],
58
+ 1 + (sum[2] % 254),
59
+ }).String(), nil
60
+}
61
+
62
+func WireGuardKeyHex(raw string) (string, error) {
63
+ key, err := decodeKey(raw)
64
+ if err != nil {
65
+ return "", err
66
+ }
67
+ return hex.EncodeToString(key[:]), nil
68
+}
69
+
70
+func ValidateWireGuardPublicKey(raw string) error {
71
+ key := strings.TrimSpace(raw)
72
+ if key == "" {
73
+ return errors.New("wireguard_public_key is required")
74
+ }
75
+ decoded, err := base64.StdEncoding.DecodeString(key)
76
+ if err != nil {
77
+ return errors.New("wireguard_public_key must be base64 encoded")
78
+ }
79
+ if len(decoded) != 32 {
80
+ return errors.New("wireguard_public_key must be 32 bytes")
81
+ }
82
+ return nil
83
+}
84
+
85
+func decodeKey(raw string) ([32]byte, error) {
86
+ var key [32]byte
87
+ value := strings.TrimSpace(raw)
88
+ if value == "" {
89
+ return key, errors.New("wireguard key is required")
90
+ }
91
+
92
+ var decoded []byte
93
+ var err error
94
+ if len(value) == 64 && !strings.Contains(value, "=") {
95
+ decoded, err = hex.DecodeString(value)
96
+ } else {
97
+ decoded, err = base64.StdEncoding.DecodeString(value)
98
+ }
99
+ if err != nil {
100
+ return key, errors.New("wireguard key must be base64 or hex encoded")
101
+ }
102
+ if len(decoded) != len(key) {
103
+ return key, errors.New("wireguard key must be 32 bytes")
104
+ }
105
+ copy(key[:], decoded)
106
+ return key, nil
107
+}
108
+
109
+func clampPrivateKey(key *[32]byte) {
110
+ key[0] &= 248
111
+ key[31] = (key[31] & 127) | 64
112
+}
portal/lease.go
+46
-42
@@ -15,6 +15,7 @@ import (
15
16
"github.com/gosuda/portal-tunnel/v2/portal/acme"
17
"github.com/gosuda/portal-tunnel/v2/portal/auth"
18
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
19
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
20
"github.com/gosuda/portal-tunnel/v2/portal/policy"
21
"github.com/gosuda/portal-tunnel/v2/portal/transport"
@@ -32,38 +33,41 @@ const (
33
)
34
35
type leaseRegistry struct {
35
- records []*leaseRecord
36
- rootHostname string
37
- sniPort int
38
- tokenPrivateKey string
39
- tokenPublicKey string
40
- tokenKeyID string
41
- tokenIssuer string
42
- policy *policy.Runtime
43
- udpPorts *transport.PortAllocator
44
- tcpPorts *transport.PortAllocator
45
- proxy *proxy
46
- mu sync.RWMutex
36
+ records []*leaseRecord
37
+ rootHostname string
38
+ sniPort int
39
+ tokenAuthority identity.Authority
40
+ tokenIssuer string
41
+ policy *policy.Runtime
42
+ udpPorts *transport.PortAllocator
43
+ tcpPorts *transport.PortAllocator
44
+ proxy *proxy
45
+ mu sync.RWMutex
46
}
47
49
-func newLeaseRegistry(udpEnabled, tcpPortEnabled bool, minPort, maxPort int, rootHostname string, sniPort int, tokenPrivateKey, tokenPublicKey, tokenKeyID, tokenIssuer string, trustProxyHeaders bool, rawTrustedProxyCIDRs string) (*leaseRegistry, error) {
48
+func newLeaseRegistry(udpEnabled, tcpPortEnabled bool, minPort, maxPort int, rootHostname string, sniPort int, tokenAuthority identity.Authority, tokenIssuer string, trustProxyHeaders bool, rawTrustedProxyCIDRs string) (*leaseRegistry, error) {
49
+ if tokenAuthority == nil {
50
+ return nil, errors.New("lease token authority is required")
51
+ }
52
+ tokenIdentity := tokenAuthority.Identity()
53
+ if strings.TrimSpace(tokenIdentity.PublicKey) == "" {
54
+ return nil, errors.New("lease token authority public key is required")
55
+ }
56
runtime, err := policy.NewRuntime(udpEnabled, tcpPortEnabled, trustProxyHeaders, rawTrustedProxyCIDRs)
57
if err != nil {
58
return nil, err
59
}
60
61
return &leaseRegistry{
56
- records: make([]*leaseRecord, 0),
57
- rootHostname: utils.NormalizeHostname(rootHostname),
58
- sniPort: sniPort,
59
- tokenPrivateKey: tokenPrivateKey,
60
- tokenPublicKey: tokenPublicKey,
61
- tokenKeyID: tokenKeyID,
62
- tokenIssuer: tokenIssuer,
63
- policy: runtime,
64
- udpPorts: transport.NewPortAllocator(minPort, maxPort, defaultPortReservationGrace),
65
- tcpPorts: transport.NewPortAllocator(minPort, maxPort, defaultPortReservationGrace),
66
- proxy: &proxy{},
62
+ records: make([]*leaseRecord, 0),
63
+ rootHostname: utils.NormalizeHostname(rootHostname),
64
+ sniPort: sniPort,
65
+ tokenAuthority: tokenAuthority,
66
+ tokenIssuer: tokenIssuer,
67
+ policy: runtime,
68
+ udpPorts: transport.NewPortAllocator(minPort, maxPort, defaultPortReservationGrace),
69
+ tcpPorts: transport.NewPortAllocator(minPort, maxPort, defaultPortReservationGrace),
70
+ proxy: &proxy{},
71
}, nil
72
}
73
@@ -150,7 +154,7 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
154
if r == nil {
155
return nil, types.RegisterResponse{}, errFeatureUnavailable
156
}
153
- identity, err := utils.NormalizeIdentity(req.Identity)
157
+ leaseIdentity, err := identity.NormalizeIdentity(req.Identity)
158
if err != nil {
159
return nil, types.RegisterResponse{}, err
160
}
@@ -163,7 +167,7 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
167
ttl = time.Duration(req.TTL) * time.Second
168
}
169
166
- identityKey := identity.Key()
170
+ identityKey := leaseIdentity.Key()
171
hopToken := strings.TrimSpace(req.HopToken)
172
routeHostname := utils.NormalizeHostname(req.RouteHostname)
173
hostnameHash := strings.TrimSpace(req.HostnameHash)
@@ -188,7 +192,7 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
192
return nil, types.RegisterResponse{}, errors.New("route hostname must be a child of relay root hostname")
193
}
194
191
- publicHostname, err = utils.LeaseHostname(identity.Name, r.rootHostname)
195
+ publicHostname, err = utils.LeaseHostname(leaseIdentity.Name, r.rootHostname)
196
if err != nil {
197
return nil, types.RegisterResponse{}, err
198
}
@@ -222,13 +226,13 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
226
227
hostname := routeHostname
228
if hostname == "" && hopToken == "" {
225
- hostname, err = utils.LeaseHostname(identity.Name, r.rootHostname)
229
+ hostname, err = utils.LeaseHostname(leaseIdentity.Name, r.rootHostname)
230
if err != nil {
231
return nil, types.RegisterResponse{}, err
232
}
233
}
234
231
- accessToken, claims, err := auth.IssueLeaseAccessToken(r.tokenPrivateKey, r.tokenKeyID, r.tokenIssuer, identity, ttl)
235
+ accessToken, claims, err := auth.IssueLeaseAccessToken(r.tokenAuthority, r.tokenIssuer, leaseIdentity, ttl)
236
if err != nil {
237
return nil, types.RegisterResponse{}, err
238
}
@@ -237,7 +241,7 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
241
242
stream := transport.NewRelayStream(identityKey, defaultIdleKeepalive, defaultReadyQueueLimit)
243
record := &leaseRecord{
240
- Identity: identity,
244
+ Identity: leaseIdentity,
245
Hostname: hostname,
246
HostnameHash: hostnameHash,
247
ECHConfigList: echConfigList,
@@ -256,7 +260,7 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
260
if r.udpPorts == nil {
261
return nil, types.RegisterResponse{}, errors.New("udp port allocation not available")
262
}
259
- port, err := r.udpPorts.Allocate(identity.Name)
263
+ port, err := r.udpPorts.Allocate(leaseIdentity.Name)
264
if err != nil {
265
if errors.Is(err, transport.ErrPortExhausted) {
266
return nil, types.RegisterResponse{}, errUDPPortExhausted
@@ -272,7 +276,7 @@ func (r *leaseRegistry) Register(req types.RegisterChallengeRequest, clientIP, r
276
record.Close()
277
return nil, types.RegisterResponse{}, errors.New("tcp port allocation not available")
278
}
275
- port, err := r.tcpPorts.Allocate(identity.Name)
279
+ port, err := r.tcpPorts.Allocate(leaseIdentity.Name)
280
if err != nil {
281
record.Close()
282
if errors.Is(err, transport.ErrPortExhausted) {
@@ -387,7 +391,7 @@ func (r *leaseRegistry) admitLeaseByToken(token string, requireDatagram bool) (*
391
return nil, errFeatureUnavailable
392
}
393
now := time.Now().UTC()
390
- claims, err := auth.VerifyLeaseAccessToken(token, r.tokenPublicKey, r.tokenIssuer, now)
394
+ claims, err := auth.VerifyLeaseAccessToken(token, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, now)
395
if err != nil {
396
return nil, errUnauthorized
397
}
@@ -410,7 +414,7 @@ func (r *leaseRegistry) Renew(req types.RenewRequest, clientIP string) (types.Re
414
if r == nil {
415
return types.RenewResponse{}, errFeatureUnavailable
416
}
413
- claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, r.tokenPublicKey, r.tokenIssuer, time.Now().UTC())
417
+ claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, time.Now().UTC())
418
if err != nil {
419
return types.RenewResponse{}, errUnauthorized
420
}
@@ -440,10 +444,10 @@ func (r *leaseRegistry) Renew(req types.RenewRequest, clientIP string) (types.Re
444
}
445
record.Metadata = req.Metadata.Copy()
446
r.policy.IPFilter().RegisterIdentityIP(leaseKey, clientIP)
443
- identity := record.Identity
447
+ recordIdentity := record.Identity
448
r.mu.Unlock()
449
446
- nextAccessToken, _, err := auth.IssueLeaseAccessToken(r.tokenPrivateKey, r.tokenKeyID, r.tokenIssuer, identity, ttl)
450
+ nextAccessToken, _, err := auth.IssueLeaseAccessToken(r.tokenAuthority, r.tokenIssuer, recordIdentity, ttl)
451
if err != nil {
452
return types.RenewResponse{}, &apiError{types.APIErrorCodeInternal, err.Error(), http.StatusInternalServerError}
453
}
@@ -458,7 +462,7 @@ func (r *leaseRegistry) Unregister(req types.UnregisterRequest) (*leaseRecord, e
462
if r == nil {
463
return nil, errFeatureUnavailable
464
}
461
- claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, r.tokenPublicKey, r.tokenIssuer, time.Now().UTC())
465
+ claims, err := auth.VerifyLeaseAccessToken(req.AccessToken, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, time.Now().UTC())
466
if err != nil {
467
return nil, errUnauthorized
468
}
@@ -483,7 +487,7 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
487
if route == nil {
488
return nil, errors.New("hop route is required")
489
}
486
- ownerKey, err := utils.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
490
+ ownerKey, err := identity.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
491
if err != nil {
492
return nil, err
493
}
@@ -492,7 +496,7 @@ func (r *leaseRegistry) RegisterHopRoute(route *types.HopRoute, now time.Time) (
496
echConfigList := bytes.Clone(route.ECHConfigList)
497
publicHostname := utils.NormalizeHostname(route.PublicHostname)
498
matchToken := route.MatchToken
495
- overlayIPv4, overlayErr := utils.DeriveWireGuardOverlayIPv4(route.ForwardRelay.WireGuardPublicKey)
499
+ overlayIPv4, overlayErr := identity.DeriveWireGuardOverlayIPv4(route.ForwardRelay.WireGuardPublicKey)
500
forwardToken := route.ForwardToken
501
expiresAt := route.ExpiresAt.UTC()
502
hasPublicMatcher := routeHostname != "" || hostnameHash != ""
@@ -611,7 +615,7 @@ func (r *leaseRegistry) DeleteHopRoute(route *types.HopRoute) *leaseRecord {
615
if r == nil || route == nil {
616
return nil
617
}
614
- ownerKey, err := utils.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
618
+ ownerKey, err := identity.AddressFromCompressedPublicKeyHex(route.OwnerPublicKey)
619
if err != nil {
620
return nil
621
}
@@ -788,13 +792,13 @@ func (r *leaseRegistry) consumeVerifiedRegisterChallenge(req types.RegisterReque
792
}
793
794
func (r *leaseRegistry) issueLeaseAccessToken(record *leaseRecord, now time.Time) (string, error) {
791
- token, _, err := auth.IssueLeaseAccessToken(r.tokenPrivateKey, r.tokenKeyID, r.tokenIssuer, record.Identity, record.ExpiresAt.Sub(now))
795
+ token, _, err := auth.IssueLeaseAccessToken(r.tokenAuthority, r.tokenIssuer, record.Identity, record.ExpiresAt.Sub(now))
796
return token, err
797
}
798
799
func (r *leaseRegistry) verifySigningAccessToken(token string) error {
800
now := time.Now().UTC()
797
- claims, err := auth.VerifyLeaseAccessToken(token, r.tokenPublicKey, r.tokenIssuer, time.Now().UTC())
801
+ claims, err := auth.VerifyLeaseAccessToken(token, r.tokenAuthority.Identity().PublicKey, r.tokenIssuer, now)
802
if err != nil {
803
return errUnauthorized
804
}
portal/lease_test.go
+15
-10
@@ -8,6 +8,7 @@ import (
8
"testing"
9
"time"
10
11
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
12
"github.com/gosuda/portal-tunnel/v2/portal/policy"
13
"github.com/gosuda/portal-tunnel/v2/portal/transport"
14
"github.com/gosuda/portal-tunnel/v2/types"
@@ -16,11 +17,15 @@ import (
17
18
func newTestRegistry(t *testing.T) *leaseRegistry {
19
t.Helper()
19
- relay, err := utils.LoadOrCreateRelayIdentity(t.TempDir(), "example.com", false)
20
+ relay, err := identity.LoadOrCreateRelayIdentity(t.TempDir(), "example.com", false)
21
if err != nil {
22
t.Fatalf("LoadOrCreateRelayIdentity() error = %v", err)
23
}
23
- registry, err := newLeaseRegistry(false, false, 10000, 10100, relay.Name, 443, relay.PrivateKey, relay.PublicKey, relay.Address, "https://example.com", false, "")
24
+ relayAuthority, err := identity.NewLocalAuthority(relay.Identity)
25
+ if err != nil {
26
+ t.Fatalf("identity.NewLocalAuthority() error = %v", err)
27
+ }
28
+ registry, err := newLeaseRegistry(false, false, 10000, 10100, relay.Name, 443, relayAuthority, "https://example.com", false, "")
29
if err != nil {
30
t.Fatalf("newLeaseRegistry() error = %v", err)
31
}
@@ -29,12 +34,12 @@ func newTestRegistry(t *testing.T) *leaseRegistry {
34
35
func newTestLeaseIdentity(t *testing.T, name string) types.Identity {
36
t.Helper()
32
- identity, err := utils.ResolveSecp256k1Identity("")
37
+ testIdentity, err := identity.ResolveSecp256k1Identity("")
38
if err != nil {
34
- t.Fatalf("ResolveSecp256k1Identity() error = %v", err)
39
+ t.Fatalf("identity.ResolveSecp256k1Identity() error = %v", err)
40
}
36
- identity.Name = name
37
- return identity
41
+ testIdentity.Name = name
42
+ return testIdentity
43
}
44
45
func TestLeaseRegistryLifecycle(t *testing.T) {
@@ -157,13 +162,13 @@ func TestLeaseRegistryHopRouteCanExposeECHAndPlainSNIFallback(t *testing.T) {
162
163
registry := newTestRegistry(t)
164
owner := newTestLeaseIdentity(t, "multi-hop-owner")
160
- wgPrivate, err := utils.GenerateWireGuardPrivateKey()
165
+ wgPrivate, err := identity.GenerateWireGuardPrivateKey()
166
if err != nil {
162
- t.Fatalf("GenerateWireGuardPrivateKey() error = %v", err)
167
+ t.Fatalf("identity.GenerateWireGuardPrivateKey() error = %v", err)
168
}
164
- wgPublic, err := utils.WireGuardPublicKeyFromPrivate(wgPrivate)
169
+ wgPublic, err := identity.WireGuardPublicKeyFromPrivate(wgPrivate)
170
if err != nil {
166
- t.Fatalf("WireGuardPublicKeyFromPrivate() error = %v", err)
171
+ t.Fatalf("identity.WireGuardPublicKeyFromPrivate() error = %v", err)
172
}
173
now := time.Now()
174
baseRoute := types.HopRoute{
portal/multihop_local_test.go
+12
-12
@@ -17,10 +17,10 @@ import (
17
"github.com/gosuda/portal-tunnel/v2/portal/acme"
18
"github.com/gosuda/portal-tunnel/v2/portal/auth"
19
"github.com/gosuda/portal-tunnel/v2/portal/discovery"
20
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
21
"github.com/gosuda/portal-tunnel/v2/portal/overlay"
22
"github.com/gosuda/portal-tunnel/v2/sdk"
23
"github.com/gosuda/portal-tunnel/v2/types"
23
- "github.com/gosuda/portal-tunnel/v2/utils"
24
)
25
26
type localRelayCluster struct {
@@ -54,10 +54,10 @@ func (o *fakeOverlay) Config() overlay.Config {
54
return o.cfg.Copy()
55
}
56
57
-func (o *fakeOverlay) Sync(states []discovery.RelayState) error {
58
- synced := make(map[string]struct{}, len(states))
59
- for _, state := range states {
60
- overlayIP, err := utils.DeriveWireGuardOverlayIPv4(state.Descriptor.WireGuardPublicKey)
57
+func (o *fakeOverlay) Sync(descriptors []types.RelayDescriptor) error {
58
+ synced := make(map[string]struct{}, len(descriptors))
59
+ for _, desc := range descriptors {
60
+ overlayIP, err := identity.DeriveWireGuardOverlayIPv4(desc.WireGuardPublicKey)
61
if err != nil {
62
return err
63
}
@@ -178,13 +178,13 @@ func startLocalRelay(t *testing.T, ctx context.Context, spec localRelaySpec) *lo
178
spec.ServerMutator(server)
179
}
180
181
- wgPrivate, err := utils.GenerateWireGuardPrivateKey()
181
+ wgPrivate, err := identity.GenerateWireGuardPrivateKey()
182
if err != nil {
183
- t.Fatalf("GenerateWireGuardPrivateKey(%s) error = %v", name, err)
183
+ t.Fatalf("identity.GenerateWireGuardPrivateKey(%s) error = %v", name, err)
184
}
185
- wgPublic, err := utils.WireGuardPublicKeyFromPrivate(wgPrivate)
185
+ wgPublic, err := identity.WireGuardPublicKeyFromPrivate(wgPrivate)
186
if err != nil {
187
- t.Fatalf("WireGuardPublicKeyFromPrivate(%s) error = %v", name, err)
187
+ t.Fatalf("identity.WireGuardPublicKeyFromPrivate(%s) error = %v", name, err)
188
}
189
fakeOverlay := &fakeOverlay{cfg: overlay.Config{
190
PublicKey: wgPublic,
@@ -198,9 +198,9 @@ func startLocalRelay(t *testing.T, ctx context.Context, spec localRelaySpec) *lo
198
}
199
server.cfg.DiscoveryEnabled = true
200
201
- overlayIP, err := utils.DeriveWireGuardOverlayIPv4(wgPublic)
201
+ overlayIP, err := identity.DeriveWireGuardOverlayIPv4(wgPublic)
202
if err != nil {
203
- t.Fatalf("DeriveWireGuardOverlayIPv4(%s) error = %v", name, err)
203
+ t.Fatalf("identity.DeriveWireGuardOverlayIPv4(%s) error = %v", name, err)
204
}
205
return &localRelay{
206
name: name,
@@ -225,7 +225,7 @@ func (c *localRelayCluster) seedDiscovery(t *testing.T) {
225
}
226
if relay.descriptorMutator != nil {
227
relay.descriptorMutator(&desc)
228
- desc, err = auth.SignRelayDescriptor(desc, relay.server.identity.PrivateKey)
228
+ desc, err = auth.SignRelayDescriptor(desc, relay.server.authority)
229
if err != nil {
230
t.Fatalf("SignRelayDescriptor(%s) error = %v", relay.name, err)
231
}
portal/overlay/overlay.go
+6
-7
@@ -18,7 +18,7 @@ import (
18
"github.com/hashicorp/yamux"
19
"github.com/rs/zerolog/log"
20
21
- "github.com/gosuda/portal-tunnel/v2/portal/discovery"
21
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
22
"github.com/gosuda/portal-tunnel/v2/types"
23
"github.com/gosuda/portal-tunnel/v2/utils"
24
)
@@ -60,11 +60,11 @@ func NormalizeConfig(cfg Config) (Config, error) {
60
return Config{}, errors.New("wireguard private key is required when relay overlay is enabled")
61
}
62
63
- privateKey, err := utils.NormalizeWireGuardPrivateKey(cfg.PrivateKey)
63
+ privateKey, err := identity.NormalizeWireGuardPrivateKey(cfg.PrivateKey)
64
if err != nil {
65
return Config{}, fmt.Errorf("normalize wireguard private key: %w", err)
66
}
67
- publicKey, err := utils.WireGuardPublicKeyFromPrivate(privateKey)
67
+ publicKey, err := identity.WireGuardPublicKeyFromPrivate(privateKey)
68
if err != nil {
69
return Config{}, fmt.Errorf("derive wireguard public key: %w", err)
70
}
@@ -468,7 +468,7 @@ func (o *Overlay) DiscoverRelay(ctx context.Context, relay types.RelayDescriptor
468
if !relay.HasOverlayPeer() {
469
return types.DiscoveryResponse{}, errors.New("relay wireguard overlay metadata is required")
470
}
471
- overlayIPv4, err := utils.DeriveWireGuardOverlayIPv4(relay.WireGuardPublicKey)
471
+ overlayIPv4, err := identity.DeriveWireGuardOverlayIPv4(relay.WireGuardPublicKey)
472
if err != nil {
473
return types.DiscoveryResponse{}, err
474
}
@@ -484,14 +484,13 @@ func (o *Overlay) DiscoverRelay(ctx context.Context, relay types.RelayDescriptor
484
return resp, nil
485
}
486
487
-func (o *Overlay) Sync(relays []discovery.RelayState) error {
487
+func (o *Overlay) Sync(relays []types.RelayDescriptor) error {
488
if o == nil || o.stack == nil {
489
return nil
490
}
491
492
peers := make([]types.RelayDescriptor, 0, len(relays))
493
- for _, relay := range relays {
494
- desc := relay.Descriptor
493
+ for _, desc := range relays {
494
if !desc.HasOverlayPeer() {
495
continue
496
}
portal/overlay/stack.go
+7
-7
@@ -17,8 +17,8 @@ import (
17
"golang.zx2c4.com/wireguard/device"
18
"golang.zx2c4.com/wireguard/tun/netstack"
19
20
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
21
"github.com/gosuda/portal-tunnel/v2/types"
21
- "github.com/gosuda/portal-tunnel/v2/utils"
22
)
23
24
const defaultEndpointResolveTTL = 3 * time.Second
@@ -36,7 +36,7 @@ type stack struct {
36
}
37
38
func newStack(cfg Config) (*stack, error) {
39
- canonicalPrivateKey, err := utils.NormalizeWireGuardPrivateKey(cfg.PrivateKey)
39
+ canonicalPrivateKey, err := identity.NormalizeWireGuardPrivateKey(cfg.PrivateKey)
40
if err != nil {
41
return nil, fmt.Errorf("normalize wireguard private key: %w", err)
42
}
@@ -46,7 +46,7 @@ func newStack(cfg Config) (*stack, error) {
46
return nil, errors.New("wireguard listen port is invalid")
47
}
48
49
- overlayIPv4, err := utils.DeriveWireGuardOverlayIPv4(cfg.PublicKey)
49
+ overlayIPv4, err := identity.DeriveWireGuardOverlayIPv4(cfg.PublicKey)
50
if err != nil {
51
return nil, fmt.Errorf("derive overlay ipv4: %w", err)
52
}
@@ -61,7 +61,7 @@ func newStack(cfg Config) (*stack, error) {
61
}
62
63
wgDevice := device.NewDevice(tunDevice, conn.NewDefaultBind(), device.NewLogger(device.LogLevelError, "portal-wg"))
64
- privateKeyHex, err := utils.WireGuardKeyHex(canonicalPrivateKey)
64
+ privateKeyHex, err := identity.WireGuardKeyHex(canonicalPrivateKey)
65
if err != nil {
66
wgDevice.Close()
67
<-wgDevice.Wait()
@@ -143,15 +143,15 @@ func (s *stack) ApplyPeers(peers []types.RelayDescriptor) error {
143
144
for _, peer := range peers {
145
peerKey := strings.TrimSpace(peer.WireGuardPublicKey)
146
- overlayIPv4, err := utils.DeriveWireGuardOverlayIPv4(peer.WireGuardPublicKey)
146
+ overlayIPv4, err := identity.DeriveWireGuardOverlayIPv4(peer.WireGuardPublicKey)
147
if err != nil {
148
continue
149
}
150
- wireGuardEndpoint, err := utils.RelayWireGuardEndpoint(peer)
150
+ wireGuardEndpoint, err := identity.RelayWireGuardEndpoint(peer)
151
if err != nil {
152
continue
153
}
154
- publicKeyHex, err := utils.WireGuardKeyHex(peer.WireGuardPublicKey)
154
+ publicKeyHex, err := identity.WireGuardKeyHex(peer.WireGuardPublicKey)
155
if err != nil {
156
return fmt.Errorf("normalize peer %q public key: %w", peerKey, err)
157
}
portal/server.go
+19
-11
@@ -21,6 +21,7 @@ import (
21
"github.com/gosuda/portal-tunnel/v2/portal/acme"
22
"github.com/gosuda/portal-tunnel/v2/portal/auth"
23
"github.com/gosuda/portal-tunnel/v2/portal/discovery"
24
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
25
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
26
"github.com/gosuda/portal-tunnel/v2/portal/overlay"
27
"github.com/gosuda/portal-tunnel/v2/portal/policy"
@@ -60,7 +61,7 @@ type ServerConfig struct {
61
62
func normalizeServerConfig(cfg ServerConfig) (ServerConfig, error) {
63
cfg.PortalURL = strings.TrimSuffix(strings.TrimSpace(cfg.PortalURL), "/")
63
- cfg.IdentityPath = utils.ResolveRelayStateDir(cfg.IdentityPath)
64
+ cfg.IdentityPath = identity.ResolveRelayStateDir(cfg.IdentityPath)
65
if cfg.IdentityPath == "" {
66
return ServerConfig{}, errors.New("identity path is required")
67
}
@@ -114,6 +115,7 @@ type Server struct {
115
116
cfg ServerConfig
117
identity types.RelayIdentity
118
+ authority identity.Authority
119
acmeManager *acme.Manager
120
proxy proxy
121
@@ -134,7 +136,7 @@ type Server struct {
136
137
type serverTestHooks struct {
138
overlayConfig func() overlay.Config
137
- syncOverlayPeers func([]discovery.RelayState) error
139
+ syncOverlayPeers func([]types.RelayDescriptor) error
140
openHopStream func(context.Context, string, string) (net.Conn, error)
141
}
142
@@ -144,11 +146,15 @@ func NewServer(cfg ServerConfig) (*Server, error) {
146
return nil, err
147
}
148
147
- identity, err := utils.LoadOrCreateRelayIdentity(cfg.IdentityPath, utils.PortalRootHost(cfg.PortalURL), cfg.DiscoveryEnabled)
149
+ relayIdentity, err := identity.LoadOrCreateRelayIdentity(cfg.IdentityPath, utils.PortalRootHost(cfg.PortalURL), cfg.DiscoveryEnabled)
150
if err != nil {
151
return nil, fmt.Errorf("load relay identity: %w", err)
152
}
151
- registry, err := newLeaseRegistry(cfg.UDPEnabled, cfg.TCPEnabled, cfg.MinPort, cfg.MaxPort, identity.Name, cfg.SNIPort, identity.PrivateKey, identity.PublicKey, identity.Address, cfg.PortalURL, cfg.TrustProxyHeaders, cfg.TrustedProxyCIDRs)
153
+ relayAuthority, err := identity.NewLocalAuthority(relayIdentity.Identity)
154
+ if err != nil {
155
+ return nil, fmt.Errorf("load relay authority: %w", err)
156
+ }
157
+ registry, err := newLeaseRegistry(cfg.UDPEnabled, cfg.TCPEnabled, cfg.MinPort, cfg.MaxPort, relayIdentity.Name, cfg.SNIPort, relayAuthority, cfg.PortalURL, cfg.TrustProxyHeaders, cfg.TrustedProxyCIDRs)
158
if err != nil {
159
return nil, err
160
}
@@ -164,7 +170,8 @@ func NewServer(cfg ServerConfig) (*Server, error) {
170
171
server := &Server{
172
cfg: cfg,
167
- identity: identity,
173
+ identity: relayIdentity,
174
+ authority: relayAuthority,
175
registry: registry,
176
relaySet: relaySet,
177
announceLimiter: discovery.NewAnnounceLimiter(0, 0),
@@ -444,7 +451,8 @@ func (s *Server) prepareAPITLS(ctx context.Context) (keyless.TLSMaterialConfig,
451
CertPEM: certPEM,
452
KeyPEM: keyPEM,
453
}
447
- echSeed, err := s.identity.DeriveToken(
454
+ echSeed, err := identity.DeriveToken(
455
+ s.identity.Identity,
456
"relay-ech",
457
s.identity.EncryptedClientHelloSeed,
458
s.identity.Name,
@@ -616,15 +624,15 @@ func (s *Server) currentOverlayConfig() (overlay.Config, bool) {
624
return overlay.Config{}, false
625
}
626
619
-func (s *Server) syncOverlayPeers(states []discovery.RelayState) error {
627
+func (s *Server) syncOverlayPeers(descriptors []types.RelayDescriptor) error {
628
if s == nil {
629
return errors.New("server is unavailable")
630
}
631
if s.overlay != nil {
624
- return s.overlay.Sync(states)
632
+ return s.overlay.Sync(descriptors)
633
}
634
if s.testHooks != nil && s.testHooks.syncOverlayPeers != nil {
627
- return s.testHooks.syncOverlayPeers(states)
635
+ return s.testHooks.syncOverlayPeers(descriptors)
636
}
637
return errors.New("relay overlay is unavailable")
638
}
@@ -767,7 +775,7 @@ func (s *Server) startOverlay() (*overlay.Overlay, error) {
775
}
776
})
777
770
- if err := ov.Sync(s.relaySet.OverlayPeerStates()); err != nil {
778
+ if err := ov.Sync(s.relaySet.OverlayPeerDescriptor()); err != nil {
779
_ = ov.Shutdown(context.Background())
780
return nil, fmt.Errorf("sync wireguard peers: %w", err)
781
}
@@ -837,5 +845,5 @@ func (s *Server) newSelfDescriptor(now time.Time) (types.RelayDescriptor, error)
845
SupportsTCP: s.cfg.TCPEnabled,
846
ActiveConnections: s.proxy.activeConnectionCount(),
847
TCPBPS: s.proxy.currentTCPBPS(now),
840
- }, s.identity.PrivateKey)
848
+ }, s.authority)
849
}
sdk/api_client.go
+20
-6
@@ -15,6 +15,7 @@ import (
15
"time"
16
17
"github.com/gosuda/portal-tunnel/v2/portal/auth"
18
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
19
"github.com/gosuda/portal-tunnel/v2/types"
20
"github.com/gosuda/portal-tunnel/v2/utils"
21
)
@@ -90,7 +91,8 @@ func (l *listener) buildHopRoutes(hopPath []types.RelayDescriptor, publicHostnam
91
hopRoutes := make([]types.HopRoute, 0, len(hopPath)-1)
92
var previousHopToken string
93
for i := 0; i < len(hopPath)-1; i++ {
93
- token, err := l.identity.DeriveToken(
94
+ token, err := identity.DeriveToken(
95
+ l.identity,
96
"hop-token",
97
publicHostname,
98
strconv.Itoa(i),
@@ -163,7 +165,7 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
165
return types.RegisterResponse{}, nil, "", "", err
166
}
167
if streamLease {
166
- routeToken, err := l.identity.DeriveToken("ech-route", publicHostname, rootHostname)
168
+ routeToken, err := identity.DeriveToken(l.identity, "ech-route", publicHostname, rootHostname)
169
if err != nil {
170
return types.RegisterResponse{}, nil, "", "", err
171
}
@@ -209,7 +211,11 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
211
return types.RegisterResponse{}, nil, "", "", err
212
}
213
212
- signature, err := utils.SignEthereumPersonalMessage(challenge.SIWEMessage, l.identity.PrivateKey)
214
+ authority, err := identity.NewLocalAuthority(l.identity)
215
+ if err != nil {
216
+ return types.RegisterResponse{}, nil, "", "", err
217
+ }
218
+ signature, err := authority.SignEthereumPersonalMessage(challenge.SIWEMessage)
219
if err != nil {
220
return types.RegisterResponse{}, nil, "", "", err
221
}
@@ -223,7 +229,7 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
229
}, nil, &resp); err != nil {
230
return types.RegisterResponse{}, nil, "", "", err
231
}
226
- registeredIdentity, err := utils.NormalizeIdentity(resp.Identity)
232
+ registeredIdentity, err := identity.NormalizeIdentity(resp.Identity)
233
if err != nil {
234
_ = l.unregisterLease(context.Background(), resp.AccessToken, hopRoutes)
235
return types.RegisterResponse{}, nil, "", "", err
@@ -265,6 +271,10 @@ func (l *listener) registerHopRoutes(ctx context.Context, expiresAt time.Time, r
271
if l.relaySet == nil {
272
return "", 0, errors.New("multi-hop relay set is unavailable")
273
}
274
+ authority, err := identity.NewLocalAuthority(l.identity)
275
+ if err != nil {
276
+ return "", 0, err
277
+ }
278
279
now := time.Now().UTC()
280
for i := len(routes) - 1; i >= 0; i-- {
@@ -275,7 +285,7 @@ func (l *listener) registerHopRoutes(ctx context.Context, expiresAt time.Time, r
285
}
286
route.ForwardRelay = desc
287
route.FirstSeenAt = expiresAt.Add(-30 * time.Second)
278
- route, err := auth.SignHopRoute(http.MethodPost, route, l.identity, expiresAt)
288
+ route, err := auth.SignHopRoute(http.MethodPost, route, authority, expiresAt)
289
if err != nil {
290
return "", 0, err
291
}
@@ -312,8 +322,12 @@ func (l *listener) registerHopRoutes(ctx context.Context, expiresAt time.Time, r
322
323
func (l *listener) unregisterHopRoutes(ctx context.Context, routes []types.HopRoute) error {
324
var unregisterErr error
325
+ authority, err := identity.NewLocalAuthority(l.identity)
326
+ if err != nil {
327
+ return err
328
+ }
329
for _, route := range routes {
316
- route, err := auth.SignHopRoute(http.MethodDelete, route, l.identity, time.Time{})
330
+ route, err := auth.SignHopRoute(http.MethodDelete, route, authority, time.Time{})
331
if err != nil {
332
unregisterErr = errors.Join(unregisterErr, err)
333
continue
sdk/expose.go
+9
-7
@@ -15,6 +15,7 @@ import (
15
"github.com/rs/zerolog/log"
16
17
"github.com/gosuda/portal-tunnel/v2/portal/discovery"
18
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
19
"github.com/gosuda/portal-tunnel/v2/portal/telemetry"
20
"github.com/gosuda/portal-tunnel/v2/types"
21
"github.com/gosuda/portal-tunnel/v2/utils"
@@ -120,7 +121,7 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
121
listenerRelayURLs = append([]string(nil), explicitRelayURLs...)
122
}
123
123
- identity, createdIdentity, err := utils.ResolveListenerIdentity(
124
+ listenerIdentity, createdIdentity, err := identity.ResolveListenerIdentity(
125
types.Identity{Name: cfg.Name},
126
cfg.TargetAddr,
127
cfg.IdentityPath,
@@ -132,7 +133,7 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
133
if createdIdentity {
134
log.Info().
135
Str("identity_path", strings.TrimSpace(cfg.IdentityPath)).
135
- Str("address", identity.Address).
136
+ Str("address", listenerIdentity.Address).
137
Msg("generated tunnel identity and saved it to disk")
138
}
139
targetAddr, err := utils.NormalizeLoopbackTarget(cfg.TargetAddr)
@@ -150,7 +151,7 @@ func Expose(ctx context.Context, cfg ExposeConfig) (*Exposure, error) {
151
exposure := &Exposure{
152
cancel: cancel,
153
done: exposureCtx.Done(),
153
- identity: identity,
154
+ identity: listenerIdentity,
155
explicitRelays: explicitRelayURLs,
156
TargetAddr: targetAddr,
157
UDPAddr: udpAddr,
@@ -364,7 +365,8 @@ func (e *Exposure) Snapshot() types.AgentTunnelStatus {
365
}
366
if e.relaySet != nil {
367
for _, state := range e.relaySet.AllRelays() {
367
- relayURL := strings.TrimSpace(state.Descriptor.APIHTTPSAddr)
368
+ relay := state.Descriptor
369
+ relayURL := strings.TrimSpace(relay.APIHTTPSAddr)
370
if relayURL == "" {
371
continue
372
}
@@ -373,9 +375,9 @@ func (e *Exposure) Snapshot() types.AgentTunnelStatus {
375
snap.Explicit = slices.Contains(explicitRelays, relayURL)
376
snap.Bootstrap = state.Bootstrap
377
snap.Banned = state.Banned
376
- snap.SupportsOverlay = state.Descriptor.SupportsOverlay
377
- snap.SupportsUDP = state.Descriptor.SupportsUDP
378
- snap.SupportsTCP = state.Descriptor.SupportsTCP
378
+ snap.SupportsOverlay = relay.SupportsOverlay
379
+ snap.SupportsUDP = relay.SupportsUDP
380
+ snap.SupportsTCP = relay.SupportsTCP
381
relayByURL[relayURL] = snap
382
}
383
}
sdk/listener.go
+2
-1
@@ -20,6 +20,7 @@ import (
20
"github.com/rs/zerolog/log"
21
22
"github.com/gosuda/portal-tunnel/v2/portal/discovery"
23
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
24
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
25
"github.com/gosuda/portal-tunnel/v2/portal/transport"
26
"github.com/gosuda/portal-tunnel/v2/types"
@@ -850,7 +851,7 @@ func (l *listener) tenantECHMaterials(publicHostname, routeHostname string) ([]t
851
if routeHostname == "" {
852
return nil, nil, nil
853
}
853
- echSeed, err := l.identity.DeriveToken("tenant-ech", publicHostname, routeHostname)
854
+ echSeed, err := identity.DeriveToken(l.identity, "tenant-ech", publicHostname, routeHostname)
855
if err != nil {
856
return nil, nil, fmt.Errorf("derive tenant ech seed: %w", err)
857
}
sdk/mitm_test.go
+4
-6
@@ -18,6 +18,7 @@ import (
18
"testing"
19
"time"
20
21
+ "github.com/gosuda/portal-tunnel/v2/portal/discovery"
22
"github.com/gosuda/portal-tunnel/v2/types"
23
)
24
@@ -223,8 +224,8 @@ func TestMITMProbeDetectionBansListener(t *testing.T) {
224
Reason: types.MITMProbeReasonExporterMismatch,
225
}, nil)
226
226
- for _, state := range listener.relaySet.AggregateRelays() {
227
- if state.Descriptor.APIHTTPSAddr == relayURL.String() {
227
+ for _, activeRelayURL := range listener.relaySet.PriorityRelays(discovery.ClientState{}) {
228
+ if activeRelayURL == relayURL.String() {
229
t.Fatal("relay still active after mitm detection")
230
}
231
}
@@ -255,10 +256,7 @@ func TestMITMProbeDetectionWarnsWithoutBanningListener(t *testing.T) {
256
Reason: types.MITMProbeReasonExporterMismatch,
257
}, nil)
258
258
- activeRelayURLs := make([]string, 0)
259
- for _, state := range listener.relaySet.AggregateRelays() {
260
- activeRelayURLs = append(activeRelayURLs, state.Descriptor.APIHTTPSAddr)
261
- }
259
+ activeRelayURLs := listener.relaySet.PriorityRelays(discovery.ClientState{})
260
if len(activeRelayURLs) != 1 || activeRelayURLs[0] != relayURL.String() {
261
t.Fatalf("ActiveRelayURLs() = %v, want [%q]", activeRelayURLs, relayURL.String())
262
}
types/identity.go
+10
-38
@@ -1,12 +1,7 @@
1
package types
2
3
import (
4
- "crypto/hmac"
5
- "crypto/sha256"
6
- "encoding/base64"
4
"encoding/json"
8
- "errors"
9
- "strconv"
5
"strings"
6
"time"
7
)
@@ -18,18 +13,20 @@ const (
13
)
14
15
type Identity struct {
21
- Name string `json:"name,omitempty"`
22
- Address string `json:"address,omitempty"`
23
- PublicKey string `json:"-"`
24
- PrivateKey string `json:"-"`
16
+ Name string `json:"name,omitempty"`
17
+ Address string `json:"address,omitempty"`
18
+ PublicKey string `json:"-"`
19
+ PrivateKey string `json:"-"`
20
+ TokenSecret string `json:"-"`
21
}
22
23
func (i Identity) Copy() Identity {
24
return Identity{
29
- Name: i.Name,
30
- Address: i.Address,
31
- PublicKey: i.PublicKey,
32
- PrivateKey: i.PrivateKey,
25
+ Name: i.Name,
26
+ Address: i.Address,
27
+ PublicKey: i.PublicKey,
28
+ PrivateKey: i.PrivateKey,
29
+ TokenSecret: i.TokenSecret,
30
}
31
}
32
@@ -51,10 +48,6 @@ func (i RelayIdentity) Copy() RelayIdentity {
48
}
49
}
50
54
-func (i RelayIdentity) Base() Identity {
55
- return i.Identity.Copy()
56
-}
57
-
51
func (i Identity) Key() string {
52
name := strings.TrimSpace(strings.ToLower(i.Name))
53
address := strings.TrimSpace(strings.ToLower(i.Address))
@@ -64,27 +57,6 @@ func (i Identity) Key() string {
57
return name + IdentityKeySeparator + address
58
}
59
67
-// DeriveToken derives a deterministic identity-scoped token from ordered
68
-// length-prefixed token parts. The first part should identify the token family.
69
-func (i Identity) DeriveToken(parts ...string) (string, error) {
70
- privateKey := strings.TrimSpace(i.PrivateKey)
71
- if privateKey == "" {
72
- return "", errors.New("identity private key is required")
73
- }
74
-
75
- mac := hmac.New(sha256.New, []byte(privateKey))
76
- _, _ = mac.Write([]byte("Portal identity token v1\n"))
77
- _, _ = mac.Write([]byte(i.Key()))
78
- for _, part := range parts {
79
- part = strings.TrimSpace(part)
80
- _, _ = mac.Write([]byte("\n"))
81
- _, _ = mac.Write([]byte(strconv.Itoa(len(part))))
82
- _, _ = mac.Write([]byte(":"))
83
- _, _ = mac.Write([]byte(part))
84
- }
85
- return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)), nil
86
-}
87
-
60
type LeaseMetadata struct {
61
Description string `json:"description,omitempty"`
62
Owner string `json:"owner,omitempty"`
utils/name.go
new
+128
@@ -0,0 +1,128 @@
1
+package utils
2
+
3
+import (
4
+ "net"
5
+ "net/url"
6
+ "strings"
7
+)
8
+
9
+var exposeNameOpeners = []string{
10
+ "arcade", "bouncy", "bravo", "bubble", "candy", "cosmic", "dapper", "electric",
11
+ "fancy", "fizzy", "flashy", "fuzzy", "gentle", "glitter", "golden", "happy",
12
+ "hyper", "jazzy", "jolly", "lively", "lucky", "magic", "mellow", "minty",
13
+ "misty", "moonlit", "mystic", "neon", "nova", "peppy", "pixel", "playful",
14
+ "poppy", "rapid", "rocket", "rowdy", "snappy", "snazzy", "sparkly", "spicy",
15
+ "sprightly", "starry", "sunny", "swift", "tangy", "tidy", "toasty", "turbo",
16
+ "velvet", "vivid", "wavy", "whimsy", "wild", "wonky", "zany", "zesty",
17
+}
18
+
19
+var exposeNameCenters = []string{
20
+ "alpaca", "badger", "banjo", "beacon", "biscuit", "capybara", "comet", "cricket",
21
+ "dragon", "falcon", "feather", "fjord", "fox", "gadget", "gecko", "gizmo",
22
+ "harbor", "heron", "iguana", "jelly", "koala", "lemur", "mango", "narwhal",
23
+ "nebula", "noodle", "octopus", "otter", "panda", "pepper", "phoenix", "pickle",
24
+ "puffin", "quokka", "radar", "ranger", "rocket", "scooter", "seahorse", "skylark",
25
+ "sprocket", "starling", "sunbeam", "taco", "thimble", "tiger", "toucan", "triton",
26
+ "walrus", "widget", "willow", "wombat", "yeti", "zeppelin", "zigzag", "zinnia",
27
+}
28
+
29
+var exposeNameClosers = []string{
30
+ "arcade", "beacon", "boogie", "bounce", "burst", "cascade", "chorus", "dash",
31
+ "disco", "drift", "echo", "fiesta", "flare", "flash", "flight", "flip",
32
+ "glow", "groove", "jam", "jive", "launch", "loop", "march", "orbit",
33
+ "parade", "party", "pulse", "quest", "rally", "riot", "ripple", "rodeo",
34
+ "roll", "rush", "serenade", "shuffle", "signal", "sketch", "spark", "sprint",
35
+ "starlight", "stride", "sway", "swoop", "twirl", "uplift", "vibe", "voyage",
36
+ "whirl", "wink", "zap", "zenith", "zip", "zoom", "zest", "zone",
37
+}
38
+
39
+const (
40
+ defaultExposeTargetPort = "3000"
41
+ defaultExposeTargetHost = "127.0.0.1"
42
+)
43
+
44
+// DefaultExposeName generates a deterministic 3-word DNS label from a target
45
+// address and seed using FNV-1a hashing. The algorithm matches the frontend
46
+// implementation in frontend/src/lib/exposeName.ts:buildDefaultExposeName.
47
+func DefaultExposeName(target, rawSeed string) (string, error) {
48
+ seed := strings.TrimSpace(rawSeed)
49
+ if cut, ok := strings.CutPrefix(seed, "cli_"); ok {
50
+ seed = cut
51
+ }
52
+ if seed == "" {
53
+ seed = "portal"
54
+ }
55
+
56
+ input := []byte(seed + "|" + normalizeExposeTarget(target))
57
+ first := fnv1a32(input, 0x811c9dc5)
58
+ second := fnv1a32(input, 0x9e3779b9)
59
+ third := fnv1a32(input, 0x85ebca6b)
60
+
61
+ label := strings.Join([]string{
62
+ exposeNameOpeners[int(first&0xff)%len(exposeNameOpeners)],
63
+ exposeNameCenters[int(second&0xff)%len(exposeNameCenters)],
64
+ exposeNameClosers[int(third&0xff)%len(exposeNameClosers)],
65
+ }, "-")
66
+
67
+ return NormalizeDNSLabel(label)
68
+}
69
+
70
+// normalizeExposeTarget normalizes a target address for deterministic name
71
+// generation. Must match frontend/src/lib/exposeName.ts:normalizeExposeTarget.
72
+func normalizeExposeTarget(raw string) string {
73
+ trimmed := strings.TrimSpace(raw)
74
+ candidate := trimmed
75
+ if candidate == "" {
76
+ candidate = defaultExposeTargetPort
77
+ }
78
+
79
+ if isAllDigits(candidate) {
80
+ return defaultExposeTargetHost + ":" + candidate
81
+ }
82
+
83
+ if strings.Contains(candidate, "://") {
84
+ u, err := url.Parse(candidate)
85
+ if err != nil {
86
+ return candidate
87
+ }
88
+ if (u.Scheme == "http" || u.Scheme == "https") &&
89
+ u.Host != "" &&
90
+ (u.Path == "" || u.Path == "/") &&
91
+ u.RawQuery == "" &&
92
+ u.Fragment == "" {
93
+ return u.Host
94
+ }
95
+ return candidate
96
+ }
97
+
98
+ u, err := url.Parse("tcp://" + candidate)
99
+ if err != nil || u.Hostname() == "" {
100
+ return candidate
101
+ }
102
+ port := u.Port()
103
+ if port == "" {
104
+ port = "80"
105
+ }
106
+ return net.JoinHostPort(u.Hostname(), port)
107
+}
108
+
109
+func isAllDigits(s string) bool {
110
+ if s == "" {
111
+ return false
112
+ }
113
+ for _, c := range s {
114
+ if c < '0' || c > '9' {
115
+ return false
116
+ }
117
+ }
118
+ return true
119
+}
120
+
121
+func fnv1a32(data []byte, seed uint32) uint32 {
122
+ h := seed
123
+ for _, b := range data {
124
+ h ^= uint32(b)
125
+ h *= 0x01000193
126
+ }
127
+ return h
128
+}