2021-08-15 16:56:26 +02:00
package internal
2021-05-01 12:45:37 +02:00
import (
"context"
"fmt"
2021-05-19 11:13:25 +02:00
ice "github.com/pion/ice/v2"
2021-05-01 12:45:37 +02:00
log "github.com/sirupsen/logrus"
2021-06-17 14:27:33 +02:00
"github.com/wiretrustee/wiretrustee/iface"
2021-05-01 12:45:37 +02:00
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
2021-08-15 16:56:26 +02:00
"net"
2021-05-01 12:45:37 +02:00
"sync"
"time"
)
var (
2021-05-15 12:23:56 +02:00
// DefaultWgKeepAlive default Wireguard keep alive constant
2021-05-01 12:45:37 +02:00
DefaultWgKeepAlive = 20 * time . Second
2021-08-15 16:56:26 +02:00
privateIPBlocks [ ] * net . IPNet
2021-05-01 12:45:37 +02:00
)
2021-07-19 15:02:11 +02:00
type Status string
const (
StatusConnected Status = "Connected"
StatusConnecting Status = "Connecting"
StatusDisconnected Status = "Disconnected"
)
2021-08-15 16:56:26 +02:00
func init ( ) {
for _ , cidr := range [ ] string {
"127.0.0.0/8" , // IPv4 loopback
"10.0.0.0/8" , // RFC1918
"172.16.0.0/12" , // RFC1918
"192.168.0.0/16" , // RFC1918
"169.254.0.0/16" , // RFC3927 link-local
"::1/128" , // IPv6 loopback
"fe80::/10" , // IPv6 link-local
"fc00::/7" , // IPv6 unique local addr
} {
_ , block , err := net . ParseCIDR ( cidr )
if err != nil {
panic ( fmt . Errorf ( "parse error on %q: %v" , cidr , err ) )
}
privateIPBlocks = append ( privateIPBlocks , block )
}
}
2021-05-15 12:23:56 +02:00
// ConnConfig Connection configuration struct
2021-05-01 12:45:37 +02:00
type ConnConfig struct {
// Local Wireguard listening address e.g. 127.0.0.1:51820
WgListenAddr string
// A Local Wireguard Peer IP address in CIDR notation e.g. 10.30.30.1/24
2021-05-15 12:23:56 +02:00
WgPeerIP string
2021-05-01 12:45:37 +02:00
// Local Wireguard Interface name (e.g. wg0)
WgIface string
// Wireguard allowed IPs (e.g. 10.30.30.2/32)
WgAllowedIPs string
// Local Wireguard private key
WgKey wgtypes . Key
// Remote Wireguard public key
RemoteWgKey wgtypes . Key
StunTurnURLS [ ] * ice . URL
2021-05-16 18:05:08 +02:00
iFaceBlackList map [ string ] struct { }
2021-05-01 12:45:37 +02:00
}
2021-05-15 12:23:56 +02:00
// IceCredentials ICE protocol credentials struct
2021-05-01 12:45:37 +02:00
type IceCredentials struct {
uFrag string
pwd string
}
2021-05-15 12:23:56 +02:00
// Connection Holds information about a connection and handles signal protocol
2021-05-01 12:45:37 +02:00
type Connection struct {
Config ConnConfig
// signalCandidate is a handler function to signal remote peer about local connection candidate
signalCandidate func ( candidate ice . Candidate ) error
// signalOffer is a handler function to signal remote peer our connection offer (credentials)
signalOffer func ( uFrag string , pwd string ) error
// signalOffer is a handler function to signal remote peer our connection answer (credentials)
signalAnswer func ( uFrag string , pwd string ) error
// remoteAuthChannel is a channel used to wait for remote credentials to proceed with the connection
remoteAuthChannel chan IceCredentials
// agent is an actual ice.Agent that is used to negotiate and maintain a connection to a remote peer
agent * ice . Agent
wgProxy * WgProxy
connected * Cond
closeCond * Cond
remoteAuthCond sync . Once
2021-07-19 15:02:11 +02:00
Status Status
2021-05-01 12:45:37 +02:00
}
2021-05-15 12:23:56 +02:00
// NewConnection Creates a new connection and sets handling functions for signal protocol
2021-05-01 12:45:37 +02:00
func NewConnection ( config ConnConfig ,
signalCandidate func ( candidate ice . Candidate ) error ,
signalOffer func ( uFrag string , pwd string ) error ,
signalAnswer func ( uFrag string , pwd string ) error ,
) * Connection {
return & Connection {
Config : config ,
signalCandidate : signalCandidate ,
signalOffer : signalOffer ,
signalAnswer : signalAnswer ,
remoteAuthChannel : make ( chan IceCredentials , 1 ) ,
closeCond : NewCond ( ) ,
connected : NewCond ( ) ,
agent : nil ,
wgProxy : NewWgProxy ( config . WgIface , config . RemoteWgKey . String ( ) , config . WgAllowedIPs , config . WgListenAddr ) ,
2021-07-19 15:02:11 +02:00
Status : StatusDisconnected ,
2021-05-01 12:45:37 +02:00
}
}
// Open opens connection to a remote peer.
// Will block until the connection has successfully established
func ( conn * Connection ) Open ( timeout time . Duration ) error {
// create an ice.Agent that will be responsible for negotiating and establishing actual peer-to-peer connection
a , err := ice . NewAgent ( & ice . AgentConfig {
2021-06-18 13:01:43 +02:00
// MulticastDNSMode: ice.MulticastDNSModeQueryAndGather,
NetworkTypes : [ ] ice . NetworkType { ice . NetworkTypeUDP4 } ,
Urls : conn . Config . StunTurnURLS ,
2021-05-16 18:05:08 +02:00
InterfaceFilter : func ( s string ) bool {
if conn . Config . iFaceBlackList == nil {
return true
}
_ , ok := conn . Config . iFaceBlackList [ s ]
return ! ok
} ,
2021-05-01 12:45:37 +02:00
} )
conn . agent = a
if err != nil {
return err
}
err = conn . listenOnLocalCandidates ( )
if err != nil {
return err
}
err = conn . listenOnConnectionStateChanges ( )
if err != nil {
return err
}
err = conn . signalCredentials ( )
if err != nil {
return err
}
2021-07-19 15:02:11 +02:00
conn . Status = StatusConnecting
2021-05-01 12:45:37 +02:00
log . Infof ( "trying to connect to peer %s" , conn . Config . RemoteWgKey . String ( ) )
// wait until credentials have been sent from the remote peer (will arrive via a signal server)
select {
case remoteAuth := <- conn . remoteAuthChannel :
log . Infof ( "got a connection confirmation from peer %s" , conn . Config . RemoteWgKey . String ( ) )
err = conn . agent . GatherCandidates ( )
if err != nil {
return err
}
isControlling := conn . Config . WgKey . PublicKey ( ) . String ( ) > conn . Config . RemoteWgKey . String ( )
2021-08-29 16:12:39 +02:00
var remoteConn * ice . Conn
remoteConn , err = conn . openConnectionToRemote ( isControlling , remoteAuth )
2021-05-01 12:45:37 +02:00
if err != nil {
log . Errorf ( "failed establishing connection with the remote peer %s %s" , conn . Config . RemoteWgKey . String ( ) , err )
return err
}
2021-08-29 16:12:39 +02:00
var pair * ice . CandidatePair
pair , err = conn . agent . GetSelectedCandidatePair ( )
2021-05-01 12:45:37 +02:00
if err != nil {
return err
}
2021-08-15 16:56:26 +02:00
// in case the remote peer is in the local network or one of the peers has public static IP -> no need for a Wireguard proxy, direct communication is possible.
2021-08-29 16:12:39 +02:00
if ! useProxy ( pair ) {
2021-08-15 16:56:26 +02:00
log . Debugf ( "it is possible to establish a direct connection (without proxy) to peer %s - my addr: %s, remote addr: %s" , conn . Config . RemoteWgKey . String ( ) , pair . Local . Address ( ) , pair . Remote . Address ( ) )
2021-06-17 14:27:33 +02:00
err = conn . wgProxy . StartLocal ( fmt . Sprintf ( "%s:%d" , pair . Remote . Address ( ) , iface . WgPort ) )
2021-06-24 10:55:05 +02:00
if err != nil {
return err
}
2021-06-17 14:27:33 +02:00
} else {
2021-08-15 16:56:26 +02:00
log . Infof ( "establishing secure tunnel to peer %s via selected candidate pair %s" , conn . Config . RemoteWgKey . String ( ) , pair )
2021-06-17 14:27:33 +02:00
err = conn . wgProxy . Start ( remoteConn )
if err != nil {
return err
}
}
2021-05-01 12:45:37 +02:00
2021-07-19 15:02:11 +02:00
conn . Status = StatusConnected
2021-05-01 12:45:37 +02:00
log . Infof ( "opened connection to peer %s" , conn . Config . RemoteWgKey . String ( ) )
2021-07-19 15:02:11 +02:00
case <- conn . closeCond . C :
conn . Status = StatusDisconnected
return fmt . Errorf ( "connection to peer %s has been closed" , conn . Config . RemoteWgKey . String ( ) )
2021-05-01 12:45:37 +02:00
case <- time . After ( timeout ) :
2021-08-29 16:12:39 +02:00
err = conn . Close ( )
2021-05-01 12:45:37 +02:00
if err != nil {
log . Warnf ( "error while closing connection to peer %s -> %s" , conn . Config . RemoteWgKey . String ( ) , err . Error ( ) )
}
2021-07-19 15:02:11 +02:00
conn . Status = StatusDisconnected
2021-05-01 12:45:37 +02:00
return fmt . Errorf ( "timeout of %vs exceeded while waiting for the remote peer %s" , timeout . Seconds ( ) , conn . Config . RemoteWgKey . String ( ) )
}
// wait until connection has been closed
2021-05-19 10:58:21 +02:00
<- conn . closeCond . C
2021-07-19 15:02:11 +02:00
conn . Status = StatusDisconnected
2021-05-19 10:58:21 +02:00
return fmt . Errorf ( "connection to peer %s has been closed" , conn . Config . RemoteWgKey . String ( ) )
2021-05-01 12:45:37 +02:00
}
2021-08-15 16:56:26 +02:00
func isPublicIP ( ip net . IP ) bool {
if ip . IsLoopback ( ) || ip . IsLinkLocalUnicast ( ) || ip . IsLinkLocalMulticast ( ) {
return false
}
for _ , block := range privateIPBlocks {
if block . Contains ( ip ) {
return false
}
}
return true
}
2021-08-29 16:12:39 +02:00
//useProxy determines whether a direct connection (without a go proxy) is possible
//There are 2 cases: one of the peers has a public IP or both peers are in teh same private network.
//Please note, that this check happens when peers were already able to ping each other with ICE layer.
func useProxy ( pair * ice . CandidatePair ) bool {
remoteIP := net . ParseIP ( pair . Remote . Address ( ) )
myIp := net . ParseIP ( pair . Local . Address ( ) )
if pair . Local . Type ( ) == ice . CandidateTypeHost && pair . Remote . Type ( ) == ice . CandidateTypeHost {
if isPublicIP ( remoteIP ) || isPublicIP ( myIp ) {
//one of the hosts has a public IP
return false
}
if ! isPublicIP ( remoteIP ) && ! isPublicIP ( myIp ) {
//both hosts are in the same private network
return false
}
}
return true
}
2021-05-15 12:23:56 +02:00
// Close Closes a peer connection
2021-05-01 12:45:37 +02:00
func ( conn * Connection ) Close ( ) error {
var err error
conn . closeCond . Do ( func ( ) {
log . Warnf ( "closing connection to peer %s" , conn . Config . RemoteWgKey . String ( ) )
if a := conn . agent ; a != nil {
e := a . Close ( )
if e != nil {
log . Warnf ( "error while closing ICE agent of peer connection %s" , conn . Config . RemoteWgKey . String ( ) )
err = e
}
}
if c := conn . wgProxy ; c != nil {
e := c . Close ( )
if e != nil {
log . Warnf ( "error while closingWireguard proxy connection of peer connection %s" , conn . Config . RemoteWgKey . String ( ) )
err = e
}
}
} )
return err
}
2021-05-15 12:23:56 +02:00
// OnAnswer Handles the answer from the other peer
2021-05-01 12:45:37 +02:00
func ( conn * Connection ) OnAnswer ( remoteAuth IceCredentials ) error {
conn . remoteAuthCond . Do ( func ( ) {
log . Debugf ( "OnAnswer from peer %s" , conn . Config . RemoteWgKey . String ( ) )
conn . remoteAuthChannel <- remoteAuth
} )
return nil
}
2021-05-15 12:23:56 +02:00
// OnOffer Handles the offer from the other peer
2021-05-01 12:45:37 +02:00
func ( conn * Connection ) OnOffer ( remoteAuth IceCredentials ) error {
conn . remoteAuthCond . Do ( func ( ) {
log . Debugf ( "OnOffer from peer %s" , conn . Config . RemoteWgKey . String ( ) )
conn . remoteAuthChannel <- remoteAuth
uFrag , pwd , err := conn . agent . GetLocalUserCredentials ( )
2021-05-15 12:23:56 +02:00
if err != nil { //nolint
2021-05-01 12:45:37 +02:00
}
err = conn . signalAnswer ( uFrag , pwd )
2021-05-15 12:23:56 +02:00
if err != nil { //nolint
2021-05-01 12:45:37 +02:00
}
} )
return nil
}
2021-05-15 12:23:56 +02:00
// OnRemoteCandidate Handles remote candidate provided by the peer.
2021-05-01 12:45:37 +02:00
func ( conn * Connection ) OnRemoteCandidate ( candidate ice . Candidate ) error {
log . Debugf ( "onRemoteCandidate from peer %s -> %s" , conn . Config . RemoteWgKey . String ( ) , candidate . String ( ) )
err := conn . agent . AddRemoteCandidate ( candidate )
if err != nil {
return err
}
return nil
}
// openConnectionToRemote opens an ice.Conn to the remote peer. This is a real peer-to-peer connection
// blocks until connection has been established
func ( conn * Connection ) openConnectionToRemote ( isControlling bool , credentials IceCredentials ) ( * ice . Conn , error ) {
var realConn * ice . Conn
var err error
if isControlling {
realConn , err = conn . agent . Dial ( context . TODO ( ) , credentials . uFrag , credentials . pwd )
} else {
realConn , err = conn . agent . Accept ( context . TODO ( ) , credentials . uFrag , credentials . pwd )
}
if err != nil {
return nil , err
}
return realConn , err
}
// signalCredentials prepares local user credentials and signals them to the remote peer
func ( conn * Connection ) signalCredentials ( ) error {
localUFrag , localPwd , err := conn . agent . GetLocalUserCredentials ( )
if err != nil {
return err
}
err = conn . signalOffer ( localUFrag , localPwd )
if err != nil {
return err
}
return nil
}
// listenOnLocalCandidates registers callback of an ICE Agent to receive new local connection candidates and then
// signals them to the remote peer
func ( conn * Connection ) listenOnLocalCandidates ( ) error {
err := conn . agent . OnCandidate ( func ( candidate ice . Candidate ) {
if candidate != nil {
log . Debugf ( "discovered local candidate %s" , candidate . String ( ) )
err := conn . signalCandidate ( candidate )
if err != nil {
log . Errorf ( "failed signaling candidate to the remote peer %s %s" , conn . Config . RemoteWgKey . String ( ) , err )
//todo ??
return
}
}
} )
if err != nil {
return err
}
return nil
}
// listenOnConnectionStateChanges registers callback of an ICE Agent to track connection state
func ( conn * Connection ) listenOnConnectionStateChanges ( ) error {
err := conn . agent . OnConnectionStateChange ( func ( state ice . ConnectionState ) {
log . Debugf ( "ICE Connection State has changed for peer %s -> %s" , conn . Config . RemoteWgKey . String ( ) , state . String ( ) )
if state == ice . ConnectionStateConnected {
// closed the connection has been established we can check the selected candidate pair
pair , err := conn . agent . GetSelectedCandidatePair ( )
if err != nil {
log . Errorf ( "failed selecting active ICE candidate pair %s" , err )
return
}
2021-08-15 16:56:26 +02:00
log . Debugf ( "ICE connected to peer %s via a selected connnection candidate pair %s" , conn . Config . RemoteWgKey . String ( ) , pair )
2021-05-01 12:45:37 +02:00
} else if state == ice . ConnectionStateDisconnected || state == ice . ConnectionStateFailed {
err := conn . Close ( )
if err != nil {
log . Warnf ( "error while closing connection to peer %s -> %s" , conn . Config . RemoteWgKey . String ( ) , err . Error ( ) )
}
}
} )
if err != nil {
return err
}
return nil
}