feat: per-peer access enforcement, live peer status, auto IP assignment
Server-side access control: - firewall_manager.py: per-peer iptables FORWARD rules in WireGuard container; virtual IPs on Caddy (172.20.0.21-24) for per-service DROP/ACCEPT targeting - CoreDNS Corefile regenerated with ACL blocks for blocked services per peer - POST /api/wireguard/apply-enforcement re-applies rules after WireGuard restart; wg0.conf PostUp calls it via curl so rules restore automatically on container start WireGuard fixes: - _syncconf uses `wg set peer` instead of `wg syncconf` to avoid resetting ListenPort - add_peer validates AllowedIPs must be /32 — rejects full/split tunnel CIDRs that would route internet or LAN traffic to that peer - _config_file() checks for linuxserver wg_confs/ subdirectory first UI: - Peers page fetches /api/wireguard/peers/statuses for live handshake data; status badge now shows real Online/Offline + seconds since last handshake - IP field removed from Add Peer form (auto-assigned from 10.0.0.0/24) Tests (246 pass): - test_firewall_manager.py: 22 tests for ACL generation, iptables rule correctness, comment tagging, clear_peer_rules filter logic - test_peer_wg_integration.py: 10 tests for /32 enforcement, IP auto-assignment, syncconf called on add/remove - test_wireguard_manager.py: updated to reflect correct IPs and /32 requirement Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ from wireguard_manager import WireGuardManager
|
||||
|
||||
class TestWireGuardManager(unittest.TestCase):
|
||||
"""Test cases for WireGuardManager class"""
|
||||
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment"""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
@@ -34,10 +34,14 @@ class TestWireGuardManager(unittest.TestCase):
|
||||
self.config_dir = os.path.join(self.test_dir, 'config')
|
||||
os.makedirs(self.data_dir, exist_ok=True)
|
||||
os.makedirs(self.config_dir, exist_ok=True)
|
||||
|
||||
|
||||
patcher = patch.object(WireGuardManager, '_syncconf', return_value=None)
|
||||
self.mock_sync = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
|
||||
# Create WireGuardManager instance
|
||||
self.wg_manager = WireGuardManager(self.data_dir, self.config_dir)
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment"""
|
||||
shutil.rmtree(self.test_dir)
|
||||
@@ -100,54 +104,51 @@ class TestWireGuardManager(unittest.TestCase):
|
||||
def test_generate_config(self):
|
||||
"""Test WireGuard configuration generation"""
|
||||
config = self.wg_manager.generate_config('wg0', 51820)
|
||||
|
||||
|
||||
self.assertIsInstance(config, str)
|
||||
self.assertIn('[Interface]', config)
|
||||
self.assertIn('PrivateKey', config)
|
||||
self.assertIn('Address = 172.20.0.1/16', config)
|
||||
self.assertIn('Address = 10.0.0.1/24', config)
|
||||
self.assertIn('ListenPort = 51820', config)
|
||||
self.assertIn('PostUp', config)
|
||||
self.assertIn('PostDown', config)
|
||||
|
||||
def test_add_peer(self):
|
||||
"""Test adding a peer to WireGuard configuration"""
|
||||
# Generate peer keys first
|
||||
"""Test adding a peer — server-side AllowedIPs must be /32."""
|
||||
peer_keys = self.wg_manager.generate_peer_keys('testpeer')
|
||||
|
||||
|
||||
success = self.wg_manager.add_peer(
|
||||
'testpeer',
|
||||
peer_keys['public_key'],
|
||||
'192.168.1.100',
|
||||
'172.20.0.0/16',
|
||||
'',
|
||||
'10.0.0.2/32',
|
||||
25
|
||||
)
|
||||
|
||||
|
||||
self.assertTrue(success)
|
||||
|
||||
# Check if config file was created
|
||||
config_file = os.path.join(self.wg_manager.wireguard_dir, 'wg0.conf')
|
||||
|
||||
config_file = self.wg_manager._config_file()
|
||||
self.assertTrue(os.path.exists(config_file))
|
||||
|
||||
# Check config content
|
||||
|
||||
with open(config_file, 'r') as f:
|
||||
config = f.read()
|
||||
self.assertIn('[Peer]', config)
|
||||
self.assertIn(peer_keys['public_key'], config)
|
||||
self.assertIn('AllowedIPs = 172.20.0.0/16', config)
|
||||
self.assertIn('AllowedIPs = 10.0.0.2/32', config)
|
||||
self.assertIn('PersistentKeepalive = 25', config)
|
||||
|
||||
def test_remove_peer(self):
|
||||
"""Test removing a peer from WireGuard configuration"""
|
||||
# Add a peer first
|
||||
peer_keys = self.wg_manager.generate_peer_keys('testpeer')
|
||||
self.wg_manager.add_peer('testpeer', peer_keys['public_key'], '192.168.1.100')
|
||||
self.wg_manager.add_peer('testpeer', peer_keys['public_key'], '', '10.0.0.2/32')
|
||||
|
||||
# Remove the peer
|
||||
success = self.wg_manager.remove_peer(peer_keys['public_key'])
|
||||
self.assertTrue(success)
|
||||
|
||||
# Check if peer was removed
|
||||
config_file = os.path.join(self.wg_manager.wireguard_dir, 'wg0.conf')
|
||||
config_file = self.wg_manager._config_file()
|
||||
with open(config_file, 'r') as f:
|
||||
config = f.read()
|
||||
self.assertNotIn(peer_keys['public_key'], config)
|
||||
@@ -156,7 +157,7 @@ class TestWireGuardManager(unittest.TestCase):
|
||||
"""Test getting list of configured peers"""
|
||||
# Add a peer first
|
||||
peer_keys = self.wg_manager.generate_peer_keys('testpeer')
|
||||
self.wg_manager.add_peer('testpeer', peer_keys['public_key'], '192.168.1.100')
|
||||
self.wg_manager.add_peer('testpeer', peer_keys['public_key'], '', '10.0.0.2/32')
|
||||
|
||||
peers = self.wg_manager.get_peers()
|
||||
|
||||
@@ -221,46 +222,40 @@ class TestWireGuardManager(unittest.TestCase):
|
||||
|
||||
def test_update_peer_ip(self):
|
||||
"""Test updating peer IP address"""
|
||||
# Add a peer first
|
||||
peer_keys = self.wg_manager.generate_peer_keys('testpeer')
|
||||
self.wg_manager.add_peer('testpeer', peer_keys['public_key'], '192.168.1.100')
|
||||
|
||||
# Update peer IP
|
||||
success = self.wg_manager.update_peer_ip(peer_keys['public_key'], '192.168.1.200')
|
||||
self.wg_manager.add_peer('testpeer', peer_keys['public_key'], '', '10.0.0.2/32')
|
||||
|
||||
success = self.wg_manager.update_peer_ip(peer_keys['public_key'], '10.0.0.9/32')
|
||||
self.assertTrue(success)
|
||||
|
||||
# Check if IP was updated in config
|
||||
config_file = os.path.join(self.wg_manager.wireguard_dir, 'wg0.conf')
|
||||
with open(config_file, 'r') as f:
|
||||
|
||||
with open(self.wg_manager._config_file(), 'r') as f:
|
||||
config = f.read()
|
||||
self.assertIn('192.168.1.200', config)
|
||||
self.assertIn('10.0.0.9/32', config)
|
||||
|
||||
def test_get_peer_config(self):
|
||||
"""Test generating peer configuration"""
|
||||
"""Test generating peer client configuration."""
|
||||
peer_keys = self.wg_manager.generate_peer_keys('testpeer')
|
||||
keys = self.wg_manager.get_keys()
|
||||
|
||||
config = self.wg_manager.get_peer_config('testpeer', '192.168.1.100', peer_keys['private_key'])
|
||||
|
||||
|
||||
config = self.wg_manager.get_peer_config('testpeer', '10.0.0.2', peer_keys['private_key'])
|
||||
|
||||
self.assertIsInstance(config, str)
|
||||
self.assertIn('[Interface]', config)
|
||||
self.assertIn('[Peer]', config)
|
||||
self.assertIn('PrivateKey', config)
|
||||
self.assertIn('Address = 192.168.1.100/32', config)
|
||||
self.assertIn('DNS = 172.20.0.2', config)
|
||||
self.assertIn('Address = 10.0.0.2/32', config)
|
||||
self.assertIn('DNS = 172.20.0.3', config)
|
||||
self.assertIn(keys['public_key'], config)
|
||||
self.assertIn('AllowedIPs = 172.20.0.0/16', config)
|
||||
self.assertIn('AllowedIPs', config)
|
||||
|
||||
def test_multiple_peers(self):
|
||||
"""Test managing multiple peers"""
|
||||
# Add first peer
|
||||
peer1_keys = self.wg_manager.generate_peer_keys('peer1')
|
||||
success1 = self.wg_manager.add_peer('peer1', peer1_keys['public_key'], '192.168.1.100')
|
||||
success1 = self.wg_manager.add_peer('peer1', peer1_keys['public_key'], '', '10.0.0.2/32')
|
||||
self.assertTrue(success1)
|
||||
|
||||
# Add second peer
|
||||
|
||||
peer2_keys = self.wg_manager.generate_peer_keys('peer2')
|
||||
success2 = self.wg_manager.add_peer('peer2', peer2_keys['public_key'], '192.168.1.101')
|
||||
success2 = self.wg_manager.add_peer('peer2', peer2_keys['public_key'], '', '10.0.0.3/32')
|
||||
self.assertTrue(success2)
|
||||
|
||||
# Get peers
|
||||
@@ -310,18 +305,21 @@ PersistentKeepalive = 30
|
||||
self.assertEqual(peers[1]['persistent_keepalive'], 30)
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling in WireGuard operations"""
|
||||
# Test with invalid public key
|
||||
success = self.wg_manager.add_peer('testpeer', 'invalid_key', '192.168.1.100')
|
||||
# Should still return True as it writes to config file
|
||||
"""Test error handling in WireGuard operations."""
|
||||
# Wide CIDR rejected — server-side AllowedIPs must be /32
|
||||
success = self.wg_manager.add_peer('testpeer', 'invalid_key', '', '172.20.0.0/16')
|
||||
self.assertFalse(success, "Wide CIDR must be rejected")
|
||||
|
||||
# Valid /32 with any key string is accepted (key format not validated at this layer)
|
||||
success = self.wg_manager.add_peer('testpeer', 'any_key_string=', '', '10.0.0.2/32')
|
||||
self.assertTrue(success)
|
||||
|
||||
# Test removing non-existent peer
|
||||
|
||||
# Removing non-existent peer is a no-op, not an error
|
||||
success = self.wg_manager.remove_peer('non_existent_key')
|
||||
self.assertTrue(success)
|
||||
|
||||
# Test updating non-existent peer IP
|
||||
success = self.wg_manager.update_peer_ip('non_existent_key', '192.168.1.200')
|
||||
|
||||
# Updating IP for peer not in config returns False
|
||||
success = self.wg_manager.update_peer_ip('non_existent_key', '10.0.0.9/32')
|
||||
self.assertFalse(success)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in New Issue
Block a user