diff options
| -rw-r--r-- | CHANGELOG.md | 1 | ||||
| -rw-r--r-- | gui/packages/components/src/ConnectionInfo.tsx | 105 | ||||
| -rw-r--r-- | gui/packages/components/src/index.ts | 1 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/app.js | 14 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/components/Connect.js | 307 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/lib/daemon-rpc.js | 46 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/connection/actions.js | 10 | ||||
| -rw-r--r-- | gui/packages/desktop/src/renderer/redux/connection/reducers.js | 4 | ||||
| -rw-r--r-- | mullvad-cli/src/cmds/status.rs | 9 | ||||
| -rw-r--r-- | mullvad-daemon/src/geoip.rs | 8 | ||||
| -rw-r--r-- | mullvad-daemon/src/lib.rs | 81 | ||||
| -rw-r--r-- | mullvad-types/src/location.rs | 3 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connected_state.rs | 3 | ||||
| -rw-r--r-- | talpid-core/src/tunnel_state_machine/connecting_state.rs | 8 | ||||
| -rw-r--r-- | talpid-types/src/net.rs | 2 | ||||
| -rw-r--r-- | talpid-types/src/tunnel.rs | 6 |
16 files changed, 413 insertions, 195 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cc372980..638d9d935d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ Line wrap the file at 100 chars. Th with UDP fail in a row. If that also fails, alternate between UDP and TCP with random ports. - Add new system and in-app notifications to inform the user when the app becomes outdated, unsupported or may have security issues. +- Allow the user to view the relay in/out IP address in the GUI. ### Fixed - Pick new random relay for each reconnect attempt instead of just retrying with the same one. diff --git a/gui/packages/components/src/ConnectionInfo.tsx b/gui/packages/components/src/ConnectionInfo.tsx new file mode 100644 index 0000000000..b22c510c23 --- /dev/null +++ b/gui/packages/components/src/ConnectionInfo.tsx @@ -0,0 +1,105 @@ +import * as React from 'react'; +import { Component, Styles, Text, View } from 'reactxp'; +import { default as Accordion } from './Accordion'; + +const styles = { + toggle: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 14, + fontWeight: '800', + color: 'rgb(255, 255, 255, 0.4)', + paddingBottom: 2, + }), + content: Styles.createTextStyle({ + fontFamily: 'Open Sans', + fontSize: 16, + fontWeight: '800', + color: 'rgb(255, 255, 255)', + paddingBottom: 2, + }), +}; + +interface IInAddress { + ip: string; + port: number; + protocol: string; +} + +interface IOutAddress { + ipv4: string | null; + ipv6: string | null; +} + +interface IProps { + inAddress: IInAddress; + outAddress: IOutAddress; + startExpanded: boolean; + onToggle: (expanded: boolean) => void; +} + +interface IState { + expanded: boolean; +} + +export default class ConnectionInfo extends Component<IProps, IState> { + public static defaultProps = { + inAddress: { + ip: null, + port: null, + protocol: null, + }, + outAddress: null, + startExpanded: false, + onToggle: (_: boolean) => {}, + }; + + constructor(props: IProps) { + super(props); + + this.state = { + expanded: props.startExpanded, + }; + } + + public render() { + return ( + <View> + <Accordion height={this.state.expanded ? 'auto' : 0}> + <Text style={styles.content}>{this.inAddress()}</Text> + <Text style={styles.content}>{this.outAddress()}</Text> + </Accordion> + <Text style={styles.toggle} onPress={() => this.toggle()}> + {this.state.expanded ? 'LESS' : 'MORE'} + </Text> + </View> + ); + } + + private toggle() { + this.setState((state, props) => { + const expanded = !state.expanded; + + props.onToggle(expanded); + + return { expanded }; + }); + } + + private inAddress() { + const { ip, port, protocol } = this.props.inAddress; + + return ( + 'IN: ' + (ip || '<unknown>') + (port ? `:${port}` : '') + (protocol ? ` - ${protocol}` : '') + ); + } + + private outAddress() { + const { ipv4, ipv6 } = this.props.outAddress; + + if (ipv4 || ipv6) { + return `OUT: ${ipv4}` + (ipv6 ? ` / ${ipv6}` : ''); + } else { + return ''; + } + } +} diff --git a/gui/packages/components/src/index.ts b/gui/packages/components/src/index.ts index 7d5ad156f0..648ddabdf9 100644 --- a/gui/packages/components/src/index.ts +++ b/gui/packages/components/src/index.ts @@ -1,3 +1,4 @@ export { default as Accordion } from './Accordion'; export { default as ClipboardLabel } from './ClipboardLabel'; +export { default as ConnectionInfo } from './ConnectionInfo'; export { default as SecuredLabel, SecuredDisplayStyle } from './SecuredLabel'; diff --git a/gui/packages/desktop/src/renderer/app.js b/gui/packages/desktop/src/renderer/app.js index 1ef54ca4e9..2ae7b66f9c 100644 --- a/gui/packages/desktop/src/renderer/app.js +++ b/gui/packages/desktop/src/renderer/app.js @@ -229,7 +229,7 @@ export default class AppRenderer { if (tunnelState.state === 'disconnected' || tunnelState.state === 'blocked') { // switch to connecting state immediately to prevent a lag that may be caused by RPC // communication delay - actions.connection.connecting(); + actions.connection.connecting(null); await this._daemonRpc.connectTunnel(); } @@ -478,12 +478,6 @@ export default class AppRenderer { await this._setStartView(); try { - await this._fetchLocation(); - } catch (error) { - log.error(`Cannot fetch the location: ${error.message}`); - } - - try { await this._fetchLatestVersionInfo(); } catch (error) { log.error(`Cannot fetch the latest version information: ${error.message}`); @@ -602,7 +596,7 @@ export default class AppRenderer { } async _updateUserLocation(tunnelState: TunnelState) { - if (tunnelState === 'connecting' || tunnelState === 'disconnected') { + if (['connected', 'connecting', 'disconnected'].includes(tunnelState)) { try { await this._fetchLocation(); } catch (error) { @@ -616,11 +610,11 @@ export default class AppRenderer { switch (stateTransition.state) { case 'connecting': - actions.connection.connecting(); + actions.connection.connecting(stateTransition.details); break; case 'connected': - actions.connection.connected(); + actions.connection.connected(stateTransition.details); break; case 'disconnecting': diff --git a/gui/packages/desktop/src/renderer/components/Connect.js b/gui/packages/desktop/src/renderer/components/Connect.js index a50227b289..3602410c86 100644 --- a/gui/packages/desktop/src/renderer/components/Connect.js +++ b/gui/packages/desktop/src/renderer/components/Connect.js @@ -3,7 +3,7 @@ import moment from 'moment'; import * as React from 'react'; import { Component, Text, View, Types } from 'reactxp'; -import { SecuredLabel, SecuredDisplayStyle } from '@mullvad/components'; +import { ConnectionInfo, SecuredLabel, SecuredDisplayStyle } from '@mullvad/components'; import { Layout, Container, Header } from './Layout'; import { SettingsBarButton, Brand } from './HeaderBar'; import NotificationArea from './NotificationArea'; @@ -136,6 +136,19 @@ export default class Connect extends Component<Props> { } renderMap() { + const tunnelState = this.props.connection.status.state; + const details = this.props.connection.status.details; + + let relayIp = null; + let relayPort = null; + let relayProtocol = null; + + if ((tunnelState === 'connecting' || tunnelState === 'connected') && details) { + relayIp = details.address; + relayPort = details.tunnel.openvpn.port; + relayProtocol = details.tunnel.openvpn.protocol; + } + return ( <View style={styles.connect}> <View style={styles.map}> @@ -143,19 +156,23 @@ export default class Connect extends Component<Props> { </View> <View style={styles.container}> {/* show spinner when connecting */} - {this.props.connection.status.state === 'connecting' ? ( + {tunnelState === 'connecting' ? ( <View style={styles.status_icon}> <Img source="icon-spinner" height={60} width={60} alt="" /> </View> ) : null} <TunnelControl - style={styles.tunnel_control} tunnelState={this.props.connection.status.state} selectedRelayName={this.props.selectedRelayName} city={this.props.connection.city} country={this.props.connection.country} hostname={this.props.connection.hostname} + relayIp={relayIp} + relayPort={relayPort} + relayProtocol={relayProtocol} + outIpv4={this.props.connection.ip} + outIpv6={null} onConnect={this.props.onConnect} onDisconnect={this.props.onDisconnect} onSelectLocation={this.props.onSelectLocation} @@ -220,144 +237,190 @@ type TunnelControlProps = { city: ?string, country: ?string, hostname: ?string, + relayIp: ?string, + relayPort: ?number, + relayProtocol: ?string, + outIpv4: ?string, + outIpv6: ?string, onConnect: () => void, onDisconnect: () => void, onSelectLocation: () => void, - style: Types.ViewStyleRuleSet, }; -export function TunnelControl(props: TunnelControlProps) { - const Location = ({ children }) => <View style={styles.status_location}>{children}</View>; - const City = () => <Text style={styles.status_location_text}>{props.city}</Text>; - const Country = () => <Text style={styles.status_location_text}>{props.country}</Text>; - const Hostname = () => <Text style={styles.status_hostname}>{props.hostname || ''}</Text>; +type TunnelControlState = { + showConnectionInfo: boolean, +}; - const SwitchLocation = () => { - return ( +class TunnelControl extends Component<TunnelControlProps, TunnelControlState> { + state = { + showConnectionInfo: false, + }; + + render() { + const Location = ({ children }) => <View style={styles.status_location}>{children}</View>; + const City = () => <Text style={styles.status_location_text}>{this.props.city}</Text>; + const Country = () => <Text style={styles.status_location_text}>{this.props.country}</Text>; + const Hostname = () => <Text style={styles.status_hostname}>{this.props.hostname || ''}</Text>; + + const SwitchLocation = () => { + return ( + <AppButton.TransparentButton + style={styles.switch_location_button} + onPress={this.props.onSelectLocation}> + <AppButton.Label>{'Switch location'}</AppButton.Label> + </AppButton.TransparentButton> + ); + }; + + const SelectedLocation = () => ( <AppButton.TransparentButton style={styles.switch_location_button} - onPress={props.onSelectLocation}> - <AppButton.Label>{'Switch location'}</AppButton.Label> + onPress={this.props.onSelectLocation}> + <AppButton.Label>{this.props.selectedRelayName}</AppButton.Label> + <Img height={12} width={7} source="icon-chevron" /> </AppButton.TransparentButton> ); - }; - const SelectedLocation = () => ( - <AppButton.TransparentButton - style={styles.switch_location_button} - onPress={props.onSelectLocation}> - <AppButton.Label>{props.selectedRelayName}</AppButton.Label> - <Img height={12} width={7} source="icon-chevron" /> - </AppButton.TransparentButton> - ); + const Connect = () => ( + <AppButton.GreenButton onPress={this.props.onConnect}> + {'Secure my connection'} + </AppButton.GreenButton> + ); - const Connect = () => ( - <AppButton.GreenButton onPress={props.onConnect}> - {'Secure my connection'} - </AppButton.GreenButton> - ); + const Disconnect = () => ( + <AppButton.RedTransparentButton onPress={this.props.onDisconnect}> + {'Disconnect'} + </AppButton.RedTransparentButton> + ); - const Disconnect = () => ( - <AppButton.RedTransparentButton onPress={props.onDisconnect}> - {'Disconnect'} - </AppButton.RedTransparentButton> - ); + const Cancel = () => ( + <AppButton.RedTransparentButton onPress={this.props.onDisconnect}> + {'Cancel'} + </AppButton.RedTransparentButton> + ); - const Cancel = () => ( - <AppButton.RedTransparentButton onPress={props.onDisconnect}> - {'Cancel'} - </AppButton.RedTransparentButton> - ); + const Secured = ({ displayStyle }) => ( + <SecuredLabel style={styles.status_security} displayStyle={displayStyle} /> + ); + const Footer = ({ children }) => <View style={styles.footer}>{children}</View>; - const Secured = ({ displayStyle }) => ( - <SecuredLabel style={styles.status_security} displayStyle={displayStyle} /> - ); - const Wrapper = ({ children }) => <View style={props.style}>{children}</View>; - const Body = ({ children }) => <View style={styles.body}>{children}</View>; - const Footer = ({ children }) => <View style={styles.footer}>{children}</View>; + const connectionInfoProps = { + inAddress: { + ip: this.props.relayIp, + port: this.props.relayPort, + protocol: this.props.relayProtocol, + }, + outAddress: { + ipv4: this.props.outIpv4, + ipv6: this.props.outIpv6, + }, + startExpanded: this.state.showConnectionInfo, + onToggle: (expanded) => { + this.setState({ showConnectionInfo: expanded }); + }, + }; - switch (props.tunnelState) { - case 'connecting': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.securing} /> - <Location> - <City /> - <Country /> - </Location> - <Hostname /> - </Body> - <Footer> - <SwitchLocation /> - <Cancel /> - </Footer> - </Wrapper> - ); - case 'connected': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.secured} /> - <Location> - <City /> - <Country /> - </Location> - <Hostname /> - </Body> - <Footer> - <SwitchLocation /> - <Disconnect /> - </Footer> - </Wrapper> - ); + switch (this.props.tunnelState) { + case 'connecting': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.securing} /> + <Location> + <City /> + <Country /> + </Location> + <Hostname /> + <ConnectionInfo {...connectionInfoProps} /> + </Body> + <Footer> + <SwitchLocation /> + <Cancel /> + </Footer> + </Wrapper> + ); + case 'connected': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.secured} /> + <Location> + <City /> + <Country /> + </Location> + <Hostname /> + <ConnectionInfo {...connectionInfoProps} /> + </Body> + <Footer> + <SwitchLocation /> + <Disconnect /> + </Footer> + </Wrapper> + ); - case 'blocked': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.blocked} /> - </Body> - <Footer> - <SwitchLocation /> - <Cancel /> - </Footer> - </Wrapper> - ); + case 'blocked': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.blocked} /> + </Body> + <Footer> + <SwitchLocation /> + <Cancel /> + </Footer> + </Wrapper> + ); - case 'disconnecting': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.secured} /> - <Location> - <Country /> - </Location> - </Body> - <Footer> - <SelectedLocation /> - <Connect /> - </Footer> - </Wrapper> - ); + case 'disconnecting': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.secured} /> + <Location> + <Country /> + </Location> + </Body> + <Footer> + <SelectedLocation /> + <Connect /> + </Footer> + </Wrapper> + ); - case 'disconnected': - return ( - <Wrapper> - <Body> - <Secured displayStyle={SecuredDisplayStyle.unsecured} /> - <Location> - <Country /> - </Location> - </Body> - <Footer> - <SelectedLocation /> - <Connect /> - </Footer> - </Wrapper> - ); + case 'disconnected': + return ( + <Wrapper> + <Body> + <Secured displayStyle={SecuredDisplayStyle.unsecured} /> + <Location> + <Country /> + </Location> + </Body> + <Footer> + <SelectedLocation /> + <Connect /> + </Footer> + </Wrapper> + ); + + default: + throw new Error(`Unknown TunnelState: ${(this.props.tunnelState: empty)}`); + } + } +} + +type ContainerProps = { + children?: Types.ReactNode, +}; - default: - throw new Error(`Unknown TunnelState: ${(props.tunnelState: empty)}`); +class Wrapper extends Component<ContainerProps> { + render() { + return <View style={styles.tunnel_control}>{this.props.children}</View>; + } +} + +class Body extends Component<ContainerProps> { + render() { + return <View style={styles.body}>{this.props.children}</View>; } } diff --git a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js index 572954b26b..594a33fdf6 100644 --- a/gui/packages/desktop/src/renderer/lib/daemon-rpc.js +++ b/gui/packages/desktop/src/renderer/lib/daemon-rpc.js @@ -25,6 +25,7 @@ export type AccountData = { expiry: string }; export type AccountToken = string; export type Ip = string; export type Location = { + ip: ?string, country: string, city: ?string, latitude: number, @@ -33,6 +34,7 @@ export type Location = { hostname: ?string, }; const LocationSchema = object({ + ip: maybe(string), country: string, city: maybe(string), latitude: number, @@ -55,8 +57,22 @@ export type AfterDisconnect = 'nothing' | 'block' | 'reconnect'; export type TunnelState = 'connecting' | 'connected' | 'disconnecting' | 'disconnected' | 'blocked'; +export type TunnelEndpoint = { + address: string, + tunnel: TunnelEndpointData, +}; + +export type TunnelEndpointData = { + openvpn: { + port: number, + protocol: RelayProtocol, + }, +}; + export type TunnelStateTransition = - | { state: 'disconnected' | 'connecting' | 'connected' } + | { state: 'disconnected' } + | { state: 'connecting', details: ?TunnelEndpoint } + | { state: 'connected', details: TunnelEndpoint } | { state: 'disconnecting', details: AfterDisconnect } | { state: 'blocked', details: BlockReason }; @@ -91,12 +107,7 @@ type RelaySettingsNormal<TTunnelConstraints> = { // types describing the structure of RelaySettings export type RelaySettingsCustom = { host: string, - tunnel: { - openvpn: { - port: number, - protocol: RelayProtocol, - }, - }, + tunnel: TunnelEndpointData, }; export type RelaySettings = | {| @@ -127,6 +138,13 @@ const constraint = <T>(constraintValue: SchemaNode<T>) => { ); }; +const TunnelEndpointDataSchema = object({ + openvpn: object({ + port: number, + protocol: enumeration('udp', 'tcp'), + }), +}); + const RelaySettingsSchema = oneOf( object({ normal: object({ @@ -156,12 +174,7 @@ const RelaySettingsSchema = oneOf( object({ custom_tunnel_endpoint: object({ host: string, - tunnel: object({ - openvpn: object({ - port: number, - protocol: enumeration('udp', 'tcp'), - }), - }), + tunnel: TunnelEndpointDataSchema, }), }), ); @@ -240,6 +253,13 @@ const TunnelStateTransitionSchema = oneOf( details: enumeration('nothing', 'block', 'reconnect'), }), object({ + state: enumeration('connecting', 'connected'), + details: object({ + address: string, + tunnel: TunnelEndpointDataSchema, + }), + }), + object({ state: enumeration('blocked'), details: oneOf( object({ diff --git a/gui/packages/desktop/src/renderer/redux/connection/actions.js b/gui/packages/desktop/src/renderer/redux/connection/actions.js index 668eb2a67b..6f8a297678 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/actions.js +++ b/gui/packages/desktop/src/renderer/redux/connection/actions.js @@ -1,13 +1,15 @@ // @flow -import type { AfterDisconnect, BlockReason } from '../../lib/daemon-rpc'; +import type { AfterDisconnect, BlockReason, TunnelEndpoint } from '../../lib/daemon-rpc'; type ConnectingAction = { type: 'CONNECTING', + tunnelEndpoint: ?TunnelEndpoint, }; type ConnectedAction = { type: 'CONNECTED', + tunnelEndpoint: TunnelEndpoint, }; type DisconnectedAction = { @@ -54,15 +56,17 @@ export type ConnectionAction = | OnlineAction | OfflineAction; -function connecting(): ConnectingAction { +function connecting(tunnelEndpoint: ?TunnelEndpoint): ConnectingAction { return { type: 'CONNECTING', + tunnelEndpoint, }; } -function connected(): ConnectedAction { +function connected(tunnelEndpoint: TunnelEndpoint): ConnectedAction { return { type: 'CONNECTED', + tunnelEndpoint, }; } diff --git a/gui/packages/desktop/src/renderer/redux/connection/reducers.js b/gui/packages/desktop/src/renderer/redux/connection/reducers.js index b90f285e4f..622663dc14 100644 --- a/gui/packages/desktop/src/renderer/redux/connection/reducers.js +++ b/gui/packages/desktop/src/renderer/redux/connection/reducers.js @@ -32,10 +32,10 @@ export default function( return { ...state, ...action.newLocation }; case 'CONNECTING': - return { ...state, status: { state: 'connecting' } }; + return { ...state, status: { state: 'connecting', details: action.tunnelEndpoint } }; case 'CONNECTED': - return { ...state, status: { state: 'connected' } }; + return { ...state, status: { state: 'connected', details: action.tunnelEndpoint } }; case 'DISCONNECTED': return { ...state, status: { state: 'disconnected' } }; diff --git a/mullvad-cli/src/cmds/status.rs b/mullvad-cli/src/cmds/status.rs index 11e436d34a..d25a591726 100644 --- a/mullvad-cli/src/cmds/status.rs +++ b/mullvad-cli/src/cmds/status.rs @@ -31,8 +31,9 @@ impl Command for Status { for new_state in rpc.new_state_subscribe()? { print_state(&new_state); - if new_state == Connected || new_state == Disconnected { - print_location(&mut rpc)?; + match new_state { + Connected(_) | Disconnected => print_location(&mut rpc)?, + _ => {} } } } @@ -44,8 +45,8 @@ fn print_state(state: &TunnelStateTransition) { print!("Tunnel status: "); match state { Blocked(reason) => println!("Blocked ({})", reason), - Connected => println!("Connected"), - Connecting => println!("Connecting..."), + Connected(_) => println!("Connected"), + Connecting(_) => println!("Connecting..."), Disconnected => println!("Disconnected"), Disconnecting(_) => println!("Disconnecting..."), } diff --git a/mullvad-daemon/src/geoip.rs b/mullvad-daemon/src/geoip.rs index 01f42ec6c9..2faed344a6 100644 --- a/mullvad-daemon/src/geoip.rs +++ b/mullvad-daemon/src/geoip.rs @@ -21,13 +21,13 @@ error_chain! { pub fn send_location_request( request_sender: mullvad_rpc::rest::RequestSender, -) -> Box<Future<Item = GeoIpLocation, Error = Error>> { +) -> impl Future<Item = GeoIpLocation, Error = Error> { let (response_tx, response_rx) = futures::sync::oneshot::channel(); let request = mullvad_rpc::rest::create_get_request(URI.parse().unwrap()); - let future = futures::Sink::send(request_sender.clone(), (request, response_tx)) + + futures::Sink::send(request_sender, (request, response_tx)) .map_err(|e| Error::with_chain(e, ErrorKind::NoResponse)) .and_then(|_| response_rx.map_err(|e| Error::with_chain(e, ErrorKind::NoResponse))) .and_then(|response_result| response_result.map_err(Error::from)) - .and_then(|response| serde_json::from_slice(&response).map_err(Error::from)); - Box::new(future) + .and_then(|response| serde_json::from_slice(&response).map_err(Error::from)) } diff --git a/mullvad-daemon/src/lib.rs b/mullvad-daemon/src/lib.rs index ad62336a38..e88c4cd0e1 100644 --- a/mullvad-daemon/src/lib.rs +++ b/mullvad-daemon/src/lib.rs @@ -45,6 +45,7 @@ mod rpc_uniqueness_check; use error_chain::ChainedError; use futures::{ + future, sync::{mpsc::UnboundedSender, oneshot}, Future, Sink, }; @@ -444,36 +445,58 @@ impl Daemon { fn on_get_current_location(&self, tx: oneshot::Sender<GeoIpLocation>) { use self::TunnelStateTransition::*; - match self.tunnel_state { - Disconnected => { - let https_handle = self.https_handle.clone(); - self.tokio_remote.spawn(move |_| { - geoip::send_location_request(https_handle) - .map(move |location| Self::oneshot_send(tx, location, "current location")) - .map_err(|e| { - warn!("Unable to fetch GeoIP location: {}", e.display_chain()); - }) - }); - } - Connecting | Connected | Disconnecting(..) => { - if let Some(ref relay) = self.last_generated_relay { - let location = relay.location.as_ref().cloned().unwrap(); - let hostname = relay.hostname.clone(); - let geo_ip_location = GeoIpLocation { - country: location.country, - city: Some(location.city), - latitude: location.latitude, - longitude: location.longitude, - mullvad_exit_ip: true, - hostname: Some(hostname), - }; - Self::oneshot_send(tx, geo_ip_location, "current location"); + let get_location: Box<dyn Future<Item = GeoIpLocation, Error = ()> + Send> = + match self.tunnel_state { + Disconnected => Box::new(self.get_geo_location()), + Connecting(_) | Disconnecting(..) => { + Box::new(future::result(Ok(self.build_location_from_relay()))) } - } - Blocked(..) => { - // We are not online at all at this stage. Return error. - mem::drop(tx); - } + Connected(_) => { + let location_from_relay = self.build_location_from_relay(); + Box::new( + self.get_geo_location() + .map(|fetched_location| GeoIpLocation { + ip: fetched_location.ip, + ..location_from_relay + }), + ) + } + Blocked(..) => { + // We are not online at all at this stage. Return error. + mem::drop(tx); + return; + } + }; + + self.tokio_remote.spawn(move |_| { + get_location.map(|location| Self::oneshot_send(tx, location, "current location")) + }); + } + + fn get_geo_location(&self) -> impl Future<Item = GeoIpLocation, Error = ()> { + let https_handle = self.https_handle.clone(); + + geoip::send_location_request(https_handle).map_err(|e| { + warn!("Unable to fetch GeoIP location: {}", e.display_chain()); + }) + } + + fn build_location_from_relay(&self) -> GeoIpLocation { + let relay = self + .last_generated_relay + .as_ref() + .expect("Can't build location from relay in disconnected state"); + let location = relay.location.as_ref().cloned().unwrap(); + let hostname = relay.hostname.clone(); + + GeoIpLocation { + ip: None, + country: location.country, + city: Some(location.city), + latitude: location.latitude, + longitude: location.longitude, + mullvad_exit_ip: true, + hostname: Some(hostname), } } diff --git a/mullvad-types/src/location.rs b/mullvad-types/src/location.rs index 57d1e31269..2c420fafc5 100644 --- a/mullvad-types/src/location.rs +++ b/mullvad-types/src/location.rs @@ -1,3 +1,5 @@ +use std::net::IpAddr; + pub type CountryCode = String; pub type CityCode = String; pub type Hostname = String; @@ -14,6 +16,7 @@ pub struct Location { #[derive(Debug, Serialize, Deserialize)] pub struct GeoIpLocation { + pub ip: Option<IpAddr>, pub country: String, pub city: Option<String>, pub latitude: f64, diff --git a/talpid-core/src/tunnel_state_machine/connected_state.rs b/talpid-core/src/tunnel_state_machine/connected_state.rs index a969ece39f..d648b43ec4 100644 --- a/talpid-core/src/tunnel_state_machine/connected_state.rs +++ b/talpid-core/src/tunnel_state_machine/connected_state.rs @@ -149,12 +149,13 @@ impl TunnelState for ConnectedState { shared_values: &mut SharedTunnelStateValues, bootstrap: Self::Bootstrap, ) -> (TunnelStateWrapper, TunnelStateTransition) { + let tunnel_endpoint = bootstrap.tunnel_parameters.endpoint; let connected_state = ConnectedState::from(bootstrap); match connected_state.set_security_policy(shared_values) { Ok(()) => ( TunnelStateWrapper::from(connected_state), - TunnelStateTransition::Connected, + TunnelStateTransition::Connected(tunnel_endpoint), ), Err(error) => { error!("{}", error.display_chain()); diff --git a/talpid-core/src/tunnel_state_machine/connecting_state.rs b/talpid-core/src/tunnel_state_machine/connecting_state.rs index 1e0880141e..f2ce3e0b4e 100644 --- a/talpid-core/src/tunnel_state_machine/connecting_state.rs +++ b/talpid-core/src/tunnel_state_machine/connecting_state.rs @@ -283,9 +283,9 @@ impl TunnelState for ConnectingState { { None => BlockedState::enter(shared_values, BlockReason::NoMatchingRelay), Some(tunnel_parameters) => { - if let Err(error) = - Self::set_security_policy(shared_values, tunnel_parameters.endpoint) - { + let tunnel_endpoint = tunnel_parameters.endpoint; + + if let Err(error) = Self::set_security_policy(shared_values, tunnel_endpoint) { error!("{}", error.display_chain()); BlockedState::enter(shared_values, BlockReason::StartTunnelError) } else { @@ -297,7 +297,7 @@ impl TunnelState for ConnectingState { ) { Ok(connecting_state) => ( TunnelStateWrapper::from(connecting_state), - TunnelStateTransition::Connecting, + TunnelStateTransition::Connecting(tunnel_endpoint), ), Err(error) => { let block_reason = match *error.kind() { diff --git a/talpid-types/src/net.rs b/talpid-types/src/net.rs index e00d0d9fe4..e871e7a93e 100644 --- a/talpid-types/src/net.rs +++ b/talpid-types/src/net.rs @@ -4,7 +4,7 @@ use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; /// Represents one tunnel endpoint. Address, plus extra parameters specific to tunnel protocol. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct TunnelEndpoint { pub address: IpAddr, pub tunnel: TunnelEndpointData, diff --git a/talpid-types/src/tunnel.rs b/talpid-types/src/tunnel.rs index 3d911f640a..cfe1472cdf 100644 --- a/talpid-types/src/tunnel.rs +++ b/talpid-types/src/tunnel.rs @@ -1,5 +1,7 @@ use std::fmt; +use super::net::TunnelEndpoint; + /// Event resulting from a transition to a new tunnel state. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] @@ -8,9 +10,9 @@ pub enum TunnelStateTransition { /// No connection is established and network is unsecured. Disconnected, /// Network is secured but tunnel is still connecting. - Connecting, + Connecting(TunnelEndpoint), /// Tunnel is connected. - Connected, + Connected(TunnelEndpoint), /// Disconnecting tunnel. Disconnecting(ActionAfterDisconnect), /// Tunnel is disconnected but secured by blocking all connections. |
