Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Show actual ip address of the user #734

Open
wants to merge 41 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
65dc7d9
add missing `ip` field in Insights
gbaranski Jan 8, 2025
38f861a
update insights data on connect
gbaranski Jan 8, 2025
334ee8f
use ip from insights data for rpc_status
gbaranski Jan 8, 2025
3b24e3a
job_actual_ip and actualIP in DataManager
gbaranski Jan 9, 2025
3f5e1a1
remove time.Sleep that was used for testing
gbaranski Jan 9, 2025
d994c3f
on disconnect, don't spawn a new goroutine
gbaranski Jan 9, 2025
7041597
no context cancellation in case of other events
gbaranski Jan 9, 2025
557ef1d
fix confusion of && and ||
gbaranski Jan 9, 2025
a0bde68
fix incorrect use of switch
gbaranski Jan 10, 2025
2521a58
insightsUntilSuccess -> insightsIPUntilSuccess
gbaranski Jan 10, 2025
b6ba8d0
fix: remove trailing newline
gbaranski Jan 10, 2025
5c02879
feat: add InsightsViaTunnel()
gbaranski Jan 13, 2025
6c594e1
feat: log error for actualIP()
gbaranski Jan 13, 2025
c2d42db
fix: mispelling of necessary
gbaranski Jan 13, 2025
d6c50d6
fix: make requests via api.doWithClient()
gbaranski Jan 13, 2025
0b0fb1a
style: remove trailing newline
gbaranski Jan 13, 2025
341aeb7
fix: make to actually use InsightsViaTunnel
gbaranski Jan 13, 2025
0262e66
fix: do not print error if it is ctx cancellation
gbaranski Jan 13, 2025
03e034a
test: TestInsightsIPUntilSuccess
gbaranski Jan 13, 2025
8cbf8c3
test: TestJobActualIP
gbaranski Jan 13, 2025
30915b3
fix: use errors.Is instead weird of comparsions
gbaranski Jan 14, 2025
8f3afa5
fix: utilize assert library for tests
gbaranski Jan 14, 2025
a40b324
fix: continue retrying after ip parse error
gbaranski Jan 14, 2025
8538a44
fix: call api in a goroutine
gbaranski Jan 14, 2025
c33d1f5
fix: put each attempt for api in goroutine
gbaranski Jan 14, 2025
8425660
fix: reorganize the job actual ip related functions
gbaranski Jan 15, 2025
a7d425b
feat: notifying about actual ip update through protobuf
gbaranski Jan 16, 2025
af4e60e
test: TestJobUpdateActualIP
gbaranski Jan 16, 2025
0f27891
test: testing ip status in qa tests
gbaranski Jan 16, 2025
07956a2
refactor: move JobActualIP out of jobs
gbaranski Jan 22, 2025
f2aa444
refactor: ActualIP -> ActualIPResolver
gbaranski Jan 22, 2025
e4171da
fix: handle context cancellation
gbaranski Jan 22, 2025
bda07b3
refactor: now call new fn name
gbaranski Jan 23, 2025
10fc237
fix: cleaner behaviour of update fn
gbaranski Jan 23, 2025
db6775c
fix: stop actual ip update if ctx cancelled
gbaranski Jan 23, 2025
b3513e8
fix: use a buffered channel for actual ip result
gbaranski Jan 24, 2025
60bb1b8
fix: simpliy if else
gbaranski Jan 24, 2025
84deb23
Merge branch 'main' into show-public-ip
gbaranski Jan 28, 2025
63ff929
chore: regenerate protobuf
gbaranski Jan 28, 2025
6230a7b
fix: skip connect events that are attempts
gbaranski Jan 28, 2025
84a37d4
test: add EventStatus to DataConnect
gbaranski Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions core/core.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type CredentialsAPI interface {

type InsightsAPI interface {
Insights() (*Insights, error)
InsightsViaTunnel() (*Insights, error)
}

type ServersAPI interface {
Expand Down Expand Up @@ -99,9 +100,9 @@ func (api *DefaultAPI) request(path, method string, data []byte, token string) (
return api.do(req)
}

// do request regardless of the authentication.
func (api *DefaultAPI) do(req *http.Request) (*http.Response, error) {
resp, err := api.client.Do(req)
// doWithClient makes a request with the provided client.
func (api *DefaultAPI) doWithClient(req *http.Request, client *http.Client) (*http.Response, error) {
resp, err := client.Do(req)

// Transport of the request is already up to date

Expand Down Expand Up @@ -145,6 +146,11 @@ func (api *DefaultAPI) do(req *http.Request) (*http.Response, error) {
return resp, nil
}

// do request regardless of the authentication.
func (api *DefaultAPI) do(req *http.Request) (*http.Response, error) {
return api.doWithClient(req, api.client)
}

func (api *DefaultAPI) Plans() (*Plans, error) {
var ret *Plans
req, err := request.NewRequest(http.MethodGet, api.agent, api.baseURL, PlanURL, "application/json", "", "gzip, deflate", nil)
Expand Down Expand Up @@ -418,14 +424,13 @@ func (api *DefaultAPI) Server(id int64) (*Server, error) {
return &ret[0], nil
}

// Insights returns insights about user
func (api *DefaultAPI) Insights() (*Insights, error) {
func (api *DefaultAPI) insightsWithClient(client *http.Client) (*Insights, error) {
req, err := request.NewRequest(http.MethodGet, api.agent, api.baseURL, InsightsURL, "application/json", "", "gzip, deflate", nil)
if err != nil {
return nil, err
}

resp, err := api.do(req)
resp, err := api.doWithClient(req, client)
if err != nil {
return nil, err
}
Expand All @@ -438,6 +443,19 @@ func (api *DefaultAPI) Insights() (*Insights, error) {
return &ret, nil
}

// Insights returns insights about user
func (api *DefaultAPI) Insights() (*Insights, error) {
return api.insightsWithClient(api.client)
}

// InsightsViaTunnel returns insights about user, but the request is made through a tunnel
// the method is not using the default client, but creates a new one
// the request might not necessary go through a tunnel, if there's no tunnel open
func (api *DefaultAPI) InsightsViaTunnel() (*Insights, error) {
client := request.NewStdHTTP()
return api.insightsWithClient(client)
}

type NotificationCredentialsRequest struct {
AppUserID string `json:"app_user_uid"`
PlatformID int `json:"platform_id"`
Expand Down
1 change: 1 addition & 0 deletions core/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,7 @@ type Pivot struct {
}

type Insights struct {
IP string `json:"ip"`
City string `json:"city"`
Country string `json:"country"`
Isp string `json:"isp"`
Expand Down
15 changes: 15 additions & 0 deletions daemon/data_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"log"
"net/netip"
"sort"
"strings"
"sync"
Expand All @@ -30,6 +31,7 @@ type DataManager struct {
insightsData InsightsData
serversData ServersData
versionData VersionData
actualIP netip.Addr
dataUpdateEvents *events.DataUpdateEvents
mu sync.Mutex
}
Expand All @@ -44,6 +46,7 @@ func NewDataManager(insightsFilePath,
insightsData: InsightsData{filePath: insightsFilePath},
serversData: ServersData{filePath: serversFilePath},
versionData: VersionData{filePath: versionFilePath},
actualIP: netip.Addr{},
dataUpdateEvents: dataUpdateEvents,
}
}
Expand Down Expand Up @@ -196,6 +199,18 @@ func (dm *DataManager) SetVersionData(version semver.Version, newerAvailable boo
}
}

func (dm *DataManager) GetActualIP() netip.Addr {
dm.mu.Lock()
defer dm.mu.Unlock()
return dm.actualIP
}

func (dm *DataManager) SetActualIP(ip netip.Addr) {
dm.mu.Lock()
defer dm.mu.Unlock()
dm.actualIP = ip
}

func toServerTechnology(
technology config.Technology,
protocol config.Protocol,
Expand Down
135 changes: 135 additions & 0 deletions daemon/job_actual_ip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package daemon

import (
"context"
"log"
"net/netip"
"time"

"github.com/NordSecurity/nordvpn-linux/core"
"github.com/NordSecurity/nordvpn-linux/daemon/state"
"github.com/NordSecurity/nordvpn-linux/events"
"github.com/NordSecurity/nordvpn-linux/internal"
"github.com/NordSecurity/nordvpn-linux/network"
)

// tryInsightsIP is an attempt to get insights IP
// the IP it returns is always valid
func tryInsightsIP(api core.InsightsAPI) (netip.Addr, error) {
insights, err := api.InsightsViaTunnel()
if err == nil && insights != nil {
ip, err := netip.ParseAddr(insights.IP)
if err == nil {
return ip, nil
} else {
return netip.Addr{}, err
}
} else {
return netip.Addr{}, err
}
}

func insightsIPUntilSuccess(ctx context.Context, api core.InsightsAPI, backoff func(int) time.Duration) (netip.Addr, error) {
type Result struct {
netip.Addr
error
}
result := make(chan Result)
for i := 0; ; i++ {
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
if ctx.Err() != nil {
return netip.Addr{}, ctx.Err()
}

// this goroutine is used so that this function is immediately stopped once the context is cancelled
go func() {
ip, err := tryInsightsIP(api)
result <- Result{ip, err}
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
}()

select {
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
case r := <-result:
if r.error == nil {
return r.Addr, nil
} else {
log.Println(internal.ErrorPrefix, "insights ip attempt failed: ", r.error)
}
case <-ctx.Done():
return netip.Addr{}, ctx.Err()
}

select {
case <-time.After(backoff(i)): // wait before retrying
// PS: there's no fallthrough
case <-ctx.Done():
return netip.Addr{}, ctx.Err()
}
}
}

func updateActualIP(dm *DataManager, api core.InsightsAPI, ctx context.Context, isConnected bool) error {
var newIP netip.Addr
defer func() {
dm.SetActualIP(newIP)
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
}()

if !isConnected {
return nil
}

insightsIP, err := insightsIPUntilSuccess(ctx, api, network.ExponentialBackoff)
if err != nil {
return err
}
if insightsIP.IsValid() {
newIP = insightsIP
}

return nil
}

// JobActualIP is a long-running job that will update the actual IP address indefinitely
// it reacts to state updates from the statePublisher
func JobActualIP(statePublisher *state.StatePublisher, dm *DataManager, api core.InsightsAPI) {
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
call := func(ctx context.Context, isConnected bool) {
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
err := updateActualIP(dm, api, ctx, isConnected)
if err == nil {
err := statePublisher.NotifyActualIPUpdate()
if err != nil {
log.Println(internal.ErrorPrefix, "notify about actual ip update failed: ", err)
}
} else {
if err == context.Canceled {
return
}
log.Println(internal.ErrorPrefix, "actual ip job error: ", err)
}
}

stateChan, _ := statePublisher.AddSubscriber()
var cancel context.CancelFunc

for ev := range stateChan {
_, isConnect := ev.(events.DataConnect)
_, isDisconnect := ev.(events.DataDisconnect)

if isConnect || isDisconnect {
if cancel != nil {
cancel()
}

var ctx context.Context
ctx, cancel = context.WithCancel(context.Background())

if isConnect {
go call(ctx, true)
} else {
call(ctx, false) // should finish immediately, that's why it's not a separate goroutine
gbaranski marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

// Ensure the context is canceled when the loop exits
if cancel != nil {
cancel()
}
}
Loading
Loading