mirror of
https://github.com/aljazceru/lspd.git
synced 2025-12-18 22:34:22 +01:00
Update lnd (and corresponding btcd)
This commit is contained in:
211
btceclegacy/ciphering.go
Normal file
211
btceclegacy/ciphering.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
// Copyright (c) 2015-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package btceclegacy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidMAC occurs when Message Authentication Check (MAC) fails
|
||||||
|
// during decryption. This happens because of either invalid private key or
|
||||||
|
// corrupt ciphertext.
|
||||||
|
ErrInvalidMAC = errors.New("invalid mac hash")
|
||||||
|
|
||||||
|
// errInputTooShort occurs when the input ciphertext to the Decrypt
|
||||||
|
// function is less than 134 bytes long.
|
||||||
|
errInputTooShort = errors.New("ciphertext too short")
|
||||||
|
|
||||||
|
// errUnsupportedCurve occurs when the first two bytes of the encrypted
|
||||||
|
// text aren't 0x02CA (= 712 = secp256k1, from OpenSSL).
|
||||||
|
errUnsupportedCurve = errors.New("unsupported curve")
|
||||||
|
|
||||||
|
errInvalidXLength = errors.New("invalid X length, must be 32")
|
||||||
|
errInvalidYLength = errors.New("invalid Y length, must be 32")
|
||||||
|
errInvalidPadding = errors.New("invalid PKCS#7 padding")
|
||||||
|
|
||||||
|
// 0x02CA = 714
|
||||||
|
ciphCurveBytes = [2]byte{0x02, 0xCA}
|
||||||
|
// 0x20 = 32
|
||||||
|
ciphCoordLength = [2]byte{0x00, 0x20}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt encrypts data for the target public key using AES-256-CBC. It also
|
||||||
|
// generates a private key (the pubkey of which is also in the output). The only
|
||||||
|
// supported curve is secp256k1. The `structure' that it encodes everything into
|
||||||
|
// is:
|
||||||
|
//
|
||||||
|
// struct {
|
||||||
|
// // Initialization Vector used for AES-256-CBC
|
||||||
|
// IV [16]byte
|
||||||
|
// // Public Key: curve(2) + len_of_pubkeyX(2) + pubkeyX +
|
||||||
|
// // len_of_pubkeyY(2) + pubkeyY (curve = 714)
|
||||||
|
// PublicKey [70]byte
|
||||||
|
// // Cipher text
|
||||||
|
// Data []byte
|
||||||
|
// // HMAC-SHA-256 Message Authentication Code
|
||||||
|
// HMAC [32]byte
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The primary aim is to ensure byte compatibility with Pyelliptic. Also, refer
|
||||||
|
// to section 5.8.1 of ANSI X9.63 for rationale on this format.
|
||||||
|
func Encrypt(pubkey *btcec.PublicKey, in []byte) ([]byte, error) {
|
||||||
|
ephemeral, err := btcec.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ecdhKey := secp256k1.GenerateSharedSecret(ephemeral, pubkey)
|
||||||
|
derivedKey := sha512.Sum512(ecdhKey)
|
||||||
|
keyE := derivedKey[:32]
|
||||||
|
keyM := derivedKey[32:]
|
||||||
|
|
||||||
|
paddedIn := addPKCSPadding(in)
|
||||||
|
// IV + Curve params/X/Y + padded plaintext/ciphertext + HMAC-256
|
||||||
|
out := make([]byte, aes.BlockSize+70+len(paddedIn)+sha256.Size)
|
||||||
|
iv := out[:aes.BlockSize]
|
||||||
|
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// start writing public key
|
||||||
|
pb := ephemeral.PubKey().SerializeUncompressed()
|
||||||
|
offset := aes.BlockSize
|
||||||
|
|
||||||
|
// curve and X length
|
||||||
|
copy(out[offset:offset+4], append(ciphCurveBytes[:], ciphCoordLength[:]...))
|
||||||
|
offset += 4
|
||||||
|
// X
|
||||||
|
copy(out[offset:offset+32], pb[1:33])
|
||||||
|
offset += 32
|
||||||
|
// Y length
|
||||||
|
copy(out[offset:offset+2], ciphCoordLength[:])
|
||||||
|
offset += 2
|
||||||
|
// Y
|
||||||
|
copy(out[offset:offset+32], pb[33:])
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
// start encryption
|
||||||
|
block, err := aes.NewCipher(keyE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
mode.CryptBlocks(out[offset:len(out)-sha256.Size], paddedIn)
|
||||||
|
|
||||||
|
// start HMAC-SHA-256
|
||||||
|
hm := hmac.New(sha256.New, keyM)
|
||||||
|
hm.Write(out[:len(out)-sha256.Size]) // everything is hashed
|
||||||
|
copy(out[len(out)-sha256.Size:], hm.Sum(nil)) // write checksum
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data that was encrypted using the Encrypt function.
|
||||||
|
func Decrypt(priv *btcec.PrivateKey, in []byte) ([]byte, error) {
|
||||||
|
// IV + Curve params/X/Y + 1 block + HMAC-256
|
||||||
|
if len(in) < aes.BlockSize+70+aes.BlockSize+sha256.Size {
|
||||||
|
return nil, errInputTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
// read iv
|
||||||
|
iv := in[:aes.BlockSize]
|
||||||
|
offset := aes.BlockSize
|
||||||
|
|
||||||
|
// start reading pubkey
|
||||||
|
if !bytes.Equal(in[offset:offset+2], ciphCurveBytes[:]) {
|
||||||
|
return nil, errUnsupportedCurve
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
if !bytes.Equal(in[offset:offset+2], ciphCoordLength[:]) {
|
||||||
|
return nil, errInvalidXLength
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
xBytes := in[offset : offset+32]
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
if !bytes.Equal(in[offset:offset+2], ciphCoordLength[:]) {
|
||||||
|
return nil, errInvalidYLength
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
yBytes := in[offset : offset+32]
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
pb := make([]byte, 65)
|
||||||
|
pb[0] = byte(0x04) // uncompressed
|
||||||
|
copy(pb[1:33], xBytes)
|
||||||
|
copy(pb[33:], yBytes)
|
||||||
|
// check if (X, Y) lies on the curve and create a Pubkey if it does
|
||||||
|
pubkey, err := btcec.ParsePubKey(pb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for cipher text length
|
||||||
|
if (len(in)-aes.BlockSize-offset-sha256.Size)%aes.BlockSize != 0 {
|
||||||
|
return nil, errInvalidPadding // not padded to 16 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// read hmac
|
||||||
|
messageMAC := in[len(in)-sha256.Size:]
|
||||||
|
|
||||||
|
// generate shared secret
|
||||||
|
ecdhKey := secp256k1.GenerateSharedSecret(priv, pubkey)
|
||||||
|
derivedKey := sha512.Sum512(ecdhKey)
|
||||||
|
keyE := derivedKey[:32]
|
||||||
|
keyM := derivedKey[32:]
|
||||||
|
|
||||||
|
// verify mac
|
||||||
|
hm := hmac.New(sha256.New, keyM)
|
||||||
|
hm.Write(in[:len(in)-sha256.Size]) // everything is hashed
|
||||||
|
expectedMAC := hm.Sum(nil)
|
||||||
|
if !hmac.Equal(messageMAC, expectedMAC) {
|
||||||
|
return nil, ErrInvalidMAC
|
||||||
|
}
|
||||||
|
|
||||||
|
// start decryption
|
||||||
|
block, err := aes.NewCipher(keyE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
// same length as ciphertext
|
||||||
|
plaintext := make([]byte, len(in)-offset-sha256.Size)
|
||||||
|
mode.CryptBlocks(plaintext, in[offset:len(in)-sha256.Size])
|
||||||
|
|
||||||
|
return removePKCSPadding(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement PKCS#7 padding with block size of 16 (AES block size).
|
||||||
|
|
||||||
|
// addPKCSPadding adds padding to a block of data
|
||||||
|
func addPKCSPadding(src []byte) []byte {
|
||||||
|
padding := aes.BlockSize - len(src)%aes.BlockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(src, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removePKCSPadding removes padding from data that was added with addPKCSPadding
|
||||||
|
func removePKCSPadding(src []byte) ([]byte, error) {
|
||||||
|
length := len(src)
|
||||||
|
padLength := int(src[length-1])
|
||||||
|
if padLength > aes.BlockSize || length < aes.BlockSize {
|
||||||
|
return nil, errInvalidPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
return src[:length-padLength], nil
|
||||||
|
}
|
||||||
26
go.mod
26
go.mod
@@ -4,19 +4,19 @@ go 1.14
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/aws/aws-sdk-go v1.30.20
|
github.com/aws/aws-sdk-go v1.30.20
|
||||||
github.com/btcsuite/btcd v0.20.1-beta.0.20200730232343-1db1b6f8217f
|
github.com/btcsuite/btcd v0.23.1
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.2.0
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
|
||||||
github.com/caddyserver/certmagic v0.11.2
|
github.com/caddyserver/certmagic v0.11.2
|
||||||
github.com/coreos/etcd v3.3.25+incompatible // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
|
||||||
github.com/coreos/go-semver v0.3.0 // indirect
|
github.com/golang/protobuf v1.5.2
|
||||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
github.com/golang/protobuf v1.4.2
|
github.com/jackc/pgtype v1.8.1
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0
|
github.com/jackc/pgx/v4 v4.13.0
|
||||||
github.com/jackc/pgtype v1.4.2
|
github.com/lightningnetwork/lightning-onion v1.0.2-0.20220211021909-bb84a1ccb0c5
|
||||||
github.com/jackc/pgx/v4 v4.8.1
|
github.com/lightningnetwork/lnd v0.15.0-beta
|
||||||
github.com/lightningnetwork/lightning-onion v1.0.2-0.20200501022730-3c8c8d0b89ea
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||||
github.com/lightningnetwork/lnd v0.11.0-beta
|
google.golang.org/grpc v1.38.0
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
|
||||||
google.golang.org/grpc v1.29.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/lightningnetwork/lnd v0.11.0-beta => github.com/breez/lnd v0.11.0-beta.rc4.0.20210125150416-0c10146b223c
|
replace github.com/lightningnetwork/lnd v0.15.0-beta => github.com/breez/lnd v0.15.0-beta.rc6.0.20220715110145-7f7cfa410adc
|
||||||
|
|||||||
10
intercept.go
10
intercept.go
@@ -10,7 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
@@ -159,9 +159,9 @@ func intercept() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else { //probing
|
} else { //probing
|
||||||
failureCode := routerrpc.ForwardHtlcInterceptResponse_TEMPORARY_CHANNEL_FAILURE
|
failureCode := lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE
|
||||||
if err := isConnected(clientCtx, client, destination); err == nil {
|
if err := isConnected(clientCtx, client, destination); err == nil {
|
||||||
failureCode = routerrpc.ForwardHtlcInterceptResponse_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS
|
failureCode = lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS
|
||||||
}
|
}
|
||||||
interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
IncomingCircuitKey: request.IncomingCircuitKey,
|
IncomingCircuitKey: request.IncomingCircuitKey,
|
||||||
@@ -172,14 +172,14 @@ func intercept() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pubKey, err := btcec.ParsePubKey(destination, btcec.S256())
|
pubKey, err := btcec.ParsePubKey(destination)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("btcec.ParsePubKey(%x): %v", destination, err)
|
log.Printf("btcec.ParsePubKey(%x): %v", destination, err)
|
||||||
failForwardSend(interceptorClient, request.IncomingCircuitKey)
|
failForwardSend(interceptorClient, request.IncomingCircuitKey)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionKey, err := btcec.NewPrivateKey(btcec.S256())
|
sessionKey, err := btcec.NewPrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("btcec.NewPrivateKey(): %v", err)
|
log.Printf("btcec.NewPrivateKey(): %v", err)
|
||||||
failForwardSend(interceptorClient, request.IncomingCircuitKey)
|
failForwardSend(interceptorClient, request.IncomingCircuitKey)
|
||||||
|
|||||||
17
server.go
17
server.go
@@ -12,10 +12,11 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/btceclegacy"
|
||||||
lspdrpc "github.com/breez/lspd/rpc"
|
lspdrpc "github.com/breez/lspd/rpc"
|
||||||
"github.com/golang/protobuf/proto"
|
"github.com/golang/protobuf/proto"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/caddyserver/certmagic"
|
"github.com/caddyserver/certmagic"
|
||||||
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||||
@@ -76,7 +77,7 @@ func (s *server) ChannelInformation(ctx context.Context, in *lspdrpc.ChannelInfo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *server) RegisterPayment(ctx context.Context, in *lspdrpc.RegisterPaymentRequest) (*lspdrpc.RegisterPaymentReply, error) {
|
func (s *server) RegisterPayment(ctx context.Context, in *lspdrpc.RegisterPaymentRequest) (*lspdrpc.RegisterPaymentReply, error) {
|
||||||
data, err := btcec.Decrypt(privateKey, in.Blob)
|
data, err := btceclegacy.Decrypt(privateKey, in.Blob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("btcec.Decrypt(%x) error: %v", in.Blob, err)
|
log.Printf("btcec.Decrypt(%x) error: %v", in.Blob, err)
|
||||||
return nil, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Blob, err)
|
return nil, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Blob, err)
|
||||||
@@ -161,7 +162,7 @@ func (s *server) OpenChannel(ctx context.Context, in *lspdrpc.OpenChannelRequest
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getSignedEncryptedData(in *lspdrpc.Encrypted) (string, []byte, error) {
|
func getSignedEncryptedData(in *lspdrpc.Encrypted) (string, []byte, error) {
|
||||||
signedBlob, err := btcec.Decrypt(privateKey, in.Data)
|
signedBlob, err := btceclegacy.Decrypt(privateKey, in.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("btcec.Decrypt(%x) error: %v", in.Data, err)
|
log.Printf("btcec.Decrypt(%x) error: %v", in.Data, err)
|
||||||
return "", nil, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Data, err)
|
return "", nil, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Data, err)
|
||||||
@@ -172,7 +173,7 @@ func getSignedEncryptedData(in *lspdrpc.Encrypted) (string, []byte, error) {
|
|||||||
log.Printf("proto.Unmarshal(%x) error: %v", signedBlob, err)
|
log.Printf("proto.Unmarshal(%x) error: %v", signedBlob, err)
|
||||||
return "", nil, fmt.Errorf("proto.Unmarshal(%x) error: %w", signedBlob, err)
|
return "", nil, fmt.Errorf("proto.Unmarshal(%x) error: %w", signedBlob, err)
|
||||||
}
|
}
|
||||||
pubkey, err := btcec.ParsePubKey(signed.Pubkey, btcec.S256())
|
pubkey, err := btcec.ParsePubKey(signed.Pubkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("unable to parse pubkey: %v", err)
|
log.Printf("unable to parse pubkey: %v", err)
|
||||||
return "", nil, fmt.Errorf("unable to parse pubkey: %w", err)
|
return "", nil, fmt.Errorf("unable to parse pubkey: %w", err)
|
||||||
@@ -225,12 +226,12 @@ func (s *server) CheckChannels(ctx context.Context, in *lspdrpc.Encrypted) (*lsp
|
|||||||
log.Printf("proto.Marshall() error: %v", err)
|
log.Printf("proto.Marshall() error: %v", err)
|
||||||
return nil, fmt.Errorf("proto.Marshal() error: %w", err)
|
return nil, fmt.Errorf("proto.Marshal() error: %w", err)
|
||||||
}
|
}
|
||||||
pubkey, err := btcec.ParsePubKey(checkChannelsRequest.EncryptPubkey, btcec.S256())
|
pubkey, err := btcec.ParsePubKey(checkChannelsRequest.EncryptPubkey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("unable to parse pubkey: %v", err)
|
log.Printf("unable to parse pubkey: %v", err)
|
||||||
return nil, fmt.Errorf("unable to parse pubkey: %w", err)
|
return nil, fmt.Errorf("unable to parse pubkey: %w", err)
|
||||||
}
|
}
|
||||||
encrypted, err := btcec.Encrypt(pubkey, dataReply)
|
encrypted, err := btceclegacy.Encrypt(pubkey, dataReply)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("btcec.Encrypt() error: %v", err)
|
log.Printf("btcec.Encrypt() error: %v", err)
|
||||||
return nil, fmt.Errorf("btcec.Encrypt() error: %w", err)
|
return nil, fmt.Errorf("btcec.Encrypt() error: %w", err)
|
||||||
@@ -323,7 +324,7 @@ func getPendingNodeChannels(nodeID string) ([]*lnrpc.PendingChannelsResponse_Pen
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) > 1 && os.Args[1] == "genkey" {
|
if len(os.Args) > 1 && os.Args[1] == "genkey" {
|
||||||
p, err := btcec.NewPrivateKey(btcec.S256())
|
p, err := btcec.NewPrivateKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("btcec.NewPrivateKey() error: %v", err)
|
log.Fatalf("btcec.NewPrivateKey() error: %v", err)
|
||||||
}
|
}
|
||||||
@@ -340,7 +341,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("hex.DecodeString(os.Getenv(\"LSPD_PRIVATE_KEY\")=%v) error: %v", os.Getenv("LSPD_PRIVATE_KEY"), err)
|
log.Fatalf("hex.DecodeString(os.Getenv(\"LSPD_PRIVATE_KEY\")=%v) error: %v", os.Getenv("LSPD_PRIVATE_KEY"), err)
|
||||||
}
|
}
|
||||||
privateKey, publicKey = btcec.PrivKeyFromBytes(btcec.S256(), privateKeyBytes)
|
privateKey, publicKey = btcec.PrivKeyFromBytes(privateKeyBytes)
|
||||||
|
|
||||||
certmagicDomain := os.Getenv("CERTMAGIC_DOMAIN")
|
certmagicDomain := os.Getenv("CERTMAGIC_DOMAIN")
|
||||||
address := os.Getenv("LISTEN_ADDRESS")
|
address := os.Getenv("LISTEN_ADDRESS")
|
||||||
|
|||||||
Reference in New Issue
Block a user