Skip to content

Commit 3810bae

Browse files
committed
feat: add IP version preference support in configuration and FFI types
1 parent 5e4bf18 commit 3810bae

File tree

5 files changed

+236
-37
lines changed

5 files changed

+236
-37
lines changed

include/SoftEtherVPN.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ typedef enum {
4848
SOFTETHER_STATE_ERROR = 7,
4949
} SoftEtherState;
5050

51+
// =============================================================================
52+
// IP Version Preference
53+
// =============================================================================
54+
55+
typedef enum {
56+
SOFTETHER_IP_AUTO = 0, // Auto: Try both IPv4 and IPv6 (IPv4 required, IPv6 optional)
57+
SOFTETHER_IP_V4_ONLY = 1, // IPv4 only: Skip DHCPv6
58+
SOFTETHER_IP_V6_ONLY = 2, // IPv6 only: Skip IPv4 DHCP
59+
} SoftEtherIpVersion;
60+
5161
// =============================================================================
5262
// Configuration
5363
// =============================================================================
@@ -67,8 +77,10 @@ typedef struct {
6777

6878
// Connection Settings
6979
unsigned int max_connections; // Max TCP connections (1-32, default 1)
80+
int half_connection; // Half-connection mode (1 = yes, 0 = no). Requires max_connections >= 2
7081
unsigned int timeout_seconds; // Connection timeout in seconds (default 30)
7182
unsigned int mtu; // MTU size (576-1500, default 1400)
83+
SoftEtherIpVersion ip_version; // IP version preference (default: Auto)
7284

7385
// Protocol Features
7486
int use_encrypt; // Use RC4 encryption within TLS (1 = yes, 0 = no)

src/config.rs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,36 @@ use std::net::Ipv4Addr;
55
use std::path::Path;
66
use std::time::Duration;
77

8+
/// IP version preference for DHCP configuration.
9+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
10+
#[serde(rename_all = "lowercase")]
11+
pub enum IpVersion {
12+
/// Auto-detect: Try both IPv4 (DHCP) and IPv6 (DHCPv6).
13+
/// IPv4 is required, IPv6 is optional.
14+
#[default]
15+
Auto,
16+
/// IPv4 only: Only perform DHCP for IPv4 address.
17+
/// Skip DHCPv6 entirely.
18+
#[serde(rename = "ipv4")]
19+
IPv4Only,
20+
/// IPv6 only: Only perform DHCPv6 for IPv6 address.
21+
/// Skip IPv4 DHCP entirely.
22+
#[serde(rename = "ipv6")]
23+
IPv6Only,
24+
}
25+
26+
impl IpVersion {
27+
/// Check if IPv4 DHCP should be attempted.
28+
pub fn wants_ipv4(&self) -> bool {
29+
matches!(self, IpVersion::Auto | IpVersion::IPv4Only)
30+
}
31+
32+
/// Check if IPv6 DHCPv6 should be attempted.
33+
pub fn wants_ipv6(&self) -> bool {
34+
matches!(self, IpVersion::Auto | IpVersion::IPv6Only)
35+
}
36+
}
37+
838
/// VPN client configuration.
939
#[derive(Debug, Clone, Serialize, Deserialize)]
1040
#[serde(default)]
@@ -102,6 +132,16 @@ pub struct VpnConfig {
102132
/// Used for packet handling, not sent to server.
103133
pub mtu: u16,
104134

135+
// ─────────────────────────────────────────────────────────────────────────
136+
// IP Version
137+
// ─────────────────────────────────────────────────────────────────────────
138+
/// IP version preference for DHCP (default: Auto).
139+
/// - auto: Try both IPv4 and IPv6 (IPv4 required, IPv6 optional)
140+
/// - ipv4: IPv4 only (skip DHCPv6)
141+
/// - ipv6: IPv6 only (skip IPv4 DHCP)
142+
#[serde(default)]
143+
pub ip_version: IpVersion,
144+
105145
// ─────────────────────────────────────────────────────────────────────────
106146
// Routing
107147
// ─────────────────────────────────────────────────────────────────────────
@@ -318,6 +358,8 @@ impl Default for VpnConfig {
318358
max_connections: 1,
319359
half_connection: false,
320360
mtu: 1400,
361+
// IP Version
362+
ip_version: IpVersion::Auto,
321363
// Routing
322364
routing: RoutingConfig::default(),
323365
// Static IP

src/ffi/android.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ pub extern "system" fn Java_com_worxvpn_app_vpn_SoftEtherBridge_nativeCreate(
379379
half_connection: 0, // Android doesn't expose this yet, default to false
380380
timeout_seconds: timeout_seconds as u32,
381381
mtu: mtu as u32,
382+
ip_version: SoftEtherIpVersion::Auto, // Android doesn't expose this yet, default to Auto
382383
use_encrypt: if use_encrypt != 0 { 1 } else { 0 },
383384
use_compress: if use_compress != 0 { 1 } else { 0 },
384385
udp_accel: if udp_accel != 0 { 1 } else { 0 },

src/ffi/client.rs

Lines changed: 149 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ pub unsafe extern "C" fn softether_create(
309309
half_connection: config.half_connection != 0,
310310
timeout_seconds: config.timeout_seconds.max(5) as u64,
311311
mtu: config.mtu.clamp(576, 1500) as u16,
312+
ip_version: config.ip_version.into(),
312313
use_encrypt: config.use_encrypt != 0,
313314
use_compress: config.use_compress != 0,
314315
udp_accel: config.udp_accel != 0,
@@ -819,25 +820,111 @@ async fn connect_and_run_inner(
819820
// established yet. DHCP needs to both send and receive on the same connection.
820821
let original_direction = conn_mgr.enable_primary_bidirectional();
821822

822-
let dhcp_config = match perform_dhcp(&mut conn_mgr, mac, callbacks, config.use_compress).await {
823-
Ok(config) => {
823+
// Perform DHCP based on ip_version setting
824+
let (dhcp_config, dhcpv6_config) = match config.ip_version {
825+
crate::config::IpVersion::Auto => {
826+
// Auto: Try IPv4 DHCP (required), then try DHCPv6 (optional)
824827
log_message(
825828
callbacks,
826829
1,
827-
&format!(
828-
"[RUST] DHCP complete: IP={}, Gateway={:?}, DNS={:?}",
829-
config.ip, config.gateway, config.dns1
830-
),
830+
"[RUST] IP version: Auto (IPv4 required, IPv6 optional)",
831831
);
832-
config
832+
let dhcp = match perform_dhcp(&mut conn_mgr, mac, callbacks, config.use_compress).await
833+
{
834+
Ok(cfg) => {
835+
log_message(
836+
callbacks,
837+
1,
838+
&format!(
839+
"[RUST] DHCP complete: IP={}, Gateway={:?}, DNS={:?}",
840+
cfg.ip, cfg.gateway, cfg.dns1
841+
),
842+
);
843+
cfg
844+
}
845+
Err(e) => {
846+
if let Some(dir) = original_direction {
847+
conn_mgr.restore_primary_direction(dir);
848+
}
849+
log_message(callbacks, 3, &format!("[RUST] DHCP failed: {e}"));
850+
return Err(e);
851+
}
852+
};
853+
854+
// Try DHCPv6 (optional)
855+
log_message(callbacks, 1, "[RUST] Attempting DHCPv6 for IPv6 address...");
856+
let dhcpv6 = perform_dhcpv6(&mut conn_mgr, mac, callbacks, config.use_compress).await;
857+
if dhcpv6.is_some() {
858+
log_message(
859+
callbacks,
860+
1,
861+
"[RUST] DHCPv6 successful - dual-stack configured",
862+
);
863+
} else {
864+
log_message(callbacks, 1, "[RUST] DHCPv6 not available - IPv4 only");
865+
}
866+
867+
(Some(dhcp), dhcpv6)
833868
}
834-
Err(e) => {
835-
// Restore direction before returning error
836-
if let Some(dir) = original_direction {
837-
conn_mgr.restore_primary_direction(dir);
869+
crate::config::IpVersion::IPv4Only => {
870+
// IPv4 only: Only perform DHCP, skip DHCPv6
871+
log_message(
872+
callbacks,
873+
1,
874+
"[RUST] IP version: IPv4 only (skipping DHCPv6)",
875+
);
876+
let dhcp = match perform_dhcp(&mut conn_mgr, mac, callbacks, config.use_compress).await
877+
{
878+
Ok(cfg) => {
879+
log_message(
880+
callbacks,
881+
1,
882+
&format!(
883+
"[RUST] DHCP complete: IP={}, Gateway={:?}, DNS={:?}",
884+
cfg.ip, cfg.gateway, cfg.dns1
885+
),
886+
);
887+
cfg
888+
}
889+
Err(e) => {
890+
if let Some(dir) = original_direction {
891+
conn_mgr.restore_primary_direction(dir);
892+
}
893+
log_message(callbacks, 3, &format!("[RUST] DHCP failed: {e}"));
894+
return Err(e);
895+
}
896+
};
897+
(Some(dhcp), None)
898+
}
899+
crate::config::IpVersion::IPv6Only => {
900+
// IPv6 only: Only perform DHCPv6, skip DHCP
901+
log_message(
902+
callbacks,
903+
1,
904+
"[RUST] IP version: IPv6 only (skipping IPv4 DHCP)",
905+
);
906+
let dhcpv6 = perform_dhcpv6(&mut conn_mgr, mac, callbacks, config.use_compress).await;
907+
match dhcpv6 {
908+
Some(cfg) => {
909+
log_message(
910+
callbacks,
911+
1,
912+
&format!("[RUST] DHCPv6 complete: IPv6 address obtained"),
913+
);
914+
(None, Some(cfg))
915+
}
916+
None => {
917+
if let Some(dir) = original_direction {
918+
conn_mgr.restore_primary_direction(dir);
919+
}
920+
log_message(
921+
callbacks,
922+
3,
923+
"[RUST] DHCPv6 failed - no IPv6 address available",
924+
);
925+
return Err(crate::error::Error::DhcpFailed("DHCPv6 failed".into()));
926+
}
838927
}
839-
log_message(callbacks, 3, &format!("[RUST] DHCP failed: {e}"));
840-
return Err(e);
841928
}
842929
};
843930

@@ -883,22 +970,9 @@ async fn connect_and_run_inner(
883970
);
884971
}
885972

886-
// Try DHCPv6 for IPv6 address (optional - doesn't fail if server doesn't support it)
887-
log_message(callbacks, 1, "[RUST] Attempting DHCPv6 for IPv6 address...");
888-
let dhcpv6_config = perform_dhcpv6(&mut conn_mgr, mac, callbacks, config.use_compress).await;
889-
if dhcpv6_config.is_some() {
890-
log_message(
891-
callbacks,
892-
1,
893-
"[RUST] DHCPv6 successful - dual-stack configured",
894-
);
895-
} else {
896-
log_message(callbacks, 1, "[RUST] DHCPv6 not available - IPv4 only");
897-
}
898-
899973
// Create session info from DHCP config (include MAC for Kotlin to use)
900974
let session = create_session_from_dhcp(
901-
&dhcp_config,
975+
dhcp_config.as_ref(),
902976
dhcpv6_config.as_ref(),
903977
actual_server_ip,
904978
server_ip,
@@ -912,12 +986,19 @@ async fn connect_and_run_inner(
912986
}
913987
update_state(atomic_state, callbacks, SoftEtherState::Connected);
914988

989+
// Log connection info
990+
let ip_info = match (&dhcp_config, &dhcpv6_config) {
991+
(Some(v4), Some(_v6)) => format!("IPv4: {}, IPv6: configured", v4.ip),
992+
(Some(v4), None) => format!("IPv4: {}", v4.ip),
993+
(None, Some(v6)) => format!("IPv6: {}", v6.ip),
994+
(None, None) => "No IP configured".to_string(),
995+
};
915996
log_message(
916997
callbacks,
917998
1,
918999
&format!(
919-
"[RUST] Connected! IP: {}, Server: {}",
920-
dhcp_config.ip, actual_server_ip
1000+
"[RUST] Connected! {}, Server: {}",
1001+
ip_info, actual_server_ip
9211002
),
9221003
);
9231004

@@ -982,14 +1063,31 @@ async fn connect_and_run_inner(
9821063
};
9831064

9841065
// Run the packet loop
1066+
// Note: packet loop requires DhcpConfig for ARP. IPv6-only mode needs a dummy config.
1067+
let dhcp_for_loop = dhcp_config.unwrap_or_else(|| {
1068+
// For IPv6-only mode, create a dummy DhcpConfig
1069+
// ARP won't work, but IPv6 NDP will handle neighbor discovery
1070+
DhcpConfig {
1071+
ip: std::net::Ipv4Addr::UNSPECIFIED,
1072+
netmask: std::net::Ipv4Addr::UNSPECIFIED,
1073+
gateway: None,
1074+
dns1: None,
1075+
dns2: None,
1076+
server_id: None,
1077+
lease_time: 0,
1078+
renewal_time: 0,
1079+
rebinding_time: 0,
1080+
domain_name: String::new(),
1081+
}
1082+
});
9851083
log_message(callbacks, 1, "[RUST] Starting packet loop...");
9861084
run_packet_loop(
9871085
&mut conn_mgr,
9881086
running,
9891087
callbacks.clone(),
9901088
tx_recv,
9911089
mac,
992-
dhcp_config,
1090+
dhcp_for_loop,
9931091
final_auth.rc4_key_pair.as_ref(),
9941092
config.qos,
9951093
config.use_compress,
@@ -999,9 +1097,10 @@ async fn connect_and_run_inner(
9991097
.await
10001098
}
10011099

1002-
/// Create session info from DHCP and optional DHCPv6 config
1100+
/// Create session info from optional DHCP and optional DHCPv6 config.
1101+
/// At least one of dhcp or dhcpv6 must be Some.
10031102
fn create_session_from_dhcp(
1004-
dhcp: &DhcpConfig,
1103+
dhcp: Option<&DhcpConfig>,
10051104
dhcpv6: Option<&Dhcpv6Config>,
10061105
server_ip: Ipv4Addr,
10071106
original_server_ip: Ipv4Addr,
@@ -1031,6 +1130,19 @@ fn create_session_from_dhcp(
10311130
| (octets[3] as u32)
10321131
}
10331132

1133+
// Extract IPv4 info if available
1134+
let (ip_address, subnet_mask, gateway, dns1, dns2) = if let Some(v4) = dhcp {
1135+
(
1136+
ip_to_u32(v4.ip),
1137+
ip_to_u32(v4.netmask),
1138+
v4.gateway.map(ip_to_u32).unwrap_or(0),
1139+
v4.dns1.map(ip_to_u32).unwrap_or(0),
1140+
v4.dns2.map(ip_to_u32).unwrap_or(0),
1141+
)
1142+
} else {
1143+
(0, 0, 0, 0, 0)
1144+
};
1145+
10341146
// Extract IPv6 info if available
10351147
let (ipv6_address, ipv6_prefix_len, dns1_v6, dns2_v6) = if let Some(v6) = dhcpv6 {
10361148
(
@@ -1044,11 +1156,11 @@ fn create_session_from_dhcp(
10441156
};
10451157

10461158
SoftEtherSession {
1047-
ip_address: ip_to_u32(dhcp.ip),
1048-
subnet_mask: ip_to_u32(dhcp.netmask),
1049-
gateway: dhcp.gateway.map(ip_to_u32).unwrap_or(0),
1050-
dns1: dhcp.dns1.map(ip_to_u32).unwrap_or(0),
1051-
dns2: dhcp.dns2.map(ip_to_u32).unwrap_or(0),
1159+
ip_address,
1160+
subnet_mask,
1161+
gateway,
1162+
dns1,
1163+
dns2,
10521164
connected_server_ip: server_ip_str,
10531165
original_server_ip: original_ip_str,
10541166
server_version: 0,

0 commit comments

Comments
 (0)