Skip to content

Commit 1216e41

Browse files
committed
feat: Add OpenVPN authentication support (v1.1.0)
Features: - Username/Password authentication UI - TOTP/2FA token support (concatenated with password) - Auto-detection of auth-user-pass in OpenVPN configs - Secure auth file creation with restrictive permissions - Auth file auto-mounted to /etc/openvpn/auth.txt - Config auto-modification to use auth file - Auth panel auto-shows/hides based on VPN type Security: - Credentials stored in memory only (not persisted) - Auth files have 600 permissions - Password cleared from char array after use - Auth files deleted on cleanup Improvements: - Disconnect confirmation dialog - Better UI responsiveness - Professional README - Maker credit (j0lt) - Enhanced error handling
1 parent a38a120 commit 1216e41

File tree

6 files changed

+217
-29
lines changed

6 files changed

+217
-29
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ wireup-config-*.conf
3232
wireup-config-*.conf.lock
3333
client.conf
3434
wg0.conf
35+
RELEASE_NOTES.md

src/main/java/com/wireup/docker/DockerManager.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,18 +133,15 @@ public String createAndStartContainer(com.wireup.vpn.VpnConfig config) throws Ex
133133
String configContent = config.getRawConfig();
134134
String configFileName;
135135
String containerConfigPath;
136-
String containerConfigDir;
137136
String vpnTypeEnv;
138137

139138
if (config.getType() == com.wireup.vpn.VpnConfig.VpnType.OPENVPN) {
140139
configFileName = "client.conf";
141140
containerConfigPath = "/etc/openvpn/client.conf";
142-
containerConfigDir = "/etc/openvpn";
143141
vpnTypeEnv = "openvpn";
144142
} else {
145143
configFileName = "wg0.conf";
146144
containerConfigPath = "/etc/wireguard/wg0.conf";
147-
containerConfigDir = "/etc/wireguard";
148145
vpnTypeEnv = "wireguard";
149146
}
150147

@@ -164,6 +161,34 @@ public String createAndStartContainer(com.wireup.vpn.VpnConfig config) throws Ex
164161

165162
logger.debug(vpnTypeEnv + " config written to temp directory (path redacted for security)");
166163

164+
// Handle OpenVPN authentication if credentials are provided
165+
Bind authBind = null;
166+
if (config.getType() == com.wireup.vpn.VpnConfig.VpnType.OPENVPN) {
167+
com.wireup.vpn.OpenVpnConfig ovpnConfig = (com.wireup.vpn.OpenVpnConfig) config;
168+
if (ovpnConfig.hasCredentials()) {
169+
// Create auth file
170+
File authFile = new File(tempConfigDir.toFile(), "auth.txt");
171+
try (FileWriter authWriter = new FileWriter(authFile)) {
172+
authWriter.write(ovpnConfig.getUsername() + "\n");
173+
authWriter.write(ovpnConfig.getPassword() + "\n");
174+
}
175+
176+
// Set restrictive permissions on auth file
177+
try {
178+
SecurityUtils.setRestrictivePermissions(authFile.toPath());
179+
logger.securityInfo("OpenVPN auth file created with restrictive permissions");
180+
} catch (Exception e) {
181+
logger.warn("Could not set restrictive permissions on auth file: " + e.getMessage());
182+
}
183+
184+
// Create volume binding for auth file
185+
Volume authVolume = new Volume("/etc/openvpn/auth.txt");
186+
authBind = new Bind(authFile.getAbsolutePath(), authVolume);
187+
188+
logger.info("OpenVPN authentication file mounted");
189+
}
190+
}
191+
167192
// Create volume binding for config
168193
Volume configVolume = new Volume(containerConfigPath);
169194
Bind configBind = new Bind(configFile.getAbsolutePath(), configVolume);
@@ -178,13 +203,11 @@ public String createAndStartContainer(com.wireup.vpn.VpnConfig config) throws Ex
178203
// Create host config with all settings
179204
HostConfig hostConfig = HostConfig.newHostConfig()
180205
.withPrivileged(true)
181-
.withPortBindings(portBindings)
182206
.withCapAdd(Capability.NET_ADMIN, Capability.SYS_MODULE)
183-
.withNetworkMode("bridge")
184-
.withBinds(new Bind(configFile.getParentFile().getAbsolutePath(), new Volume(containerConfigDir)))
185-
.withMemory(512L * 1024 * 1024)
186-
.withMemorySwap(512L * 1024 * 1024)
187-
.withCpuQuota(50000L)
207+
.withBinds(authBind != null ? new Bind[] { configBind, authBind } : new Bind[] { configBind })
208+
.withPortBindings(portBindings)
209+
.withMemory(512L * 1024 * 1024) // 512MB RAM limit
210+
.withCpuQuota(50000L) // 0.5 CPU limit
188211
.withPidsLimit(100L)
189212
.withPidsLimit(100L)
190213
// .withDns("8.8.8.8") // COMMENTED OUT: Caused resolution issues with

