check in examples, perform round-trip-tests, fix yaml conversion bugs due to inconsistent nesting

This commit is contained in:
eyedeekay
2025-10-18 22:13:02 -04:00
parent ef04999f62
commit 154fddcd28
23 changed files with 976 additions and 53 deletions

View File

@@ -51,6 +51,18 @@ go-i2ptunnel-config --batch "*.config"
go-i2ptunnel-config --batch --out-format ini "tunnels/*.properties"
```
## Examples
The `examples/` directory contains ready-to-use configuration templates for common tunnel types in all three formats:
- HTTP client (web proxy)
- HTTP server (eepsite hosting)
- SOCKS proxy
- Generic client tunnel
- Generic server tunnel
Each example includes detailed comments explaining the configuration options. See [examples/README.md](examples/README.md) for more information.
## Contributing
1. Fork repository

231
examples/README.md Normal file
View File

@@ -0,0 +1,231 @@
# I2P Tunnel Configuration Examples
This directory contains example tunnel configuration files for all three supported formats:
- **Java I2P format** (`.properties` files)
- **i2pd format** (`.conf` files)
- **go-i2p format** (`.yaml` files)
## Available Examples
### HTTP Client Tunnel (HTTP Proxy)
Creates a local HTTP proxy to access I2P websites (eepsites).
- `httpclient.properties` - Java I2P format
- `httpclient.conf` - i2pd format
- `httpclient.yaml` - go-i2p format
**Use case**: Browse eepsites through your web browser configured to use the proxy.
**Default port**: 4444
### HTTP Server Tunnel (Eepsite)
Publishes a local web server as an I2P hidden service.
- `httpserver.properties` - Java I2P format
- `httpserver.conf` - i2pd format
- `httpserver.yaml` - go-i2p format
**Use case**: Host your own website accessible only through I2P.
**Target**: Local web server (default: 127.0.0.1:8080)
### SOCKS Tunnel (SOCKS Proxy)
Creates a SOCKS5 proxy for general I2P network access.
- `socks.properties` - Java I2P format
- `socks.conf` - i2pd format
- `socks.yaml` - go-i2p format
**Use case**: Route any SOCKS-compatible application through I2P (IRC, SSH, etc.).
**Default port**: 9050
### Generic Client Tunnel
Creates a TCP client tunnel for any protocol.
- `client.properties` - Java I2P format
- `client.conf` - i2pd format
- `client.yaml` - go-i2p format
**Use case**: Connect to any I2P service using a custom protocol.
**Default port**: 7000
### Generic Server Tunnel
Publishes any local TCP service as an I2P hidden service.
- `server.properties` - Java I2P format
- `server.conf` - i2pd format
- `server.yaml` - go-i2p format
**Use case**: Make any TCP service accessible through I2P (SSH server, game server, etc.).
**Target**: Local service (default: 127.0.0.1:9000)
## Using the Examples
### Converting Between Formats
Convert any example to a different format using the tool:
```bash
# Convert Java I2P properties to YAML
go-i2ptunnel-config httpclient.properties
# Convert i2pd conf to properties
go-i2ptunnel-config --out-format properties httpserver.conf
# Convert YAML to i2pd conf
go-i2ptunnel-config --out-format ini socks.yaml
```
### Validating Configuration
Before using a configuration file, validate it:
```bash
go-i2ptunnel-config --validate httpclient.properties
go-i2ptunnel-config --validate --strict server.yaml
```
### Customizing Examples
All examples contain sensible defaults. Modify these fields for your use case:
#### Required Fields
- **name**: Unique identifier for your tunnel
- **type**: Tunnel type (don't change unless you know what you're doing)
#### Common Fields to Customize
**For Client Tunnels (HTTP Client, SOCKS, Generic Client)**:
- **interface**: Network interface to bind (default: 127.0.0.1)
- **port**: Local port to listen on
- **target/destination**: I2P destination to connect to (for generic client)
**For Server Tunnels (HTTP Server, Generic Server)**:
- **target**: Local service address (format: `host:port`)
- **spoofedHost**: Custom hostname for your eepsite (optional)
#### Advanced Options
**I2CP Options** (i2cp.*):
- `leaseSetEncType`: Encryption type for the lease set (recommended: "4,0")
- `closeIdleTime`: Time in milliseconds before closing idle connections
- `newDestOnResume`: Whether to create a new destination on restart
**Tunnel Options** (inbound/outbound):
- `length`: Number of hops in the tunnel (higher = more anonymous, slower)
- `quantity`: Number of parallel tunnels (higher = more reliable, more resources)
**Other Options**:
- `persistentKey`: Keep the same I2P address across restarts (true/false)
- `gzip`: Enable compression (true/false)
## Format Differences
### Java I2P Properties Format (`.properties`)
- Flat key-value pairs
- Uses prefixes like `option.i2cp.*`, `option.inbound.*`
- Standard in Java I2P router
- Example: `option.inbound.length=3`
### i2pd INI Format (`.conf`)
- Section-based structure `[TunnelName]`
- Simpler syntax, more readable
- Native to i2pd router
- Example: `inbound.length = 3`
### go-i2p YAML Format (`.yaml`)
- Hierarchical nested structure with `tunnels` map
- Most readable and maintainable
- Native to go-i2p implementation
- Supports multiple tunnels in one file
- Example:
```yaml
tunnels:
MyTunnel:
type: client
port: 7000
inbound:
length: 3
```
## Tunnel Types Reference
| Type | Description | Requires Port | Requires Target |
|------|-------------|---------------|-----------------|
| `httpclient` | HTTP proxy client | Yes | No |
| `httpserver` | HTTP server (eepsite) | No | Yes |
| `sockstunnel` | SOCKS5 proxy | Yes | No |
| `client` | Generic client tunnel | Yes | Optional |
| `server` | Generic server tunnel | No | Yes |
| `ircclient` | IRC client tunnel | Yes | No |
| `ircserver` | IRC server tunnel | No | Yes |
| `streamrclient` | Streaming client | Yes | No |
| `streamrserver` | Streaming server | No | Yes |
## Security Considerations
- **Persistent Keys**: Set `persistentKey: true` to keep the same I2P address across restarts. This is important for servers so users can find you again.
- **Tunnel Length**: Higher values (3-7) provide better anonymity but slower performance.
- **Tunnel Quantity**: More tunnels provide better reliability and performance but use more resources.
- **Local Interface**: Binding to `127.0.0.1` ensures only local applications can use your tunnel. Never bind to `0.0.0.0` unless you understand the security implications.
## Testing Configurations
Use dry-run mode to test conversions without creating files:
```bash
go-i2ptunnel-config --dry-run httpclient.properties
go-i2ptunnel-config --dry-run --out-format yaml httpserver.conf
```
## Common Use Cases
### Setting up an eepsite
1. Use `httpserver.properties` (or `.conf`/`.yaml`)
2. Modify `target` to point to your web server (e.g., `127.0.0.1:8080`)
3. Set `persistentKey: true` so your address stays the same
4. Optionally set `spoofedHost` to a memorable `.i2p` hostname
### Browsing eepsites
1. Use `httpclient.properties` (or `.conf`/`.yaml`)
2. Keep default port `4444` or choose your own
3. Configure your browser to use `127.0.0.1:4444` as HTTP proxy
4. Visit `.i2p` addresses in your browser
### General I2P Network Access
1. Use `socks.properties` (or `.conf`/`.yaml`)
2. Keep default port `9050` or choose your own
3. Configure applications to use `127.0.0.1:9050` as SOCKS5 proxy
4. Works with SSH, IRC, and other SOCKS-compatible applications
## Further Reading
- [I2P Documentation](https://geti2p.net/en/docs)
- [i2pd Documentation](https://i2pd.readthedocs.io/)
- [go-i2p GitHub](https://github.com/go-i2p)
## Contributing
Found an issue with the examples or want to add more? Please submit a pull request or open an issue on GitHub.

14
examples/client.conf Normal file
View File

@@ -0,0 +1,14 @@
# Generic Client Tunnel Configuration (i2pd format)
# This creates a standard TCP client tunnel for any protocol
[MyClientTunnel]
type = client
address = 127.0.0.1
port = 7000
destination = example.i2p
keys = client-keys.dat
inbound.length = 3
inbound.quantity = 2
outbound.length = 3
outbound.quantity = 2
i2cp.leaseSetEncType = 4,0

View File

@@ -0,0 +1,28 @@
# Generic Client Tunnel Configuration (Java I2P format)
# This creates a standard TCP client tunnel for any protocol
# Basic tunnel identification
name=MyClientTunnel
type=client
description=Generic client tunnel for custom protocols
# Network settings
interface=127.0.0.1
listenPort=7000
# Target destination (I2P destination base64 or hostname)
targetDestination=example.i2p
# I2CP options
option.i2cp.leaseSetEncType=4,0
option.i2cp.closeIdleTime=1800000
# Tunnel options
option.inbound.length=3
option.inbound.quantity=2
option.outbound.length=3
option.outbound.quantity=2
# Client options
option.persistentClientKey=true
option.i2ptunnel.gzip=false

30
examples/client.yaml Normal file
View File

@@ -0,0 +1,30 @@
# Generic Client Tunnel Configuration (YAML format - go-i2p)
# This creates a standard TCP client tunnel for any protocol
tunnels:
MyClientTunnel:
type: client
description: Generic client tunnel for custom protocols
interface: 127.0.0.1
port: 7000
target: example.i2p
persistentKey: true
# I2CP options
i2cp:
leaseSetEncType: "4,0"
closeIdleTime: 1800000
# Tunnel configuration options
options:
gzip: false
# Inbound tunnel settings
inbound:
length: 3
quantity: 2
# Outbound tunnel settings
outbound:
length: 3
quantity: 2

14
examples/httpclient.conf Normal file
View File

@@ -0,0 +1,14 @@
# HTTP Client Tunnel Configuration (i2pd format)
# This creates an HTTP proxy that allows you to access I2P sites (eepsites)
[MyHTTPProxy]
type = http
address = 127.0.0.1
port = 4444
keys = httpclient-keys.dat
inbound.length = 3
inbound.quantity = 2
outbound.length = 3
outbound.quantity = 2
i2cp.leaseSetEncType = 4,0
gzip = true

View File

@@ -0,0 +1,28 @@
# HTTP Client Tunnel Configuration (Java I2P format)
# This creates an HTTP proxy that allows you to access I2P sites (eepsites)
# through a local proxy on your computer.
# Basic tunnel identification
name=MyHTTPProxy
type=httpclient
description=HTTP proxy for accessing eepsites
# Network settings - where the local proxy will listen
interface=127.0.0.1
listenPort=4444
# I2CP options - control how the tunnel connects to the I2P router
option.i2cp.leaseSetEncType=4,0
option.i2cp.closeIdleTime=1800000
option.i2cp.newDestOnResume=false
# Tunnel options - configure tunnel behavior
option.inbound.length=3
option.inbound.quantity=2
option.outbound.length=3
option.outbound.quantity=2
# Client options
option.persistentClientKey=true
option.i2ptunnel.httpclient.allowInternalSSL=false
option.outbound.randomKey=true

31
examples/httpclient.yaml Normal file
View File

@@ -0,0 +1,31 @@
# HTTP Client Tunnel Configuration (YAML format - go-i2p)
# This creates an HTTP proxy that allows you to access I2P sites (eepsites)
tunnels:
MyHTTPProxy:
type: httpclient
description: HTTP proxy for accessing eepsites
interface: 127.0.0.1
port: 4444
persistentKey: true
# I2CP options - control how the tunnel connects to the I2P router
i2cp:
leaseSetEncType: "4,0"
closeIdleTime: 1800000
newDestOnResume: false
# Tunnel configuration options
options:
gzip: true
# Inbound tunnel settings
inbound:
length: 3
quantity: 2
# Outbound tunnel settings
outbound:
length: 3
quantity: 2
randomKey: true

14
examples/httpserver.conf Normal file
View File

@@ -0,0 +1,14 @@
# HTTP Server Tunnel Configuration (i2pd format)
# This publishes a local web server as an I2P hidden service (eepsite)
[MyWebsite]
type = http
address = 127.0.0.1:8080
keys = mywebsite-keys.dat
inbound.length = 3
inbound.quantity = 3
outbound.length = 3
outbound.quantity = 3
i2cp.leaseSetEncType = 4,0
gzip = true
hostoverride = mysite.i2p

View File

@@ -0,0 +1,26 @@
# HTTP Server Tunnel Configuration (Java I2P format)
# This publishes a local web server as an I2P hidden service (eepsite)
# Basic tunnel identification
name=MyWebsite
type=httpserver
description=My personal eepsite
# Target - the local web server to publish
targetHost=127.0.0.1
targetPort=8080
# I2CP options - control destination management
option.i2cp.leaseSetEncType=4,0
option.i2cp.closeIdleTime=1800000
option.i2cp.newDestOnResume=false
# Tunnel options - configure tunnel behavior
option.inbound.length=3
option.inbound.quantity=3
option.outbound.length=3
option.outbound.quantity=3
# Server options
option.persistentClientKey=true
spoofedHost=mysite.i2p

30
examples/httpserver.yaml Normal file
View File

@@ -0,0 +1,30 @@
# HTTP Server Tunnel Configuration (YAML format - go-i2p)
# This publishes a local web server as an I2P hidden service (eepsite)
tunnels:
MyWebsite:
type: httpserver
description: My personal eepsite
target: 127.0.0.1:8080
persistentKey: true
# I2CP options - control destination management
i2cp:
leaseSetEncType: "4,0"
closeIdleTime: 1800000
newDestOnResume: false
# Tunnel configuration options
options:
gzip: true
spoofedHost: mysite.i2p
# Inbound tunnel settings
inbound:
length: 3
quantity: 3
# Outbound tunnel settings
outbound:
length: 3
quantity: 3

12
examples/server.conf Normal file
View File

@@ -0,0 +1,12 @@
# Generic Server Tunnel Configuration (i2pd format)
# This publishes a local TCP service as an I2P hidden service
[MyServerTunnel]
type = server
address = 127.0.0.1:9000
keys = server-keys.dat
inbound.length = 3
inbound.quantity = 3
outbound.length = 3
outbound.quantity = 3
i2cp.leaseSetEncType = 4,0

View File

@@ -0,0 +1,25 @@
# Generic Server Tunnel Configuration (Java I2P format)
# This publishes a local TCP service as an I2P hidden service
# Basic tunnel identification
name=MyServerTunnel
type=server
description=Generic server tunnel for custom protocols
# Target - the local service to publish
targetHost=127.0.0.1
targetPort=9000
# I2CP options
option.i2cp.leaseSetEncType=4,0
option.i2cp.closeIdleTime=1800000
# Tunnel options
option.inbound.length=3
option.inbound.quantity=3
option.outbound.length=3
option.outbound.quantity=3
# Server options
option.persistentClientKey=true
option.i2ptunnel.gzip=false

28
examples/server.yaml Normal file
View File

@@ -0,0 +1,28 @@
# Generic Server Tunnel Configuration (YAML format - go-i2p)
# This publishes a local TCP service as an I2P hidden service
tunnels:
MyServerTunnel:
type: server
description: Generic server tunnel for custom protocols
target: 127.0.0.1:9000
persistentKey: true
# I2CP options
i2cp:
leaseSetEncType: "4,0"
closeIdleTime: 1800000
# Tunnel configuration options
options:
gzip: false
# Inbound tunnel settings
inbound:
length: 3
quantity: 3
# Outbound tunnel settings
outbound:
length: 3
quantity: 3

14
examples/socks.conf Normal file
View File

@@ -0,0 +1,14 @@
# SOCKS Tunnel Configuration (i2pd format)
# This creates a SOCKS proxy that provides access to I2P network services
[MySOCKSProxy]
type = socks
address = 127.0.0.1
port = 9050
keys = socksproxy-keys.dat
inbound.length = 3
inbound.quantity = 2
outbound.length = 3
outbound.quantity = 2
i2cp.leaseSetEncType = 4,0
gzip = true

26
examples/socks.properties Normal file
View File

@@ -0,0 +1,26 @@
# SOCKS Tunnel Configuration (Java I2P format)
# This creates a SOCKS proxy that provides access to I2P network services
# Basic tunnel identification
name=MySOCKSProxy
type=sockstunnel
description=SOCKS5 proxy for I2P network access
# Network settings - where the SOCKS proxy will listen
interface=127.0.0.1
listenPort=9050
# I2CP options
option.i2cp.leaseSetEncType=4,0
option.i2cp.closeIdleTime=1800000
# Tunnel options
option.inbound.length=3
option.inbound.quantity=2
option.outbound.length=3
option.outbound.quantity=2
# SOCKS-specific options
option.persistentClientKey=true
option.outbound.randomKey=true
option.i2ptunnel.gzip=true

30
examples/socks.yaml Normal file
View File

@@ -0,0 +1,30 @@
# SOCKS Tunnel Configuration (YAML format - go-i2p)
# This creates a SOCKS proxy that provides access to I2P network services
tunnels:
MySOCKSProxy:
type: sockstunnel
description: SOCKS5 proxy for I2P network access
interface: 127.0.0.1
port: 9050
persistentKey: true
# I2CP options
i2cp:
leaseSetEncType: "4,0"
closeIdleTime: 1800000
# Tunnel configuration options
options:
gzip: true
# Inbound tunnel settings
inbound:
length: 3
quantity: 2
# Outbound tunnel settings
outbound:
length: 3
quantity: 2
randomKey: true

View File

@@ -145,7 +145,7 @@ func TestCrossFormatConversion(t *testing.T) {
})
}
// TestYAMLParser tests the parseYAML function specifically (0% coverage)
// TestYAMLParser tests the parseYAML function with the nested tunnels format
func TestYAMLParser(t *testing.T) {
converter := &Converter{}
@@ -156,8 +156,9 @@ func TestYAMLParser(t *testing.T) {
wantErr bool
}{
{
name: "valid_yaml_direct_format",
input: `name: HttpProxy
name: "valid_yaml_nested_format",
input: `tunnels:
HttpProxy:
type: httpclient
interface: 127.0.0.1
port: 4444
@@ -203,7 +204,8 @@ outbound:
},
{
name: "minimal_yaml_tunnel",
input: `name: MinimalTunnel
input: `tunnels:
MinimalTunnel:
type: client`,
expected: &TunnelConfig{
Name: "MinimalTunnel",
@@ -213,7 +215,8 @@ type: client`,
},
{
name: "invalid_yaml_syntax",
input: `name: BadTunnel
input: `tunnels:
BadTunnel:
type: httpclient
port: "not_a_number"
invalid_yaml: [
@@ -222,10 +225,10 @@ invalid_yaml: [
wantErr: true,
},
{
name: "empty_yaml",
input: "",
expected: &TunnelConfig{},
wantErr: false,
name: "empty_tunnels_map",
input: "tunnels: {}",
expected: nil,
wantErr: true,
},
}
@@ -420,9 +423,10 @@ description=Load test tunnel`
t.Errorf("LoadConfig() Port = %d, want %d", config.Port, 8080)
}
// Test loading YAML file (direct format, not wrapped)
// Test loading YAML file (nested format with tunnels map)
yamlFile := filepath.Join(tempDir, "test.yaml")
yamlContent := `name: YamlTest
yamlContent := `tunnels:
YamlTest:
type: server
port: 9090`
err = os.WriteFile(yamlFile, []byte(yamlContent), 0644)

287
lib/examples_test.go Normal file
View File

@@ -0,0 +1,287 @@
package i2pconv
import (
"os"
"path/filepath"
"testing"
)
// TestExampleFiles validates all example configuration files in the examples directory.
// This ensures that the provided examples are syntactically correct and can be parsed.
func TestExampleFiles(t *testing.T) {
examplesDir := filepath.Join("..", "examples")
// Check if examples directory exists
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
t.Skip("examples directory not found, skipping example validation tests")
return
}
testCases := []struct {
name string
format string
}{
// Properties format examples
{"httpclient.properties", "properties"},
{"httpserver.properties", "properties"},
{"socks.properties", "properties"},
{"client.properties", "properties"},
{"server.properties", "properties"},
// INI format examples
{"httpclient.conf", "ini"},
{"httpserver.conf", "ini"},
{"socks.conf", "ini"},
{"client.conf", "ini"},
{"server.conf", "ini"},
// YAML format examples
{"httpclient.yaml", "yaml"},
{"httpserver.yaml", "yaml"},
{"socks.yaml", "yaml"},
{"client.yaml", "yaml"},
{"server.yaml", "yaml"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
filePath := filepath.Join(examplesDir, tc.name)
// Read the example file
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Failed to read example file %s: %v", tc.name, err)
}
// Parse the configuration
conv := &Converter{strict: false}
config, err := conv.ParseInput(data, tc.format)
if err != nil {
t.Fatalf("Failed to parse example file %s: %v", tc.name, err)
}
// Validate the configuration
if err := conv.validate(config); err != nil {
t.Errorf("Example file %s failed validation: %v", tc.name, err)
}
// Verify basic required fields
if config.Name == "" {
t.Errorf("Example file %s has empty name", tc.name)
}
if config.Type == "" {
t.Errorf("Example file %s has empty type", tc.name)
}
})
}
}
// TestExampleFileConversion verifies that example files can be converted between formats.
// This ensures cross-format compatibility and that conversions maintain data integrity.
// Note: YAML format has a nested structure (tunnels: map) so conversions TO YAML
// produce valid output, but may not round-trip perfectly without special handling.
func TestExampleFileConversion(t *testing.T) {
examplesDir := filepath.Join("..", "examples")
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
t.Skip("examples directory not found, skipping conversion tests")
return
}
testCases := []struct {
sourceFile string
sourceFormat string
targetFormat string
}{
// Properties and INI formats are flat and convert well in both directions
{"httpclient.properties", "properties", "ini"},
{"httpserver.conf", "ini", "properties"},
// YAML format converts FROM yaml to other formats reliably
{"socks.yaml", "yaml", "properties"},
{"socks.yaml", "yaml", "ini"},
}
for _, tc := range testCases {
t.Run(tc.sourceFile+"_to_"+tc.targetFormat, func(t *testing.T) {
filePath := filepath.Join(examplesDir, tc.sourceFile)
// Read source file
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Failed to read source file: %v", err)
}
// Convert to target format
conv := &Converter{strict: false}
output, err := conv.Convert(data, tc.sourceFormat, tc.targetFormat)
if err != nil {
t.Fatalf("Conversion failed: %v", err)
}
// Verify output is not empty
if len(output) == 0 {
t.Error("Conversion produced empty output")
}
// Parse the converted output
config, err := conv.ParseInput(output, tc.targetFormat)
if err != nil {
t.Fatalf("Failed to parse converted output: %v", err)
}
// Validate the converted configuration
if err := conv.validate(config); err != nil {
t.Errorf("Converted configuration failed validation: %v", err)
}
})
}
}
// TestExampleFileRoundTrip verifies that converting an example file to another format
// and back produces equivalent configuration (round-trip conversion).
// With the nested YAML format, all formats now support proper round-trip conversion.
func TestExampleFileRoundTrip(t *testing.T) {
examplesDir := filepath.Join("..", "examples")
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
t.Skip("examples directory not found, skipping round-trip tests")
return
}
testCases := []struct {
file string
format string
intermediate string
}{
// Properties <-> INI conversions work well in both directions
{"httpclient.properties", "properties", "ini"},
{"httpserver.conf", "ini", "properties"},
{"client.properties", "properties", "ini"},
// Properties/INI <-> YAML conversions now work with nested format
{"httpclient.properties", "properties", "yaml"},
{"socks.conf", "ini", "yaml"},
}
for _, tc := range testCases {
t.Run(tc.file+"_via_"+tc.intermediate, func(t *testing.T) {
filePath := filepath.Join(examplesDir, tc.file)
// Read original file
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
conv := &Converter{strict: false}
// Parse original
original, err := conv.ParseInput(data, tc.format)
if err != nil {
t.Fatalf("Failed to parse original: %v", err)
}
// Convert to intermediate format
intermediate, err := conv.generateOutput(original, tc.intermediate)
if err != nil {
t.Fatalf("Failed to convert to intermediate: %v", err)
}
// Parse intermediate
intermediateConfig, err := conv.ParseInput(intermediate, tc.intermediate)
if err != nil {
t.Fatalf("Failed to parse intermediate: %v", err)
}
// Convert back to original format
final, err := conv.generateOutput(intermediateConfig, tc.format)
if err != nil {
t.Fatalf("Failed to convert back to original: %v", err)
}
// Parse final result
finalConfig, err := conv.ParseInput(final, tc.format)
if err != nil {
t.Fatalf("Failed to parse final: %v", err)
}
// Compare key fields between original and final
if original.Name != finalConfig.Name {
t.Errorf("Name mismatch: original=%s, final=%s", original.Name, finalConfig.Name)
}
if original.Type != finalConfig.Type {
t.Errorf("Type mismatch: original=%s, final=%s", original.Type, finalConfig.Type)
}
if original.Port != finalConfig.Port {
t.Errorf("Port mismatch: original=%d, final=%d", original.Port, finalConfig.Port)
}
})
}
}
// TestExampleFilesAgainstTunnelTypes verifies that each example file uses
// a valid tunnel type and meets the requirements for that type.
func TestExampleFilesAgainstTunnelTypes(t *testing.T) {
examplesDir := filepath.Join("..", "examples")
if _, err := os.Stat(examplesDir); os.IsNotExist(err) {
t.Skip("examples directory not found, skipping tunnel type tests")
return
}
testCases := []struct {
file string
format string
tunnelType string
requirePort bool
requireTarget bool
}{
{"httpclient.properties", "properties", "httpclient", true, false},
{"httpclient.conf", "ini", "http", true, false},
{"httpclient.yaml", "yaml", "httpclient", true, false},
{"httpserver.properties", "properties", "httpserver", false, true},
{"httpserver.conf", "ini", "http", false, true},
{"httpserver.yaml", "yaml", "httpserver", false, true},
{"socks.properties", "properties", "sockstunnel", true, false},
{"socks.conf", "ini", "socks", true, false},
{"socks.yaml", "yaml", "sockstunnel", true, false},
{"client.properties", "properties", "client", true, false},
{"client.conf", "ini", "client", true, false},
{"client.yaml", "yaml", "client", true, false},
{"server.properties", "properties", "server", false, true},
{"server.conf", "ini", "server", false, true},
{"server.yaml", "yaml", "server", false, true},
}
for _, tc := range testCases {
t.Run(tc.file, func(t *testing.T) {
filePath := filepath.Join(examplesDir, tc.file)
data, err := os.ReadFile(filePath)
if err != nil {
t.Fatalf("Failed to read file: %v", err)
}
conv := &Converter{strict: false}
config, err := conv.ParseInput(data, tc.format)
if err != nil {
t.Fatalf("Failed to parse: %v", err)
}
// Note: tunnel types may vary between formats (e.g., "http" in i2pd vs "httpclient" in Java I2P)
// We check if the type is reasonable but don't enforce exact matches across formats
if config.Type == "" {
t.Error("Tunnel type is empty")
}
// Check port requirement
if tc.requirePort && config.Port == 0 {
t.Errorf("Expected port to be set for %s tunnel type", tc.tunnelType)
}
// Check target requirement
if tc.requireTarget && config.Target == "" {
t.Errorf("Expected target to be set for %s tunnel type", tc.tunnelType)
}
})
}
}

View File

@@ -173,7 +173,7 @@ type client`,
}
}
// TestYAMLParserErrors tests enhanced error reporting for YAML format
// TestYAMLParserErrors tests enhanced error reporting for YAML format (nested structure)
func TestYAMLParserErrors(t *testing.T) {
tests := []struct {
name string
@@ -184,7 +184,8 @@ func TestYAMLParserErrors(t *testing.T) {
}{
{
name: "valid yaml",
input: `name: test-tunnel
input: `tunnels:
test-tunnel:
type: client
interface: 127.0.0.1
port: 4444`,
@@ -192,7 +193,8 @@ port: 4444`,
},
{
name: "invalid indentation",
input: `name: test
input: `tunnels:
test:
type: client
interface: 127.0.0.1`,
expectError: true,
@@ -200,15 +202,18 @@ interface: 127.0.0.1`,
},
{
name: "invalid yaml structure",
input: `name: test
type client`,
input: `tunnels:
test
type: client`,
expectError: true,
expectParseErr: true,
expectLineNum: 3, // YAML error reports line 3 (end of file)
expectLineNum: 3, // YAML error reports line 3
},
{
name: "unclosed quote",
input: `name: "test
input: `tunnels:
test:
name: "test
type: client`,
expectError: true,
expectParseErr: true,

View File

@@ -331,8 +331,19 @@ func (c *Converter) generateJavaProperties(config *TunnelConfig) ([]byte, error)
// formatPropertyValue formats a property value for output
// Arrays/slices are formatted as comma-separated values
func formatPropertyValue(v interface{}) string {
// Handle []string
if slice, ok := v.([]string); ok {
return strings.Join(slice, ",")
}
// Handle []interface{} (common from YAML unmarshaling)
if slice, ok := v.([]interface{}); ok {
strSlice := make([]string, len(slice))
for i, item := range slice {
strSlice[i] = fmt.Sprint(item)
}
return strings.Join(strSlice, ",")
}
return fmt.Sprint(v)
}

View File

@@ -7,15 +7,35 @@ import (
"gopkg.in/yaml.v2"
)
// parseYAML parses YAML using the standard nested structure with "tunnels" map.
// This is the go-i2p format where tunnels are defined in a "tunnels" map.
// The parser extracts the first tunnel for single-tunnel conversion workflows.
// The tunnel name is set from the map key.
func (c *Converter) parseYAML(input []byte) (*TunnelConfig, error) {
config := &TunnelConfig{}
err := yaml.Unmarshal(input, config)
type wrapper struct {
Tunnels map[string]*TunnelConfig `yaml:"tunnels"`
}
var w wrapper
err := yaml.Unmarshal(input, &w)
if err != nil {
return nil, c.enhanceYAMLError(input, err)
}
// Extract the first tunnel (for single-tunnel conversion)
if len(w.Tunnels) == 0 {
return nil, fmt.Errorf("yaml: no tunnels found in tunnels map")
}
// Return the first tunnel with its name set from the map key
for name, config := range w.Tunnels {
config.Name = name
return config, nil
}
return nil, fmt.Errorf("yaml: failed to extract tunnel from nested structure")
}
// enhanceYAMLError wraps YAML parsing errors with line context.
// The yaml.v2 library provides line numbers in its error messages.
func (c *Converter) enhanceYAMLError(input []byte, err error) error {
@@ -60,6 +80,9 @@ func (c *Converter) enhanceYAMLError(input []byte, err error) error {
return fmt.Errorf("yaml parse error: %w", err)
}
// generateYAML creates YAML output in the standard nested structure format.
// This is the go-i2p format where tunnels are defined in a "tunnels" map.
// The tunnel is keyed by its name in the map, allowing for future multi-tunnel support.
func (c *Converter) generateYAML(config *TunnelConfig) ([]byte, error) {
type wrapper struct {
Tunnels map[string]*TunnelConfig `yaml:"tunnels"`

View File

@@ -13,7 +13,7 @@ func main() {
cmd := &cli.App{
Name: "go-i2ptunnel-config",
Usage: "Convert I2P tunnel configurations between formats",
Version: "1.0.0",
Version: "0.33.0",
Description: `A command line utility to convert I2P tunnel configurations between Java I2P, i2pd, and go-i2p formats.
Supports automatic format detection based on file extensions: