Publish and retrieve your local ip addresses!
This project originally served a research project for my uni. Since then I did not need it anymore. The idea and implementation might still be of interest tho, so feel free to get inspired and create your own solution.
lip (acronym for Local Ip Publisher) is a small webservice for publishing and retrieving (local) ip addresses, written in Typescript. It's mainly designed for distributed architectures that run in local networks, but are not able to find each other through local solutions (e.g. multicasts).
It's ...
- fast and lightweight by using bun as its javascript runtime, ElysiaJS as the fastest web framework currently available and SQLite for a local database
- small, the project only focusses on being what's intended to be
- extensible, so alternative web frameworks and runtimes can be used if desired (you will need to rewrite some code tho)
- easy-to-use and straight-forward, because we don't like complicated APIs
lip is currently in development. The main
branch is stable and can be used in production. The application starts on 0.0.0.0:8080
by default.
Prerequesites:
- working bun environment
- this repository
Then, run
bun run src/index.ts
and you are good to go.
Clone this repository and execute
docker build -t lip .
You can now run the application with
docker run -p 8080:8080 -it lip
Rename docker-compose_template.yaml
to docker-compose.yaml
and modify/remove the environment variables as you like.
version: "3.9"
services:
app:
build: .
ports:
- "8080:8080"
environment:
- LIP_HOSTNAME: <HOSTNAME>
- LIP_PORT: <PORT>
- LIP_DB_NAME: <MY_DB_NAME>
- LIP_JWT_SECRET: <MY_JWT_SECRET>
- LIP_TO_STDOUT: <TO_STDOUT>
container_name: lip
restart: always
Then, run
docker compose up -d
and the container should be build and executed automagically.
LIP_HOSTNAME
Change hostname lip starts on. 0.0.0.0 by default.
LIP_PORT
Change port lip starts on. 8080 by default.
LIP_DB_NAME
lip creates a new SQLite database called lip.sqlite
at the directory you started the command from. You can change this by setting the variable to the directory and name of your choice.
LIP_JWT_SECRET
JWT generation and validation requires a secret token. lip generates a new secret on every startup, but it is advised to use your own one.
LIP_TO_STDOUT
In case you do not want lip to print anything to the console, set the variable to 'false'. Every other value will keep lip printing.
You'll be better when using this solution in a closed, project-intern environment. Because of it's small footprint, it can be easily deployed on an existing project server, behind a secure proxy. This way, id's and ip's stay internal and id's do most likely not collide.
lip is in development. The main
branch will contain stable releases, whereas development takes place in dev
and features are developed in feat/<feature-name>
branches.
Currently, there are several features needed for the project to be "completed":
- fix publishing/retrieving inconsistencies created by longer hashing times
- use bcrypt password hashing instead of sha256 for improved security
- source code documentation
- endpoint tests
- protect reading/writing id's using passwords
- use JWTs for regular address updating
- add JWT requiring cooldown + modify JWT expire date
- add lifetime for id to free it after certain amount of time (infinite should also be possible)
- easy deployment with docker and docker compose
Possible features for post project completion could be:
- overview on existing id's and their lifetime (by e.g. application password)
- password reset feature, connected to an email
- multiple sdk implementations for both publishing and retrieving ip addresses
A multiplatform, binary command line application is planned. It will be developed with Go.
Basically lip is a text-sharing application, where each text is secured with a pre-known id and password. So when starting a "server" and "client" that should communicate, but are unable to find each other (because of e.g. outer restrictions), they can use the set id and password to publish and retrieve the correct ip addresses.
Mabye an example explains it better.
You have an architecture (let's say server-client) that is supposed to be run in a local, dynamic network/environment. Unfortunately, the client cannot find the server through existing solutions (like broadcasts or multicasts) and you don't like typing ip addresses manually or hole punching your network.
This means you need a small, easy to host web application that is freely accessible on the web by all your architecture participants. But because it's on the open web, you need password protection. And because the application may be moved to another location and clients may vary, you need a fixed id that is reserved and can be shipped to the client.
A tangible example would be a server on a local machine and a web application that requires data from the local machine. The web application needs to directly access the local machine, but is unable to find it's ip address (multicasts are disabled within the browser sandbox). Now you can take a small detour with this application, retrieve the needed ip address and directly communicate with your local server.
TLDR;
- when creating an ip at
/create
, you'll need to specify an access and master password - updating and retrieving ip addresses require a JWT
- possible token modes are read (only usable at
/retrieve
) and write (only usable at/update
) - get a JWT at
/jwt
by providing credential and a token mode - JWTs can be acquired with access passwords only
- tokens expire after 6 minutes
- the creation of read-tokens is limited to 6 tokens per minute per id
- write-tokens should be invalidated at
/invalidatejwt
after usage - an ip can be deleted at
/delete
with the master password only - when deleting an ip, all existing tokens for that id won't be usable again, even if id is recreated afterwards
When creating a new id, you'll be required to define two passwords. This is to protect the stored ip from being read, changed or deleted by unauthorized users. There are two types of passwords: access passwords and master passwords. While the access password can be shared with other clients/users, the master password should be kept internal. You cannot update or retrieve ip addresses with credentials. You'll need a JWT.
To create a JWT, use the /jwt
endpoint. Authenticate with id and access password, and set a JWt mode. There are two available modes: read and write. read-tokens can only be used at /retrieve
, and write-tokens can only be used at /update
. In addition, a write-token can be generated only once per id, while there are "infinite" read-tokens (rate limited to 6/minute per id). Once a write-token has been generated, the id can only be updated through the valid JWT. Tokens last for 6 minutes.
read-tokens stay valid over multiple application restarts, while write-tokens need to be created after every restart. To invalidate all existing tokens, modify the JWT secret. It should be best practice to invalidate write-tokens at /invalidatejwt
after usage. This happens automatically after 6 minutes, but during this time, no other write-token can be created.
When deleting an id a
and recreating it (a'
), all existing JWTs for a
will not work for a'
. This ensures that JWTs must be generated for every "new" id. An id can be deleted at /delete
using the master password.
Each token can have a lifetime, measured in seconds. It can be set to everything between -1 and 31536000 (one year). -1
stands for infinite, 0
for a token deletion within the next second. The lifetime is automatically refreshed with each token update.
Token lifetimes are evaluated lazily. Thus exceeded id are deleted on the next access attempt.
lip supports both IPv4 and IPv6 addresses. Port notations are allowed.
Examples:
- 234.123.241.242
- 234.123.241.242:4000
- ::1 (defaults to [::1])
- [::1]
- [::1]:4000
Some general things to consider, before going into the details:
- if not specified, all endpoints are
POST
- JSON is the only supported body content type. Make sure to set the
Content-Type
-header toapplication/json
, else the application won't work and you'll receive a non-200
status code - you'll always get a meaningful status code and response JSON
{"info": "<info here>"}
(specified per endpoint) info
always contains some information in case of an error,200
's don't contain more information except when specified
The return JSON examples below are only returned on code 200
.
Check service availability.
Returns:
{
"info": "hello lip!"
}
200
,info
contains 'hello lip!'
Creates a new id to store an ip address.
Requires:
{
"id": "<new_id>",
"access_password": "<access_password>",
"master_password": "<master_password>",
"lifetime": -1
}
Returns:
{
"info": "created new address '<new_id>'"
}
200
on a successful id creation400
on invalid id, passwords or lifetime409
if the id already exists
Updates an id.
Requires:
{
"jwt": "<jwt>",
"ip_address": "<ip address>"
}
Returns:
{
"info": "",
"last_update": -1
}
200
on a successful update400
invalid ip address401
invalid authentication (invalid JWT, wrong JWT mode)
Gets the ip address to an id.
Requires:
{
"jwt": "<jwt>"
}
Returns:
{
"info": "<ip address>",
"last_update": -1,
"lifetime": -1
}
200
on a successful update401
invalid authentication (invalid JWT, wrong JWT mode)
Delete an id.
Requires:
{
"id": "<id>",
"password": "<master password>"
}
Returns:
{
"info": "deleted address '<id>'"
}
200
on a successful id creation401
invalid authentication (id does not exist, wrong password)
Get a JWT for easier long-term updating/retrieving.
Requires:
{
"id": "<id>",
"password": "<access password>",
"mode": "<mode>"
}
Returns:
{
"info": "<jwt>"
}
200
successful JWT generation400
invalid/unknown mode401
invalid authentication (id does not exist, wrong password)409
write JWT for id already exists
Invalidates a JWT for updating an id.
Requires:
{
"id": "<id>",
"password": "<access password>",
"jwt": "<jwt>"
}
Returns:
{
"info": ""
}
200
successful JWT invalidation400
JWT invalid, wrong token mode401
invalid authentication (id does not exist, wrong password)
I was in need of this kind of webservice for a much larger project. After some research, I found no simple, lightweight solution to fix my problem. So then I wrote my own one.
I wanted to learn Javascript/Typescript to have an advantage in future development propjects (I mean, every developer has to visit Javascript at one time in his boring life). And because I'm interested in new technologies, I chose Bun as the runtime.
I understand that Bun is not the first choise for Javascript developers. The architecture is kept generic, so the three main parts (routing, handling, database) can be reprogrammed and swap to your likings. The code is not that complicated, so just start at src/index.ts
for a better understanding.
Yes, but also no. I have ~8 years experience in coding, but using Typescript is new for me. Please have mercy and feel free to educate me on best practices and coding standarts :-)