src/main/java/com/wireup/ui/ConfigPanel.java

Lines changed: 124 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ public class ConfigPanel {
2828
private JRadioButton openVpnButton;
2929
private ButtonGroup protocolGroup;
3030

31+
// OpenVPN authentication fields
32+
private JPanel authPanel;
33+
private JTextField usernameField;
34+
private JPasswordField passwordField;
35+
private JTextField totpField;
36+
private JLabel authStatusLabel;
37+
3138
public ConfigPanel(ConnectionManager connectionManager, Logger logger) {
3239
this.logger = logger;
3340

@@ -57,8 +64,14 @@ private void initializeUI() {
5764
protocolPanel.add(wireGuardButton);
5865
protocolPanel.add(openVpnButton);
5966

60-
// Add listener to re-validate on switch
61-
java.awt.event.ActionListener protocolListener = e -> validateConfig();
67+
// Add listener to re-validate on switch and toggle auth panel
68+
java.awt.event.ActionListener protocolListener = e -> {
69+
validateConfig();
70+
// Show auth panel only for OpenVPN
71+
if (authPanel != null) {
72+
authPanel.setVisible(openVpnButton.isSelected());
73+
}
74+
};
6275
wireGuardButton.addActionListener(protocolListener);
6376
openVpnButton.addActionListener(protocolListener);
6477

@@ -82,10 +95,15 @@ private void initializeUI() {
8295
JScrollPane scrollPane = new JScrollPane(configTextArea);
8396
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
8497

98+
// Authentication Panel (for OpenVPN)
99+
authPanel = createAuthPanel();
100+
authPanel.setVisible(false); // Hidden by default
101+
85102
// Main content wrapper
86103
JPanel centerPanel = new JPanel(new BorderLayout(5, 5));
87104
centerPanel.add(protocolPanel, BorderLayout.NORTH);
88105
centerPanel.add(scrollPane, BorderLayout.CENTER);
106+
centerPanel.add(authPanel, BorderLayout.SOUTH);
89107

90108
panel.add(centerPanel, BorderLayout.CENTER);
91109

@@ -197,6 +215,110 @@ public com.wireup.vpn.VpnConfig.VpnType getVpnType() {
197215
: com.wireup.vpn.VpnConfig.VpnType.OPENVPN;
198216
}
199217

218+
private JPanel createAuthPanel() {
219+
JPanel panel = new JPanel(new GridBagLayout());
220+
panel.setBorder(BorderFactory.createTitledBorder(
221+
BorderFactory.createEtchedBorder(),
222+
"OpenVPN Authentication (Optional)",
223+
TitledBorder.LEFT,
224+
TitledBorder.TOP,
225+
new Font("Arial", Font.BOLD, 11)));
226+
227+
GridBagConstraints gbc = new GridBagConstraints();
228+
gbc.insets = new Insets(5, 5, 5, 5);
229+
gbc.anchor = GridBagConstraints.WEST;
230+
gbc.fill = GridBagConstraints.HORIZONTAL;
231+
232+
Font labelFont = new Font("Arial", Font.PLAIN, 11);
233+
234+
// Username
235+
gbc.gridx = 0;
236+
gbc.gridy = 0;
237+
gbc.weightx = 0;
238+
JLabel userLabel = new JLabel("Username:");
239+
userLabel.setFont(labelFont);
240+
panel.add(userLabel, gbc);
241+
242+
gbc.gridx = 1;
243+
gbc.weightx = 1.0;
244+
usernameField = new JTextField(20);
245+
usernameField.setFont(labelFont);
246+
panel.add(usernameField, gbc);
247+
248+
// Password
249+
gbc.gridx = 0;
250+
gbc.gridy = 1;
251+
gbc.weightx = 0;
252+
JLabel passLabel = new JLabel("Password:");
253+
passLabel.setFont(labelFont);
254+
panel.add(passLabel, gbc);
255+
256+
gbc.gridx = 1;
257+
gbc.weightx = 1.0;
258+
passwordField = new JPasswordField(20);
259+
passwordField.setFont(labelFont);
260+
panel.add(passwordField, gbc);
261+
262+
// TOTP/2FA Token
263+
gbc.gridx = 0;
264+
gbc.gridy = 2;
265+
gbc.weightx = 0;
266+
JLabel totpLabel = new JLabel("2FA/TOTP Token:");
267+
totpLabel.setFont(labelFont);
268+
totpLabel.setToolTipText("Leave empty if not using 2FA");
269+
panel.add(totpLabel, gbc);
270+
271+
gbc.gridx = 1;
272+
gbc.weightx = 1.0;
273+
totpField = new JTextField(10);
274+
totpField.setFont(labelFont);
275+
totpField.setToolTipText("Enter 6-digit code if required");
276+
panel.add(totpField, gbc);
277+
278+
// Status label
279+
gbc.gridx = 0;
280+
gbc.gridy = 3;
281+
gbc.gridwidth = 2;
282+
authStatusLabel = new JLabel("Leave blank if your VPN doesn't require authentication");
283+
authStatusLabel.setFont(new Font("Arial", Font.ITALIC, 10));
284+
authStatusLabel.setForeground(Color.GRAY);
285+
panel.add(authStatusLabel, gbc);
286+
287+
return panel;
288+
}
289+
290+
public String getUsername() {
291+
return usernameField != null ? usernameField.getText().trim() : "";
292+
}
293+
294+
public String getPassword() {
295+
if (passwordField == null)
296+
return "";
297+
298+
char[] pass = passwordField.getPassword();
299+
String password = new String(pass);
300+
301+
// Append TOTP token if provided (some VPNs require password+token)
302+
String totp = getTotpToken();
303+
if (!totp.isEmpty()) {
304+
password = password + totp;
305+
}
306+
307+
// Clear the char array for security
308+
java.util.Arrays.fill(pass, '0');
309+
310+
return password;
311+
}
312+
313+
public String getTotpToken() {
314+
return totpField != null ? totpField.getText().trim() : "";
315+
}
316+
317+
public boolean hasCredentials() {
318+
return getUsername() != null && !getUsername().isEmpty() &&
319+
getPassword() != null && !getPassword().isEmpty();
320+
}
321+
200322
public JPanel getPanel() {
201323
return panel;
202324
}

src/main/java/com/wireup/ui/ControlPanel.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,19 @@ private void onConnect() {
108108
config = new WireGuardConfig(configText);
109109
} else {
110110
config = new com.wireup.vpn.OpenVpnConfig(configText);
111+
112+
// Set credentials if provided for OpenVPN
113+
com.wireup.vpn.OpenVpnConfig ovpnConfig = (com.wireup.vpn.OpenVpnConfig) config;
114+
String username = configPanel.getUsername();
115+
String password = configPanel.getPassword();
116+
117+
if (username != null && !username.isEmpty() &&
118+
password != null && !password.isEmpty()) {
119+
ovpnConfig.setCredentials(username, password);
120+
logger.info("OpenVPN credentials provided for authentication");
121+
} else if (ovpnConfig.requiresAuth() && !ovpnConfig.hasCredentials()) {
122+
logger.warn("OpenVPN config requires authentication but no credentials provided");
123+
}
111124
}
112125

113126
if (!config.isValid()) {

src/main/java/com/wireup/vpn/OpenVpnConfig.java

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,39 @@ public class OpenVpnConfig implements VpnConfig {
1212
private boolean isValid;
1313
private String errorMessage;
1414
private String remoteEndpoint;
15+
private boolean requiresAuth;
16+
17+
// Authentication credentials (optional)
18+
private String username;
19+
private String password;
1520

1621
public OpenVpnConfig(String rawConfig) {
1722
this.rawConfig = rawConfig;
1823
parseAndValidate();
1924
}
2025

26+
public void setCredentials(String username, String password) {
27+
this.username = username;
28+
this.password = password;
29+
}
30+
31+
public boolean requiresAuth() {
32+
return requiresAuth;
33+
}
34+
35+
public String getUsername() {
36+
return username;
37+
}
38+
39+
public String getPassword() {
40+
return password;
41+
}
42+
43+
public boolean hasCredentials() {
44+
return username != null && !username.isEmpty() &&
45+
password != null && !password.isEmpty();
46+
}
47+
2148
private void parseAndValidate() {
2249
if (rawConfig == null || rawConfig.trim().isEmpty()) {
2350
isValid = false;
@@ -59,22 +86,19 @@ private void parseAndValidate() {
5986
// Check for credentials
6087
// We support:
6188
// 1. Inline certificates (<ca>, <cert>, <key>)
62-
// 2. Inline static key (<secret>)
63-
// 3. auth-user-pass (requires separate credentials file, which we don't support
64-
// well yet without UI)
65-
66-
boolean hasInlineCert = rawConfig.contains("<cert>");
67-
boolean hasAuthUserPass = rawConfig.contains("auth-user-pass");
68-
69-
if (hasAuthUserPass && !hasInlineCert) {
70-
// For now, warn if auth-user-pass is used without inline certs, as we might
71-
// need credentials
72-
// But many configs use both. We'll mark valid but user might fail auth if
73-
// prompt needed.
74-
// Ideally we'd validte if auth-user-pass points to a file, which wouldn't exist
75-
// in container.
76-
// So we should check if it's just "auth-user-pass" (interactive) or
77-
// "auth-user-pass file"
89+
// 2. auth-user-pass (requires username/password from UI)
90+
91+
boolean hasEmbeddedCert = rawConfig.contains("<ca>") || rawConfig.contains("<cert>") ||
92+
rawConfig.contains("<key>");
93+
boolean hasAuthDirective = rawConfig.contains("auth-user-pass");
94+
95+
// Set requiresAuth flag
96+
this.requiresAuth = hasAuthDirective;
97+
98+
if (!hasEmbeddedCert && !hasAuthDirective) {
99+
isValid = false;
100+
errorMessage = "Missing authentication: need either embedded certificates or 'auth-user-pass' directive";
101+
return;
78102
}
79103

80104
isValid = true;

src/main/resources/dockerfile/Dockerfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,14 @@ RUN echo '#!/bin/sh' > /start.sh && \
4545
echo ' # Create TUN device' >> /start.sh && \
4646
echo ' mkdir -p /dev/net' >> /start.sh && \
4747
echo ' mknod /dev/net/tun c 10 200 || true' >> /start.sh && \
48-
echo ' # Start OpenVPN in background to capture logs to stdout' >> /start.sh && \
48+
echo ' # Check if auth file exists and modify config' >> /start.sh && \
49+
echo ' if [ -f /etc/openvpn/auth.txt ]; then' >> /start.sh && \
50+
echo ' echo "Using authentication file"' >> /start.sh && \
51+
echo ' sed -i "s/auth-user-pass$/auth-user-pass \/etc\/openvpn\/auth.txt/" /etc/openvpn/client.conf 2>/dev/null || true' >> /start.sh && \
52+
echo ' fi' >> /start.sh && \
53+
echo ' # Start OpenVPN in foreground, exit if it fails' >> /start.sh && \
4954
echo ' # Use mssfix to prevent fragmentation issues' >> /start.sh && \
50-
echo ' openvpn --config /etc/openvpn/client.conf --mssfix 1000 &' >> /start.sh && \
55+
echo ' openvpn --config /etc/openvpn/client.conf --mssfix 1000 || { echo "OpenVPN failed to start"; exit 1; }' >> /start.sh && \
5156
echo ' VPN_IFACE="tun0"' >> /start.sh && \
5257
echo 'else' >> /start.sh && \
5358
echo ' echo "Starting WireGuard..."' >> /start.sh && \

0 commit comments

Comments
 (0)