EdgeLink is a Go library designed to establish secure, point-to-point network links between two applications. It leverages a userspace WireGuard implementation to create secure tunnels, making it ideal for containerized environments without requiring specific container permissions.
It enables secure bi-directional communication between applications that reside on different networks. A secure link can be established as long as one of the application (the source) can reach the other application (the destination).
To establish a secure link using EdgeLink, the following conditions must be met:
-
Network Reachability:
- The source node must be able to reach the destination node over UDP. This is the primary requirement for establishing a connection.
-
Key Pair Configuration:
- Since EdgeLink utilizes WireGuard for secure communication, both nodes need to be configured with cryptographic key pairs.
- By default, EdgeLink will automatically generate a key pair for each node if none is provided, and it will handle the key exchange process seamlessly.
- Alternatively, you can specify your own set of key pairs for each node, which will bypass the key exchange process.
-
TCP Connectivity:
- In addition to UDP, the source node must also be able to reach the destination node over TCP on a designated port for the key exchange process.
-
Default Ports:
- The default configuration uses port
51820
for the WireGuard tunnel. - Port
50777
is used for the key exchange service.
- The default configuration uses port
These requirements ensure that EdgeLink can establish a secure and reliable connection between applications across different networks.
Install the library:
go get github.com/portainer/edgelink
Create the destination node in your application:
node, err := edgelink.NewNode()
if err != nil {
log.Fatalf("Failed to create node: %v", err)
}
err = node.SetupAsDestination()
if err != nil {
log.Fatalf("Failed to setup node as destination node: %v", err)
}
err = node.Link()
if err != nil {
log.Fatalf("Unable to establish link: %v", err)
}
Create the source node in your application:
// This is the public IP address of the destination node
// It must be reachable from the source node
destinationPublicIPAddress := "192.168.1.100"
node, err := edgelink.NewNode()
if err != nil {
log.Fatalf("Failed to create node: %v", err)
}
err = node.SetupAsSource(destinationPublicIPAddress)
if err != nil {
log.Fatalf("Failed to setup node as source node: %v", err)
}
err = node.Link()
if err != nil {
log.Fatalf("Unable to establish link: %v", err)
}
The library provides default configuration for both the source and destination nodes. The source node will use the private IP address 10.0.0.2
and the destination node will use the private IP address 10.0.0.1
.
For more information on the configuration options, see the configuration section below.
Once the link is established, you can use the GetVirtualNetwork()
method to get the virtual network interface that the two applications are connected to.
import (
"github.com/portainer/edgelink/examples/ping"
)
// Assuming the node is linked already
// Retrieve the virtual network interface
virtualNetwork := node.GetVirtualNetwork()
// Test the ping between the two nodes
// See the ping implementation in the examples directory for more information
err = ping.TestPingIPv4("10.0.0.2", "10.0.0.1", 1500, 3*time.Second, virtualNetwork)
if err != nil {
log.Printf("Failed to ping other node: %v", err)
}
For more examples, see the examples directory.
Once the nodes are created and configured, you can establish a link by calling the Link()
method on both the source and destination nodes.
When called on the destination node and provided that you are using the default configuration, the node will start by listening for incoming key exchange requests on the key exchange port.
Once a key exchange request is received, the node will stop listening on the key exchange port and will create a networking stack and a WireGuard interface that will be used to communicate with the source node.
When called on the source node and provided that you are using the default configuration, the node will start by connecting to the destination node on the key exchange port and will initiate the key exchange process.
Once the key exchange process is complete, the node will create a networking stack and a WireGuard interface that will be used to communicate with the destination node.
A Node
is the main object in the library. It holds common configuration for both the source and destination nodes.
The Node
object is created using the NewNode
function and uses the Options pattern to allow for configuration.
The Node
object is then configured as a source or destination node using the SetupAsSource
or SetupAsDestination
methods.
The default configuration will be used if no options are provided. You can find more information about the default values used by the library in the node.go file.
node, err := edgelink.NewNode()
By default, the library uses 1.1.1.1 as the DNS server. You can use the WithDNS
option to set one or more custom DNS servers.
node, err := edgelink.NewNode(
edgelink.WithDNS([]string{"8.8.8.8", "8.8.4.4"}),
)
By default, the library won't log anything. You can use the WithLogger
option to set a custom logging function.
func logf(format string, args ...any) {
format = "edgelink: " + format
log.Printf(format, args...)
}
node, err := edgelink.NewNode(
edgelink.WithLogger(logf),
)
By default, the library uses an MTU of 1500. You can use the WithMTU
option to set a custom MTU.
node, err := edgelink.NewNode(
edgelink.WithMTU(1400),
)
By default, the library will generate a key pair for each node if one is not provided and take care of the key exchange process. You can use the WithPrivateKey
and WithPeerPublicKey
options to use a predefined set of keys.
Note
Note that when both of these options are provided, the library will use the provided keys and won't initiate the key exchange process.
These keys can be generated using the wg
CLI. See https://www.wireguard.com/quickstart/#key-generation for more information.
sourceNode, err := edgelink.NewNode(
// This is the private key of the source node
edgelink.WithPrivateKey("WKriFZV6wu0PHonDnpjf9u84oIDLL8FgKB025lAxrnA="),
// This is the public key of the destination node
edgelink.WithPeerPublicKey("NAqot6ASJg3QyDQXpcGqsQYAbhp60gTgsGByN0lKnCk="),
)
By default, the library will keep the node configuration in memory. You can use the WithPersistentConfig
option to persist the node configuration to a file.
node, err := edgelink.NewNode(
edgelink.WithPersistentConfig("/path/to/config.yaml"),
)
This is particularly useful when running the library in a containerized environment, as it allows the node configuration to be persisted across container restarts.
A node can be configured as a destination node using the SetupAsDestination
method. It uses the Options pattern to allow for configuration.
The default configuration will be used if no options are provided.
err := node.SetupAsDestination()
By default, the library will use port 50777
for the key exchange service. This is the port that the destination node listens on for incoming key exchange requests from the source node.
Note
If you have specified a predefined set of keys for the node, the key exchange process will be skipped and setting a custom port will have no effect.
You can use the WithDestinationKeyxPort
option to set a custom port.
err := node.SetupAsDestination(
edgelink.WithDestinationKeyxPort(7777),
)
By default, the library will use the private IP addresses 10.0.0.1
and 10.0.0.2
for the destination and source nodes respectively.
You can use the WithDestinationLocalIP
and WithDestinationOriginIP
options to set custom IP addresses.
Warning
Make sure to use the same IP address configuration on the source node using the SetupAsSource
method.
err := node.SetupAsDestination(
// This is the local private IP address of the destination node
edgelink.WithDestinationLocalIP("192.168.1.100"),
// This is the private IP address of the source node
edgelink.WithDestinationOriginIP("192.168.1.101"),
)
By default, the library will use port 51820
for the WireGuard tunnel. You can use the WithDestinationWGPort
option to set a custom port.
err := node.SetupAsDestination(
edgelink.WithDestinationWGPort(51821),
)
By default, the library will use a keepalive interval of 25 seconds for the WireGuard tunnel. You can use the WithDestinationWGKeepalive
option to set a custom keepalive interval.
err := node.SetupAsDestination(
edgelink.WithDestinationWGKeepalive(10),
)
By default, the underlying WireGuard implementation of the library does not log anything. You can use the WithDestinationWGVerboseLogging
option to enable verbose logging.
This is useful for debugging the WireGuard implementation.
err := node.SetupAsDestination(
edgelink.WithDestinationWGVerboseLogging(true),
)
A node can be configured as a source node using the SetupAsSource
method. It uses the Options pattern to allow for configuration.
The default configuration will be used if no options are provided.
err := node.SetupAsSource()
By default, the library will use port 50777
for the key exchange service. This is the public port that the source node will connect to on the destination node to initiate the key exchange process.
Note
You only need to use this option if you have specified a custom key exchange port on the destination node using the WithDestinationKeyxPort
option.
err := node.SetupAsSource(
edgelink.WithSourceKeyxPort(7777),
)
By default, the library will use a retry interval of 15 seconds and a timeout of 3 seconds for the key exchange process.
You can use the WithSourceKeyxRetryInterval
and WithSourceKeyxTimeout
options to set custom values.
err := node.SetupAsSource(
edgelink.WithSourceKeyxRetryInterval(10 * time.Second),
edgelink.WithSourceKeyxTimeout(5 * time.Second),
)
By default, the library will use the private IP addresses 10.0.0.1
and 10.0.0.2
for the destination and source nodes respectively.
You can use the WithSourceLocalIP
and WithSourceTargetIP
options to set custom IP addresses.
Warning
Make sure to use the same IP address configuration on the destination node using the SetupAsDestination
method.
err := node.SetupAsSource(
// This is the local private IP address of the source node
edgelink.WithSourceLocalIP("192.168.1.100"),
// This is the private IP address of the destination node
edgelink.WithSourceTargetIP("192.168.1.101"),
)
By default, the library will use port 51820
for the WireGuard tunnel. You can use the WithSourceWGPort
option to set a custom port.
Note
You only need to use this option if you have specified a custom WireGuard port on the destination node using the WithDestinationWGPort
option.
err := node.SetupAsSource(
edgelink.WithSourceWGPort(51821),
)
By default, the underlying WireGuard implementation of the library does not log anything. You can use the WithSourceWGVerboseLogging
option to enable verbose logging.
This is useful for debugging the WireGuard implementation.
err := node.SetupAsSource(
edgelink.WithSourceWGVerboseLogging(true),
)
To run the tests, use the following command:
go test
EdgeLink is licensed under the MIT License. See the LICENSE file for more details.